Entity Framework Core Extension tips & tricks - Design Time Services

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. I am constantly getting surprised by just how smart it can be. 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. What's there is usually either outdated or not that helpful. I want to emphasize here that I am not putting any blame on the EF team - they have been making huge strides with each release of EF Core. And the regular docs are being updated. With all that, it's not surprising that there is no time to document intricate details that very few people will see.

Useful resources

That does not mean there is no information about how to approach this. This is the Internet, after all. I wanted to highlight some places that helped me while developing the auditing solution described in a previous post.

The resource that has the most information is, naturally, the EF Core repository:

GitHub - dotnet/efcore: EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations. - GitHub - dotnet/efcore: EF Core is a modern object-database mapper ...

There's a lot of information that can be found in the reported issues. What's more, the code itself is usually pretty easy to follow, even if you haven't seen it before.

Another great place is this StackOverflow answer:

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

I found it after looking pouring through the EF Core sources and stitching together a mental image of how it's all connected. Of course, all this time it was already figured out and explained on SO.

Last but not least, if you're planning on customising Entity Framework, make sure to check out the Laraue.EfCoreTriggers project:

GitHub - win7user10/Laraue.EfCoreTriggers: Library for writing triggers in C# using package EFCore
Library for writing triggers in C# using package EFCore - GitHub - win7user10/Laraue.EfCoreTriggers: Library for writing triggers in C# using package EFCore

It's very impressive and helped me understand the internals of EF Core better than I had before. Even though my project took a slightly different route, it's still an amazing project.

Even though there's a lot of information at the links above, I believe I still have some tricks worth sharing.

Design Time Services

There are some services that EF uses only at design time (think migration scaffolding etc.). There's a complete list of them at List of services. A good example of those services are implementations of IMigrationsCodeGenerator and ICSharpMigrationOperationGenerator, which are responsible for generating the code that's placed in the migration .cs file.

Replacing the services with your own implementations is comically easy. The only thing that needs to be done is to implement the IDesignTimeServices interface:

namespace Microsoft.EntityFrameworkCore.Design
{
    /// <summary>
    ///     Enables configuring design-time services. Tools will automatically discover implementations of this
    ///     interface that are in the startup assembly.
    /// </summary>
    /// <remarks>
    ///     See <see href="https://aka.ms/efcore-docs-providers">Implementation of database providers and extensions</see>
    ///     for more information.
    /// </remarks>
    public interface IDesignTimeServices
    {
        /// <summary>
        ///     Configures design-time services. Use this method to override the default design-time services with your
        ///     own implementations.
        /// </summary>
        /// <param name="serviceCollection">The design-time service collection.</param>
        void ConfigureDesignTimeServices(IServiceCollection serviceCollection);
    }
}
source

Entity Framework will use magic reflection to find the implementation and execute the ConfigureDesignTimeServices method. Cool.

BUT, if you look at the code doing just that in the DesignTimeServicesBuilder class:

private void ConfigureUserServices(IServiceCollection services)
{
    var designTimeServicesType = _startupAssembly.GetLoadableDefinedTypes()
        .Where(t => typeof(IDesignTimeServices).IsAssignableFrom(t)).Select(t => t.AsType())
        .FirstOrDefault();
    if (designTimeServicesType == null)
    {
        return;
    }

    ConfigureDesignTimeServices(designTimeServicesType, services);
}
source

You will notice that only the startup assembly is scanned. That's very unfortunate if you plan of releasing your extensions as a library or even if you just wanted to isolate all that EF-specific code in a separate project.

However, if you spend some more time looking at the DesignTimeServicesBuilder you will find this bit of code:

private void ConfigureReferencedServices(IServiceCollection services, string provider)
{
    var references = _startupAssembly.GetCustomAttributes<DesignTimeServicesReferenceAttribute>()
        .Concat(_assembly.GetCustomAttributes<DesignTimeServicesReferenceAttribute>())
        .Distinct()
        .ToList();

    if (references.Count == 0)
    {
        return;
    }

    foreach (var reference in references)
    {
        if (reference.ForProvider != null
            && !string.Equals(reference.ForProvider, provider, StringComparison.OrdinalIgnoreCase))
        {
            continue;
        }

        var designTimeServicesType = Type.GetType(reference.TypeName, throwOnError: true)!;

        ConfigureDesignTimeServices(designTimeServicesType, services);
    }
}
source

This means that the startup assembly just needs to include the DesignTimeServicesReferenceAttribute assembly attribute pointing to the IDesignTimeServices implementation. That's so much better. Here's an example of how the attribute should be used:

[assembly: DesignTimeServicesReference("EFCore.AuditExtensions.Common.EfCore.DesignTimeServices, EFCore.AuditExtensions.Common")]

Note: If your startup project is separated from the project containing your DbContext and you use the -s|--startup-project and -p|--project flags when using the dotnet-ef tool, the DesignTimeServicesReference attribute can be located in the project specified with the -p|--project flag (because of line #4 in the above snippet).

If you want to read the background behind the attribute and another example, visit the links below:

Auto-bootstrap design time provider services through Attribute · Issue #10154 · dotnet/efcore
As described in #5617, EF Core tooling currently scans the startup assembly for a class that implements IDesignTimeServices, then invokes ConfigureDesignTimeServices. This allows for registering im...
GitHub - bricelam/EFCore.Pluralizer: Adds design-time pluralization to EF Core
Adds design-time pluralization to EF Core. Contribute to bricelam/EFCore.Pluralizer development by creating an account on GitHub.

Cover photo by Daniel McCullough on Unsplash

Show Comments