Writing your own Entity Framework extensions can teach you a lot about the internals of building and applying migrations. One of the things you might consider is writing your own migration operations. Unfortunately, the official documentation regarding the topic is rather short, and you might find a couple of key details missing.
Thankfully, this is the Internet, so the answer is probably on StackOverflow. And guess what, it is:
User lauxjpn gives an excellent overview of all the steps you have to take in order to develop your own migration operation:
To do this properly, you would need to do the following:
- Add your own annotation to the tables in question (e.g.
MyPrefix:Trigger
)- Implement your own
MigrationOperation
(e.g.CreateTriggerMigrationOperation
)]- Provide your own
IMigrationsModelDiffer
implementation (derived fromMigrationsModelDiffer
; this is internal) that returns your ownMigrationOperation
- Provide your own
ICSharpMigrationOperationGenerator
implementation (derived fromCSharpMigrationOperationGenerator
), that then generates the C# code for your ownMigrationOperation
- Provide your own
IMigrationsSqlGenerator
implementation (derived fromSqlServerMigrationsSqlGenerator
) that then handles translating your ownMigrationOperation
to SQL
Having done all that in my EFCore.AuditExtensions project, I've linked an example for each of the steps.
If you wanted to keep the migration files nice and clean, you probably wrapped the MigrationOperation
creation in an extension method on the MigrationBuilder
class. If so, then you were likely greeted with the following error after generating a migration:
20221024065200_Initial.cs(22, 30)
: [CS1061]MigrationBuilder
does not contain a definition forCreateAuditTrigger
and no accessible extension methodCreateAuditTrigger
accepting a first argument of typeMigrationBuilder
could be found (are you missing a using directive or an assembly reference?)
Note: A similar error might appear even if you did not use your own extension method. Another example would be using an enum as a parameter in a method call.
The error makes sense. In your ICSharpMigrationOperationGenerator
implementation you simply built a string with C# code in it - how should Entity Framework know that it needs to reference another namespace? But don't worry, there are a few ways to fix that, and below you'll find two of them.
The very ugly way
This approach is ugly, but it works and does not require further interference into Entity Framework. Have a look at these fragments of MigrationsCodeGenerator
:
That's quite a bit of code, but the crux of it is that Entity Framework will add the namespaces of every type whose objects (instances) are present in your operation's annotations.
To make use of this, go to the place where you generate migration operations (most likely that's your implementation of IMigrationsModelDiffer
) and add an annotation to them, e.g.:
operation.AddAnnotation("Dependency_StatementType", target.OperationType);
operation.AddAnnotation("Dependency_List", new List<string>());
It gets a little tricky when you need to depend on an extension method (e.g. on the MigrationsBuilder
class). These are inside static classes, so there's no possibility to add an instance of the class as an annotation. There is a way out of this, though. You only need something from the same namespace as the extensions. This can be a dummy class, like:
namespace AuditExample.Extensions;
internal class _DummyDependencyClass { }
public static class MigrationBuilderExtensions
{
public static OperationBuilder<CreateTriggerOperation> CreateTrigger(
this MigrationBuilder migrationBuilder,
...)
{ ... }
}
Then, use an instance of the class as the annotation:
operation.AddAnnotation("Dependency_MigrationBuilderExtensions", new _DummyDependencyClass());
It can be a record
, struct
or even an enum
:
namespace AuditExample.Extensions;
internal enum NamespaceDependency
{
MigrationBuilderExtensions,
}
public static class MigrationBuilderExtensions
{
public static OperationBuilder<CreateTriggerOperation> CreateTrigger(
this MigrationBuilder migrationBuilder,
...)
{ ... }
}
Used in the same way:
operation.AddAnnotation("Dependency_MigrationBuilderExtensions", NamespaceDependency.MigrationBuilderExtensions);
That's it - problem solved. I do not like this approach because of the workarounds that need to be performed in order to make use of static classes.
The nice way
This approach includes modifying Entity Framework's IMigrationsCodeGenerator
, but the change is very subtle. Some people might argue that the previous approach is better because no changes are made to EF internals. Well, that may be true - but the annotation method is still based on internal and undocumented EF code that might change in the future without any notice. So in my opinion, the methods are similarly dangerous, but this one is much cleaner.
Let's start with a simple interface:
public interface IDependentMigrationOperation
{
Type[] DependsOn { get; }
}
It is pretty self-explanatory but just to be clear: the DependsOn
collection should contain types which need to be accessible in the migration file. The interface needs to be implemented in the custom migration operations:
public class CreateAuditTriggerOperation : MigrationOperation, IDependentMigrationOperation
{
public string TriggerName { get; }
public StatementType OperationType { get; }
public string AuditedEntityTableName { get; }
public string AuditedEntityTableKeyColumnName { get; }
public AuditColumnType AuditedEntityTableKeyColumnType { get; }
public string AuditTableName { get; }
public Type[] DependsOn { get; } = { typeof(MigrationBuilderExtensions), typeof(StatementType), typeof(AuditColumnType) };
}
The MigrationBuilderExtensions
is a static class that contains a CreateAuditTriger
extension method on MigrationBuilder
that is then present in the migration file. Access to StatementType
and AuditColumnType
are needed because they are later passed as parameters to the CreateAuditTrigger
method.
The last piece of the puzzle is the custom IMigrationsCodeGenerator
implementation. This should be derived from the class that Entity Framework normally uses, like Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGenerator
.
internal class CSharpMigrationsGenerator : Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGenerator
{
public CSharpMigrationsGenerator(MigrationsCodeGeneratorDependencies dependencies, CSharpMigrationsGeneratorDependencies csharpDependencies) : base(dependencies, csharpDependencies)
{ }
protected override IEnumerable<string> GetNamespaces(IEnumerable<MigrationOperation> operations) => base.GetNamespaces(operations).Concat(operations.OfType<IDependentMigrationOperation>().SelectMany(GetDependentNamespaces));
private static IEnumerable<string> GetDependentNamespaces(IDependentMigrationOperation migrationOperation) => migrationOperation.DependsOn.Select(type => type.Namespace!);
}
The modifications here are kept to the minimum in order to make sure that nothing breaks. There's not much to explain here - all the namespaces collected by the original class are returned with the addition of namespaces gathered from the IDependentMigrationOperation.DependsOn
types.
Because IMigrationsCodeGenerator
is a design-time service, it needs to be registered using the IDesignTimeServices
implementation:
public class DesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
=> services.AddSingleton<ICSharpMigrationOperationGenerator, CSharpMigrationOperationGenerator>();
}
And that's it 🎉. If you want to see a real example of implementing custom migration operations, I'll shamelessly plug my EFCore.AuditExtensions project again:
Also, if you're creating a custom Entity Framework extension, you might find the previous tips & tricks post useful:
Cover photo by Shannon Potter on Unsplash