3

I'm writing a library that's going to provide new APIs around the DbContext class in Entity Framework Core 5+. I already have a version of these new APIs working, but it requires manual intervention in the final DbContext implementation, e.g.:

// Code in the library.
public static class AwesomeExtensions
{
    public static ModelBuilder AddAwesomeExtensionsSupportingEntities(this ModelBuilder modelBuilder)
    {
        // Set up custom entities I need to make this work.
        return modelBuilder;
    }
}

// Code somewhere else.
public class MyBusinessDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // I would like to avoid doing this here.
        modelBuilder.AddAwesomeExtensionsSupportingEntities();

        // Business as usual (database entities).
    }
}

After an extended search I haven't found an extension point in the EF Core API that allows me to do this in a non-intrusive way.

This is what I have found so far:

  • CustomDbContext class: I could inherit from DbContext and override the OnModelCreating method, but this is not superior to what I'm doing right now.
  • DbContextOptionsBuilder.UseModel: I thought this may be something I could use but adds too much complexity. By using this API the OnModelCreating method won't be called by the framework.
  • IEntityTypeConfiguration<TEntity>: I was rooting for this one but it also requires you to have access to the ModelBuilder instance, then you can use the ModelBuilder.ApplyConfigurationsFromAssembly method.

Ideally, I would like to do this via the DbContextOptionsBuilder object provided when registering the DbContext dependency, e.g.:

// Code in some application.
public void AddServices(IServiceCollection services)
{
    services.AddDbContext<MyBusinessDbContext>(options =>
    {
        // The ideal solution.
        options.UseAwesomeExtensions();
    });
}

If I could only intercept the instance of the ModelBuilder just before it is provided to the OnModelCreating method, in a way that does not requires the modification of the DbContext implementation, that would help me.

Any ideas are welcome.

Thank you.

yv989c
  • 1,453
  • 1
  • 14
  • 20
  • [This video](https://youtu.be/pYhe-Mt0HzI) by the EF Core team provides good insights relevant to this question (around 01:03:00). – yv989c Jun 04 '22 at 17:05

1 Answers1

5

EF Core service responsible for calling OnModelCreating is called IModelCustomizer with single method

public void Customize(ModelBuilder modelBuilder, DbContext context);

Intercepting that method allows you to do what you need. The only problem is that EF Core does not provide an easy way to override existing implementation. The only available method is ReplaceService which is all or nothing, with the obvious drawback that if you want to just perform pre/post processing of the base implementation, you need to know which is the class you are replacing. And of course some other extension can replace your implementation as well (last wins).

Implementing that correctly requires a bunch of boilerplate for registering custom IDbContextOptionsExtension to be able to manipulate directly services collection inside ApplyServices method. If you are interested, you can find examples in other EF Core extension libraries (for instance, LinqKit).

Assuming no other extension is overriding the service in question, and knowing that the default EF Core implementation currently is provided by the ModelCustomizer class in general or RelationalModelCustomizer class for relational database (but currently doesn't add anything to the base implementation which simply calls OnModelCreating), the simplified implementation is a matter of inheriting one of these and replacing the service with your implementation. e.g. something like

namespace Microsoft.EntityFrameworkCore
{
    using Infrastructure;

    public static class AwesomeDbContextOptionsExtensions
    {
        public static DbContextOptionsBuilder UseAwesomeExtensions(
            this DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder.ReplaceService<IModelCustomizer, AwesomeModelCustomizer>();
    }
}

namespace Microsoft.EntityFrameworkCore.Infrastructure
{
    public class AwesomeModelCustomizer : RelationalModelCustomizer
    {
        public AwesomeModelCustomizer(ModelCustomizerDependencies dependencies)
            : base(dependencies) { }
        public override void Customize(ModelBuilder modelBuilder, DbContext context)
        {
            // Do something before context.OnModelCreating(modelBuilder)...
            base.Customize(modelBuilder, context);
            // Do something after context.OnModelCreating(modelBuilder)...
        }
    }
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • This is very useful. I'll give it a try and post back my results. I'll also take a look at LinqKit. Thank you. – yv989c Oct 17 '21 at 15:28
  • Thanks a lot. I decided to go with the correct approach that involves the `IDbContextOptionsExtension` interface. I can use the code in [LinqKit](https://github.com/scottksmith95/LINQKit/blob/master/src/LinqKit.Microsoft.EntityFrameworkCore/ExpandableDbContextOptionsExtension.cs) as a starting point for mine. – yv989c Oct 17 '21 at 23:06
  • btw @Ivan! How did you know about `IModelCustomizer`? is there a place in the EF docs for extension authors? – yv989c Oct 17 '21 at 23:10
  • 1
    Most of the things I know about EFC are from experimenting / traversing the source code. Regarding docs, if such place exists I would like to see it as well :) – Ivan Stoev Oct 18 '21 at 03:25
  • 1
    Hi @Ivan. Just to let you know that your help with this question allowed me to create a [solution for this problem](https://stackoverflow.com/a/70587979/2206145). Thanks again! – yv989c Jan 19 '22 at 05:50
  • 1
    @yv989c Great! So you are doing something similar to [EntityFrameworkCore.MemoryJoin](https://github.com/neisbut/EntityFramework.MemoryJoin), but for `Contains` and using different approach for passing the values. – Ivan Stoev Jan 19 '22 at 09:32
  • Yes regarding solving the same kind of problem (composing local data with db data) but the strategy used in QueryableValues does not causes SQL Server plan cache pollution which MemoryJoin badly does due to its lack of query parameterization. It's also fair to say that QueryableValues is a solution specialized for SQL Server and MemoryJoin aims to support different db technologies AFAIK. – yv989c Jan 21 '22 at 04:42
  • Hi @Ivan, I'm wondering if [this](https://youtu.be/pYhe-Mt0HzI?t=3910) is the proper way of doing the service replacement. I'll explore this when I get a chance. – yv989c Jun 04 '22 at 17:18
  • Hi @yv989c Negative, they just explain their implementation of building their internal service provider, and replaced services there are the ones you specify via `ReplaceService`. But as I explained in the answer, this way you can only completely replace existing service, you can't get the original and use delegation. Also as I wrote, you can see sample implementation in LinqKit written by [Svyatoslav Danyliv](https://stackoverflow.com/users/10646316/svyatoslav-danyliv) - see ... – Ivan Stoev Jun 04 '22 at 18:11
  • ... [here](https://github.com/scottksmith95/LINQKit/blob/master/src/LinqKit.Microsoft.EntityFrameworkCore/ExpandableDbContextOptionsBuilderExtensions.cs) and [here](https://github.com/scottksmith95/LINQKit/blob/master/src/LinqKit.Microsoft.EntityFrameworkCore/ExpandableDbContextOptionsExtension.cs) – Ivan Stoev Jun 04 '22 at 18:12
  • Hi @Ivan, you're right. My bad, I was watching the video and posted this comment without reviewing your answer. Thanks for the reply. – yv989c Jun 05 '22 at 02:59