在Entity Framework Core中动态更改架构

use*_*018 16 c# entity-framework database-schema entity-framework-core asp.net-core

UPD 这里是我解决问题的方式.虽然它可能不是最好的,但它对我有用.


我在使用EF Core时遇到问题.我想通过模式机制将项目数据库中不同公司的数据分开.我的问题是如何在运行时更改模式名称?我已经找到了关于这个问题的类似问题,但它仍然是答案,我有一些不同的条件.所以我有Resolve方法在必要时授予db-context

public static void Resolve(IServiceCollection services) {
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<DomainDbContext>()
        .AddDefaultTokenProviders();
    services.AddTransient<IOrderProvider, OrderProvider>();
    ...
}
Run Code Online (Sandbox Code Playgroud)

我可以设置schema-name OnModelCreating,但是,如前所述,这个方法只调用一次,所以我可以在这里设置模式名称globaly

protected override void OnModelCreating(ModelBuilder modelBuilder) {
    modelBuilder.HasDefaultSchema("public");
    base.OnModelCreating(modelBuilder);
}
Run Code Online (Sandbox Code Playgroud)

或者通过类似的属性在模型中

[Table("order", Schema = "public")]
public class Order{...}
Run Code Online (Sandbox Code Playgroud)

但是如何在运行时更改模式名称?我根据每个请求创建了ef的上下文,但首先我通过请求为数据库中的某个模式共享表格查找了用户的模式名称.那么组织该机制的真正方法是什么:

  1. 根据用户的凭据计算出模式名称;
  2. 从特定架构的数据库中获取特定于用户的数据.

谢谢.

PS我使用PostgreSql,这是表名低的原因.

Ghi*_*nio 14

定义您的上下文并将架构传递给构造函数。

在 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 ...
        }
    }
Run Code Online (Sandbox Code Playgroud)

实现您的 IModelCacheKeyFactory。

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

在 OnConfiguring 中,将 IModelCacheKeyFactory 的默认实现替换为您的自定义实现。

使用 IModelCacheKeyFactory 的默认实现,仅在第一次实例化上下文时执行 OnModelCreating 方法,然后缓存结果。更改实现,您可以修改 OnModelCreating 结果的缓存和检索方式。将架构包含在缓存键中,您可以为传递给上下文构造函数的每个不同架构字符串执行并缓存 OnModelCreating。

// Get a context referring SCHEMA1
var context1 = new MyContext(connectionString, "SCHEMA1");
// Get another context referring SCHEMA2
var context2 = new MyContext(connectionString, "SCHEMA2");
Run Code Online (Sandbox Code Playgroud)


H. *_*rzl 12

您是否已在EF6中使用EntityTypeConfiguration?

我认为解决方案是在DbContext类中使用OnModelCreating方法上的实体映射,如下所示:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

OnConfiguring方法上的代码强制在DbContext类的每个实例创建上执行MapProduct.

MapProduct方法的定义:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您在上面所看到的,有一行为表设置模式和名称,您可以在DbContext中为一个构造函数发送模式名称或类似的东西.

请不要使用魔术字符串,您可以创建一个包含所有可用模式的类,例如:

using System;

public class Schemas
{
    public const String HumanResources = "HumanResources";
    public const String Production = "Production";
    public const String Sales = "Production";
}
Run Code Online (Sandbox Code Playgroud)

要创建具有特定模式的DbContext,您可以这样写:

var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources);

var productionDbContext = new AdventureWorksDbContext(Schemas.Production);
Run Code Online (Sandbox Code Playgroud)

显然你应该根据schema的name参数值设置模式名称:

entity.ToTable("Product", schemaName);
Run Code Online (Sandbox Code Playgroud)

  • 正如我上面写的,在`OnModelCreating` 中设置模式名称没有问题,问题是这个方法只调用了一次,所以下一个创建的上下文将具有相同的模式。但也许我在你的回复中遗漏了一些重要的东西? (3认同)

use*_*018 6

抱歉,每个人都应该在以前发布我的解决方案,但是由于某种原因我没有发布,所以就在这里。

请记住,该解决方案可能有任何问题,因为它既未经过任何人的审查也未经过生产验证,可能我会在这里得到一些反馈。

在项目中,我使用了ASP .NET Core 1


关于我的数据库结构。我有2个上下文。第一个包含有关用户的信息(包括他们应解决的db方案),第二个包含用户特定的数据。

Startup.cs我添加两个上下文

public void ConfigureServices(IServiceCollection 
    services.AddEntityFrameworkNpgsql()
        .AddDbContext<SharedDbContext>(options =>
            options.UseNpgsql(Configuration["MasterConnection"]))
        .AddDbContext<DomainDbContext>((serviceProvider, options) => 
            options.UseNpgsql(Configuration["MasterConnection"])
                .UseInternalServiceProvider(serviceProvider));
...
    services.Replace(ServiceDescriptor.Singleton<IModelCacheKeyFactory, MultiTenantModelCacheKeyFactory>());
    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Run Code Online (Sandbox Code Playgroud)

注意UseInternalServiceProvider部分,由Nero Sule建议,并带有以下解释

在EFC 1发布周期即将结束时,EF团队决定从默认服务集合(AddEntityFramework()。AddDbContext())中删除EF的服务,这意味着这些服务是使用EF自己的服务提供商而不是应用程序服务来解决的提供者。

要强制EF改为使用应用程序的服务提供者,您需要首先将EF的服务与数据提供者一起添加到服务集合中,然后将DBContext配置为使用内部服务提供者

现在我们需要 MultiTenantModelCacheKeyFactory

public class MultiTenantModelCacheKeyFactory : ModelCacheKeyFactory {
    private string _schemaName;
    public override object Create(DbContext context) {
        var dataContext = context as DomainDbContext;
        if(dataContext != null) {
            _schemaName = dataContext.SchemaName;
        }
        return new MultiTenantModelCacheKey(_schemaName, context);
    }
}
Run Code Online (Sandbox Code Playgroud)

DomainDbContext用户特定数据的上下文在哪里

public class MultiTenantModelCacheKey : ModelCacheKey {
    private readonly string _schemaName;
    public MultiTenantModelCacheKey(string schemaName, DbContext context) : base(context) {
        _schemaName = schemaName;
    }
    public override int GetHashCode() {
        return _schemaName.GetHashCode();
    }
}
Run Code Online (Sandbox Code Playgroud)

此外,我们还必须稍微更改上下文本身以使其具有架构意识:

public class DomainDbContext : IdentityDbContext<ApplicationUser> {
    public readonly string SchemaName;
    public DbSet<Foo> Foos{ get; set; }

    public DomainDbContext(ICompanyProvider companyProvider, DbContextOptions<DomainDbContext> options)
        : base(options) {
        SchemaName = companyProvider.GetSchemaName();
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.HasDefaultSchema(SchemaName);
        base.OnModelCreating(modelBuilder);
    }
}
Run Code Online (Sandbox Code Playgroud)

共享上下文严格绑定到shared架构:

public class SharedDbContext : IdentityDbContext<ApplicationUser> {
    private const string SharedSchemaName = "shared";
    public DbSet<Foo> Foos{ get; set; }
    public SharedDbContext(DbContextOptions<SharedDbContext> options)
        : base(options) {}
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.HasDefaultSchema(SharedSchemaName);
        base.OnModelCreating(modelBuilder);
    }
}
Run Code Online (Sandbox Code Playgroud)

ICompanyProvider负责获取用户架构名称。是的,我知道它离完美的代码还有多远。

public interface ICompanyProvider {
    string GetSchemaName();
}

public class CompanyProvider : ICompanyProvider {
    private readonly SharedDbContext _context;
    private readonly IHttpContextAccessor _accesor;
    private readonly UserManager<ApplicationUser> _userManager;

    public CompanyProvider(SharedDbContext context, IHttpContextAccessor accesor, UserManager<ApplicationUser> userManager) {
        _context = context;
        _accesor = accesor;
        _userManager = userManager;
    }
    public string GetSchemaName() {
        Task<ApplicationUser> getUserTask = null;
        Task.Run(() => {
            getUserTask = _userManager.GetUserAsync(_accesor.HttpContext?.User);
        }).Wait();
        var user = getUserTask.Result;
        if(user == null) {
            return "shared";
        }
        return _context.Companies.Single(c => c.Id == user.CompanyId).SchemaName;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我什么都没错过,就是这样。现在,在经过身份验证的用户的每个请求中,都将使用适当的上下文。

希望对您有所帮助。

  • 您可以在OnModelCreating ...期间设置DefaultSchema,但是该方法仅被调用一次。那么,如何根据请求即时更改架构?我看不到此解决方案的工作方式。 (2认同)

bri*_*lam 5

有几种方法可以做到这一点:

  • 外部构建模型并通过via传入DbContextOptionsBuilder.UseModel()
  • 将服务替换IModelCacheKeyFactory为考虑架构的服务

  • 您能否提供一些详细信息或一些文档/博客/教程的链接? (4认同)

C R*_*lph 5

花了几个小时才用 EFCore 解决了这个问题。对于实现这一点的正确方法似乎有很多困惑。我相信在 EFCore 中处理自定义模型的简单而正确的方法是替换默认的 IModelCacheKeyFactory 服务,如下所示。在我的示例中,我设置自定义表名称。

  1. 在上下文类中创建一个 ModelCacheKey 变量。
  2. 在上下文构造函数中,设置 ModelCacheKey 变量
  3. 创建一个继承自IModelCacheKeyFactory的类并使用ModelCacheKey(MyModelCacheKeyFactory)
  4. 在OnConfiguring方法(MyContext)中,替换默认的IModelCacheKeyFactory
  5. 在 OnModelCreating 方法 (MyContext) 中,使用 ModelBuilder 来定义您需要的任何内容。
public class MyModelCacheKeyFactory : IModelCacheKeyFactory
{
    public object Create(DbContext context)
        => context is MyContext myContext ?
        (context.GetType(), myContext.ModelCacheKey) :
        (object)context.GetType();
}

public partial class MyContext : DbContext
{
     public string Company { get; }
     public string ModelCacheKey { get; }
     public MyContext(string connectionString, string company) : base(connectionString) 
     { 
         Company = company;
         ModelCacheKey = company; //the identifier for the model this instance will use
     }

     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {
         //This will create one model cache per key
         optionsBuilder.ReplaceService<IModelCacheKeyFactory, MyModelCacheKeyFactory();
     }

     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         modelBuilder.Entity<Order>(entity => 
         { 
             //regular entity mapping 
         });

         SetCustomConfigurations(modelBuilder);
     }

     public void SetCustomConfigurations(ModelBuilder modelBuilder)
     {
         //Here you will set the schema. 
         //In my example I am setting custom table name Order_CompanyX

         var entityType = typeof(Order);
         var tableName = entityType.Name + "_" + this.Company;
         var mutableEntityType = modelBuilder.Model.GetOrAddEntityType(entityType);
         mutableEntityType.RemoveAnnotation("Relational:TableName");
         mutableEntityType.AddAnnotation("Relational:TableName", tableName);
     }
}
Run Code Online (Sandbox Code Playgroud)

结果是上下文的每个实例都会导致 efcore 根据 ModelCacheKey 变量进行缓存。