从去耦BL EF查询 - 扩展方法VS类的每个查询 [英] Decouple EF queries from BL - Extension Methods VS Class-Per-Query

查看:220
本文介绍了从去耦BL EF查询 - 扩展方法VS类的每个查询的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我看了几十个有关试图嘲弄\\假EF业务逻辑利弊帖子。
我还没有决定做什么 - 但有一点我知道的是 - 我要查询从业务逻辑分离。
在<一个href=\"http://stackoverflow.com/questions/6950433/reusable-querying-in-entity-framework-without-repository-how\">this帖子我看到拉吉斯拉夫已经回答有2个好方法:


  

      
  1. 让他们成为他们是和使用自定义扩展方法,其中,查询视图,映射数据库视图或自定义定义查询定义可重复使用的部分。

  2.   
  3. 揭露的每一个查询作为方法上的一些独立的类。方法
      决不能暴露的IQueryable并不得接受防爆pression为参数=
      整个查询逻辑必须被包裹在该方法中。但是,这将使
      类覆盖相关的方法很像库(只有一个
      可以嘲笑或伪造)。此实现接近于
      执行存储过程使用。

  4.   


  1. 你认为哪种方法更好的,为什么?

  2. 是否有任何缺点把查询自己的地方? (也许失去从EF或类似的东西一些功能)

  3. 我必须封装甚至像最简单的疑问:

     使用(MyDbContext实体=新MyDbContext)
    {
        用户的用户= entities.Users.Find(用户ID); //封装这个?    //有些BL code在这里
    }



解决方案

所以我想你的主要观点是你的code的可测性,不是吗?在这种情况下,你应该通过计算方法的责任要测试,也比使用单一职责模式重构code开始。

您例如code至少有三个职责:


  • 创建一个对象是一种责任 - 上下文是一个对象。此外,它是和对象你不想在你的单元测试使用,所以你必须移到别处它的创作。

  • 执行的查询是一种责任。此外,它是你想避免你的单元测试一种责任。

  • 做一些业务逻辑是一种责任

要简化测试,你应该重构你的code和划分这些职责分离的方法。

 公共类MyBLClass()
{
    公共无效MyBLMethod(INT用户id)
    {
        使用(IMyContext实体=的getContext())
        {
            用户的用户= GetUserFromDb(实体,用户id);            //有些BL code在这里
        }
    }    受保护的虚拟IMyContext的getContext()
    {
        返回新MyDbContext();
    }    受保护的虚拟用户GetUserFromDb(IMyDbContext实体,INT用户id)
    {
        返回entities.Users.Find(用户ID);
    }
}

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

  // NUnit的单元测试
[的TestFixture]
公共类MyBLClassTest:MyBLClass
{
    私有类FakeContext:IMyContext
    {
        //创建只是空的背景下实施的接口
    }    私人用户_testUser;    [测试]
    公共无效MyBLMethod_DoSomething()
    {
        //测试设置
        INT ID = 10;
        _testUser =新用户
            {
                ID = ID,
                //剩下的就是你预期的测试数据 - 那就是作假是什么
                //伪造方法返回简单的数据测试方法需要
            };        //测试方法的执行
        MyBLMethod(ID);        //测试验证
        //断言东西,你期望在_testUser实例发生
        //内MyBLMethod
    }    保护覆盖IMyContext的getContext()
    {
        返回新FakeContext();
    }    保护覆盖用户GetUserFromDb(IMyContext背景下,INT用户id)
    {
        返回_testUser.Id ==用户id? _testUser:空;
    }
}

当您添加更多的方法和应用的增长,你会重构这些查询的执行方法和上下文的工厂方法来分离类遵循类单一职责,以及 - 你会得到上下文工厂,要么某些查询提供商或在某些情况下,储存库(但该资源库将永远不会返回的IQueryable 或获得防爆pression 作为它的任何方法参数)。这也将让你以下内容,其中上下文的创建和最常用的查询将在一个集中的位置只能定义一次DRY原则。

所以最终你可以有这样的事情:

 公共类MyBLClass()
{
    私人IContextFactory _contextFactory;
    私人IUserQueryProvider _userProvider;    公共MyBLClass(IContextFactory contextFactory,IUserQueryProvider userProvider)
    {
        _contextFactory = contextFactory;
        _userProvider = userProvider;
    }    公共无效MyBLMethod(INT用户id)
    {
        使用(IMyContext实体= _contextFactory.GetContext())
        {
            用户的用户= _userProvider.GetSingle(实体,用户id);            //有些BL code在这里
        }
    }
}

如果这些接口看起来像:

 公共接口IContextFactory
{
    IMyContext的getContext();
}公共类MyContextFactory:IContextFactory
{
    公共IMyContext的getContext()
    {
        //这属于必要创建上下文的任何逻辑
        //比如你希望缓存每个HTTP请求上下文
        //你可以在这里实现逻辑。
        返回新MyDbContext();
    }
}

 公共接口IUserQueryProvider
{
    用户的getUser(INT用户id);    //为用户实体任何其他可重复使用的查询
    //查询非返回的IQueryable或接受防爆pression为参数
    //例如:IEnumerable的&lt;使用者&GT; GetActiveUsers();
}公共类MyUserQueryProvider:IUserQueryProvider
{
    公共用户的getUser(IMyContext背景下,INT用户id)
    {
        返回context.Users.Find(用户ID);
    }    //其他查询的实现    //只有内部查询的实现,你可以使用IQueryable的扩展方法
}

您现在测试将只使用假货的上下文工厂和查询提供。

  // NUnit的起订量+单元测试
[的TestFixture]
公共类MyBLClassTest
{
    私有类FakeContext:IMyContext
    {
        //创建只是空的背景下实施的接口
    }    [测试]
    公共无效MyBLMethod_DoSomething()
    {
        //测试设置
        INT ID = 10;
        VAR用户=新用户
            {
                ID = ID,
                //剩下的就是你预期的测试数据 - 那就是作假是什么
                //伪造方法返回简单的数据测试方法需要
            };        VAR contextFactory =新的模拟&LT; IContextFactory&GT;();
        contextFactory.Setup(F =&GT; f.GetContext())返回(新FakeContext());        VAR queryProvider =新的模拟&LT; IUserQueryProvider&GT;();
        queryProvider.Setup(F =&GT; f.GetUser(It.IsAny&LT; IContextFactory&GT;(),ID))返回(用户)。        //测试方法的执行
        VAR myBLClass =新MyBLClass(contextFactory.Object,queryProvider.Object);
        myBLClass.MyBLMethod(ID);        //测试验证
        //断言东西,你期望在用户实例发生
        //内MyBLMethod
    }
}

这将是有点其中应该有参考传递给它的构造背景资料库的情况下,在它之前注入到你的商务舱不同。
你的商业类仍然可以定义一些查询,这是从来没有在任何其他类使用 - 这些查询是最有可能的逻辑的一部分。您还可以使用扩展方法来定义查询的某些部分可重复使用,但你必须使用你的核心业务逻辑之外要单元测试(无论是在查询执行方法或查询提供/存储库)的扩展方法。这将允许您方便地查询造假提供商或查询执行的方法。

我看了<一个href=\"http://stackoverflow.com/questions/10940873/unit-testing-ef-how-to-extract-ef-$c$c-out-from-bl\">your previous问题并想过写一篇博客文章有关的话题,但我认为大约有EF测试是这个答案的核心。

编辑:

Repository是不涉及到你原来的问题不同的主题。具体的库仍然是有效的模式。我们不反对库,<一个href=\"http://stackoverflow.com/questions/5625746/generic-repository-with-ef-4-1-what-is-the-point/5626884#5626884\">we反对通用仓库的,因为他们没有提供任何额外的功能,不解决任何问题。

问题是,单单库不解决任何问题。有三种模式这必须使用在一起,形成正确的抽象:仓库,工作和规范的单位。这三种方法都已经可以在EF:DbSet /对象集作为仓库,的DbContext / ObjectContext的作为作品的单位和LINQ to Entities作为规范。到处都提到通用库的自定义实现的主要问题是,他们只替换自定义实现工作资源库和单位,但仍取决于原规格=>抽象是不完整的,在那里伪造资料库中的方法一样的行为是在测试中泄漏伪造集/上下文。

我的查询提供的主要缺点是,你需要执行的任何查询显式方法。在仓库的情况下,你不会有这样的方法,你将只有几个方法接受规范(但同样这些规范,应在干燥的原则确定),这将建立查询过滤条件,订货等。

 公共接口IUserRepository
{
    用户查找(INT用户id);
    IEnumerable的&lt;使用者&GT;的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.

这篇关于从去耦BL EF查询 - 扩展方法VS类的每个查询的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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