0

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.

gst
  • 31
  • 3
  • There is no way ... `Specification` is not dervied from `Specification` ... even if you would try use covariance or contravariance because you cannot use them both – Selvin Oct 12 '21 at 12:45
  • 1
    You are not forced to use abstract/base classes - they are implementation helpers only. Build the functionality against *contravariant interface* (e.g. `ISpecification` in your example) and extension methods. The difference is that interfaces support variance, while classes do not. – Ivan Stoev Oct 12 '21 at 12:52
  • ... and he would end with `interface ISpecification { ISpecification And(ISpecification otherSpec); }` ... which cannot be achive with covariant interface – Selvin Oct 12 '21 at 12:54
  • @Selvin My mistake, I meant *contra*. So `ISpecification` can be used as `ISpecification`. Similar to `IComparer` or `IEqualityComparer`. `And` / `Or` and similar combinators could be simply extension methods, no need to be part of the interface. – Ivan Stoev Oct 12 '21 at 12:56
  • it cannot be done with contravariance either ... with `ISpecification And(ISpecification otherSpec);` you have both out and in ... – Selvin Oct 12 '21 at 12:56
  • `public class IsAbleToTalkSpec : Specification { public override Expression> FilterExpr() => p => p is Baby b && b => b.CanTalk == true; }` – Selvin Oct 12 '21 at 13:01
  • @Selvin combination of *contravariant interface* and *extension methods* - that's it. What you are suggesting is fine if you are querying against `IQueryable` though. – Ivan Stoev Oct 12 '21 at 13:02
  • and how you would use `Expression.AndAlso` for different `T` in `Expression>` :D ... I would love to see an example how it can be achive with *combination of contravariant interface and extension methods* – Selvin Oct 12 '21 at 13:12
  • @PanagiotisKanavos: While I have admittedly not followed up in the past few months due to other projects, EF Core used to lack some of the inheritance features that EF already featured (IIRC it was table-per-concrete-type that was missing? it's been a while) This may contribute to not (yet) relying on EF Core when using .NET Core. Unless it has been added since last I looked. – Flater Oct 12 '21 at 13:33

1 Answers1

1

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));
Flater
  • 12,908
  • 4
  • 39
  • 62
  • from question: *I have no idea how to solve this without creating a NameSpec instead of reusing the NameSpec* ... so prolly OP is aware of this solution – Selvin Oct 12 '21 at 13:23
  • @Selvin: While it is still using a generic parameter, the original `NameSpec` logic is actually being reused here, which is what the primary goal was, as per the quote you referenced. It is also unclear to me whether OP is aware of generic type constraints, or whether they were suggesting to create a second namespec, one for babies, which is essentially the same. I do agree with you that the `Namespec` syntax technically only suggests the former, but the overall context of OP's statement makes me feel like he was erring more towards the latter, hence my elaboration. – Flater Oct 12 '21 at 13:25
  • I will try this soon and come back with my results, thank you for taking the time to thoroughly explain it. As you said, I oversimplified the question to show my problem, I'm sorry for all the details that I wasn't articulate enough to clarify. Do you know how your solution would get along with LinQ->SQL conversions made by EF? Also, how would my original question be solved by avoiding inheritance? Thanks in advance – gst Oct 12 '21 at 15:36
  • @gst: assuming your expression bodies steer away from incompatible things that EF can't translate (which would already need to be the case in your old code), you should be fine. At the end of the day, your code is really just working with standard expression objects. My solution doesn't impact this as far as I can see. – Flater Oct 12 '21 at 15:39
  • @gst: Avoiding inheritance here is a big topic. It would require much more contextual detail and much more elaboration on your goals. It's redefining the foundation that this question's code relies on. – Flater Oct 12 '21 at 15:40
  • Thank you for your help, I will mark your answer as the correct one after trying your solution tomorrow or the next day. – gst Oct 12 '21 at 16:03