I am working as part of the team on the web api application. The Entity Framework used as ORM, I have the idea to keep all selectors in services. E.g. we have Cat
table, then would be CatSelectorsService
.
class CatSelectorsService {
public Expressions<Func<Cat, bool>> NeedToPlay() {
var date = DateTime.Now.AddHours(8);
return it => it.LastTimePlayed > date;
}
public Expression<Func<Cat, CatView> CatView() {
var needToPlay = NeedToPlay();
return it => new CatView {
Id = it.Id,
NeedToPlay = needToPlay.Invoke(it)
}
}
}
// sample usage
var catViews = context.Cats.Select(catSelectosService.CatView());
var catIdsNeedToPlay = context.Cats.Where(catSelectorsService.NeedToPlay()).Select(it => it.Id);
That approach really helps to avoid code duplication and help organize business logic of the application. Initially was planning to have exactly one selectors service per entity. So, CatSelectorsService
- Cat - entity name
- Selectors - what class handles
- Service - can be recived via constructor DI from interface, e.g.
public MyClass {
private readonly ICatSelectorsService catSelectorsService;
public MyClass(ICatSelectorsService catSelectorsService) {
this.catSelectorsService = catSelectorsService;
}
}
Good organization. On practice have meet two issues with this approach:
- Circular depencies, when some selectors want to use other selectors, f.e. if have Cats and Users, we might want to the next inside selectors service
// inside cats selectors service
public Expressions<Func<Cat, CatView>> CatView() {
return it => new CatView {
Users = it.Users.Select(userSelectorsService.UserView()), // here reference userSelectors in catSelectors
}
}
// inside users selectors service
public Expressions<Func<User, UserViewWithCats>> UserView() {
return it => new CatView {
Users = it.Users.Select(catSelectorsService.CatView()), // here reference catSelectors in userSelectors
}
}
current solution to create shared selectors and reference them, e.g.
public Expression<Func<Cat, CatView>> CatView() => sharedSelectors.CatView(); // shared selectors can be imported in other services
- Testing, in the first sample, I'll copy it to here:
class CatSelectorsService {
public Expressions<Func<Cat, bool>> NeedToPlay() {
var date = DateTime.Now.AddHours(8);
return it => it.LastTimePlayed > date;
}
public Expression<Func<Cat, CatView> CatView() {
var needToPlay = NeedToPlay();
return it => new CatView {
Id = it.Id,
NeedToPlay = needToPlay.Invoke(it)
}
}
}
// sample usage
var catViews = context.Cats.Select(catSelectosService.CatView());
var catIdsNeedToPlay = context.Cats.Where(catSelectorsService.NeedToPlay()).Select(it => it.Id);
CatView
uses NeedToPlay
when writing tests for cat selectors, I want to mock NeedToPlay
, but to do it with xunit, I have to move NeedToPlay
to separate service. Or use some workaround like harmony, I have tried harmony, and it is works, however it also make the ecosystem of the application harder and I'd like to keep it simple. Harmony for unit testing is rather advanced library - some issues with running in parallel. So ended with next:
class SharedSelectorsService {
public Expressions<Func<Cat, bool>> NeedToPlay() {
var date = DateTime.Now.AddHours(8);
return it => it.LastTimePlayed > date;
}
public Expression<Func<Cat, CatView> CatView() {
var needToPlay = NeedToPlay();
return it => new CatView {
Id = it.Id,
NeedToPlay = needToPlay.Invoke(it)
}
}
}
class CatSelectorsService {
private readonly ISharedSelectorsService sharedSelectorsService;
public CatSelectorsService(ISharedSelectorsService sharedSelectorsService) {
this.sharedSelectorsService = sharedSelectorsService;
}
public Expressions<Func<Cat, bool>> NeedToPlay() => sharedSelectorsService.NeedToPlay()
public Expression<Func<Cat, CatView> CatView() => sharedSelectorsService.CatView();
}
I don't like that finally have lot of services, and one them is just a documentation(CatSelectorsService
), to keep things organized, but for me it seems currently the best solution. However I really wonder, can be something better?
PS. Also have thought about creating the documentation, but I have not experience and seems to me that self documentated the code cheaper then support of actual version of documenation.
Update - why want to mock the NeedToPlay
Here hidden two features:
- Cat view
- Cat need to play
We have separate tests for Cat View
and tests for need to play
. Should we change cat view
tests each time cat need to play
feature changed? Should we setup data of Need to play
in the Cat view
? For me yes looks like an illustration of interface segregation principle violation.
NeedToPlay
returns a specification that describes when are cats considered in need of play). Besides, what's hard-to-read is team-specific.Expression<Func<____, ____>
bit). If that's the case, they should leverage their way of thinking and associated abstractions even if the code may seem at first a bit opaque to a general audience, because they are not writing for a general audience - they are the ones who will be reading the code most of the time.