2

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:

  1. 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
  1. 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:

  1. Cat view
  2. 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.

9
  • 1
    @DocBrown: "a whole bunch of hard-to-read boilerplate code" - actually, it's perhaps unfamiliar, but once you get over that, it's not hard to read at all, and it's not boilerplate code, it's business logic (it's really a variant of the Specification pattern (which is, BTW, not described very well on Wikipedia, which focuses eclusively on a composite-based variant of it)). Each of these methods returns a spec of what to select from the data context (e.g. NeedToPlay returns a specification that describes when are cats considered in need of play). Besides, what's hard-to-read is team-specific. Commented Oct 31, 2020 at 12:47
  • 1
    P.S. In the sense that if the team is used to thinking in these terms, it's not hard-to-read for them (they'll just gloss over the 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. Commented Oct 31, 2020 at 12:52
  • 1
    @DocBrown: Well, I wrote this primarily for the benefit of the OP (as I didn't want them to be too quick to abandon their idea based on a single negative comment), even though I'm addressing you. Also, I thought it was appropriate to mention you out of respect and out of courtesy, given that I'm contending your point of view. Commented Oct 31, 2020 at 13:37
  • @DocBrown - Yes, I would like to hear some feedback from the OP too, but since this is work-related, and it's the weekend, maybe it'll take a day or so. Commented Nov 1, 2020 at 9:56
  • @ Nikita: I'd also be interested to hear why do you feel the need to mock NeedToPlay? At first glance, there doesn't seem to be a good reason for that. Commented Nov 1, 2020 at 9:56

2 Answers 2

1

Your CatSelectorsService is mixing two very different things:

  • Filters (i.e. Func<T, bool>), used for getting a subset of items
  • Mappings (i.e Func<T, TargetType>), used to convert items from one to the other

These are two things that are quite different from one another, and each have a (different) home already, but you've instead housed them together in CatSelectorsService. This feels forced to me.

Filters are essentially queries, and should be structured as such. There are varied ways of doing so. You could house them in specific repositories (e.g. CatRepository), or you could be using actual query objects or CQRS (e.g. GetCatsForPlayingQuery).

Your selector service stores the query logic, but not the actual data retrieval. I don't see the purpose in separating these in this way, as a query is essentially comprised of both. I'm not saying you can't promote reusability here, but having a flipbook of filter logic isn't particularly useful and takes more overhead than it's worth.

Mappings are notably different from queries, and while queries can incorporate a mapping, there is value to keeping mappings separate as the likelihood of substituting/reusing mappings across queries is something that will render you benefits.

Based on your current use case, I see no reason to not use Automapper here. It provides all the features you need, in a much more streamlined process. Especially when you consider .ProjectTo<TTargetType>(), you can see how much cleaner this same approach can be done.

In short, the idea of separating your mappings from your queries is good, but the implementation can be done a lot cleaner than it currently is being done. I suggest either using Automapper (or any equivalent mapping library), or at least taking a page from their book on how you can manage the mappings in a clean and readable fashion.

On practice have meet two issues with this approach:

  1. 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

If your concern is about the instantiation of the services; then circular dependencies aren't an issue if your dependencies are registered as anything but transient. Transient circular dependencies will cause a stack overflow, but scoped or singleton dependencies will not (since the same instance will be reused in the dependency graph).

If your concern is that you run into an infinite loop when fetching an actual query from the service, then there's a problem with how you're expecting these separate queries to interact. That's not inherently caused by circular dependency injection, but rather with actually circularly depending on your own logic.

Circular dependencies can be valid, but they need to be handled carefully. Whether you choose to take close care, or instead outright avoid doing it in the first place, is both your decision and very contextual.

  1. Testing

As I mentioned, splitting queries from their mappings has their purpose, but when testing, you're generally going to want to test the whole query. That includes the data fetching, filtering, and mapping. At least, when the mapping is part of the actual query (which is the case when you use Automapper's ProjectTo).

If the mapping is a separate step that is only done after the query has been executed, then you should indeed test the query and the mapping separately.

Though in my experience across several companies, mapping tests are commonly avoided as they're an exercise in duplication, since the test body itself is pretty much describing the mapping again. Mappings are tested as part of the output of a e.g. a query (or query service), but mappings themselves are not tested in isolation (other than Automapper's AssertConfigurationIsValid).

Whether you choose to do this isolated mapping testing or not is up to you. There are arguments pro and con.

11
  • Yes, like the word projection here more then mapping) select Name, Id from Users where UserId = 1 here Name, Id is a projection of Users where UserId = 1, the selectors are located separately as they are used on with different sources, and sometime they even can be combined, e.g. select Name, Id from Users where UserId = 1 and select Name, Id, Age from Users where UserId = 1 base projection name, id and extended name, id, age. Or when one projection use another, see how CatView use NeedToPlay in my answer. Have used word selector as it often used in linq .Select()
    – Nikita
    Commented Nov 2, 2020 at 13:11
  • I will take a look to Automapper at free time, however, I skeptical about this lib in my use-case)) because it maps objects, not manage expressions (it => new { it.Id} is an expression), so possibly it would be not as flexible and perfomant as I wish with linq-to-sql. Very possible Automapper good solution to map in-memory objects.
    – Nikita
    Commented Nov 2, 2020 at 13:15
  • "you're generally going to want to test the whole query" you are absolutely right) we have one general integration test per each query to ensure the resulting linq-query translate to sql and runs on the database. And unit tests for mappings just to cover some edge cases, f.e. sometimes the selector can be used as mapper, e.g. CatView.Compile()(cat) then it would be linq-to-objects and some other from linq-to-sql rules would play here
    – Nikita
    Commented Nov 2, 2020 at 13:23
  • @Nikita: it => new { it.Id} Anonymous types have their use, but I would advise against them for actual query results. This is a situation in which you definitely want to be using a named type for documentative purposes. I think your system enables overuse of anonymous types in this way.
    – Flater
    Commented Nov 2, 2020 at 13:25
  • no anonymous, usually: selector = it => new SomeView { }
    – Nikita
    Commented Nov 2, 2020 at 13:26
0

Thank you for all the comments under the question. As mentioned by Filip in the comments, it is for Specification. Also often some selectors can be reused. F.e. NeedToPlay can be used in the validation, and in some views.

So, after weekends, and some playing with code, the thing have been understood, the unit of work for developer is smth like implemented feature. So here to different features CatView and CatNeedsToPlay under one business domain, so they should be placed on separate services.

// service per feature
class CatNeedToPlaySelectorsService : ICatNeedsToPlaySelectorsService {
    public Expressions<Func<Cat, bool>> NeedToPlay() {
        var date = DateTime.Now.AddHours(8);
        return it => it.LastTimePlayed > date;
    }
}
 
// service per feature
class CatViewSelectorsService : ICatViewSelectorsService {
    private readonly ICanNeedToPlaySelectorsService catNeedToPlaySelectorsService;
    public CatViewSelectorsService() {
        this.catNeedToPlaySelectorsService = catNeedToPlaySelectorsService;
    }
 
    public Expression<Func<Cat, CatView> CatView() {
        var needToPlay = catNeedToPlaySelectorsService.NeedToPlay();
        return it => new CatView {
            Id = it.Id,
            NeedToPlay = needToPlay.Invoke(it)
        }.Expand(); // see LinqKit library
    }
}
 
// {EntityName}SelectorsService to keep organized
class CatSelectorsService : ICatSelectorsService {
    private readonly ICanNeedToPlaySelectorsService catNeedToPlaySelectorsService;
    private readonly ICatViewSelectorsService catViewSelectorsService;
    public CatSelectorsService(ICanNeedToPlaySelectorsService catNeedToPlaySelectorsService, ICatViewSelectorsService catViewSelectorsService) {
        this.catNeedToPlaySelectorsService = catNeedToPlaySelectorsService;
        this.catViewSelectorsService = catViewSelectorsService;
    }
     
    public Expression<Func<Cat, CatView> CatView() => this.catViewSelectorsService.CatView();
    public Expressions<Func<Cat, bool>> NeedToPlay() => this.catNeedToPlaySelectorsService.NeedToPlay();
}
  1. Much less merge conflicts, as every developer working on its own feature in the separate file, not in the same file
  2. Simpler tests - no redundant setups, services are easy to mock with widely used Mock<T>
  3. No redundant dependencies, f.e. if need some service only for CatView it would be imported only to CatView without effect on NeedToPlay

Conclusion, it is nice (independent of development language) when features are located in separate files, as separation on different files really minifies merged conflicts amount and simplify writing of unit tests. Also it is more comfortable to look over one small file then over one big file. However in this case need some way to organize all the files, f.e. group on folders or some other way.

PS. The NeedsToPlay.Invoke(it) made by LinqKit library

PPS. Also have written the ExpressionsService for case when selectors can be combined, please put comment if need the code of ExpressionsService

listItem = it => new { it.Id, it.Name }
// next is the same as: detailsItem = it => new { it.Id, id.Name, it.Details };
detailsItem = expressionsService.Combine(listItem, it => new { it.Details });

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.