Entity Framework custom migration generator decorators
In the past I created a number of custom solutions to generate the migration sql from Entity Framework Code First migrations (see my post here for soft delete automation, here for auditing migration automatically, here for specifying filtered indices and the one I created recently about adding an index to discriminators).
These are all very useful when used standalone, but what if you want to combine two of them? Let's say auditing migrations and generate the index for the discriminators? Basically, what I wanted to achieve is an option to flexibly combine any of my custom generators with any other. This screams decorator — unfortunately, that's not how the API is built. The only way to hook into the generation is to descend the SqlServerMigrationSqlGenerator (for SQL Server at least, if you want, you can go higher on the hierarchy) and override protected methods for generating a piece of SQL. If these methods were public, a simple decorator would look like this (for the CreateTable()
method, you can probably imagine the rest):
public class SpecialFeatureDecorator : SqlServerMigrationSqlGenerator
{
private readonly SqlServerMigrationSqlGenerator decoratedGenerator;
public SpecialFeatureDecorator(SqlServerMigrationSqlGenerator decoratedGenerator)
{
this.decoratedGenerator = decoratedGenerator;
}
public override void CreateTable(CreateTableOperation createTableOperation)
{
// add my fancy sql
decoratedGenerator.CreateTable(createTableOperation);
}
}
But, unfortunately the generating methods are protected, so you cannot access them. In a recent post I did a breakdown of how to handle these situations. Now here's an actual, real-life implementation based on that POC.
Implementing a decorator for a decorator-unfriendly environment
At this point, there are three things that are certain:
- We have to descend from the
SqlServerMigrationSqlGenerator
because that's the only way to hook into the generating pipeline - We need to create a custom interface to set up the basics of the decorator pattern
- We need a default implementation of the custom interface to be the "base case" of decoration that simply calls into the default functionality
Based on this, we I created an interface first:
public interface ISqlServerMigrationSqlGeneratorDecorator
{
void GenerateTable(CreateTableOperation createTableOperation);
}
Then created a base implementation (a reverse adapter of sorts):
public class DefaultSqlServerMigrationGenerator : SqlServerMigrationSqlGenerator, ISqlServerMigrationSqlGeneratorDecorator
{
public void GenerateTable(CreateTableOperation createTableOperation) =>
base.Generate(createTableOperation);
}
This class can be used instead of the framework version: it simply delegates calls to the framework version through base
, but it also implements my own custom interface. And since now I have full control over the generator interface now, I can create my own decorators for it:
public class MigrationDecorator : ISqlServerMigrationSqlGeneratorDecorator
{
private readonly ISqlServerMigrationSqlGeneratorDecorator inner;
public MigrationDecorator(ISqlServerMigrationSqlGeneratorDecorator inner)
{
this.inner = inner;
}
public void GenerateTable(CreateTableOperation createTableOperation)
{
// add some fany stuff here
inner.GenerateTable(createTableOperation);
}
}
So now, I can create a decorated generator like this:
var generator = new MigrationDecorator(new DefaultSqlServerMigrationGenerator());
But this is not yet compatible with the framework's generation mechanism. So I created an adapter as the last step:
public class MigrationDecoratorAdapter : SqlServerMigrationSqlGenerator
{
private readonly ISqlServerMigrationSqlGeneratorDecorator adaptee;
public MigrationDecoratorAdapter(ISqlServerMigrationSqlGeneratorDecorator adaptee)
{
this.adaptee = adaptee;
}
protected override void Generate(CreateTableOperation createTableOperation) =>
adaptee.GenerateTable(createTableOperation);
}
And now this can be set in the migration configuration like this:
SetSqlGenerator(new MigrationDecoratorAdapter( new MigrationDecorator ( new DefaultSqlServerMigrationGenerator())));
Whenever the generation is triggered, the adapter is called, that in turn calls into the first decorator, that triggers the decorating chain. Finally, the last element in the chain calls back to the original funtionality.