entity framework - Decouple EF queries from BL - Extension Methods VS Class-Per-Query -
i have read dozens of posts pros , cons of trying mock \ fake ef in business logic. have not yet decided - 1 thing know - have separate queries business logic. in this post saw ladislav has answered there 2 ways:
- let them , use custom extension methods, query views, mapped database views or custom defining queries define reusable parts.
- expose every single query method on separate class. method mustn't expose iqueryable , mustn't accept expression parameter = whole query logic must wrapped in method. make class covering related methods repository (the 1 can mocked or faked). implementation close implementation used stored procedures.
- which method think better why ?
- are there any downsides put queries in own place ? (maybe losing functionality ef or that)
do have encapsulate simplest queries like:
using (mydbcontext entities = new mydbcontext) { user user = entities.users.find(userid); // encapsulate ? // bl code here }
so guess main point testability of code, isn't it? in such case should start counting responsibilities of method want test , refactor code using single responsibility pattern.
your example code has @ least 3 responsibilities:
- creating object responsibility - context object. , object don't want use in unit test must move creation elsewhere.
- executing query responsibility. responsibility avoid in unit test.
- doing business logic responsibility
to simplify testing should refactor code , divide responsibilities separate methods.
public class myblclass() { public void myblmethod(int userid) { using (imycontext entities = getcontext()) { user user = getuserfromdb(entities, userid); // 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 piece of cake because unit test can inherit class , fake context factory method , query execution method , become independent on ef.
// nunit unit test [testfixture] public class myblclasstest : myblclass { private class fakecontext : imycontext { // create empty implementation of context interface } private user _testuser; [test] public void myblmethod_dosomething() { // test setup int id = 10; _testuser = new user { id = id, // rest expected test data - faking // faked method returns data test method expects }; // execution of method under test myblmethod(id); // test validation // assert expect 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 add more methods , application grows refactor query execution methods , context factory method separate classes follow single responsibility on classes - context factory , either query provider or in cases repository (but repository never return iqueryable
or expression
parameter in of methods). allow following dry principle context creation , commonly used queries defined once on 1 central place.
so @ end can have 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); // bl code here } } }
where interfaces like:
public interface icontextfactory { imycontext getcontext(); } public class mycontextfactory : icontextfactory { public imycontext getcontext() { // here belongs logic necessary create context // if example want cache context per http request // can implement logic here. return new mydbcontext(); } }
and
public interface iuserqueryprovider { user getuser(int userid); // other reusable queries user entities // non of queries returns iqueryable or accepts expression parameter // example: ienumerable<user> getactiveusers(); } public class myuserqueryprovider : iuserqueryprovider { public user getuser(imycontext context, int userid) { return context.users.find(userid); } // implementation of other queries // inside query implementations can use extension methods on iqueryable }
your test use fakes context factory , query provider.
// nunit + moq unit test [testfixture] public class myblclasstest { private class fakecontext : imycontext { // create empty implementation of context interface } [test] public void myblmethod_dosomething() { // test setup int id = 10; var user = new user { id = id, // rest expected test data - faking // faked method returns data 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 expect happen on user instance // inside myblmethod } }
it little bit different in case of repository should have reference context passed constructor prior injecting business class. business class can still define queries never use in other classes - queries part of logic. can use extension methods define reusable part of queries must use extension methods outside of core business logic want unit test (either in query execution methods or in query provider / repository). allow easy faking query provider or query execution methods.
i saw your previous question , thought writing blog post topic core of opinion testing ef in answer.
edit:
repository different topic doesn't relate original question. specific repository still valid pattern. not against repositories, we against generic repositories because don't provide additional features , don't solve problem.
the problem repository alone doesn't solve anything. there 3 patterns have used form proper abstraction: repository, unit of work , specifications. 3 available in ef: dbset / objectset repositories, dbcontext / objectcontext unit of works , linq entities specifications. main problem custom implementation of generic repositories mentioned everywhere replace repository , unit of work custom implementation still depend on original specifications => abstraction incomplete , leaking in tests faked repository behaves in same way faked set / context.
the main disadvantage of query provider explicit method query need execute. in case of repository not have such methods have few methods accepting specification (but again specifications should defined in dry principle) build query filtering conditions, ordering etc.
public interface iuserrepository { user find(int userid); ienumerable<user> findall(ispecification spec); }
the discussion of topic far beyond scope of question , requires self study.
btw. mocking , faking has different purpose - fake call if need testing data method in dependency , mock call if need assert method on dependency called expected arguments.
Comments
Post a Comment