动态更改 Entity Framework Core 中的架构

Dynamically changing schema in Entity Framework Core

提问人:user3272018 提问时间:9/15/2016 最后编辑:Ziga Petekuser3272018 更新时间:11/30/2021 访问量:82699

问:

UPD是我解决问题的方法。虽然它可能不是最好的,但它对我有用。


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

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

我可以在 中设置 schema-name,但是,如前所述,此方法仅调用一次,因此我可以像这样设置架构名称全局OnModelCreating

protected override void OnModelCreating(ModelBuilder modelBuilder) {
    modelBuilder.HasDefaultSchema("public");
    base.OnModelCreating(modelBuilder);
}

或直接在模型中通过属性

[Table("order", Schema = "public")]
public class Order{...}

但是如何在运行时更改架构名称呢?我为每个请求创建上下文,但首先我通过对数据库中的架构共享表的请求来模糊用户的架构名称。那么,组织该机制的正确方法是什么:

  1. 通过用户凭据找出架构名称;
  2. 从特定架构的数据库中获取特定于用户的数据。

谢谢。

P.S. 我使用PostgreSql,这就是表名低写的原因。

C# ASP.NET 数据库架构 实体框架核心

评论

0赞 Zinov 5/25/2018
您能否发布使用 IModelCacheKeyFactory 所做的解决方法?
0赞 user3272018 5/25/2018
@Zinov我已经做到了,但请小心。stackoverflow.com/a/50529432/3272018

答:

5赞 bricelam 9/15/2016 #1

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

  • 在外部构建模型并通过以下方式传递它DbContextOptionsBuilder.UseModel()
  • 将服务替换为考虑架构的服务IModelCacheKeyFactory

评论

4赞 user3272018 9/16/2016
您能否提供一些详细信息或指向一些文档/博客/教程的链接?
0赞 Tomas Voracek 2/1/2017
@user3272018我有同样的问题,但没有文档或示例如何在 EF Core 中正确实现 IModelCacheKeyFactory。
1赞 user3272018 2/1/2017
@tomas-voracek:哦,最终我做到了。稍后我将提供该代码作为自我回答。我敢肯定这不是实现我目标的完美方式,但它有效。也许有人可以改进我的解决方案。对不起,我没有早点这样做。
0赞 Diego Garcia 7/16/2018
嗨,@user3272018您能与我们分享您的解决方案吗?我正在努力完成同样的事情。
1赞 user3272018 7/17/2018
@Diego,正如我在问题中提到的,我已经在这里分享了我的解决方案
2赞 bubi 9/19/2016 #2

您可以在固定架构表上使用 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);
    }
}

现在,在创建上下文之前,您可以使用一些不同的方法来确定架构名称。
例如,您可以将“系统表”放在不同的上下文中,因此在每个请求中,您都可以使用系统表从用户名中检索模式名称,然后在正确的模式上创建工作上下文(您可以在上下文之间共享表)。
可以将系统表与上下文分离,并使用 ADO .Net 访问它们。
可能还有其他几种解决方案。

您还可以在此处查看
使用 Code First EF6 的多租户

你可以谷歌ef multi tenant

编辑:
还有模型缓存的问题(我忘记了)。 您必须禁用模型缓存或更改缓存的行为。

评论

0赞 user3272018 9/19/2016
感谢您的回复,但据我所知,方法只调用了一次,我无法在每个请求中通过它更改架构名称。实际上我使用的是 EF Core,但行为是一样的。@bricelam已经说过两种解决我问题的方法,所以我对一些更详细的解释感兴趣,因为 EF 的开发人员应该比其他人更了解 EF,嗯?OnModelCreatingOnModelCreating
0赞 user3272018 9/19/2016
在您发布的 SO 问题中,似乎类似于 .也许这是解决我问题的关键。我错过了那篇文章——谢谢。IDbModelCacheKeyProviderIModelCacheKeyFactory
0赞 bubi 9/19/2016
你是对的!!!我忘记了模型缓存!还要注意性能(你可以找到一些文章,也可以在 Stack Overflow 上找到)
3赞 snys98 9/19/2016 #3

我发现这个博客可能对你有用。完美!:)

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,并使用某些标识符缓存它们。

33赞 H. Herzl 9/22/2016 #4

是否已在 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);
        }
    }
}

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;
        }
    }
}

正如你在上面看到的,有一行可以设置表的模式和名称,你可以在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);

显然,您应该根据架构的 name 参数的值设置架构名称:

entity.ToTable("Product", schemaName);

评论

5赞 user3272018 9/22/2016
正如我上面所写的,坐在模式名称中没有问题,问题是这个方法只调用一次,所以下一个创建的上下文将具有相同的模式。但也许我在你的回复中遗漏了一些重要的东西?OnModelCreating
0赞 H. Herzl 9/23/2016
我明白你的意思,你是对的,我之前的答案不包括 OnModelCreating 问题的解决方案,但我在博客上搜索了这个问题的解决方案,我找到了 OnConfiguring 方法的代码,我修改了我的答案请检查它并让我知道是否有有用的链接
0赞 Tomas Voracek 2/1/2017
这可能适用于小型项目,但我无法想象为数百个表维护 MapProduct 方法,更不用说所有可能的设置和迁移......
0赞 H. Herzl 2/2/2017
你说得好,你是在谈论为这些方法编写代码还是维护一个大的代码文件?
0赞 Kate 6/1/2021
AppSettings 类来自哪个命名空间?咚。
10赞 user3272018 5/25/2018 #5

对不起大家,我之前应该发布我的解决方案,但由于某种原因我没有,所以就在这里。

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

在项目中,我使用了 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>();

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

在 EFC 1 发布周期结束时,EF 团队决定从默认服务集合 (AddEntityFramework() 中删除 EF 的服务。AddDbContext()),这意味着使用 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);
    }
}

共享上下文严格绑定到 schema: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;
    }
}

如果我没有错过任何东西,就是这样。现在,在经过身份验证的用户的每个请求中,都将使用正确的上下文。

我希望它有所帮助。

评论

2赞 Jarrich Van de Voorde 6/8/2018
在 OnModelCreating 期间设置 DefaultSchema...但该方法仅调用一次。那么,您如何根据请求动态更改架构呢?我不明白这个解决方案是如何工作的。
0赞 t.ouvre 10/10/2019
@JarrichVandeVoorde,模型由 EF 在内部缓存,但在本例中,MultiTenantModelCacheKeyFactory 配置 EF,缓存根据当前公司(或与当前公司关联的架构)进行拆分。当 dbcontext 被实例化时,键是不同的,那么如果在高速缓存中找不到 OnModelCreation,将再次调用。这种方法的缺点是每个新公司的缓存都在增长(在我的情况下,这是内存溢出异常的原因)
0赞 Robin Ding 12/13/2019
@user3272018,非常感谢。这是一个很棒的主意。我的方案是即时创建架构并重新加载映射。一个。我正在查看 ModelSource 并尝试清除 ModelSource 中的缓存。但是更改缓存键是一种更简单的方法:)b.UseInternalServiceProvider() 很棒,它解决了在 EF Core 中,ServiceProvider 将在 DbContext 初始化期间初始化的问题。
2赞 Randy 9/29/2018 #6

也许我这个答案有点晚了

我的问题是处理具有相同结构的不同架构,比如说多租户。

当我尝试为不同的模式创建相同上下文的不同实例时,实体框架 6 开始发挥作用,在第一次创建 dbContext 时捕获,然后对于以下实例,它们使用不同的模式名称创建,但从未调用过 onModelCreateing,这意味着每个实例都指向相同的先前捕获的预生成视图, 指向第一个架构。

然后我意识到,为每个模式创建一个继承自 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;
}

我希望这能帮助某人节省一些时间。

0赞 user1489673 10/30/2018 #7

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
5赞 C Rudolph 1/21/2020 #8

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

  1. 在上下文类中创建 ModelCacheKey 变量。
  2. 在上下文构造函数中,设置 ModelCacheKey 变量
  3. 创建一个继承自 IModelCacheKeyFactory 的类,并使用 ModelCacheKey (MyModelCacheKeyFactory)
  4. 在 OnConfiguring 方法 (MyContext) 中,替换默认的 IModelCacheKeyFactory
  5. 在 OnModelCreating 方法 (MyContext) 中,使用模型构建器定义所需的任何内容。
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 变量进行缓存。

14赞 Ghini Antonio 6/17/2020 #9

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

在 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 结果的缓存和检索方式。将架构包含在缓存键中,您可以为传递给上下文构造函数的每个不同架构字符串执行和缓存 OnModelCreateing。

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

评论

0赞 C Rudolph 6/22/2021
从字面上看,这是我下面的答案
0赞 Alin Ciocan 9/1/2020 #10

我实际上发现这是一个使用 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);
    }


}
0赞 maniacz 4/18/2021 #11

如果数据库之间的唯一区别是架构名称,则解决该问题的最简单方法是删除在 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。

评论

0赞 Gert Arnold 4/19/2021
如何设置所需的架构名称?
0赞 maniacz 4/21/2021
@GertArnold我修改了我的答案,请看一看。
0赞 Gert Arnold 4/28/2021
仍然与设置架构无关。检查“模式”一词在此问题上下文中的含义。
0赞 maniacz 5/5/2021
有时人们倾向于通过“添加”一些对他们有帮助的东西来解决他们的问题,而似乎忽略了基于“减法”的解决方案。在这个问题中,我可以想象一个人可能会遇到标题中描述的这种问题,正如我在回答中提到的,如果唯一的区别是架构名称,那么您基本上拥有具有相同实体的不同数据库,您可以摆脱 EF Core 设置架构并解决问题, 无论如何,它解决了我的问题,所以我决定分享这种方法。
0赞 Gert Arnold 5/5/2021
您似乎没有意识到 OP 需要设置架构,因为多租户。没有架构前缀的查询不符合要求,在运行时更改连接字符串不是问题。