在Entity Framework Core中动态更改架构
UPD就是我解决问题的方法。 虽然它可能不是最好的,但它对我有用。
我在使用EF Core时遇到问题。 我想通过模式机制在项目数据库中为不同公司分离数据。 我的问题是如何在运行时更改模式名称? 我已经找到了关于这个问题的类似问题 ,但它仍然是答案,我有一些不同的条件。 所以我有Resolve
方法,在必要时授予db-context
public static void Resolve(IServiceCollection services) { services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); services.AddTransient(); ... }
我可以在OnModelCreating
设置schema-name,但是,如前所述,这个方法只调用一次,所以我可以在这里设置模式名称globaly
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("public"); base.OnModelCreating(modelBuilder); }
或者通过类似的属性在模型中
[Table("order", Schema = "public")] public class Order{...}
但是如何在运行时更改模式名称? 我根据每个请求创建了ef的上下文,但首先我通过请求为数据库中的某个模式共享表格查找了用户的模式名称。 那么组织该机制的真正方法是什么:
- 根据用户的凭据计算出模式名称;
- 从特定架构的数据库中获取特定于用户的数据。
谢谢。
PS我使用PostgreSql,这是表名低的原因。
您是否已在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) { 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); } } }
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(); entity.ToTable("Product", schema); entity.HasKey(p => new { p.ProductID }); entity.Property(p => p.ProductID).UseSqlServerIdentityColumn(); return modelBuilder; } } }
如上所示,有一行为表设置模式和名称,您可以在DbContext中为一个构造函数发送模式名称或类似的东西。
请不要使用魔术字符串,您可以创建一个包含所有可用模式的类,例如:
using System; public class Schemas { public const String HumanResources = "HumanResources"; public const String Production = "Production"; public const String Sales = "Production"; }
要创建具有特定模式的DbContext,您可以这样写:
var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources); var productionDbContext = new AdventureWorksDbContext(Schemas.Production);
显然你应该根据schema的name参数值设置模式名称:
entity.ToTable("Product", schemaName);
有几种方法可以做到这一点:
- 在外部构建模型并通过
DbContextOptionsBuilder.UseModel()
传递它 - 将
IModelCacheKeyFactory
服务替换为考虑模式的服务
我发现这个博客可能对你有用。 完美!:)
https://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/
这个博客基于ef4,我不确定它是否能与ef核心一起运行。
public class ContactContext : DbContext { private ContactContext(DbConnection connection, DbCompiledModel model) : base(connection, model, contextOwnsConnection: false) { } public DbSet People { get; set; } public DbSet ContactInfo { get; set; } private static ConcurrentDictionary, DbCompiledModel> modelCache = new ConcurrentDictionary, DbCompiledModel>(); /// /// Creates a context that will access the specified tenant /// 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(); builder.Entity().ToTable("Person", tenantSchema); builder.Entity().ToTable("ContactInfo", tenantSchema); var model = builder.Build(connection); return model.Compile(); }); return new ContactContext(connection, compiledModel); } /// /// Creates the database and/or tables for a new tenant /// 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); } } } }
这些代码的主要思想是提供一种静态方法,通过不同的模式创建不同的DbContext,并使用某些标识符缓存它们。
您可以在固定架构表上使用Table属性。
您不能在架构更改表上使用属性,您需要通过ToTable fluent API进行配置。
如果禁用模型缓存(或编写自己的缓存),则模式可以在每个请求上更改,因此在创建上下文时(每次)都可以指定模式。
这是基本想法
class MyContext : DbContext { public string Schema { get; private set; } public MyContext(string schema) : base() { } // Your DbSets here DbSet Emps { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity () .ToTable("Emps", Schema); } }
现在,您可以在创建上下文之前使用一些不同的方法来确定模式名称。
例如,您可以在不同的上下文中使用“系统表”,以便在每次请求时使用系统表从用户名中检索模式名称,然后在正确的模式上创建工作上下文(您可以在上下文之间共享表)。
您可以将系统表与上下文分离,并使用ADO .Net访问它们。
可能还有其他几种解决方案。
你也可以看看这里
代码优先EF6的多租户
你可以google ef multi tenant
编辑
还存在模型缓存的问题(我忘记了)。 您必须禁用模型缓存或更改缓存的行为。
对不起每个人,我之前应该发布我的解决方案,但由于某种原因我没有,所以在这里。
但
请记住,解决方案可能有任何问题,因为它既没有被任何人审查过,也没有经过生产certificate,可能我会在这里得到一些反馈。
在项目中我使用了ASP .NET Core 1
关于我的数据库结构。 我有2个背景。 第一个包含有关用户的信息(包括他们应该解决的db方案),第二个包含用户特定的数据。
在Startup.cs
我添加了两个上下文
public void ConfigureServices(IServiceCollection services.AddEntityFrameworkNpgsql() .AddDbContext(options => options.UseNpgsql(Configuration["MasterConnection"])) .AddDbContext((serviceProvider, options) => options.UseNpgsql(Configuration["MasterConnection"]) .UseInternalServiceProvider(serviceProvider)); ... services.Replace(ServiceDescriptor.Singleton()); services.TryAddSingleton();
注意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); } }
其中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(); } }
此外,我们必须稍微更改上下文本身以使其具有架构感知function:
public class DomainDbContext : IdentityDbContext { public readonly string SchemaName; public DbSet Foos{ get; set; } public DomainDbContext(ICompanyProvider companyProvider, DbContextOptions options) : base(options) { SchemaName = companyProvider.GetSchemaName(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema(SchemaName); base.OnModelCreating(modelBuilder); } }
并且共享上下文严格绑定到shared
模式:
public class SharedDbContext : IdentityDbContext { private const string SharedSchemaName = "shared"; public DbSet Foos{ get; set; } public SharedDbContext(DbContextOptions options) : base(options) {} protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema(SharedSchemaName); base.OnModelCreating(modelBuilder); } }
ICompanyProvider
负责获取用户架构名称。 是的,我知道它与完美的代码有多远。
public interface ICompanyProvider { string GetSchemaName(); } public class CompanyProvider : ICompanyProvider { private readonly SharedDbContext _context; private readonly IHttpContextAccessor _accesor; private readonly UserManager _userManager; public CompanyProvider(SharedDbContext context, IHttpContextAccessor accesor, UserManager userManager) { _context = context; _accesor = accesor; _userManager = userManager; } public string GetSchemaName() { Task 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; } }
如果我没有错过任何东西,那就是它。 现在,在经过身份validation的用户的每个请求中,将使用适当的上下文。
我希望它有所帮助。
也许我对这个答案有点迟了
我的问题是处理不同的架构与相同的结构让我们说多租户。
当我尝试为不同的模式创建相同上下文的不同实例时,entity framework6开始起作用,第一次创建dbContext时捕获,然后对于以下实例,它们使用不同的模式名称创建但是onModelCreating从未被调用含义每个实例都指向先前捕获的相同的预生成视图,指向第一个模式。
然后我意识到为每个模式创建inheritance自myDBContext的新类将通过克服entity framework捕获问题来解决我的问题,为每个模式创建一个新的新上下文,但随后出现了我们将以硬编码模式结束的问题,导致另一个问题当我们需要添加另一个模式,必须添加更多类并重新编译和发布新版本的应用程序时,代码可伸缩性。
所以我决定在运行时进一步创建,编译和添加类到当前的解决方案。
这是代码
public static MyBaseContext CreateContext(string schema) { MyBaseContext instance = null; try { string code = $@" namespace MyNamespace {{ using System.Collections.Generic; using System.Data.Entity; public partial class {schema}Context : MyBaseContext {{ public {schema}Context(string SCHEMA) : base(SCHEMA) {{ }} protected override void OnModelCreating(DbModelBuilder modelBuilder) {{ base.OnModelCreating(modelBuilder); }} }} }} "; CompilerParameters dynamicParams = new CompilerParameters(); Assembly currentAssembly = Assembly.GetExecutingAssembly(); dynamicParams.ReferencedAssemblies.Add(currentAssembly.Location); // Reference the current assembly from within dynamic one // Dependent Assemblies of the above will also be needed dynamicParams.ReferencedAssemblies.AddRange( (from holdAssembly in currentAssembly.GetReferencedAssemblies() select Assembly.ReflectionOnlyLoad(holdAssembly.FullName).Location).ToArray()); // Everything below here is unchanged from the previous CodeDomProvider dynamicLoad = CodeDomProvider.CreateProvider("C#"); CompilerResults dynamicResults = dynamicLoad.CompileAssemblyFromSource(dynamicParams, code); if (!dynamicResults.Errors.HasErrors) { Type myDynamicType = dynamicResults.CompiledAssembly.GetType($"MyNamespace.{schema}Context"); Object[] args = { schema }; instance = (MyBaseContext)Activator.CreateInstance(myDynamicType, args); } else { Console.WriteLine("Failed to load dynamic assembly" + dynamicResults.Errors[0].ErrorText); } } catch (Exception ex) { string message = ex.Message; } return instance; }
我希望这可以帮助别人节省一些时间。
MVC Core 2.1的更新
您可以从具有多个模式的数据库创建模型。 该系统在命名方面与模式无关。 相同的命名表会附加“1”。 “dbo”是假设的模式,因此您不会通过在表名前加上PM命令来添加任何内容
您必须自己重命名模型文件名和类名。
在PM控制台中
Scaffold-DbContext "Data Source=localhost;Initial Catalog=YourDatabase;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -force -Tables TableA, Schema1.TableA