Search code examples
c#entity-frameworklinqexpression-treestable-per-type

Searching for records in several tables using EF TPT


I have a project with TPT inheritance mapping, now I need to add it a search functionality that will find records in several tables. That's what I currently have:

public abstract class Customer
{
    public int      Id      { get; set; }
    public string   Memo    { get; set; }
...
}

public class Person : Customer
{
    public string   GivenName   { get; set; }
    public string   Surname     { get; set; }
...
}
public class Company : Customer
{
    public string       Name     { get; set; }
...
}

I also have a unit of work with bunch of repositories, and I need to add the filtering functionality to several methods of CustomerRepository. Let's say I have a Count method with the following signature

public int Count(System.Linq.Expressions.Expression<Func<Customer, bool>> filter = null)

Now I need to get the quantity of customers whose GiveName or Surname contains searchTerm in case the customer is a Person or the same searchTerm in Name field in case it is a Company.

TL;DR
How a view with a single, searchable, paged list of Customers (containing both Person and Company types) should be implemented? I mean in terms of method with signature like public IHttpActionResult Get(string searchTerm, int pageSize, int pageNumber)...

That's what I tried:
I added to each of the classes a static method that would generate an Expression to search that specific class, that's how it looks for the Person class:

public static System.Linq.Expressions.Expression<Func<Person, bool>> GetFilter(string searchTerm)
{
    if (String.IsNullOrWhiteSpace(searchTerm))
    {
        return null;
    }
    var parameterExpression = System.Linq.Expressions.Expression.Parameter(typeof(Person));
    System.Reflection.MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });

    return System.Linq.Expressions.Expression.Lambda<Func<Person, bool>>(
            System.Linq.Expressions.Expression.OrElse(
                System.Linq.Expressions.Expression.Call(
                    System.Linq.Expressions.Expression.PropertyOrField(parameterExpression, "GivenName"),
                    method,
                    System.Linq.Expressions.Expression.Constant(searchTerm, typeof(string))
                ),
                System.Linq.Expressions.Expression.Call(
                    System.Linq.Expressions.Expression.PropertyOrField(parameterExpression, "Surname"),
                    method,
                    System.Linq.Expressions.Expression.Constant(searchTerm, typeof(string))
                )
            ), parameterExpression);
}

And tried to build an Expression that would check the type of the customer and then make an appropriate data check, but here I stumped... That's what I have right now:

    var parameterExpression = System.Linq.Expressions.Expression.Parameter(typeof(Customer));
    var typeIsPerson = System.Linq.Expressions.Expression.TypeIs(parameterExpression, typeof(Person));
    var typeIsCompany = System.Linq.Expressions.Expression.TypeIs(parameterExpression, typeof(Company));

    var q = System.Linq.Expressions.Expression.Block(
        System.Linq.Expressions.Expression.IfThen(typeIsPerson, Person.GetFilter(searchTerm)),
        System.Linq.Expressions.Expression.IfThen(typeIsCompany, Company.GetFilter(searchTerm)),
        System.Linq.Expressions.Expression.Constant(false));

    var a = System.Linq.Expressions.Expression.Lambda<Func<Customer, bool>>(
            q, parameterExpression);

Here I have two problems(at least?), first when I try to call Count, I get a very unpleasant NotSupportedException exception that says Unknown LINQ expression of type 'Block'. The second is that I don't know how to return the result of execution for each of GetFilters, I suspect that I will get false for any record since it is default value that is the last Expression in my Block...
May be I'm on a wrong track and this is something that should be done in a completely different manner?


Solution

  • Expression blocks are generally unsupported in LINQ to Entities. And normally you don't need them because you could build almost any expression just using C# conditional operator ? : (which maps to Expression.Condition).

    But before even trying to build an expression dynamically, you need to find a EF supported construct working with TPT (and other EF inheritance models) polymorphic query. Which is not so easy because all the examples use OfType method which is only applicable when you need to filter a concrete derived entities. With some trial and error, luckily there are two supported constructs - is and as (important: as, not cast!).

    So the statically built predicate expression in question could be like this:

    Expression<Func<Customer, bool>> predicate = c =>
        c is Person ? 
            ((c as Person).GivenName.Contains(searchTerm) || (c as Person).Surname.Contains(searchTerm)) :
        c is Company ? 
            (c as Company).Name.Contains(searchTerm) :
        false;
    

    (Frankly you don't want to look at the generated SQL, but it works)

    Now you can build it dynamically if you wish. You already found the is expression method (Expression.TypeIs), for as operator the corresponding expression metod is Expression.TypeAs.