The specification pattern and variance
In a previous post I described the specification pattern, its uses and benefits and also gave a pretty neat implementation. However, there was something that I didn't discuss: the variance aspect of the pattern. It is a very important aspect, but that post was long enough as it was — so now, it's time to dig in.
The basic setup
So let's assume that you have two model classes: one to represent books, and one to represent audiobooks. An audiobook is a book after all, so it might make sense to derive the AudioBook class from the Book class:
public class Book
{
public int BookId { get; set; }
public string Author { get; set; }
public string Title { get; set; }
}
public class AudioBook : Book
{
public int Duration { get; set; }
}
Then, since you have two different entities, you create two different repositories: one for the books and one for the audiobooks. The repository for the AudioBooks would probably have a method like this (following the classes and setup I gave in that aforementioned last post:
public interface IRepository<T>
{
IReadOnlyList<T> List(FilterSpecification<T> filterSpecification);
}
public class AudioBookRepository : IRepository<AudioBook>
{
private readonly BookContext bookContext;
public AudioBookRepository(BookContext bookContext)
{
this.bookContext = bookContext;
}
public IReadOnlyList<AudioBook> List(FilterSpecification<AudioBook> filterSpecification)
{
return this.bookContext.AudioBooks.Where(filterSpecification).ToList();
}
}
So far so good. Now, you create two different specifications: one for all books that checks the author, and one only for the audio books that checks the duration:
public class AuthorSpecification : FilterSpecification<Book>
{
private readonly string author;
public AuthorSpecification(string author)
{
this.author = author;
}
public override Expression<Func<Book, bool>> SpecificationExpression =>
book => book.Author == author;
}
public class ShortbookSpecification : FilterSpecification<AudioBook>
{
public override Expression<Func<AudioBook, bool>> SpecificationExpression =>
audioBook => audioBook.Duration < 500;
}
And here's when it all crumbles to dust. Question? Should you be able to use the AuthorSpecification
for audiobooks as well? Of course you should. That specification checks the Author
property, which all books have. Including the audio books (since audio books are actually books). But can you? Not really. The problem is that List()
method of the AudioBookRepository
class accepts FilterSpecification<AudioBook>
or one of its descendants. And here's the problem: even though AudioBook
descends from Book
, no such relation exists between FilterSpecification<AudioBook>
and FilterSpecification<Book>
.
Variance
Essentially what we need is to be able to assign an object of type FilterSpecification<T>
to a reference (parameter, local variable, field etc.) of type FilterSpecification<T2>
, where T2 : T. This is known as contravariance, and it is an existing language and framework feature: you can assign an Action<object>
to an Action<string>
or an IEqualityComparer<object>
to an IEqualityComparer<string>
. It's quite easy to leverage this feature actually: you just have to mark the type paramater with the in
keyword, and then make sure to only use that type argument for input parameter types (i.e. no return values of that type). But of course, in this case, it's not that easy:
- Variance (either co-, or contra) can only be applied for delegates and interfaces (and arrays, God forbid). So the base class must be changed to an interface.
- If we change it to an interface, the overloaded operators must be dropped. That's just sad.
- Even if the type is changed to an interface, here's the worst part: even though
Func<T,bool>
is contravariant forT
,Expression<Func<T,bool>>
is not (nor could it be, because it is neither an interface nor a delegate). To conclude: we are screwed.
Rewriting the expression
No matter what I did, I couldn't get around the limitation of the Expression
class itself not being contravariant. And I came to the conclusion that the only way was to rewrite the expression: that is, to create a new expression with the same body, but with a different input parameter.
public static class FilterConverter
{
private class ConvertedSpecification<TType> : FilterSpecification<TType>
{
private readonly Expression<Func<TType, bool>> specificationExpression;
public ConvertedSpecification(Expression<Func<TType, bool>> specificationExpression)
{
this.specificationExpression = specificationExpression;
}
public override Expression<Func<TType, bool>> SpecificationExpression => specificationExpression;
}
public static FilterSpecification<TDerived> ConvertSpecification<TBase, TDerived>(FilterSpecification<TBase> original) where TDerived : TBase
{
var expr1 = original.SpecificationExpression;
var arg = Expression.Parameter(typeof(TDerived));
var newBody = new ReplaceParameterVisitor { { expr1.Parameters.Single(), arg } }.Visit(expr1.Body);
return new ConvertedSpecification<TDerived>(Expression.Lambda<Func<TDerived, bool>>(newBody, arg));
}
}
So basically, I use the ReplaceParameterVisitor
from the previous post (you really should read it, by the way), to replace the parameter of the original expression with a new parameter whose type if the descendant type. And I can use a generic contraint to determine how the two type parameters relate to each other.
Of course this is not contravariance. This is a workaround to support contravariance. And to be honest, I would be quite content with this solution if I could find a way to nicely integrate it into the existing architecture. Of course, since real contravariance is not a possibility, I would have to use some other magic with generics and generic type constraints (and this also makes using the fancy overloaded operators impossible). But I couldn't even do that. Ideally, this would be something like this:
public IReadOnlyList<AudioBook> List<TBase>(FilterSpecification<TBase> filterSpecification) where AudioBook : TBase
{
return this.bookContext.AudioBooks.Where(
FilterConverter.ConvertSpecification<TBase, AudioBook(filterSpecification))
.ToList();
}
But this is not how generic type constraints work. With the constraints, you can specify a base class or interface for a type parameter, not the other way around. This would work for covariance, but now for contravariance. So for now, The only way to achieve contravariance is to hack it manually. But if anyone has any ideas, I'm open to comments. Now that I have two posts on the subject, I thought it was time to put everything up on Github, so you can check the whole code (for the previous post and this post as well) in this Github repo.