从BL扩展EF查询 - 扩展方法VS Class-Per-Query

Joh*_*ner 7 architecture extension-methods unit-testing entity-framework

我已经阅读了几十篇关于试图在业务逻辑中模拟\假EF的PRO和CON的帖子.我还没有决定做什么 - 但我知道的一件事是 - 我必须将查询与业务逻辑分开.在这篇文章中我看到拉迪斯拉夫回答说有两种好方法:

  1. 让它们成为它们的位置,并使用自定义扩展方法,查询视图,映射数据库视图或自定义定义查询来定义可重用部分.
  2. 将每个查询作为方法公开在某个单独的类上.该方法不得暴露IQueryable,也不得接受Expression作为参数=整个查询逻辑必须包含在方法中.但这将使您的类覆盖相关方法很像存储库(唯一可以被模拟或伪造的).此实现与存储过程使用的实现很接近.
  1. 您认为哪种方法更好?
  2. 是否有任何缺点将查询放在自己的位置?(可能会从EF或类似的东西中丢失一些功能)
  3. 我是否必须封装最简单的查询,例如:

    using (MyDbContext entities = new MyDbContext)
    {
        User user = entities.Users.Find(userId);  // ENCAPSULATE THIS ?
    
        // Some BL Code here
    }
    
    Run Code Online (Sandbox Code Playgroud)

Lad*_*nka 7

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

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

  • 创建对象是一种责任 - 上下文是一个对象.此外,您不希望在单元测试中使用它,因此您必须将其创建移动到其他位置.
  • 执行查询是一项责任.此外,您希望在单元测试中避免使用它.
  • 做一些业务逻辑是一种责任

为了简化测试,您应该重构代码并将这些职责划分为单独的方法.

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

现在单元测试业务逻辑应该是小菜一碟,因为您的单元测试可以继承您的类和伪上下文工厂方法和查询执行方法,并完全独立于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;
    }
}
Run Code Online (Sandbox Code Playgroud)

当您添加更多方法并且您的应用程序增长时,您将重构那些查询执行方法和上下文工厂方法以分离类以遵循对类的单一责任 - 您将获得上下文工厂和某个查询提供程序或在某些情况下存储库(但是存储库永远不会返回IQueryableExpression在其任何方法中获取参数).这也将允许您遵循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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这些接口的位置如下:

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

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

您的测试现在只会将伪造用于上下文工厂和查询提供程序.

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

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

我看到了你之前的问题,并考虑过写一篇关于该主题的博客文章,但我对EF测试的核心意见在于这个答案.

编辑:

存储库是与您原始问题无关的不同主题.特定存储库仍然是有效的模式.我们不反对存储库,我们反对通用存储库,因为它们不提供任何其他功能并且不解决任何问题.

问题是存储库本身并没有解决任何问题.有三种模式必须一起使用才能形成适当的抽象:存储库,工作单元和规范.所有这三个在EF中都可用:DbSet/ObjectSet作为存储库,DbContext/ObjectContext作为工作单元,Linq到实体作为规范.任何地方提到的通用存储库的自定义实现的主要问题是它们仅使用自定义实现替换存储库和工作单元但仍依赖于原始规范=>抽象是不完整的并且它在测试中泄漏,其中伪造的存储库的行为方式与伪造的集合/背景.

我的查询提供程序的主要缺点是您需要执行的任何查询的显式方法.在存储库的情况下,您将没有这样的方法,您将只有几个方法接受规范(但同样这些规范应该在DRY原则中定义),这将构建查询过滤条件,排序等.

public interface IUserRepository
{
    User Find(int userId);
    IEnumerable<User> FindAll(ISpecification spec);
}
Run Code Online (Sandbox Code Playgroud)

对这个主题的讨论远远超出了这个问题的范围,它要求你做一些自学.

顺便说一句.模拟和伪造有不同的目的 - 如果你需要从依赖项中的方法获取测试数据,你就假装一个调用,如果需要断言依赖的方法是用预期的参数调用的话,你就模拟调用.