Entity Framework Core Extension tips & tricks - Custom migration operation dependencies

Entity Framework Core Extension tips & tricks - Custom migration operation dependencies

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:

How to customize migration generation in EF Core Code First?
There is a special base table type in my DbContext. And when inherited from it I need to generate an additional "SQL" migration operation to create a specific trigger for it. It makes sure

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:

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 for CreateAuditTrigger and no accessible extension method CreateAuditTrigger accepting a first argument of type MigrationBuilder 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:

/// <summary>
///     Gets the namespaces required for a list of <see cref="MigrationOperation" /> objects.
/// </summary>
/// <param name="operations">The operations.</param>
/// <returns>The namespaces.</returns>
protected virtual IEnumerable<string> GetNamespaces(IEnumerable<MigrationOperation> operations)
    => operations.OfType<ColumnOperation>().SelectMany(GetColumnNamespaces)
        .Concat(operations.OfType<CreateTableOperation>().SelectMany(o => o.Columns).SelectMany(GetColumnNamespaces))

private static IEnumerable<IAnnotatable> GetAnnotatables(IEnumerable<MigrationOperation> operations)
    foreach (var operation in operations)
        yield return operation;


private IEnumerable<string> GetAnnotationNamespaces(IEnumerable<IAnnotatable> items)
    => items.SelectMany(
        i => Dependencies.AnnotationCodeGenerator.FilterIgnoredAnnotations(i.GetAnnotations())
            .Where(a => a.Value != null)
            .Select(a => new { Annotatable = i, Annotation = a })
            .SelectMany(a => GetProviderType(a.Annotatable, a.Annotation.Value!.GetType()).GetNamespaces()));
private ValueConverter? FindValueConverter(IProperty property)
    => (property.FindTypeMapping()
        ?? Dependencies.RelationalTypeMappingSource.FindMapping(property))?.Converter;

private Type GetProviderType(IAnnotatable annotatable, Type valueType)
    => annotatable is IProperty property
        && valueType.UnwrapNullableType() == property.ClrType.UnwrapNullableType()
            ? FindValueConverter(property)?.ProviderClrType ?? valueType
            : valueType;

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

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:

GitHub - mzwierzchlewski/EFCore.AuditExtensions: An Entity Framework Core 6 extension providing support for auditing entities with migrations support.
An Entity Framework Core 6 extension providing support for auditing entities with migrations support. - GitHub - mzwierzchlewski/EFCore.AuditExtensions: An Entity Framework Core 6 extension providi...

Also, if you're creating a custom Entity Framework extension, you might find the previous tips & tricks post useful:

Entity Framework Core Extension tips & tricks - Design Time Services
Entity Framework is a powerful tool with so many features it’s hard to know them all. Sometimes you might want it to do just a little bit more. Unfortunately, there is no real documentation about how to add functionality to Entity Framework.

Cover photo by Shannon Potter on Unsplash

Show Comments