这种方法是否适合管理 DbContext?

时间:2021-08-01 17:00:54

标签: c# asp.net asp.net-mvc entity-framework

我有一个在其构造函数中要求 DbContext 的存储库,然后我使用 ninject 来解决此依赖关系,并将对象范围设置为 InRequestScope,因为这意味着每个对象实例化一个对象HTTP 请求,但我不确定 HTTP 请求何时真正发生?是在加载应用程序时吗?还是在我们调用 SaveChanges() 时发生? 我管理 DbContext 的方法是这样的,正如我所说,我有一个存储库要求一个上下文,然后控制器在其构造函数中要求这个存储库:

public class PageGroupsController : Controller
{
    IGenericRepository<PageGroup> _repository;
    public PageGroupsController(IGenericRepository<PageGroup> repository)
    {
        _repository = repository;
    }

    // GET: Admin/PageGroups
    public ActionResult Index()
    {
        return View(_repository.Get());
    }
}

和存储库:

public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
    private DbContext _context;
    public GenericRepository(DbContext context)
    {
        _context = context;
    }

    public IEnumerable<TEntity> Get()
    {
        return _context.Set<TEntity>().ToList();
    }
}

还有 NinjectWebCommon.cs,这是我解决依赖项的地方:

private static void RegisterServices(IKernel kernel)
{
    kernel.Bind<DbContext>().To<MyCmsContext>().InRequestScope();
    kernel.Bind<IGenericRepository<PageGroup>>().To<GenericRepository<PageGroup>>();
}

这种方法真的好吗?我不想在我的控制器中到处使用 using {var db = new DbContext},也不想为整个应用程序创建一个单一的上下文。这种方法是否等于 using 方法(我的意思是在 using 块中查询我们需要的内容)?但耦合更少?

1 个答案:

答案 0 :(得分:2)

每次从任何 Web 客户端调用控制器操作时,都是一个请求。因此,当有人访问您的网站并访问通过路由解析的 /Pagegroups/Index 时,这就是一个请求。当您从客户端执行 Form.Submit 时,即请求,进行 Ajax 调用,即请求。

您是否希望为每个请求构建 DbContext 范围?绝对的,而不是“超过”请求。对于简单的应用程序,在操作中使用 using() 非常好,但它确实添加了一些样板代码,在各处重复它。在更复杂、更长久的应用程序中,您可能想要进行单元测试,或者可能具有更复杂的逻辑,这些逻辑可以通过分解成更小的共享组件而受益,using 块共享 DbContext 有点混乱,所以范围为请求的注入 DbContext 可以很好地满足该目的。为请求提供服务的每个类实例都被赋予完全相同的 DbContext 实例。

您不希望 DbContext 的范围比请求(即单例)更长,因为虽然来自一个客户端的请求可能是连续的,但来自多个用户的请求不是。 Web 服务器将在不同的线程上一次响应各种用户请求。 EF 的 DbContext 不是线程安全的。这让新开发人员在测试时似乎在他们的机器上一切正常,却发现一旦部署到服务器并处理并发请求,错误就会开始出现。

此外,随着 DbContext 的老化,它们在跟踪更多实体实例时变得越来越大,速度也越来越慢。这会导致逐渐的性能损失,以及由于 DbContext 提供不反映可能来自其他来源的数据更改的缓存实例的问题。一个新的开发团队可能会遇到跨线程问题,但会引入锁定等,因为他们想要使用 EF 的缓存而不是使用较短的生命周期。 (假设 DbContext 创建起来总是“昂贵的”[他们不是!:])这通常是团队呼吁放弃 EF 的原因,因为它“缓慢”而没有意识到设计决策阻止了他们利用大部分EF 的能力。

作为一般提示,我强烈建议在使用 EF 时避免通用存储库模式。除了将数据逻辑归类之外,它不会给您带来任何好处。 EF 的强大之处在于能够处理针对对象的操作及其关系到 SQL 的转换。它不仅仅是深入了解数据的包装器。像这样的方法:

public IEnumerable<TEntity> Get()
{
    return _context.Set<TEntity>().ToList();
}

完全适得其反。如果您有数万条记录想要排序和分页,请执行以下操作:

var items = repository.Get()
    .OrderBy(x => x.CreatedAt)
    .Skip(pageNumber * pageSize)
    .Take(pageSize)
    .ToList();

问题在于,您的存储库告诉 EF 在进行任何排序或分页之前加载、跟踪和具体化整个表。更糟糕的是,如果要进行任何过滤(基于搜索条件的 Where 子句等),那么在存储库返回所有记录之前,不会应用这些。

相反,如果你只是让你的控制器方法这样做:

var items = _context.PageGroups
    .OrderBy(x => x.CreatedAt)
    .Skip(pageNumber * pageSize)
    .Take(pageSize)
    .ToList();

然后 EF 将编写一个 SQL 查询,该查询执行排序并仅获取该单页实体。利用带有 Select 的 Projection 来取回您需要的详细信息,或预先加载相关实体也是如此。尝试使用通用存储库执行此操作会变得非常复杂(尝试传递表达式,或尝试处理排序、分页等的大量参数)或非常低效,通常两者兼而有之。

我建议考虑使用存储库的两个原因是:单元测试,以及处理低级常见过滤,例如软删除 (IsActive) 和/或多租户 (OwnerId) 类型的数据。基本上任何时候数据通常都必须符合存储库可以在一个地方强制执行的标准规则。在这些情况下,我推荐服务于相应控制器的非通用存储库。例如,如果我有一个 ManagePageGroupsController,我就会有一个 ManagePageGroupsRepository 来为它提供服务。这种模式的主要区别在于存储库返回 IQueryable<TEntity> 而不是 IEnumerable<TEntity> 甚至 TEntity。 (除非“创建”方法的结果)这允许消费者仍然像处理 DbContext 一样处理排序、分页、投影等,而存储库可以确保 Where 子句适用于低级规则,断言访问权限,并且可以轻松模拟存储库作为单元测试的替代品。 (模拟服务于 IQueryable 的存储库方法比模拟 DbContext/DbSet 更容易)除非您的应用程序将使用单元测试,或者有一些低级常见注意事项,如软删除,我” d 建议不要为尝试抽象 DbContext 的复杂性而烦恼,并充分利用 EF 提供的一切。

编辑:扩展IQueryable

一旦您确定存储库用于测试或基础过滤(如 IsActive),您可以通过返回 IQueryable 而不是 IEnumerable 来避免很多复杂性。

存储库的使用者通常希望执行诸如过滤结果、排序结果、分页结果、将结果项目到 DTO/ViewModel 之类的操作,或者以其他方式使用结果来执行检查,例如获取计数或检查是否存在任何项目。< /p>

如上所述,方法如下:

public IEnumerable<PageGroup> Get()
{
    return _context.PageGroups
        .Where(x => x.IsActive)
        .ToList();
}

将在考虑任何这些考虑之前返回数据库中的所有项目以由应用程序服务器存储在内存中。如果我们想支持过滤:

public IEnumerable<PageGroup> Get(PageGroupFilters filters)
{
    var query _context.PageGroups
        .Where(x => x.IsActive);

    if (!string.IsNullOrEmpty(filters.Name)
        query = query.Where(x => x.Name.StartsWith(filters.Name));
    // Repeat for any other supported filters.

    return query.ToList();
}

然后按条件添加顺序:

public IEnumerable<PageGroup> Get(PageGroupFilters filters, IEnumerable<OrderByCondition> orderBy)
{
    var query _context.PageGroups
        .Where(x => x.IsActive);

    if (!string.IsNullOrEmpty(filters.Name)
        query = query.Where(x => x.Name.StartsWith(filters.Name));
    // Repeat for any other supported filters.

    foreach(var condition in orderBy)
    {
        if (condition.Direction == Directions.Ascending)
           query = query.OrderBy(condition.Expression);
        else
           query = query.OrderByDescending(condition.Expression);
    }
    return query.ToList();
}

然后分页: public IEnumerable Get(PageGroupFilters 过滤器,IEnumerable orderBy,int pageNumber = 1,int pageSize = 0) { var 查询 _context.PageGroups .Where(x => x.IsActive);

    if (!string.IsNullOrEmpty(filters.Name)
        query = query.Where(x => x.Name.StartsWith(filters.Name));
    // Repeat for any other supported filters.

    foreach(var condition in orderBy)
    {
        if (condition.Direction == Directions.Ascending)
           query = query.OrderBy(condition.Expression);
        else
           query = query.OrderByDescending(condition.Expression);
    }

    if (pageSize != 0)
        query = query.Skip(pageNumber * pageSize).Take(pageSize);
        

    return query.ToList();
}

你可以看到这是怎么回事。您可能只需要适用实体的数量,或者检查是否至少存在一个。如上所述,这仍将始终返回实体列表。如果我们有可能需要急切加载或投影到 DTO/ViewModel 的相关实体,还有更多工作要做,或者需要接受内存/性能损失。

或者,您可以添加多种方法来处理过滤场景(GetAll 与 GetBySource 等)并传递 Func<Expression<T>> 作为参数以尝试和概括实现。这增加了相当大的复杂性,或者在消费者可用的方面留下了空白。通常,存储库模式的理由是从业务逻辑中抽象出数据逻辑 (ORM)。然而,这要么削弱了您的系统的性能和/或能力,要么在您通过抽象引入表达式的那一刻就是谎言。任何传递到存储库并提供给 EF 的表达式必须符合 EF 的规则(没有自定义函数,或 EF 无法转换为 SQL 的系统方法等),否则您必须增加相当大的解析和转换复杂度存储库中的表达式以确保一切正常。然后最重要的是,支持同步与异步。它加起来很快。

替代方案是IQueryable

public IQueryable<PageGroup> Get()
{
    return _context.PageGroups
        .Where(x => x.IsActive);
}

现在当消费者想要添加过滤、排序和分页时:

var pageGroups = Repository.Get()
    .Where(x => x.Name.StartsWith(searchText)
    .OrderBy(x => x.Name)
    .Skip(pageNumber * pageSize).Take(pageSize)
    .ToList();

如果他们只想得到一个计数:

var pageGroups = Repository.Get()
    .Where(x => x.Name.StartsWith(searchText)
    .Count();

如果我们正在处理更复杂的实体,例如带有 Orders 和 OrderLines 的 Customer,我们可以预先加载或投影:

// Top 50 customers by order count.
var customer = ManageCustomerRepository.Get()
    .Select(x => new CustomerSummaryViewModel
    {
        CustomerId = x.Id,
        Name = x.Name,
        OrderCount = x.Orders.Count()
    }).OrderByDescending(x => x.Orders.Count())
    .Take(50)
    .ToList(); 

即使我通常通过 ID 获取项目并想要像“GetById”这样的存储库方法,我也会返回 IQueryable<T> 而不是 T

public IQueryable<PageGroup> GetById(pageGroupid)
{
    return _context.PageGroups
        .Where(x => x.PageGroupId == pageGroupId);
    // rather than returning a PageGroup and using
    // return _context.PageGroups.SingleOrDefault(x =>x.PageGroupId == pageGroupid);
}

为什么?因为我的调用者仍然可以利用将项目投影到视图模型,决定是否需要预先加载任何内容,或者使用 Any() 执行诸如存在检查之类的操作。

Repository 不会抽象 DbContext 以将 EF 隐藏在业务逻辑中,而是启用一组基本规则,例如检查 IsActive,因此我们不必担心在任何地方添加 .Where(x => x.IsActive) 和忘记的后果。也很容易模拟。例如创建我们存储库的 Get 方法的模拟:

var mockRepository = new Mock<PageGroupRepository>();
mockRepository.Setup(x => x.Get())
    .Returns(buildSamplePageGroups());

其中 buildSamplePageGroups 方法保存构建适合测试的测试数据集的代码。该方法返回一个包含测试数据的 List<PageGroup>。如果您需要支持针对存储库的 async 操作,那么从测试的角度来看,这只会变得更加复杂。这需要一个适合测试数据的容器,而不是 List<T>

编辑 2:通用存储库。

通用存储库的问题在于,您最终会通过导航属性等详细信息将实体分隔开来,它们是相关的。在创建订单时,您要处理客户、地址、订单、产品等,其中创建订单的行为通常只需要有关这些实体的信息的子集。如果我有一个 ManageOrdersController 来处理编辑和创建订单和通用存储库,我最终会依赖于订单、客户、产品等的多个存储库。

通用存储库的典型论点是单一职责原则 (SRP) 和不重复自己 (DNRY/DRY) OrderRepository 仅负责订单,CustomerRepository 仅负责客户。但是,您同样可以争辩说,以这种方式组织存储库破坏 SRP,因为 SRP 背后的原则是,其中的代码应该有一个且只有一个更改的原因。尤其是在没有 IQueryable 实现的情况下,多个不同控制器和相关服务使用的存储库引用的公开方法可能会因多种原因而发生更改,因为每个控制器对存储库的操作和输出都有不同的关注。 DRY 是一个不同的论点,归结为偏好。 DRY 的关键是应该考虑代码是相同,而不仅仅是相似。使用 IQueryable 实现有一个有效的论据,您可以轻松地在多个存储库中拥有相同的方法,即在 ManageOrderRepository 和 ManageProductsRepository 中获取产品与将其集中在 ManageOrderController 和 ManageProductController 引用的 ProductsRepository 中。然而,GetProducts 的实现相当简单,几乎是单行代码。与产品相关的控制器的 GetProducts 方法可能对获取活动产品和非活动产品感兴趣,其中获取产品以完成订单可能只会查看活动产品。归结为一个决定,如果尝试满足 DRY 是否值得管理对少数(或更多)存储库依赖项的引用,而不是单个存储库。 (考虑到测试的模拟设置之类的事情)通用存储库特别期望每个实体类型的所有方法符合特定模式。泛型在实现相同的情况下很棒,但在代码可以从允许“相似”但提供独特的变化中受益的那一刻无法实现该目标。

相反,我选择将我的存储库与具有 ManageOrdersRepository 的控制器配对。这个存储库和其中的方法只有一个改变的原因,那就是为 ManageOrdersController 服务。虽然其他存储库可能对这个存储库所做的一些实体有类似的需求,但它们可以自由更改以满足其控制器的需求,而不会影响管理订单流程。这使构造函数依赖项保持紧凑且易于模拟。