在 Entity Framework 核心中动态更改架构
Dynamically changing schema in Entity Framework Core
UPD 是我解决问题的方法。虽然它可能不是最好的,但它对我有用。
我在使用 EF Core 时遇到问题。我想通过模式机制在我的项目数据库中分离不同公司的数据。我的问题是如何在运行时更改架构名称?我已经找到 similar question 关于这个问题,但仍然没有答案,我有一些不同的条件。所以我有 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{...}
但是如何在运行时更改架构名称?我为每个请求创建上下文,但首先我通过对数据库中模式共享 table 的请求来确定用户的模式名称。那么组织该机制的正确方法是什么:
- 根据用户凭据找出架构名称;
- 从特定模式的数据库中获取特定于用户的数据。
谢谢。
P.S。我使用 PostgreSql,这就是小写 table 名称的原因。
有几种方法可以做到这一点:
- 在外部构建模型并通过
DbContextOptionsBuilder.UseModel()
传入
- 将
IModelCacheKeyFactory
服务替换为考虑模式的服务
您可以在固定架构表上使用 Table 属性。
您不能在架构更改表上使用属性,您需要通过 ToTable fluent 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);
}
}
现在,您可以通过一些不同的方式在创建上下文之前确定架构名称。
例如,您可以将 "system tables" 放在不同的上下文中,这样在每次请求时,您都可以使用系统表从用户名中检索架构名称,而不是在正确的架构上创建工作上下文(您可以在上下文之间共享表) .
您可以将系统表从上下文中分离出来,并使用 ADO .Net 访问它们。
可能还有其他几种解决方案。
你也可以在这里看看
Multi-Tenant With Code First EF6
你可以googleef multi tenant
编辑
还有模型缓存的问题(我忘记了)。
您必须禁用模型缓存或更改缓存的行为。
我发现此博客可能对您有用。完美!:)
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 并使用特定的标识符对其进行缓存。
您是否已在 EF6 中使用 EntityTypeConfiguration?
我认为解决方案是在 DbContext class 的 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);
}
}
}
OnConfiguring 方法中的代码强制在为 DbContext class 创建的每个实例上执行 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;
}
}
}
正如您在上面看到的,有一行为 table 设置架构和名称,您可以在 DbContext 或类似的东西中为一个构造函数发送架构名称。
请不要使用魔法字符串,您可以创建一个包含所有可用模式的 class,例如:
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);
抱歉大家,我应该在之前发布我的解决方案,但由于某些原因我没有发布,所以在这里。
但是
请记住,解决方案可能有任何问题,因为它既没有经过任何人审查也没有经过生产验证,我可能会在这里得到一些反馈。
项目中我使用了ASP.NET Core 1
关于我的数据库结构。我有 2 个上下文。第一个包含有关用户的信息(包括他们应该处理的数据库方案),第二个包含用户特定的数据。
在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建议,解释如下
At the very end of EFC 1 release cycle, the EF team decided to remove EF's services from the default service collection (AddEntityFramework().AddDbContext()), which means that the services are resolved using EF's own service provider rather than the application service provider.
To force EF to use your application's service provider instead, you need to first add EF's services together with the data provider to your service collection, and then configure DBContext to use internal service provider
现在我们需要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;
}
}
如果我没有遗漏任何内容,就是这样。现在,在经过身份验证的用户的每个请求中,都将使用正确的上下文。
希望对您有所帮助。
也许我回答的有点晚
我的问题是处理具有相同结构的不同模式,比如说多租户。
当我尝试为不同的模式创建相同上下文的不同实例时,实体框架 6 开始发挥作用,捕捉到第一次创建 dbContext 然后为以下实例创建它们时使用不同的模式名称但是onModelCreating 从未被调用意味着每个实例都指向相同的先前捕获的预生成视图,指向第一个模式。
然后我意识到为每个模式创建一个从 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 核心 2.1 更新
您可以从具有多个模式的数据库创建模型。该系统在命名方面有点模式不可知论。同名的 tables 附加了“1”。 "dbo" 是假定的模式,因此您不会通过在 PM 命令
前加上 table 名称来添加任何内容
您将必须重命名模型文件名并 class 自己命名。
在 PM 控制台中
Scaffold-DbContext "Data Source=localhost;Initial Catalog=YourDatabase;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -force -Tables TableA, Schema1.TableA
用 EFCore 花了几个小时来解决这个问题。似乎对实施此的正确方法有很多困惑。我相信在 EFCore 中处理自定义模型的简单而正确的方法是替换默认的 IModelCacheKeyFactory 服务,如下所示。在我的示例中,我正在设置自定义 table 名称。
- 在您的上下文中创建一个 ModelCacheKey 变量 class。
- 在您的上下文构造函数中,设置 ModelCacheKey 变量
- 创建一个 class 继承自 IModelCacheKeyFactory 并使用 ModelCacheKey (MyModelCacheKeyFactory)
- 在 OnConfiguring 方法 (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 变量进行缓存。
定义上下文并将架构传递给构造函数。
在 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
};
}
}
在 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");
我实际上发现它是一个使用 EF 拦截器的更简单的解决方案。
我实际上保留了 onModeling 方法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("dbo"); // this is important to always be dbo
// ... model definition ...
}
并且此代码将在启动中:
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,您还可以将一些参数传递给该方法以帮助您选择正确的数据库。
UPD
我在使用 EF Core 时遇到问题。我想通过模式机制在我的项目数据库中分离不同公司的数据。我的问题是如何在运行时更改架构名称?我已经找到 similar question 关于这个问题,但仍然没有答案,我有一些不同的条件。所以我有 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{...}
但是如何在运行时更改架构名称?我为每个请求创建上下文,但首先我通过对数据库中模式共享 table 的请求来确定用户的模式名称。那么组织该机制的正确方法是什么:
- 根据用户凭据找出架构名称;
- 从特定模式的数据库中获取特定于用户的数据。
谢谢。
P.S。我使用 PostgreSql,这就是小写 table 名称的原因。
有几种方法可以做到这一点:
- 在外部构建模型并通过
DbContextOptionsBuilder.UseModel()
传入
- 将
IModelCacheKeyFactory
服务替换为考虑模式的服务
您可以在固定架构表上使用 Table 属性。
您不能在架构更改表上使用属性,您需要通过 ToTable fluent 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);
}
}
现在,您可以通过一些不同的方式在创建上下文之前确定架构名称。
例如,您可以将 "system tables" 放在不同的上下文中,这样在每次请求时,您都可以使用系统表从用户名中检索架构名称,而不是在正确的架构上创建工作上下文(您可以在上下文之间共享表) .
您可以将系统表从上下文中分离出来,并使用 ADO .Net 访问它们。
可能还有其他几种解决方案。
你也可以在这里看看
Multi-Tenant With Code First EF6
你可以googleef multi tenant
编辑
还有模型缓存的问题(我忘记了)。
您必须禁用模型缓存或更改缓存的行为。
我发现此博客可能对您有用。完美!:)
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 并使用特定的标识符对其进行缓存。
您是否已在 EF6 中使用 EntityTypeConfiguration?
我认为解决方案是在 DbContext class 的 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);
}
}
}
OnConfiguring 方法中的代码强制在为 DbContext class 创建的每个实例上执行 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;
}
}
}
正如您在上面看到的,有一行为 table 设置架构和名称,您可以在 DbContext 或类似的东西中为一个构造函数发送架构名称。
请不要使用魔法字符串,您可以创建一个包含所有可用模式的 class,例如:
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);
抱歉大家,我应该在之前发布我的解决方案,但由于某些原因我没有发布,所以在这里。
但是
请记住,解决方案可能有任何问题,因为它既没有经过任何人审查也没有经过生产验证,我可能会在这里得到一些反馈。
项目中我使用了ASP.NET Core 1
关于我的数据库结构。我有 2 个上下文。第一个包含有关用户的信息(包括他们应该处理的数据库方案),第二个包含用户特定的数据。
在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建议,解释如下
At the very end of EFC 1 release cycle, the EF team decided to remove EF's services from the default service collection (AddEntityFramework().AddDbContext()), which means that the services are resolved using EF's own service provider rather than the application service provider.
To force EF to use your application's service provider instead, you need to first add EF's services together with the data provider to your service collection, and then configure DBContext to use internal service provider
现在我们需要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;
}
}
如果我没有遗漏任何内容,就是这样。现在,在经过身份验证的用户的每个请求中,都将使用正确的上下文。
希望对您有所帮助。
也许我回答的有点晚
我的问题是处理具有相同结构的不同模式,比如说多租户。
当我尝试为不同的模式创建相同上下文的不同实例时,实体框架 6 开始发挥作用,捕捉到第一次创建 dbContext 然后为以下实例创建它们时使用不同的模式名称但是onModelCreating 从未被调用意味着每个实例都指向相同的先前捕获的预生成视图,指向第一个模式。
然后我意识到为每个模式创建一个从 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 核心 2.1 更新
您可以从具有多个模式的数据库创建模型。该系统在命名方面有点模式不可知论。同名的 tables 附加了“1”。 "dbo" 是假定的模式,因此您不会通过在 PM 命令
前加上 table 名称来添加任何内容您将必须重命名模型文件名并 class 自己命名。
在 PM 控制台中
Scaffold-DbContext "Data Source=localhost;Initial Catalog=YourDatabase;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -force -Tables TableA, Schema1.TableA
用 EFCore 花了几个小时来解决这个问题。似乎对实施此的正确方法有很多困惑。我相信在 EFCore 中处理自定义模型的简单而正确的方法是替换默认的 IModelCacheKeyFactory 服务,如下所示。在我的示例中,我正在设置自定义 table 名称。
- 在您的上下文中创建一个 ModelCacheKey 变量 class。
- 在您的上下文构造函数中,设置 ModelCacheKey 变量
- 创建一个 class 继承自 IModelCacheKeyFactory 并使用 ModelCacheKey (MyModelCacheKeyFactory)
- 在 OnConfiguring 方法 (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 变量进行缓存。
定义上下文并将架构传递给构造函数。
在 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
};
}
}
在 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");
我实际上发现它是一个使用 EF 拦截器的更简单的解决方案。
我实际上保留了 onModeling 方法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("dbo"); // this is important to always be dbo
// ... model definition ...
}
并且此代码将在启动中:
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,您还可以将一些参数传递给该方法以帮助您选择正确的数据库。