本文介绍了根据请求参数在运行时创建 EF Core DbContext的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景

我们正在使用 ASP.Net Core 和 Entity Framework Core 2.2 构建 Web 应用程序

We are building a web application using ASP.Net Core and Entity Framework Core 2.2

我们正在连接到旧数据库.设置是有 16 个数据库,所有数据库都具有完全相同的模式,保存不同的上下文数据.我们无法改变这一点.

We are connecting to legacy databases. The setup is that there are 16 databases, all with the exact same schema, holding different contextual data. We cannot change this.

我们需要在运行时根据请求参数连接到特定数据库.

We have a requirement to connect to a specific database at runtime, based on request parameters.

想象一下,母公司下的每个企业都有一个数据库.

Imagine that there is a database for every business under the parent company.

想象一下,每个数据库都有像 Employee、Client 和 Shift(员工为 Client 工作的轮班)这样的表.

Imagine that each database has tables like Employee, Client and Shift (a shift that the employee has worked for a Client).

还有一个中央"数据库,用于保存所有其他数据库的通用信息,例如设置等.

There is also a 'central' database which holds information common across all the other databases, like settings etc.

我们要求在一个列表视图中列出所有企业的所有员工.

We have a requirement to list all the Employees across all businesses in a single list view.

我们计划使用中央数据库中的 SQL 视图来检索这些数据,该视图只是在每个其他数据库之间执行 UNION(如果您有关于如何更好地执行此操作的建议,请分享).

We plan to retrieve this data by using a SQL View in the central database, which simply does a UNION across each of the other databases (if you have a suggestion on how to do this better, please share it).

CREATE VIEW dbo.v_all_employees AS
    SELECT EmployeeId, Fullname, 1 AS BusinessId FROM BusinessA.dbo.Employees
    UNION ALL SELECT EmployeeId, Fullname, 2 AS BusinessId FROM BusinessB.dbo.Employees
    -- etc. etc.

我们有一组模型代表所有数据库中的所有实体(表),因为它们共享完全相同的模式,例如一个Employee类,一个Client类等等

We have a single set of Models which represent all the Entities (tables) across all the databases, since they share the exact same schema e.g. one Employee class, one Client class etc.

用例

用户通过此路线转到网页以查看所有企业的员工列表:

The user goes to a web page to see the list of Employees across all the businesses via this route:

http://example.org/employees

然后,用户单击详细信息"链接,供个别员工查看更多详细信息,将用户带到以下网址:

User then clicks on a 'Details' link for an individual employee to view further details, taking the user to the url:

http://example.org/employees/details?employeeId=123&businessId=xyz

我所困扰的是我们如何在运行时实例化特定于业务的 DbContext,给定 BusinessId.

What I am stuck on is how do we instantiate the business specific DbContext at runtime, given the BusinessId.

我想出了三种方法(现在第四种感谢@mylee)来达到预期的结果,并且正在寻求社区的反馈.

I have come up with three ways (and now a fourth thanks to @mylee) to achieve the desired result and am looking for feedback from the community.

每个选项都假定每个 DbContext 将实现一个接口,该接口公开所有 DbSet 和 DbContext 方法,然后我们将使用工厂模式来确定要使用哪个 DbContext 实现.

Every option assumes that each DbContext will implement an interface which exposes all the DbSets and DbContext methods and then we'll use the Factory pattern to handle the determination of which DbContext implementation to use.

第一个选项:只需让工厂根据请求参数bizId"创建正确的 DbContext.

The first option:Simply have the Factory create the correct DbContext based on the request parameter 'bizId'.

然而,这需要每个 DbContext 覆盖 OnConfiguring 方法并设置 DbProvider - dotnet Core 框架通过其 IoC 容器扩展方法为我们做的事情AddDbContext:

However, this requires that each DbContext over-rides the OnConfiguring method and sets up the DbProvider - something that the dotnet Core framework does for us via its IoC container extension method AddDbContext:

public class ContextFactory : IContextFactory
{
    public IBIZContext GetContext(int bizId)
    {
        switch (bizId)
        {
            // Newing-up the DbContexts like this requires that the OnConfiguring method
            // for each context be present in each DbContext to setup the DbProvider
            // with the correct connection string.
            case 6:
                return new BIZ_AContext();
            case 7:
                return new BIZ_BContext();
            default:
                throw new Exception("Unexpected Business Id");
        }
    }
}

这个问题是我不喜欢我们在这里更新上下文的方式.它要求我们在每个上下文中覆盖 OnConfiguring 方法并可以访问连接字符串.

This issue with this is that I don't like how we're newing up the contexts here. It requires that we over-ride the OnConfiguring method in each context and have access to the Connection String.

第二种选择:

我更喜欢使用在 Startup.cs 中设置的内置 IoC 容器,但这展示了服务定位器反模式.此外,它会将 Web 项目中的 HttpContext 泄漏到基础设施项目中(我使用的是 Onion 架构):

I would prefer to use the built in IoC Container which is setup in Startup.cs but this exhibits the Service Locator Anti-Pattern.Also, it leaks the HttpContext out of the Web project into the Infrastructure project (I'm using an Onion architecture):

public class ContextFactoryUsingLocator : IContextFactoryUsingLocator
{
    public IBIZContext GetContext(IHttpContextAccessor httpContextFactory, int bizId)
    {
        // Injecting the HttpContextAccessor gives us access to the IoC Container via RequestServices;
        // But using it here exhibits the Service Locator anti-pattern.
        // Perhaps its ok to use the Service Locator pattern within a Factory in this case?
        switch (bizId)
        {
            case 6:
                return (BIZ_AContext)httpContextFactory.HttpContext.RequestServices.GetService(typeof(BIZ_AContext));
            case 7:
                return (BIZ_BContext)httpContextFactory.HttpContext.RequestServices.GetService(typeof(BIZ_BContext));
            default:
                throw new Exception("Unexpected Business Id");
        }
    }
}

第三种选择

将每个 DbContext 注入工厂并让工厂简单地返回正确的实例:

Inject each DbContext into the Factory and have the Factory simply return the correct instance:

public class ContextFactoryInjected : IContextFactoryInjected
{
    private readonly BIZ_AContext _bizAContext;
    private readonly BIZ_BContext _bizBContext;

    public ContextFactoryInjected(
        BIZ_AContext bizAContext,
        // 14 other DbContext dependencies omitted here for brevity
        BIZ_BContext bizBContext)
    {
        // Injecting all 16 DbContexts into the Factory seems to counter the intention of the Factory since the IoC Container
        // would be managing the creation of all the instances; isn't that the responsibility of the Factory?

        // More importantly; wouldn't this have serious performance implications, creating 16 instances of a DbContext on every Request?
        _bizAContext = bizAContext;
        _bizBContext = bizBContext;
    }

    public IBIZContext GetContext(int bizId)
    {
        switch (bizId)
        {
            case 6:
                return _bizAContext;
            case 7:
                return _bizBContext;
            default:
                throw new Exception("Unexpected Business Id");
        }
    }
}

第四个选项将 DbContext 的配置封装在 Factory 内(这个方法是@mylee 建议的)

The fourth optionEncapsulate the configuration of the DbContext within the Factory (this method was suggested by @mylee)

public class ContextFactoryConfigured : IContextFactoryConfigured
{
    public IBIZContext GetContext(int bizId)
    {
        switch (bizId)
        {
            // Newing-up the DbContexts like this encapsulates all the details required for the DbContext within the Factory
            case 6:
                var bizAOptionsBuilder = new DbContextOptionsBuilder<BizAContext>();
                bizAOptionsBuilder.UseSqlServer(Settings.BizAConnectionString);
                return new BizAContext(bizAOptionsBuilder.Options);
            case 7:
                var bizBOptionsBuilder = new DbContextOptionsBuilder<BizBContext>();
                bizBOptionsBuilder.UseSqlServer(Settings.BizBConnectionString);
                return new BizBContext(bizBOptionsBuilder.Options);
            default:
                throw new Exception("Unexpected Business Id");
        }
    }
}

您是否同意选项 2 展示了 Service Locator 反模式,即说工厂依赖于它管理创建的对象是否正确?

Do you agree that option 2 exhibits the Service Locator anti-pattern i.e. is it right to say that the Factory depends on the objects its managing the creation of?

您是否认为选项 4 是其中最好的方法,因为工厂通常有责任更新"它的对象,并且它不会导致关注点的混合(即不t 需要 HttpContext )并且它封装了在工厂中构建上下文所需的所有细节(例如 ConnectionString)?

Do you think that option 4 is the best approach out of these, given that it is normally the responsibility of a Factory to 'new-up' its objects AND it doesn't result in a mixing of concerns (i.e. doesn't require the HttpContext ) AND it encapsulates all the details (e.g. ConnectionString) required to build the context within the Factory?

或者有没有一种方法可以使用依赖注入来实现这一点,而不会导致关注点的混合?

Or is there a way to achieve this using Dependency Injection without resulting in a mixing of concerns?

或者有没有我这里没有提到的更好的方法?

Or is there an even better way that I've not mentioned here?

推荐答案

我们在使用多个结构相同的数据库的遗留系统中遇到了同样的问题,并提出了与您的选项 4 类似的解决方案:

We have the same problem with a legacy system using multiple databases of the same structure and came up with a solution that is similar to your option 4:

有一个接口和工厂方法来创建一个dbContext.它需要一个连接字符串作为参数:

There is an interface and factory method to create a dbContext. It takes a connection string as a parameter:

public interface ICustomDbContextFactory<out T> where T: DbContext
{
    T CreateDbContext(string connectionString);
}

public class CustomDbContextFactory<T> : ICustomDbContextFactory<T>  where T: DbContext
{
    public T CreateDbContext(string connectionString)
    {
        var optionsBuilder = new DbContextOptionsBuilder<T>();
        optionsBuilder.UseSqlServer(connectionString);
        return System.Activator.CreateInstance(typeof(T), optionsBuilder.Options) as T;
    }
}

工厂在 DI 注册为单身人士:

The factory is registered with the DI as a singleton:

services.AddSingleton<ICustomDbContextFactory<CustomDbContext>, CustomDbContextFactory<CustomDbContext>>();

然后你就在需要的时候使用它(需要能够注入工厂):

And then you just use it whenever needed (need to be able to get the factory injected):

using (var dbContext = customDbContextFactory.CreateDbContext(connectionString))
{
   // use your dbContext here
}

我认为它与您的 nr 非常匹配.4 解决方案(除了我们有逻辑拉出与工厂分离的连接字符串),我们发现它是我们能解决的问题的最干净的解决方案.想听听您完成了哪些实施,以及是否有人对如何解决该问题有更好的想法.

I think it pretty much matches your nr. 4 solution (except for the fact that we have the logic to pull out the connection string separated from the factory) and we found it to be the cleanest solution of the problem we could figure out. Would like to hear what did you finish up implementing and if anyone has any better idea how to approach the problem.

这篇关于根据请求参数在运行时创建 EF Core DbContext的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-11 03:33