1

Is there any robust, elegant, and/or standard way of implementing the Specification pattern for in-memory collections (IEnumerable<T>, not IQueryable<T>) that includes shaping/projecting the results?

Using Func<T, bool> for criteria obviously covers only the Where clause/method, not the Select.

The idea I came up with so far is that I can include another delegate in my specification which (no pun intended) specifically covers the Select operation. The implementation could look like the following. As you can observe at the bottom, the Repository simply executes both Where and Select, passing in the delegate members of the specification.

This solution would seem to work fine, but I found out in numerous occasions that there were existing, better solutions to the problem I was solving, so it seemed reasonable to ask.

(The reason why I'm planning to go with the Specification pattern is that my app will probably need to show a lot of results in various shapes from in-memory collections of complex objects, and it would be neat to keep the querying stuff at a single, easy to find/manage place. But feel free to suggest something completely different.)

internal interface IMySpecification<TEntity, TResult>
{
    Func<TEntity, bool> Where { get; }
    Func<TEntity, TResult> Select { get; }
    bool IsSatisfiedBy(TEntity t);
    // ...  
}

internal interface IMyRepository<TEntity>
{
    // ...
    TResult GetSingle<TResult>(IMySpecification<TEntity, TResult> spec);
    IEnumerable<TResult> List<TResult>(IMySpecification<TEntity, TResult> spec);
}

internal class MyGenericRepository<T> : IMyRepository<T>
{
    protected IEnumerable<T> _collection;

    public MyGenericRepository(IEnumerable<T> list)
        => _collection = list;

    // ...

    public TResult GetSingle<TResult>(IMySpecification<T, TResult> spec)
        => List(spec).Single();

    public IEnumerable<TResult> List<TResult>(IMySpecification<T, TResult> spec)
        => (IEnumerable<TResult>)_collection.Where(spec.Where).Select(spec.Select);
}
Leaky
  • 3,088
  • 2
  • 26
  • 35
  • 3
    I'd just skip the generic repository (or at least not expose it to your business/application layer) and go for normal repositories with specific methods (`GetCustomerWithOrders()`etc.). Now you're just splitting up code that belongs together with an unnecessary abstraction as I see it. The only reason for the specification pattern is to filter very flexible in a lot of ways depending on parameters. The transform part has nothing to do with it. – Alexander Derck Sep 19 '18 at 20:30
  • 1
    @Gabor - I implemented something similar to this a couple years ago and it worked great. I agree with Alexander about the Select and the Where being uniquely distinct things though and I wouldn't force this concept into your specification. Reason: One consumer may want fields1 and 2 while another wants fields 1 and 3, even though they both only want "active" items (ie...your spec). What I did was use AutoMapper's projection to accomplish what Select is doing in your example. This way you can use TResult as your type to AM's ProjectTo method and let it figure it all out. – user1011627 Sep 19 '18 at 20:40
  • 1
    Here is a link to AM's extension library that provides projection: http://docs.automapper.org/en/stable/Queryable-Extensions.html – user1011627 Sep 19 '18 at 20:40
  • Thanks for the comments. :) Yes, actually I think I was vehement enough trying to cram this functionality in that I didn't notice I'm restricting the flexibility of the specification by combining it with shaping... (I'm checking that library. Meanwhile I also have to think through what is the level of flexibility that the app will require.) – Leaky Sep 19 '18 at 20:47
  • @AlexanderDerck, yes, I think I'll most likely skip the specifications, and encapsulate the queries in repositories, and probably let the consumers do the shaping. But then, given that the question actually represents a bad practice to begin with, I'll probably delete it. – Leaky Sep 19 '18 at 21:16
  • 1
    @GaborBarat Pretty solid question in my opinion, you never know who you might help in the future :-) – Alexander Derck Sep 19 '18 at 21:18
  • 1
    Thanks, @AlexanderDerck, maybe I'll leave it then. I'm just disappointed that virtually all my questions end up with realizing that I was trying to cram a square peg into a round hole... :D – Leaky Sep 19 '18 at 21:23
  • 2
    @GaborBarat I think most of us have that from time to time. When I'm stuck on finding the right abstraction, I try to actually take a step back and look at the bigger picture. What is actually the problem I'm trying to solve? What parts of that problem can I actually solve with my abstraction? Everyone loves to overengineer from time to time ;-) – Alexander Derck Sep 19 '18 at 21:26
  • 1
    Yes, exactly, @AlexanderDerck. I was a quite creative spaghetti coder years ago, but now that I learned patterns, I tend to develop a tunnel vision and end up applying them in contrived ways. But time will solve this too. :) I added an answer meanwhile; feel free to correct or expand it if you'd like. – Leaky Sep 19 '18 at 22:36

1 Answers1

2

After a short discussion in the comments, I see it fitting to answer my own question:

You generally shouldn't do this.

If you ended up here because you wanted to implement the same thing, chances are you need to take a step back, and consider what problem are you actually trying to solve.

By joining the specification with the shaping of the data, you largely defeat the purpose of the specification: the specification cannot be used in a flexible way any more by multiple consumers, since different consumers might very well want to work with differing shapes of data, chain/combine the specifications, etc.

  • If you don't need that flexibility, then probably you can remove the layer of specifications from your design (and e.g. simply expose methods on your repository).
  • If you do need that flexibility, let the consumers shape/map the data.

(Feel free to edit this answer if you have anything to add.)

Leaky
  • 3,088
  • 2
  • 26
  • 35