gpt4 book ai didi

entity-framework - 将 EF 查询与 BL 分离 - 扩展方法 VS Class-Per-Query

转载 作者:行者123 更新时间:2023-12-04 11:56:37 24 4
gpt4 key购买 nike

我已经阅读了数十篇关于试图在业务逻辑中模拟\假 EF 的优点和缺点的帖子。
我还没有决定要做什么 - 但我知道的一件事是 - 我必须将查询与业务逻辑分开。
this post看到拉迪斯拉夫回答说有2个好方法:

  1. Let them be where they are and use custom extension methods, query views, mapped database views or custom defining queries to define reusable parts.
  2. Expose every single query as method on some separate class. The method mustn't expose IQueryable and mustn't accept Expression as parameter = whole query logic must be wrapped in the method. But this will make your class covering related methods much like repository (the only one which can be mocked or faked). This implementation is close to implementation used with stored procedures.

  • 你认为哪种方法更好,为什么?
  • 有没有任何 将查询放在自己的位置的缺点? (可能会从 EF 或类似的东西中丢失一些功能)
  • 我是否必须封装最简单的查询,例如:
    using (MyDbContext entities = new MyDbContext)
    {
    User user = entities.Users.Find(userId); // ENCAPSULATE THIS ?

    // Some BL Code here
    }
  • 最佳答案

    所以我想你的主要观点是你的代码的可测试性,不是吗?在这种情况下,您应该首先计算要测试的方法的职责,然后使用单一职责模式重构代码。

    您的示例代码至少有三个职责:

  • 创建对象是一种责任——上下文是一个对象。此外,它是您不想在单元测试中使用的对象,因此您必须将其创建移到其他地方。
  • 执行查询是一种责任。此外,这是您希望在单元测试中避免的责任。
  • 做一些业务逻辑是一种责任

  • 为了简化测试,您应该重构代码并将这些职责划分为单独的方法。
    public class MyBLClass()
    {
    public void MyBLMethod(int userId)
    {
    using (IMyContext entities = GetContext())
    {
    User user = GetUserFromDb(entities, userId);

    // Some BL Code here
    }
    }

    protected virtual IMyContext GetContext()
    {
    return new MyDbContext();
    }

    protected virtual User GetUserFromDb(IMyDbContext entities, int userId)
    {
    return entities.Users.Find(userId);
    }
    }

    现在单元测试业务逻辑应该是小菜一碟,因为您的单元测试可以继承您的类和伪上下文工厂方法和查询执行方法,并完全独立于 EF。
    // NUnit unit test
    [TestFixture]
    public class MyBLClassTest : MyBLClass
    {
    private class FakeContext : IMyContext
    {
    // Create just empty implementation of context interface
    }

    private User _testUser;

    [Test]
    public void MyBLMethod_DoSomething()
    {
    // Test setup
    int id = 10;
    _testUser = new User
    {
    Id = id,
    // rest is your expected test data - that is what faking is about
    // faked method returns simply data your test method expects
    };

    // Execution of method under test
    MyBLMethod(id);

    // Test validation
    // Assert something you expect to happen on _testUser instance
    // inside MyBLMethod
    }

    protected override IMyContext GetContext()
    {
    return new FakeContext();
    }

    protected override User GetUserFromDb(IMyContext context, int userId)
    {
    return _testUser.Id == userId ? _testUser : null;
    }
    }

    随着您添加更多方法并且您的应用程序增长,您将重构这些查询执行方法和上下文工厂方法以将类分开以遵循类的单一职责 - 您将获得上下文工厂和一些查询提供程序或在某些情况下的存储库(但存储库永远不会返回 IQueryable 或获取 Expression 作为其任何方法中的参数)。这也将允许您遵循 DRY 原则,即您的上下文创建和最常用的查询将仅在一个中心位置定义一次。

    所以最后你可以有这样的东西:
    public class MyBLClass()
    {
    private IContextFactory _contextFactory;
    private IUserQueryProvider _userProvider;

    public MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider)
    {
    _contextFactory = contextFactory;
    _userProvider = userProvider;
    }

    public void MyBLMethod(int userId)
    {
    using (IMyContext entities = _contextFactory.GetContext())
    {
    User user = _userProvider.GetSingle(entities, userId);

    // Some BL Code here
    }
    }
    }

    这些接口(interface)看起来像:
    public interface IContextFactory 
    {
    IMyContext GetContext();
    }

    public class MyContextFactory : IContextFactory
    {
    public IMyContext GetContext()
    {
    // Here belongs any logic necessary to create context
    // If you for example want to cache context per HTTP request
    // you can implement logic here.
    return new MyDbContext();
    }
    }


    public interface IUserQueryProvider
    {
    User GetUser(int userId);

    // Any other reusable queries for user entities
    // Non of queries returns IQueryable or accepts Expression as parameter
    // For example: IEnumerable<User> GetActiveUsers();
    }

    public class MyUserQueryProvider : IUserQueryProvider
    {
    public User GetUser(IMyContext context, int userId)
    {
    return context.Users.Find(userId);
    }

    // Implementation of other queries

    // Only inside query implementations you can use extension methods on IQueryable
    }

    您的测试现在将只对上下文工厂和查询提供程序使用假货。
    // NUnit + Moq unit test
    [TestFixture]
    public class MyBLClassTest
    {
    private class FakeContext : IMyContext
    {
    // Create just empty implementation of context interface
    }

    [Test]
    public void MyBLMethod_DoSomething()
    {
    // Test setup
    int id = 10;
    var user = new User
    {
    Id = id,
    // rest is your expected test data - that is what faking is about
    // faked method returns simply data your test method expects
    };

    var contextFactory = new Mock<IContextFactory>();
    contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext());

    var queryProvider = new Mock<IUserQueryProvider>();
    queryProvider.Setup(f => f.GetUser(It.IsAny<IContextFactory>(), id)).Returns(user);

    // Execution of method under test
    var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object);
    myBLClass.MyBLMethod(id);

    // Test validation
    // Assert something you expect to happen on user instance
    // inside MyBLMethod
    }
    }

    如果存储库应该在将上下文注入(inject)您的业务类之前将其引用传递给其构造函数,则情况会有所不同。
    您的业​​务类仍然可以定义一些从未在任何其他类中使用的查询——这些查询很可能是其逻辑的一部分。您还可以使用扩展方法来定义查询的某些可重用部分,但您必须始终在要进行单元测试的核心业务逻辑之外使用这些扩展方法(在查询执行方法中或在查询提供程序/存储库中)。这将允许您轻松伪造查询提供程序或查询执行方法。

    我看到了 your previous question并考虑过写一篇关于该主题的博客文章,但我关于使用 EF 进行测试的核心观点在此答案中。

    编辑:

    存储库是不同的主题,与您的原始问题无关。特定的存储库仍然是有效的模式。我们不反对存储库, we are against generic repositories因为它们不提供任何附加功能,也不能解决任何问题。

    问题是仅存储库并不能解决任何问题。必须结合使用三种模式来形成适当的抽象:存储库、工作单元和规范。所有三个都已在 EF 中可用:DbSet/ObjectSet 作为存储库,DbContext/ObjectContext 作为工作单元,Linq to Entities 作为规范。到处都提到的通用存储库的自定义实现的主要问题是,它们仅用自定义实现替换了存储库和工作单元,但仍依赖于原始规范 => 抽象不完整,并且它在测试中泄漏,其中伪造的存储库的行为方式与伪造的集合/上下文。

    我的查询提供程序的主要缺点是您需要执行的任何查询的显式方法。在存储库的情况下,您将没有这样的方法,您将只有少数接受规范的方法(但这些规范应再次以 DRY 原则定义),这些方法将构建查询过滤条件、排序等。
    public interface IUserRepository
    {
    User Find(int userId);
    IEnumerable<User> FindAll(ISpecification spec);
    }

    这个话题的讨论远远超出了这个问题的范围,它需要你做一些自学。

    顺便提一句。模拟和伪造有不同的目的 - 如果您需要从依赖项中的方法获取测试数据,您可以伪造一个调用,如果您需要断言该依赖项的方法是使用预期参数调用的,则您模拟该调用。

    关于entity-framework - 将 EF 查询与 BL 分离 - 扩展方法 VS Class-Per-Query,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10967921/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com