First of all, sorry for the question's title, it was hard to come up with a way to phrase it, let me explain the situation.
I am using the Specification pattern to perform db filtering with Entity Framework, and avoid doing it on memory (I roughly followed this article). My base specification class is something like this:
public abstract class Specification<T> : ISpecification<T>{
public abstract Expression<Func<T, bool>> FilterExpr();
public bool IsSatisfied(T entity)
{
Func<T, bool> func = this.FilterExpr().Compile();
return func(entity);
}
public Specification<T> And(Specification<T> otherSpec)
{
return new CombinedSpecification<T>(this, otherSpec);
}
}
From this base Specification class, multiple Strongly-typed specifications are derived, which work well on their own. However, the problem arises when trying to combine specifications of inherited types. For example, let's say I have the following models:
public abstract class Person
{
public int Age {get; set;}
public string Name {get; set;}
}
public class Baby:Person
{
public bool CanTalk {get; set;}
}
Now, I create the respective specifications to be able to filter the persons on the database:
public class NameSpec : Specification<Person>
{
private string name;
public Namespec(string aName)
{
this.name = aName;
}
public override Expression<Func<Person, bool>> FilterExpr()
{
return p => p.Name == this.name;
}
}
public class IsAbleToTalkSpec : Specification<Baby>
{
public override Expression<Func<Baby, bool>> FilterExpr()
{
return p => p.CanTalk == true;
}
}
So finally, let's say I want to filter for every baby named John who can talk:
var johnSpec = new NameSpec("John");
var combinedSpecification = johnSpec.And(new IsAbleToTalkSpec());
List<Baby> result = myRepository.Find(combinedSpecification);
Despite my models being properly binded to the DB via the EF configuration, doing this results in a compilation error, because there's no way a Specification<Baby>
can be converted to a Specification<Person>
when combining them, despite the mentioned inheritance. I understand why this happens, but I have no idea how to solve this without creating a NameSpec<Baby>
instead of reusing the NameSpec<Person>
, which scales horribly as my models grow. Additionaly, here is my CombinedSpecification<T>
class for reference:
internal class CombinedSpecification<T> : Specification<T>
{
private Specification<T> leftSpec;
private Specification<T> rightSpec;
public CombinedSpecification(Specification<T> aSpec, Specification<T> otherSpec)
{
this.leftSpec = aSpec;
this.rightSpec = otherSpec;
}
public override Expression<Func<T, bool>> FilterExpr()
{
var parameter = this.leftSpec.Parameters[0];
var combined = Expression.AndAlso(
leftSpec.Body,
rightSpec.Body.ReplaceParam(rightSpec.Parameters[0], parameter)
);
return Expression.Lambda<Func<T, bool>>(combined, parameter);
}
}
Thanks in advance for taking the time to read this lengthy rambling, I hope I was clear enough at describing my problem.
CodePudding user response:
Your class design contradicts what you aim to achieve with it.
The generic type you're using dictates the type of the object you pass into it. That is the type you have chosen to work with. But then you want to pass different (sub)types and automagically have them upcast the base type into the derived type. That's just not something the language allows even when putting generics aside (barring implicit conversions, which are not relevant here).
From a general OOP perspective, when you pass data using a base type:
public void DoStuff(Person p)
{
// logic
}
That inner logic can only work under the assumption that p
is a Person
. While it is possible to upcast, this is generally indicative of a bad OOP design and to be avoided in most cases.
You wouldn't do this:
public void DoStuff(object o)
{
var p = o as Person;
}
And therefore you shouldn't be doing this either:
public void DoStuff(Person p)
{
var b = p as Baby;
}
The principle is exactly the same.
Even though you're using generics, you're really doing the same thing here. Just like how I decided the type of my method parameter in the above snippet, you decide the generic type. In either case, once we've chosen a base type, we must therefore work with that given base type and should not try to sneakily upcast to a derived type.
There are a lot of ways to fix the issue at hand. I suspect many people will address the over-reliance on inheritance here. I do agree that this is a likely issue, but I assume your example is oversimplified and I cannot accurately judge if inheritance is justifiable here. I'm going to assume that it is, in the interest of answering the question at hand, but with an asterisk that you might want to revisit your decision to use inheritance.
One way you can make your code more workable is to specify a generic type constraint. This allows you to use subtypes when you need to.
public class NameSpec<T> : Specification<T> where T : Person
{
private string name;
public Namespec(string aName)
{
this.name = aName;
}
public override Expression<Func<T, bool>> FilterExpr()
{
return p => p.Name == this.name;
}
}
// If you want to avoid peppering your codebase with <Person> generic
// types, you can still create a default implementation.
// This allows you to use the non-generic class when dealing with
// Person objects, and use the more specific generic class when you
// are interested in using a more specific subtype.
public class Namespec : Namespec<Person> { }
Take note of the where T : Person
constraint. We've made this class generic, and the caller is allowed to choose the generic type they work with, but we have enforced that they are only allowed to choose generic types which either are of derive from Person
.
Basic usage would be:
var person = new Person() { Name = "Fred" };
var personNameSpec = new Namespec<Person>("Fred");
Assert.IsTrue(personNameSpec.IsSatisfied(person));
var baby = new Baby() { Name = "Pebbles" };
var babyNameSpec = new Namespec<Baby>("Bamm-Bamm");
Assert.IsFalse(babyNameSpec.IsSatisfied(baby));
The above logic would've worked without the generic type on Namespec
, since you could do personNameSpec.IsSatisfied(baby);
. This isn't the cool part yet.
Here's the cool part: because the babyNameSpec
is a Namespec<Baby>
, it is therefore a subtype of Specification<Baby>
, not of Specification<Person>
like personNameSpec
is!
This solves the problem of merging two specifications, as the generic types are now both Baby
and therefore there is no longer a Person
/Baby
type collision.
Specification<Baby> ableToTalkSpec = new IsAbleToTalkSpec();
Specification<Baby> babyNameSpec = new Namespec<Baby>("Bamm-Bamm");
CombinedSpecification<Baby> combinedSpec = ableToTalkSpec.And(babyNameSpec);
var baby = new Baby() { Name = "Pebbles" };
Assert.IsFalse(combinedSpec.IsSatisfied(baby));