将 EF 查询与 BL 分离 - 扩展方法 VS Class-Per-Query [英] Decouple EF queries from BL - Extension Methods VS Class-Per-Query

查看:16
本文介绍了将 EF 查询与 BL 分离 - 扩展方法 VS Class-Per-Query的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

<块引用>

  1. 让他们在原地不动,并使用自定义扩展方法、查询视图、映射数据库视图或自定义定义查询来定义可重用部分.
  2. 将每个查询公开为某个单独类的方法.方法不得公开 IQueryable 并且不得接受表达式作为参数 =整个查询逻辑必须包装在方法中.但这会让你的类涵盖了很像存储库的相关方法(唯一的一个可以被嘲笑或伪造).这个实现接近于与存储过程一起使用的实现.

  1. 您认为哪种方法更好,为什么?
  2. 将查询放在自己的位置是否有任何缺点?(可能会从 EF 或类似的东西中丢失一些功能)
  3. 我是否必须封装最简单的查询,例如:

    using (MyDbContext entity = new MyDbContext){用户用户 = entity.Users.Find(userId);//封装这个?//这里有一些 BL 代码}

解决方案

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

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

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

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

公共类 MyBLClass(){public void MyBLMethod(int userId){使用 (IMyContext 实体 = GetContext()){User user = GetUserFromDb(entities, userId);//这里有一些 BL 代码}}受保护的虚拟 IMyContext GetContext(){返回新的 MyDbContext();}受保护的虚拟用户 GetUserFromDb(IMyDbContext 实体,int userId){return entity.Users.Find(userId);}}

现在单元测试业务逻辑应该是小菜一碟,因为您的单元测试可以继承您的类和伪造的上下文工厂方法和查询执行方法,并完全独立于 EF.

//NUnit 单元测试[测试装置]公共类 MyBLClassTest : MyBLClass{私有类 FakeContext : IMyContext{//创建上下文接口的空实现}私人用户_testUser;[测试]public void MyBLMethod_DoSomething(){//测试设置整数 ID = 10;_testUser = 新用户{身份证=身份证,//其余是您预期的测试数据 - 这就是伪造的意义//伪造的方法只返回测试方法期望的数据};//执行被测方法MyBLMethod(id);//测试验证//断言你期望在 _testUser 实例上发生的事情//在 MyBLMethod 中}受保护的覆盖 IMyContext GetContext(){返回新的 FakeContext();}protected override User GetUserFromDb(IMyContext context, int userId){返回 _testUser.Id == userId ?_testUser:空;}}

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

所以最后你可以有这样的东西:

公共类 MyBLClass(){私有 IContextFactory _contextFactory;私有 IUserQueryProvider _userProvider;公共 MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider){_contextFactory = contextFactory;_userProvider = userProvider;}public void MyBLMethod(int userId){使用 (IMyContext 实体 = _contextFactory.GetContext()){用户用户 = _userProvider.GetSingle(entities, userId);//这里有一些 BL 代码}}}

这些界面的样子:

公共接口 IContextFactory{IMyContext GetContext();}公共类 MyContextFactory : IContextFactory{公共 IMyContext GetContext(){//这里属于创建上下文所需的任何逻辑//例如,如果您想缓存每个 HTTP 请求的上下文//你可以在这里实现逻辑.返回新的 MyDbContext();}}

公共接口 IUserQueryProvider{用户 GetUser(int userId);//用户实体的任何其他可重用查询//非查询返回 IQueryable 或接受表达式作为参数//例如:IEnumerable<用户>GetActiveUsers();}公共类 MyUserQueryProvider : IUserQueryProvider{公共用户 GetUser(IMyContext 上下文,int userId){返回 context.Users.Find(userId);}//其他查询的实现//只有在查询实现中,您才能在 IQueryable 上使用扩展方法}

您的测试现在将仅对上下文工厂和查询提供程序使用假货.

//NUnit + Moq 单元测试[测试装置]公共类 MyBLClassTest{私有类 FakeContext : IMyContext{//创建上下文接口的空实现}[测试]public void MyBLMethod_DoSomething(){//测试设置整数 ID = 10;var 用户 = 新用户{身份证=身份证,//其余是您预期的测试数据 - 这就是伪造的意义//伪造的方法只返回测试方法期望的数据};var contextFactory = new Mock();contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext());var queryProvider = new Mock();queryProvider.Setup(f => f.GetUser(It.IsAny(), id)).Returns(user);//执行被测方法var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object);myBLClass.MyBLMethod(id);//测试验证//断言你期望在用户实例上发生的事情//在 MyBLMethod 中}}

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

我看到了你之前的问题 并考虑过写一篇关于该主题的博客文章,但我对使用 EF 进行测试的核心观点在此答案中.

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

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

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

公共接口 IUserRepository{用户查找(int userId);IEnumerable<用户>FindAll(ISpecification 规范);}

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

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

I have read dozens of posts about PROs and CONs of trying to mock \ fake EF in the business logic. I have not yet decided what to do - but one thing I know is - I have to separate the queries from the business logic. In this post I saw that Ladislav has answered that there are 2 good ways:

  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.

  1. Which method do you think is better any why ?
  2. Are there ANY downsides to put the queries in their own place ? (maybe losing some functionality from EF or something like that)
  3. Do I have to encapsulate even the simplest queries like:

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

解决方案

So I guess your main point is testability of your code, isn't it? In such case you should start by counting responsibilities of the method you want to test and than refactor your code using single responsibility pattern.

Your example code has at least three responsibilities:

  • Creating an object is a responsibility - context is an object. Moreover it is and object you don't want to use in your unit test so you must move its creation elsewhere.
  • Executing query is a responsibility. Moreover it is a responsibility you would like to avoid in your unit test.
  • Doing some business logic is a responsibility

To simplify testing you should refactor your code and divide those responsibilities to separate methods.

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);
    }
}

Now unit testing business logic should be piece of cake because your unit test can inherit your class and fake context factory method and query execution method and become fully independent on 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;
    }
}

As you add more methods and your application grows you will refactor those query execution methods and context factory method to separate classes to follow single responsibility on classes as well - you will get context factory and either some query provider or in some cases repository (but that repository will never return IQueryable or get Expression as parameter in any of its methods). This will also allow you following DRY principle where your context creation and most commonly used queries will be defined only once on one central place.

So at the end you can have something like this:

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
        }
    }
}

Where those interfaces will look like:

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();
    } 
}

and

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
}

Your test will now only use fakes for context factory and query provider.

// 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
    }
}

It would be little bit different in case of repository which should have reference to context passed to its constructor prior to injecting it to your business class. Your business class can still define some queries which are never use in any other classes - those queries are most probably part of its logic. You can also use extension methods to define some reusable part of queries but you must always use those extension methods outside of your core business logic which you want to unit test (either in query execution methods or in query provider / repository). That will allow you easy faking query provider or query execution methods.

I saw your previous question and thought about writing a blog post about that topic but the core of my opinion about testing with EF is in this answer.

Edit:

Repository is different topic which doesn't relate to your original question. Specific repository is still valid pattern. We are not against repositories, we are against generic repositories because they don't provide any additional features and don't solve any problem.

The problem is that repository alone doesn't solve anything. There are three patterns which have to be used together to form proper abstraction: Repository, Unit of Work and Specifications. All three are already available in EF: DbSet / ObjectSet as repositories, DbContext / ObjectContext as Unit of works and Linq to Entities as specifications. The main problem with custom implementation of generic repositories mentioned everywhere is that they replace only repository and unit of work with custom implementation but still depend on original specifications => abstraction is incomplete and it is leaking in tests where faked repository behaves in the same way as faked set / context.

The main disadvantage of my query provider is explicit method for any query you will need to execute. In case of repository you will not have such methods you will have just few methods accepting specification (but again those specifications should be defined in DRY principle) which will build query filtering conditions, ordering etc.

public interface IUserRepository
{
    User Find(int userId);
    IEnumerable<User> FindAll(ISpecification spec);
}

The discussion of this topic is far beyond the scope of this question and it requires you to do some self study.

Btw. mocking and faking has different purpose - you fake a call if you need to get testing data from method in the dependency and you mock the call if you need to assert that method on dependency was called with expected arguments.

这篇关于将 EF 查询与 BL 分离 - 扩展方法 VS Class-Per-Query的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆