背景
我们正在使用 ASP.Net Core 和 Entity Framework Core 2.2 构建一个 Web 应用程序
我们正在连接到旧数据库。设置是有 16 个数据库,全部具有完全相同的架构,保存不同的上下文数据。我们无法改变这一点。
我们需要在运行时根据请求参数连接到特定数据库。
想象一下,母公司下的每个业务都有一个数据库。
想象一下,每个数据库都有诸如“员工”、“客户”和“轮班”(员工为客户工作的轮班)等表。
还有一个“中央”数据库,其中保存所有其他数据库通用的信息,例如设置等。
我们需要在单个列表视图中列出所有企业的所有员工。
我们计划通过在中央数据库中使用 SQL 视图来检索此数据,该视图只需在每个其他数据库之间执行 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.
我们有一组模型,代表所有数据库中的所有实体(表),因为它们共享完全相同的模式,例如一个 Employee 类、一个 Client 类等。
Use case
用户通过以下路线转到网页查看所有企业的员工列表:
http://example.org/employees http://example.org/employees
然后,用户单击单个员工的“详细信息”链接以查看更多详细信息,从而将用户带到以下 URL:
我所困惑的是,在给定 BusinessId 的情况下,我们如何在运行时实例化特定于业务的 DbContext。
我想出了三种方法(现在是第四种,感谢@mylee)来实现预期的结果,并正在寻求社区的反馈。
每个选项都假设每个 DbContext 将实现一个公开所有 DbSets 和 DbContext 方法的接口,然后我们将使用工厂模式来确定要使用哪个 DbContext 实现。
第一个选项:只需让工厂根据请求参数“bizId”创建正确的 DbContext 即可。
但是,这要求每个 DbContext 重写 OnConfiguring 方法并设置 DbProvider - dotnet Core 框架通过其 IoC 容器扩展方法为我们做的事情添加数据库上下文 https://learn.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext#using-dbcontext-with-dependency-injection:
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 容器,但这展示了服务定位器反模式。
此外,它将 HttpContext 从 Web 项目泄漏到基础设施项目中(我使用的是 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 注入到 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");
}
}
}
第四个选项将DbContext的配置封装在Factory中(此方法是@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)?
或者有没有一种方法可以使用依赖注入来实现这一点,而不会导致问题混合?
或者还有我在这里没有提到的更好的方法吗?