Dynamically changing schema in Entity Framework Core

后端 未结 10 2277
刺人心
刺人心 2020-11-28 09:02

UPD here is the way I solved the problem. Although it\'s likely to be not the best one, it worked for me.


I have an issue with working with EF

相关标签:
10条回答
  • 2020-11-28 09:08

    Did you already use EntityTypeConfiguration in EF6?

    I think the solution would be use mapping for entities on OnModelCreating method in DbContext class, something like this:

    using System;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
    using Microsoft.Extensions.Options;
    
    namespace AdventureWorksAPI.Models
    {
        public class AdventureWorksDbContext : Microsoft.EntityFrameworkCore.DbContext
        {
            public AdventureWorksDbContext(IOptions<AppSettings> appSettings)
            {
                ConnectionString = appSettings.Value.ConnectionString;
            }
    
            public String ConnectionString { get; }
    
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlServer(ConnectionString);
    
                // this block forces map method invoke for each instance
                var builder = new ModelBuilder(new CoreConventionSetBuilder().CreateConventionSet());
    
                OnModelCreating(builder);
    
                optionsBuilder.UseModel(builder.Model);
            }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.MapProduct();
    
                base.OnModelCreating(modelBuilder);
            }
        }
    }
    

    The code on OnConfiguring method forces the execution of MapProduct on each instance creation for DbContext class.

    Definition of MapProduct method:

    using System;
    using Microsoft.EntityFrameworkCore;
    
    namespace AdventureWorksAPI.Models
    {
        public static class ProductMap
        {
            public static ModelBuilder MapProduct(this ModelBuilder modelBuilder, String schema)
            {
                var entity = modelBuilder.Entity<Product>();
    
                entity.ToTable("Product", schema);
    
                entity.HasKey(p => new { p.ProductID });
    
                entity.Property(p => p.ProductID).UseSqlServerIdentityColumn();
    
                return modelBuilder;
            }
        }
    }
    

    As you can see above, there is a line to set schema and name for table, you can send schema name for one constructor in DbContext or something like that.

    Please don't use magic strings, you can create a class with all available schemas, for example:

    using System;
    
    public class Schemas
    {
        public const String HumanResources = "HumanResources";
        public const String Production = "Production";
        public const String Sales = "Sales";
    }
    

    For create your DbContext with specific schema you can write this:

    var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources);
    
    var productionDbContext = new AdventureWorksDbContext(Schemas.Production);
    

    Obviously you should to set schema name according schema's name parameter's value:

    entity.ToTable("Product", schemaName);
    
    0 讨论(0)
  • 2020-11-28 09:08

    There are a couple ways to do this:

    • Build the model externally and pass it in via DbContextOptionsBuilder.UseModel()
    • Replace the IModelCacheKeyFactory service with one that takes the schema into account
    0 讨论(0)
  • 2020-11-28 09:10

    Define your contex and pass the schema to the constructor.

    Set the default schema in OnModelCreating

       public class MyContext : DbContext , IDbContextSchema
        {
            private readonly string _connectionString;
            public string Schema {get;}
    
            public MyContext(string connectionString, string schema)
            {
                _connectionString = connectionString;
                Schema = schema;
            }
    
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                if (!optionsBuilder.IsConfigured)
                {
                    optionsBuilder.ReplaceService<IModelCacheKeyFactory, DbSchemaAwareModelCacheKeyFactory>();
                    optionsBuilder.UseSqlServer(_connectionString);
                }
    
                base.OnConfiguring(optionsBuilder);
            }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.HasDefaultSchema(Schema);
    
                // ... model definition ...
            }
        }
    

    Implement your IModelCacheKeyFactory.

    public class DbSchemaAwareModelCacheKeyFactory : IModelCacheKeyFactory
        {
    
            public object Create(DbContext context)
            {
                return new {
                    Type = context.GetType(),
                    Schema = context is IDbContextSchema schema 
                        ? schema.Schema 
                        :  null
                };
            }
        }
    

    Replace in OnConfiguring the default implementation of IModelCacheKeyFactory with your custom implementation.

    With the default implementation of IModelCacheKeyFactory the method OnModelCreating is executed only the first time the context is instantiated and then the result is cached. Changing the implementation you can modify how the result of OnModelCreating is cached and retrieve. Including the schema in the caching key you can get the OnModelCreating executed and cached for every different schema string passed to the context constructor.

    // Get a context referring SCHEMA1
    var context1 = new MyContext(connectionString, "SCHEMA1");
    // Get another context referring SCHEMA2
    var context2 = new MyContext(connectionString, "SCHEMA2");
    
    0 讨论(0)
  • 2020-11-28 09:18

    I find this blog might be useful for you. Perfect !:)

    https://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/

    This blog is based on ef4, I'm not sure whether it will work fine with ef core.

    public class ContactContext : DbContext
    {
        private ContactContext(DbConnection connection, DbCompiledModel model)
            : base(connection, model, contextOwnsConnection: false)
        { }
    
        public DbSet<Person> People { get; set; }
        public DbSet<ContactInfo> ContactInfo { get; set; }
    
        private static ConcurrentDictionary<Tuple<string, string>, DbCompiledModel> modelCache
            = new ConcurrentDictionary<Tuple<string, string>, DbCompiledModel>();
    
        /// <summary>
        /// Creates a context that will access the specified tenant
        /// </summary>
        public static ContactContext Create(string tenantSchema, DbConnection connection)
        {
            var compiledModel = modelCache.GetOrAdd(
                Tuple.Create(connection.ConnectionString, tenantSchema),
                t =>
                {
                    var builder = new DbModelBuilder();
                    builder.Conventions.Remove<IncludeMetadataConvention>();
                    builder.Entity<Person>().ToTable("Person", tenantSchema);
                    builder.Entity<ContactInfo>().ToTable("ContactInfo", tenantSchema);
    
                    var model = builder.Build(connection);
                    return model.Compile();
                });
    
            return new ContactContext(connection, compiledModel);
        }
    
        /// <summary>
        /// Creates the database and/or tables for a new tenant
        /// </summary>
        public static void ProvisionTenant(string tenantSchema, DbConnection connection)
        {
            using (var ctx = Create(tenantSchema, connection))
            {
                if (!ctx.Database.Exists())
                {
                    ctx.Database.Create();
                }
                else
                {
                    var createScript = ((IObjectContextAdapter)ctx).ObjectContext.CreateDatabaseScript();
                    ctx.Database.ExecuteSqlCommand(createScript);
                }
            }
        }
    }
    

    The main idea of these codes is to provide a static method to create different DbContext by different schema and cache them with certain identifiers.

    0 讨论(0)
  • 2020-11-28 09:21

    I actually found it to be a simpler solution with an EF interceptor.

    I actually keep the onModeling method:

      protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasDefaultSchema("dbo"); // this is important to always be dbo
    
            // ... model definition ...
        }
    

    And this code will be in Startup:

        public void ConfigureServices(IServiceCollection services)
        {
            // if I add a service I can have the lambda (factory method) to read from request the schema (I put it in a cookie)
            services.AddScoped<ISchemeInterceptor, SchemeInterceptor>(provider =>
            {
                var context = provider.GetService<IHttpContextAccessor>().HttpContext;
    
                var scheme = "dbo";
                if (context.Request.Cookies["schema"] != null)
                {
                    scheme = context.Request.Cookies["schema"];
                }
    
                return new SchemeInterceptor(scheme);
            });
    
            services.AddDbContext<MyContext>(options =>
            {
                var sp = services.BuildServiceProvider();
                var interceptor = sp.GetService<ISchemeInterceptor>();
                options.UseSqlServer(Configuration.GetConnectionString("Default"))
                    .AddInterceptors(interceptor);
            });
    

    And the interceptor code looks something like this (but basically we use ReplaceSchema):

    public interface ISchemeInterceptor : IDbCommandInterceptor
    {
    
    }
    
    public class SchemeInterceptor : DbCommandInterceptor, ISchemeInterceptor
    {
        private readonly string _schema;
    
        public SchemeInterceptor(string schema)
        {
            _schema = schema;
        }
    
        public override Task<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result,
            CancellationToken cancellationToken = new CancellationToken())
        {
            ReplaceSchema(command);
            return base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
        }
    
        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
        {
            ReplaceSchema(command);
            return base.ScalarExecuting(command, eventData, result);
        }
    
        public override Task<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<int> result,
            CancellationToken cancellationToken = new CancellationToken())
        {
            ReplaceSchema(command);
            return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);
        }
    
        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
        {
            ReplaceSchema(command);
            return base.NonQueryExecuting(command, eventData, result);
        }
    
        public override InterceptionResult<DbDataReader> ReaderExecuting(
            DbCommand command,
            CommandEventData eventData,
            InterceptionResult<DbDataReader> result)
        {
            ReplaceSchema(command);
            return result;
        }
    
        public override Task<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result,
            CancellationToken cancellationToken = new CancellationToken())
        {
            ReplaceSchema(command);
            return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
        }
    
        private void ReplaceSchema(DbCommand command)
        {
            command.CommandText = command.CommandText.Replace("[dbo]", $"[{_schema}]");
        }
    
        public override void CommandFailed(DbCommand command, CommandErrorEventData eventData)
        {
            // here you can handle cases like schema not found
            base.CommandFailed(command, eventData);
        }
    
        public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData,
            CancellationToken cancellationToken = new CancellationToken())
        {
            // here you can handle cases like schema not found
            return base.CommandFailedAsync(command, eventData, cancellationToken);
        }
    
    
    }
    
    0 讨论(0)
  • 2020-11-28 09:22

    Update for MVC Core 2.1

    You can create a model from a database with multiple schemas. The system is a bit schema-agnostic in its naming. Same named tables get a "1" appended. "dbo" is the assumed schema so you don't add anything by prefixing a table name with it the PM command

    You will have to rename model file names and class names yourself.

    In the PM console

    Scaffold-DbContext "Data Source=localhost;Initial Catalog=YourDatabase;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -force -Tables TableA, Schema1.TableA
    
    0 讨论(0)
提交回复
热议问题