c# - 根据请求参数在运行时创建 EF Core DbContext

标签 c# .net-core factory-pattern service-locator ef-core-2.2

背景

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

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

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

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

假设每个数据库都有 Employee、Client 和 Shift(员工为客户工作的类次)等表。

还有一个“中央”数据库,它保存所有其他数据库共有的信息,如设置等。

我们需要在一个 ListView 中列出所有企业的所有员工。

我们计划通过使用中央数据库中的 SQL View 来检索这些数据,它只是在每个其他数据库之间执行一个 UNION(如果您有关于如何更好地做到这一点的建议,请分享)。

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.

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

用例

用户通过以下路径访问网页以查看所有企业的员工列表:

http://example.org/employees

用户然后单击单个员工的“详细信息”链接以查看更多详细信息,将用户带到 url:

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

我坚持的是,在给定 BusinessId 的情况下,我们如何在运行时实例化特定于业务的 DbContext。

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

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

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

但是,这需要每个 DbContext 覆盖 OnConfiguring 方法并设置 DbProvider——dotnet Core 框架通过其 IoC 容器扩展方法 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 方法并有权访问连接字符串。

第二个选项:

我更喜欢使用在 Startup.cs 中设置的内置 IoC 容器,但这展示了服务定位器反模式。
此外,它会将 Web 项目中的 HttpContext 泄漏到 Infrastructure 项目中(我使用的是 Onion 架构):
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) Factory 并让 Factory 简单地返回正确的实例:
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");
        }
    }
}

第四个选项
在 Factory 中封装 DbContext 的配置(此方法由 @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 展示了服务定位器反模式,即是否可以说工厂依赖于其管理创建的对象?

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

或者有没有办法使用依赖注入(inject)来实现这一点而不会导致问题的混合?

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

最佳答案

对于使用相同结构的多个数据库的遗留系统,我们遇到了同样的问题,并提出了与您的选项 4 类似的解决方案:
有一个接口(interface)和工厂方法来创建一个 dbContext。它将连接字符串作为参数:

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 中注册为单例:
services.AddSingleton<ICustomDbContextFactory<CustomDbContext>, CustomDbContextFactory<CustomDbContext>>();
然后您只需在需要时使用它(需要能够注入(inject)工厂):
using (var dbContext = customDbContextFactory.CreateDbContext(connectionString))
{
   // use your dbContext here
}
我认为它与您的 nr 非常匹配。 4 解决方案(除了我们有逻辑将连接字符串从工厂中分离出来),我们发现它是我们能解决的问题的最干净的解决方案。想听听您完成了哪些实现,以及是否有人对如何解决问题有更好的了解。

关于c# - 根据请求参数在运行时创建 EF Core DbContext,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54760793/

相关文章:

C#抽象方法和公共(public)代码

c# - C# 中的 C++ union — 奇怪的行为

javascript - 如何使用带有 Razor 的 MVC4 设置 javascript 变量

c++ - 工厂模式,实现动态工厂列表

c# - 带有依赖注入(inject)的工厂模式,serviceProvider 总是返回 null

c# - 如何将对象中的属性列表转换为对象列表

c# - SingleLine 选项和\n 字符串结尾之前的正则表达式问题

c# - C#中如何处理异步方法和IDisposable?

c# - 替换 Net Core 中的 AppDomain

delphi - 从 TObject 转换为接口(interface)类型