在实体框架核心中动态更改模式
本文关键字:模式 动态 实体 框架 核心 | 更新日期: 2023-09-27 18:06:58
UPD这是我解决问题的方法。虽然它可能不是最好的,但它对我很有效。
我有一个问题与EF核心工作。我想通过模式机制在我的项目数据库中分离不同公司的数据。我的问题是如何在运行时更改模式名称?关于这个问题,我发现了类似的问题,但仍然没有答案,我有一些不同的条件。所以我有Resolve
方法在必要时授予db-context
public static void Resolve(IServiceCollection services) {
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<DomainDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IOrderProvider, OrderProvider>();
...
}
我可以在OnModelCreating
中设置模式名称,但是,正如前面所发现的,这个方法只被调用一次,所以我可以像这样全局地设置模式名称
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.HasDefaultSchema("public");
base.OnModelCreating(modelBuilder);
}
或通过属性
在模型中右[Table("order", Schema = "public")]
public class Order{...}
但是我如何在运行时更改模式名称?我为每个请求创建上下文,但首先通过对数据库中的模式共享表的请求确定用户的模式名称。那么,如何正确地组织这种机制呢?
- 根据用户凭据计算模式名称; 从数据库中获取用户特定的数据。
谢谢。
注:我使用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> 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);
}
}
}
onconfiguration方法的代码强制在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;
}
}
}
正如您在上面看到的,有一行用来设置表的模式和名称,您可以在DbContext中为一个构造函数发送模式名称或类似的东西。
请不要使用魔法字符串,你可以用所有可用的模式创建一个类,例如:
using System;
public class Schemas
{
public const String HumanResources = "HumanResources";
public const String Production = "Production";
public const String Sales = "Sales";
}
使用特定的模式创建DbContext,你可以这样写:
var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources);
var productionDbContext = new AdventureWorksDbContext(Schemas.Production);
显然,你应该根据模式的名称参数值来设置模式名称:
entity.ToTable("Product", schemaName);
定义上下文并将模式传递给构造函数。
在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 ...
}
}
实现IModelCacheKeyFactory.
public class DbSchemaAwareModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
{
return new {
Type = context.GetType(),
Schema = context is IDbContextSchema schema
? schema.Schema
: null
};
}
}
在onconfiguration中将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");
对不起大家,我应该在之前发布我的解决方案,但由于某种原因我没有,所以在这里。
请记住,解决方案可能有任何问题,因为它既没有经过任何人的审查,也没有经过生产验证,可能我会在这里得到一些反馈。
在项目中我使用的是asp.net Core 1
关于数据库结构。我有两种情况。第一个包含关于用户的信息(包括他们应该处理的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>();
注意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();
}
}
我们还必须稍微改变上下文本身,使其能够识别模式:
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);
}
}
和共享上下文严格绑定到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);
}
}
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;
}
}
如果我没有错过任何东西,那就是它。现在,在经过身份验证的用户的每个请求中,都将使用适当的上下文。
我希望它有帮助。
有两种方法:
- 外部构建模型并通过
DbContextOptionsBuilder.UseModel()
传递 - 将
IModelCacheKeyFactory
服务替换为考虑模式的服务
花了几个小时用EFCore解决这个问题。似乎有很多混乱的正确方式来实现这一点。我相信在EFCore中处理自定义模型的简单而正确的方法是替换默认的IModelCacheKeyFactory服务,如下所示。在我的例子中,我正在设置自定义表名。
- 在你的上下文类中创建一个ModelCacheKey变量。在你的上下文构造函数中,设置ModelCacheKey变量
- 创建一个从IModelCacheKeyFactory继承的类,并使用ModelCacheKey (MyModelCacheKeyFactory) 在onconfigurationmethod (MyContext)中,替换默认的IModelCacheKeyFactory
- 在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);
}
}
结果是上下文的每个实例将导致efcore基于ModelCacheKey变量进行缓存。
我觉得这个博客可能对你有用。完美!:)
https://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/这个博客是基于ef4的,我不确定它是否能很好地与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);
}
}
}
}
这些代码的主要思想是提供一个静态方法,通过不同的模式创建不同的DbContext,并用特定的标识符缓存它们。
可以在固定模式表上使用Table属性。
你不能在模式改变表上使用属性,你需要通过ToTable流畅的API来配置。
如果您禁用模型缓存(或者您编写自己的缓存),模式可以在每次请求时更改,因此在上下文创建(每次)时您可以指定模式。
基本思路
class MyContext : DbContext
{
public string Schema { get; private set; }
public MyContext(string schema) : base()
{
}
// Your DbSets here
DbSet<Emp> Emps { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Emp>()
.ToTable("Emps", Schema);
}
}
现在,您可以使用几种不同的方法在创建上下文之前确定模式名称。
例如,你可以把"系统表"放在不同的上下文中,这样你就可以在每个请求中使用系统表从用户名中检索模式名,然后在正确的模式上创建工作上下文(你可以在上下文之间共享表)。
您可以将系统表从上下文中分离出来,并使用ado.net来访问它们。
也许还有其他的解决办法。
你也可以在这里看看
多租户代码优先EF6
ef multi tenant
编辑
还有模型缓存的问题(我忘记了)。您必须禁用模型缓存或更改缓存的行为。
也许我回答这个问题有点晚了
我的问题是用相同的结构处理不同的模式,比如多租户。
当我试图为不同的模式创建相同上下文的不同实例时,实体框架6开始发挥作用,捕捉dbContext第一次被创建,然后对于以下实例,它们被创建为不同的模式名称,但onModelCreating从未被调用,这意味着每个实例都指向相同的先前捕获的预生成视图,指向第一个模式。
然后我意识到,为每个模式创建从myDBContext继承的新类将通过克服实体框架捕获问题来解决我的问题,为每个模式创建一个新的新上下文,但随之而来的问题是,我们将以硬编码模式结束,当我们需要添加另一个模式时,在代码可伸缩性方面造成另一个问题,不得不添加更多的类并重新编译和发布新版本的应用程序。
所以我决定在运行时进一步创建、编译和添加类到当前解决方案。
代码
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
我实际上发现使用EF拦截器是一个更简单的解决方案。
我实际上保留了onModeling方法: protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("dbo"); // this is important to always be dbo
// ... model definition ...
}
这段代码在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);
});
拦截器代码看起来像这样(但基本上我们使用的是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);
}
}
如果数据库之间的唯一区别是模式名称,那么解决这个问题的最简单方法是删除在OnModelCreating方法中设置默认模式的代码行:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.HasDefaultSchema("YourSchemaName"); <-- remove or comment this line
...
}
在这种情况下,EF Core运行的sql查询在FROM子句中不包含模式名。然后,您将能够编写一个方法,该方法将根据您的自定义条件设置正确的DbContext。这里有一个例子,我用相同的数据库结构连接到不同的Oracle数据库(简而言之,我们说在Oracle模式中是相同的用户)。如果你使用的是另一个数据库,你只需要把正确的连接字符串,然后修改它。
private YourDbContext SetDbContext()
{
string connStr = @"Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=server_ip)(PORT=1521)))(CONNECT_DATA=(SID = server_sid)));User Id=server_user ;Password=server_password";
//You can get db connection details e.g. from app config
List<string> connections = config.GetSection("DbConneections");
string serverIp;
string dbSid;
string dBUser;
string dbPassword;
/* some logic to choose a connection from config and set up string variables for a connection*/
connStr = connStr.Replace("server_ip", serverIp);
connStr = connStr.Replace("server_sid", dbSid);
connStr = connStr.Replace("server_user", dBUser);
connStr = connStr.Replace("server_password", dbPassword);
var dbContext = dbContextFactory.CreateDbContext();
dbContext.Database.CloseConnection();
dbContext.Database.SetConnectionString(connStr);
return dbContext;
}
最后,您将能够设置所需的dbContext在调用此方法之前,您还可以传递一些参数给该方法,以帮助您选择正确的db。