在 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