Entity Framework Core - Disable Model caching , call onModelCreating() for each instance dcontext

3k views Asked by At

Documentation Says : The model for that context is cached and is for all further instances of the context in the app domain. This caching can be disabled by setting the ModelCaching property on the given ModelBuidler

But i can't find way to do it. I have to disable caching because I am adding Model at runtime and loading all the models from assembly and creating database.

I found this link which says one way of achieving this is using DBModelBuilding - adding model mannually to context but it is for Entity Framework, Not helped for EF Core.

Entity Framework 6. Disable ModelCaching

I hope some one has solution for this.

Thank you

2

There are 2 answers

0
MGot90 On

You'll need to change the cache key to properly represent the model that you are building/make it distinct.

  1. Implement IDbModelCacheKeyProvider Interface on derived DbContext. Check this out https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.infrastructure.idbmodelcachekeyprovider?redirectedfrom=MSDN&view=entity-framework-6.2.0

  2. Build the model outside the DbContext and then provide it in the options.

0
Yennefer On

Once a model is successfully created, EF Core will cache it forever, unless you implement a cache manager that is able to tell whether a model is equivalent to another, and therefore it can be cached or not.

The entry point is to implement the cache manager:

internal sealed class MyModelCacheKeyFactory : IModelCacheKeyFactory
{
    public object Create([NotNull] DbContext context)
    {
        return GetKey(context);
    }
}

The GetKey method which you have to write must return an object that will be used as key. This method should inspect the provided context and return the same key when the models are the same, and something different when they are not. More on IModelCacheKeyFactory Interface.

I understand, this might not be clear (and it wasn't for me either), so I write a full example of what I have in production.

A Working Example

My target is to use the same context for different schemas. What we need to do is

  1. create a new context option
  2. implement the logic in the context
  3. create the cache key factory
  4. make the extension method to specify the schema
  5. call the extension method on the db context

1. Create a new context option

Here there is a boilerplate containing _schemaName only. The boilerplate is necessary as the extension option is immutable by design and we need to preserve the contract.

internal class MySchemaOptionsExtension : IDbContextOptionsExtension
{
    private DbContextOptionsExtensionInfo? _info;
    private string _schemaName = string.Empty;

    public MySchemaOptionsExtension()
    {
    }

    protected MySchemaOptionsExtension(MySchemaOptionsExtension copyFrom)
    {
        _schemaName = copyFrom._schemaName;
    }

    public virtual DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this);

    public virtual string SchemaName => _schemaName;

    public virtual void ApplyServices(IServiceCollection services)
    {
        // not used
    }

    public virtual void Validate(IDbContextOptions options)
    {
        // always ok
    }

    public virtual MySchemaOptionsExtension WithSchemaName(string schemaName)
    {
        var clone = Clone();

        clone._schemaName = schemaName;

        return clone;
    }

    protected virtual MySchemaOptionsExtension Clone() => new(this);

    private sealed class ExtensionInfo : DbContextOptionsExtensionInfo
    {
        private const long ExtensionHashCode = 741; // this value has chosen has nobody else is using it

        private string? _logFragment;

        public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension)
        {
        }

        private new MySchemaOptionsExtension Extension => (MySchemaOptionsExtension)base.Extension;

        public override bool IsDatabaseProvider => false;

        public override string LogFragment => _logFragment ??= $"using schema {Extension.SchemaName}";

        public override long GetServiceProviderHashCode() => ExtensionHashCode;

        public override void PopulateDebugInfo([NotNull] IDictionary<string, string> debugInfo)
        {
            debugInfo["MySchema:" + nameof(DbContextOptionsBuilderExtensions.UseMySchema)] = (ExtensionHashCode).ToString(CultureInfo.InvariantCulture);
        }
    }
}

2. The logic in the context

Here we force the schema to all the real entities. The schema is obtained by the option attached to the context

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   var options = this.GetService<IDbContextOptions>().FindExtension<MySchemaOptionsExtension>();
   if (options == null)
   {
       // nothing to apply, this is a supported scenario.
       return;
   }

   var schema = options.SchemaName;

   foreach (var item in modelBuilder.Model.GetEntityTypes())
   {
       if (item.ClrType != null)
           item.SetSchema(schema);
   }
}

3. Create the cache key factory

Here we need to the create the cache factory which will tel EF Core that it can cache all the models on the same context, i.e. all the contexts with the same schema will use the same model:

internal sealed class MyModelCacheKeyFactory : IModelCacheKeyFactory
{
    public object Create([NotNull] DbContext context)
    {
        const string defaultSchema = "dbo";
        var extension = context.GetService<IDbContextOptions>().FindExtension<MySchemaOptionsExtension>();

        string schema;
        if (extension == null)
            schema = defaultSchema;
        else
            schema = extension.SchemaName;

        if (string.IsNullOrWhiteSpace(schema))
            schema = defaultSchema;
        // ** this is the magic **
        return (context.GetType(), schema.ToUpperInvariant());
    }
}

The magic is here is in this line

return (context.GetType(), schema.ToUpperInvariant());

that we return a tuple with the type of our context and the schema. The hash of a tuple combines the hash of each entry, therefore the type and schema name are the logical discriminator here. When they match, the model is reused; when they do not, a new model is created and then cached.

4. Make the extension method

The extension method simply hides the addition of the option and the replacement of the cache service.

public static DbContextOptionsBuilder UseMySchema(this DbContextOptionsBuilder optionsBuilder, string schemaName)
{
    if (optionsBuilder == null)
        throw new ArgumentNullException(nameof(optionsBuilder));
    if (string.IsNullOrEmpty(schemaName))
        throw new ArgumentNullException(nameof(schemaName));

    var extension = optionsBuilder.Options.FindExtension<MySchemaOptionsExtension>() ?? new MySchemaOptionsExtension();

    extension = extension.WithSchemaName(schemaName);

    ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

    optionsBuilder.ReplaceService<IModelCacheKeyFactory, MyModelCacheKeyFactory>();

    return optionsBuilder;
}

In particular, the following line applies our cache manager:

optionsBuilder.ReplaceService<IModelCacheKeyFactory, MyModelCacheKeyFactory>();

5. Call the extension method

You can manually create the context as follows:

var options = new DbContextOptionsBuilder<DataContext>();

options.UseMySchema("schema1")
options.UseSqlServer("connection string omitted");

var context = new DataContext(options.Options)

Alternatively, you can use IDbContextFactory with dependency injection. More on IDbContextFactory Interface.