在实体框架中创建通用 DbContext 工厂

rea*_*ist 2 c# asp.net entity-framework asp.net-core-2.1

我正在使用 .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();
    }
}
Run Code Online (Sandbox Code Playgroud)

工作单元.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();
    }
 }
Run Code Online (Sandbox Code Playgroud)

MyDbContext.cs

public class MyDbContext : DbContext
{
    private readonly IJwtHelper jwtHelper;

    public MyDbContext(IJwtHelper jwtHelper) : base()
    {
        this.jwtHelper = jwtHelper;
    }
}
Run Code Online (Sandbox Code Playgroud)

Har*_*lse 7

所以你有一个数据库和一个代表这个数据库的类:你的DbContext,它应该代表你的数据库中的表和表之间的关系,仅此而已。

您决定将数据库操作与数据库本身分开。这是一件好事,因为如果您的数据库的多个用户想要做同样的事情,他们可以重用代码来做这件事。

例如,如果您想为具有多个 OrderLines 的客户创建一个订单,其中包含订购的产品、商定的价格、金额等,您需要对您的数据库做几件事:检查客户是否已经存在,检查如果所有产品已经存在,请检查是否有足够的项目等。

这些东西通常是你不应该在你的 DbContext 中实现的东西,而是在一个单独的类中。

如果您添加一个函数,例如:CreateOrder,那么多个用户可以重复使用该函数。您只需对此进行一次测试,如果您决定更改订单模型中的某些内容,那么您只需在一处更改订单的创建。

将代表您的数据库 (DbContext) 的类与处理此数据的类分开的其他优点是,可以更轻松地更改内部结构,而无需更改数据库的用户。您甚至可以决定从 Dapper 更改为 Entity Framework,而无需更改用法。这也使得模拟数据库以进行测试变得更加容易。

像 CreateOrder、QueryOrder、UpdateOrder 这样的函数已经表明它们不是通用的数据库操作,它们是用于 Ordering 数据库的,而不是用于 School 数据库的。

这可能会导致工作单元可能不是您想要在单独的类中实现的功能的正确名称。几年前,工作单元主要用于表示对数据库的操作,而不是真正的特定数据库,我对此不太确定,因为我很快就看到真正的工作单元类不增强我的 DbContext 的功能。

您经常会看到以下内容:

  • 代表您的数据库的DbContext 类:您创建的数据库,而不是数据库的任何通用概念
  • 一个 Repository 类,表示将数据存储在某处的想法,它是如何存储的,它可以是 DbContext,也可以是 CSV 文件,或者是为测试目的而创建的 Dictionary 类的集合。这个 Repository 有一个 IQueryable,以及添加/更新/删除的功能(根据需要)
  • 代表您的问题的类:订购模型:添加订单/更新订单/获取客户的订单:该类真正了解订单的所有信息,例如它有一个 OrderTotal,这可能在您的 Ordering 中找不到数据库。

在 DbContext 之外,您有时可能需要 SQL,例如提高调用效率。在 Repository 之外,不应看到您正在使用 SQL

考虑分离关注点:如何保存数据(DbContext),如何 CRUD(创建、获取、更新等)数据(存储库),如何使用数据(组合表)

我认为你想在你的工作单元中做的事情应该在存储库中完成。您的 Ordering 类应该创建 Repository(它创建 DbContext),查询多个项目以检查它必须添加/更新的数据,执行添加和更新并保存更改。之后,您的排序类应该处置存储库,这反过来将处置 DbContext。

Repository 类看起来与 DbContext 类非常相似。它有几个代表表格的集合。每个集合都将实现IQueryable<...>并允许添加/更新/删除,无论需要什么。

由于函数的这种相似性,您可以省略 Repository 类并让 Ordering 类直接使用 DbContext。但是,请记住,如果将来您决定不再使用实体框架而是使用一些更新的概念,或者可能返回到 Dapper,甚至更低的级别,那么更改将会更大。SQL 将渗透到您的 Ordering 类中

选择什么

我认为你应该自己回答几个问题:

  • 是否真的只有一个数据库应该由您的 DbContext 表示,是否应该在具有相同布局的第二个数据库中使用相同的 DbContext。想想测试数据库或开发数据库。让您的程序创建要使用的 DbContext 不是更容易/更好的可测试性/更好的可变性吗?
  • 您的 DbContext 是否真的有一组用户:每个人都可以删除吗?创造?会不会是有些程序只想查询数据(发送订单的程序),而订单程序需要添加Customers。也许另一个程序需要添加和更新产品,以及仓库中的产品数量。考虑为它们创建不同的 Repository 类。每个 Repository 获得相同的 DbContext,因为它们都访问相同的数据库
  • 类似地:只有一个数据处理类(上面提到的订购类):处理订单的过程是否应该能够更改产品价格并将项目添加到库存中?

您需要工厂的原因是因为您不想让“主”程序决定它应该为它现在运行的目的创建哪些项目。如果您自己创建项目,代码会容易得多:

订购流程的创建顺序:

 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();
}
Run Code Online (Sandbox Code Playgroud)

当进程被处置时,存储库被处置,它依次处置 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();
Run Code Online (Sandbox Code Playgroud)

看看你让创建过程变得多么容易,你使它变得更加通用:给 DbContext 一个不同的 JwtHelper,数据以不同的方式记录,给 Repository 一个不同的 DbContext,数据保存在不同的数据库中,给处理不同的存储库,您将使用 Dapper 来执行您的查询。

测试将更容易:创建一个使用列表来保存表的存储库,并且使用测试数据测试您的过程将变得容易

数据库中的更改将更容易。例如,如果您后来决定将您的数据库分成一个用于您的股票和股票价格,一个用于客户和订单,则只需更改一个存储库。没有一个进程会注意到这个变化。

结论

不要让类决定他们需要哪些对象。让类的创建者说:“嘿,您需要一个 DbContext?使用这个!” 这将省略工厂等的需要

将您的实际数据库(DbContext)与存储和检索数据(Repository)的概念分开,使用一个单独的类来处理数据而不知道这些数据是如何存储或检索的(流程类)

创建几个只能访问他们执行任务所需的数据的存储库(+预期更改后可以预见的数据)。不要做太多的 Repositories,也不要做一个什么都能做的。

创建仅执行其应执行的操作的流程类。不要创建一个包含 20 个不同任务的流程类。它只会让它更难描述它应该做什么,更难测试它,更难改变任务