Elegant way to shape the data when implementing Specification pattern for in-memory collections

610 Views Asked by At

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);
}
1

There are 1 best solutions below

0
On

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