在 Entity Framework 中创建通用 DbContext 工厂
Creating generic DbContext Factory in Entity Framework
我正在使用 .Net Core 2.1。我使用了不止一个 DbContext
。我正在为每个上下文创建一个 DbContextFactory
。但是,我想以一种通用的方式来做到这一点。我只想创建一个 DbContextFactory
。我怎样才能做到这一点?
MyDbContextFactory.cs
public interface IDbContextFactory<TContext> where TContext : DbContext
{
DbContext Create();
}
public class MyDbContextFactory : IDbContextFactory<MyDbContext>
{
public IJwtHelper JwtHelper { get; set; }
public MyDbContextCreate()
{
return new MyDbContext(this.JwtHelper);
}
DbContext IDbContextFactory<MyDbContext>.Create()
{
throw new NotImplementedException();
}
}
UnitOfWork.cs
public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext
{
public static Func<TContext> CreateDbContextFunction { get; set; }
protected readonly DbContext DataContext;
public UnitOfWork()
{
DataContext = CreateDbContextFunction();
}
}
MyDbContext.cs
public class MyDbContext : DbContext
{
private readonly IJwtHelper jwtHelper;
public MyDbContext(IJwtHelper jwtHelper) : base()
{
this.jwtHelper = jwtHelper;
}
}
所以你有一个数据库,还有一个代表这个数据库的 class:你的 DbContext
,它应该代表你数据库中的表和表之间的关系,仅此而已。
您决定将对数据库的操作与数据库本身分开。这是一件好事,因为如果您的数据库的多个用户想要做同样的事情,他们可以重新使用代码来完成。
例如,如果您想创建 "an Order for a Customer with several OrderLines, containing ordered Products, agreed Prices, amount, etc",您需要对数据库做几件事:检查客户是否已经存在,检查所有产品是否已经存在,检查是否有足够的物品等
这些东西通常是您不应该在 DbContext 中实现的东西,而应该在单独的 class.
中实现
如果您添加如下功能:CreateOrder
,那么多个用户可以重复使用该功能。您只需测试一次,如果您决定更改订单模型中的某些内容,则只有一个地方需要更改订单的创建。
分离代表您的数据库 (DbContext) 的 class 的其他优点
从处理此数据的 class 可以更轻松地更改内部结构,而无需更改数据库的用户。您甚至可以决定从 Dapper 更改为 Entity Framework,而无需更改用法。这也使得出于测试目的模拟数据库变得更加容易。
CreateOrder、QueryOrder、UpdateOrder 等函数已经表明它们不是通用数据库操作,它们适用于订购数据库,而不适用于学校数据库。
这可能会导致工作单元可能不是单独 class 中所需功能的正确名称。几年前,工作单元主要是表示对数据库的操作,而不是真正的特定数据库,我对此不太确定,因为我很快就看到了真正的工作单元 class 不会增强我的 DbContext 的功能。
您经常会看到以下内容:
- 代表您的 数据库的 DbContext class:您创建的数据库,不是任何通用的数据库概念
- 一个存储库 class 表示将数据存储在某处的想法,它是如何存储的,它可以是一个 DbContext,也可以是一个 CSV 文件,或者字典的集合 class es 创建用于测试目的。这个存储库有一个 IQueryable,并具有添加/更新/删除的功能(根据需要
- 代表您的问题的 class:订购模型:添加订单/更新订单/获取客户订单:这个 class 确实了解订单的一切,例如它有一个 OrderTotal,它可能在您的 Ordering 数据库中找不到。
在 DbContext 之外,您有时可能需要 SQL,例如提高调用效率。在 Repository 之外,不应该看到您正在使用 SQL
考虑分离关注点:如何保存数据 (DbContext),如何 CRUD(创建、获取、更新等)数据 (Repository),如何使用数据(合并表)
我认为你想在你的工作单元中做的事情应该在存储库中完成。您的订购 class 应该创建存储库(创建 DbContext),查询多个项目以检查它必须添加/更新的数据,执行添加和更新并保存更改。之后,您的订购 class 应该处置存储库,而存储库又将处置 DbContext。
存储库 class 看起来与 DbContext class 非常相似。它有几个代表表格的集合。每组都将实现 IQueryable<...>
并允许添加/更新/删除,无论需要什么。
由于功能上的这种相似性,您可以省略存储库 class 并让您的订购 class 直接使用 DbContext。但是,请记住,如果将来您决定不再使用 entity framework,而是使用一些更新的概念,或者可能 return 回到 Dapper,甚至更低,那么变化会更大等级。 SQL 会渗透到您的订单中 class
选择什么
我认为你应该自己回答几个问题:
- 是否真的只有一个数据库应该由您的 DbContext 表示,是否应该在具有相同布局的第二个数据库中使用相同的 DbContext。想想测试数据库或开发数据库。让您的程序创建要使用的 DbContext 不是更容易/更好的可测试性/更好的可变性吗?
- 你的DbContext真的有一组Users吗:每个人都有删除的可能吗?去创造?会不会是有些程序只想查询数据(e-mail订单的程序),而订单程序需要添加Customers。也许另一个程序需要添加和更新产品,以及仓库中的产品数量。考虑为他们创建不同的存储库 classes。每个 Repository 获得相同的 DbContext,因为它们都在访问相同的数据库
- 类似地:只有一个数据处理class(上面提到的订购class):处理订单的流程应该能够更改产品价格并向库存添加商品吗?
您需要工厂的原因是,您不想让您的 "main" 程序决定它应该为现在的目的 运行 创建哪些项目。如果您自己创建项目,代码会容易得多:
订购流程的创建顺序:
IJwtHelper jwtHelper = ...;
// The product database: all functionality to do everything with Products and Orders
ProductDbContext dbContext = new ProductDbContext(...)
{
JwtHelper = jwtHelper,
...
};
// The Ordering repository: everything to place Orders,
// It can't change ProductPrices, nor can it stock the wharehouse
// So no AddProduct, not AddProductCount,
// of course it has TakeNrOfProducts, to decrease Stock of ordered Products
OrderingRepository repository = new OrderingRepository(...) {DbContext = dbContext};
// The ordering process: Create Order, find Order, ...
// when an order is created, it checks if items are in stock
// the prices of the items, if the Customer exists, etc.
using (OrderingProcess process = new OrderingProcess(...) {Repository = repository})
{
... // check if Customer exists, check if all items in stock, create the Order
process.SaveChanges();
}
处置进程时,处置存储库,进而处置 DbContext。
与通过电子邮件发送订单的过程类似:它不必检查产品,也不必创建客户,它只需要获取数据,并可能更新订单已通过电子邮件发送,或者电子邮件发送失败。
IJwtHelper jwtHelper = ...;
// The product database: all functionality to do everything with Products and Orders
ProductDbContext dbContext = new ProductDbContext(...) {JwtHelper = jwtHelper};
// The E-mail order repository: everything to fetch order data
// It can't change ProductPrices, nor can it stock the wharehouse
// It can't add Customers, nor create Orders
// However it can query a lot: customers, orders, ...
EmailOrderRepository repository = new EmailOrderRepository(...){DbContext = dbContext};
// The e-mail order process: fetch non-emailed orders,
// e-mail them and update the e-mail result
using (EmailOrderProcess process = new EmailOrderProcess(...){Repository = repository}
{
... // fetch the orders that are not e-mailed yet
// email the orders
// warning about orders that can't be emailed
// update successfully logged orders
repository.SaveChanges();
看看您使创建过程变得多简单,多用途:为 DbContext 提供不同的 JwtHelper,数据以不同的方式记录,为 Repository 提供不同的 DbContext,数据保存在不同的数据库,给进程一个不同的存储库,你将使用 Dapper 来执行你的查询。
测试会更容易:创建一个使用列表保存表的存储库,使用测试数据测试您的流程会很容易
数据库的更改会更容易。例如,如果您后来决定将您的数据库分成一个用于您的股票和股票价格,一个用于客户和订单,那么只需要更改一个存储库。 None 个进程会注意到此更改。
结论
不要让 classes 决定他们需要哪些对象。让 class 的创建者说:"hey, you need a DbContext? Use this one!" 这将省略工厂等
的需要
将您的实际数据库 (DbContext) 与存储和检索数据 (Repository) 的概念分开,使用一个单独的 class 来处理数据,而不知道这些数据是如何存储或检索的(过程 class)
创建几个存储库,这些存储库只能访问执行任务所需的数据(+预期更改后将来可以预见的数据)。 Repositories不要做太多,但也不要做一个万能的。
创建进程 classes 只做他们想做的事。不要创建一个包含 20 个不同任务的进程 class。只会让它更难描述它应该做什么,更难测试它,更难改变任务
如果您想重用现有的实现,EF Core 5 现在提供开箱即用的 DbContext Factory:
https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew#dbcontextfactory
确保正确处置它,因为它的实例不由应用程序的服务提供商管理。
查看微软文档
Using a DbContext factory
我正在使用 .Net Core 2.1。我使用了不止一个 DbContext
。我正在为每个上下文创建一个 DbContextFactory
。但是,我想以一种通用的方式来做到这一点。我只想创建一个 DbContextFactory
。我怎样才能做到这一点?
MyDbContextFactory.cs
public interface IDbContextFactory<TContext> where TContext : DbContext
{
DbContext Create();
}
public class MyDbContextFactory : IDbContextFactory<MyDbContext>
{
public IJwtHelper JwtHelper { get; set; }
public MyDbContextCreate()
{
return new MyDbContext(this.JwtHelper);
}
DbContext IDbContextFactory<MyDbContext>.Create()
{
throw new NotImplementedException();
}
}
UnitOfWork.cs
public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext
{
public static Func<TContext> CreateDbContextFunction { get; set; }
protected readonly DbContext DataContext;
public UnitOfWork()
{
DataContext = CreateDbContextFunction();
}
}
MyDbContext.cs
public class MyDbContext : DbContext
{
private readonly IJwtHelper jwtHelper;
public MyDbContext(IJwtHelper jwtHelper) : base()
{
this.jwtHelper = jwtHelper;
}
}
所以你有一个数据库,还有一个代表这个数据库的 class:你的 DbContext
,它应该代表你数据库中的表和表之间的关系,仅此而已。
您决定将对数据库的操作与数据库本身分开。这是一件好事,因为如果您的数据库的多个用户想要做同样的事情,他们可以重新使用代码来完成。
例如,如果您想创建 "an Order for a Customer with several OrderLines, containing ordered Products, agreed Prices, amount, etc",您需要对数据库做几件事:检查客户是否已经存在,检查所有产品是否已经存在,检查是否有足够的物品等
这些东西通常是您不应该在 DbContext 中实现的东西,而应该在单独的 class.
中实现如果您添加如下功能:CreateOrder
,那么多个用户可以重复使用该功能。您只需测试一次,如果您决定更改订单模型中的某些内容,则只有一个地方需要更改订单的创建。
分离代表您的数据库 (DbContext) 的 class 的其他优点 从处理此数据的 class 可以更轻松地更改内部结构,而无需更改数据库的用户。您甚至可以决定从 Dapper 更改为 Entity Framework,而无需更改用法。这也使得出于测试目的模拟数据库变得更加容易。
CreateOrder、QueryOrder、UpdateOrder 等函数已经表明它们不是通用数据库操作,它们适用于订购数据库,而不适用于学校数据库。
这可能会导致工作单元可能不是单独 class 中所需功能的正确名称。几年前,工作单元主要是表示对数据库的操作,而不是真正的特定数据库,我对此不太确定,因为我很快就看到了真正的工作单元 class 不会增强我的 DbContext 的功能。
您经常会看到以下内容:
- 代表您的 数据库的 DbContext class:您创建的数据库,不是任何通用的数据库概念
- 一个存储库 class 表示将数据存储在某处的想法,它是如何存储的,它可以是一个 DbContext,也可以是一个 CSV 文件,或者字典的集合 class es 创建用于测试目的。这个存储库有一个 IQueryable,并具有添加/更新/删除的功能(根据需要
- 代表您的问题的 class:订购模型:添加订单/更新订单/获取客户订单:这个 class 确实了解订单的一切,例如它有一个 OrderTotal,它可能在您的 Ordering 数据库中找不到。
在 DbContext 之外,您有时可能需要 SQL,例如提高调用效率。在 Repository 之外,不应该看到您正在使用 SQL
考虑分离关注点:如何保存数据 (DbContext),如何 CRUD(创建、获取、更新等)数据 (Repository),如何使用数据(合并表)
我认为你想在你的工作单元中做的事情应该在存储库中完成。您的订购 class 应该创建存储库(创建 DbContext),查询多个项目以检查它必须添加/更新的数据,执行添加和更新并保存更改。之后,您的订购 class 应该处置存储库,而存储库又将处置 DbContext。
存储库 class 看起来与 DbContext class 非常相似。它有几个代表表格的集合。每组都将实现 IQueryable<...>
并允许添加/更新/删除,无论需要什么。
由于功能上的这种相似性,您可以省略存储库 class 并让您的订购 class 直接使用 DbContext。但是,请记住,如果将来您决定不再使用 entity framework,而是使用一些更新的概念,或者可能 return 回到 Dapper,甚至更低,那么变化会更大等级。 SQL 会渗透到您的订单中 class
选择什么
我认为你应该自己回答几个问题:
- 是否真的只有一个数据库应该由您的 DbContext 表示,是否应该在具有相同布局的第二个数据库中使用相同的 DbContext。想想测试数据库或开发数据库。让您的程序创建要使用的 DbContext 不是更容易/更好的可测试性/更好的可变性吗?
- 你的DbContext真的有一组Users吗:每个人都有删除的可能吗?去创造?会不会是有些程序只想查询数据(e-mail订单的程序),而订单程序需要添加Customers。也许另一个程序需要添加和更新产品,以及仓库中的产品数量。考虑为他们创建不同的存储库 classes。每个 Repository 获得相同的 DbContext,因为它们都在访问相同的数据库
- 类似地:只有一个数据处理class(上面提到的订购class):处理订单的流程应该能够更改产品价格并向库存添加商品吗?
您需要工厂的原因是,您不想让您的 "main" 程序决定它应该为现在的目的 运行 创建哪些项目。如果您自己创建项目,代码会容易得多:
订购流程的创建顺序:
IJwtHelper jwtHelper = ...;
// The product database: all functionality to do everything with Products and Orders
ProductDbContext dbContext = new ProductDbContext(...)
{
JwtHelper = jwtHelper,
...
};
// The Ordering repository: everything to place Orders,
// It can't change ProductPrices, nor can it stock the wharehouse
// So no AddProduct, not AddProductCount,
// of course it has TakeNrOfProducts, to decrease Stock of ordered Products
OrderingRepository repository = new OrderingRepository(...) {DbContext = dbContext};
// The ordering process: Create Order, find Order, ...
// when an order is created, it checks if items are in stock
// the prices of the items, if the Customer exists, etc.
using (OrderingProcess process = new OrderingProcess(...) {Repository = repository})
{
... // check if Customer exists, check if all items in stock, create the Order
process.SaveChanges();
}
处置进程时,处置存储库,进而处置 DbContext。
与通过电子邮件发送订单的过程类似:它不必检查产品,也不必创建客户,它只需要获取数据,并可能更新订单已通过电子邮件发送,或者电子邮件发送失败。
IJwtHelper jwtHelper = ...;
// The product database: all functionality to do everything with Products and Orders
ProductDbContext dbContext = new ProductDbContext(...) {JwtHelper = jwtHelper};
// The E-mail order repository: everything to fetch order data
// It can't change ProductPrices, nor can it stock the wharehouse
// It can't add Customers, nor create Orders
// However it can query a lot: customers, orders, ...
EmailOrderRepository repository = new EmailOrderRepository(...){DbContext = dbContext};
// The e-mail order process: fetch non-emailed orders,
// e-mail them and update the e-mail result
using (EmailOrderProcess process = new EmailOrderProcess(...){Repository = repository}
{
... // fetch the orders that are not e-mailed yet
// email the orders
// warning about orders that can't be emailed
// update successfully logged orders
repository.SaveChanges();
看看您使创建过程变得多简单,多用途:为 DbContext 提供不同的 JwtHelper,数据以不同的方式记录,为 Repository 提供不同的 DbContext,数据保存在不同的数据库,给进程一个不同的存储库,你将使用 Dapper 来执行你的查询。
测试会更容易:创建一个使用列表保存表的存储库,使用测试数据测试您的流程会很容易
数据库的更改会更容易。例如,如果您后来决定将您的数据库分成一个用于您的股票和股票价格,一个用于客户和订单,那么只需要更改一个存储库。 None 个进程会注意到此更改。
结论
不要让 classes 决定他们需要哪些对象。让 class 的创建者说:"hey, you need a DbContext? Use this one!" 这将省略工厂等
的需要将您的实际数据库 (DbContext) 与存储和检索数据 (Repository) 的概念分开,使用一个单独的 class 来处理数据,而不知道这些数据是如何存储或检索的(过程 class)
创建几个存储库,这些存储库只能访问执行任务所需的数据(+预期更改后将来可以预见的数据)。 Repositories不要做太多,但也不要做一个万能的。
创建进程 classes 只做他们想做的事。不要创建一个包含 20 个不同任务的进程 class。只会让它更难描述它应该做什么,更难测试它,更难改变任务
如果您想重用现有的实现,EF Core 5 现在提供开箱即用的 DbContext Factory: https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew#dbcontextfactory
确保正确处置它,因为它的实例不由应用程序的服务提供商管理。
查看微软文档 Using a DbContext factory