Search code examples
c#entity-frameworkexpression-trees

How to determine if Expression<Func<T, object>> is valid for EntityFramework .Include


I have a method for retrieving an entity using EntityFramework that accepts an array of the Navigation Properties that should be included:

public virtual T GetSingle(Guid id, params Expression<Func<T, object>>[] navigationProperties)
{
    T item = null;

    IQueryable<T> dbQuery = DBContext.Set<T>();

    Expression<Func<T, bool>> where = t => t.ID == id;
    where = FilterDeleted(where);

    //Apply eager loading
    if (navigationProperties != null)
        foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
            dbQuery = dbQuery.Include<T, object>(navigationProperty);

    item = dbQuery
        .Where(where)//Apply where clause
        .FirstOrDefault(); 

    return item;
}

I'm considering using EntityFramework-Plus to allow filtering on these navigation properties. However, I'm concerned about the complexity/inefficiency of the queries it generates so I want to try using it only on navigation properties that the built in .Include can't handle.

So first I need to determine what is a valid Expression<Func<T, object>> parameter for the .Include method. As far as I know it can only contain a property selector or a .Select. E.G.

t => t.Property;
t => t.Collection.Select(c => c.InnerCollection.Select(ic => ic.Property));

I've barely worked with Expression trees so I'm not sure how to begin taking this on. How would I go about analyzing an Expression<Func<T, object>> to determine if it contains anything other than these such as a .Where or .OrderBy or .Take?


Solution

  • Nice question :) I tried to create a little piece of code that suits your needs. Tested it with a number of possible inputs; if it's not working for something and someone points it out, I'm happy to work on it :)

    See the comments for what's what.

    static bool IsValid(LambdaExpression expression)
    {
      // Expression is in the form of parameter => something
      // Body is the 'something' part
      var body = expression.Body;
    
      // MemberExpression are like p.Name, that's a valid body
      if (body is MemberExpression) 
         return true;
    
      // MethodCallExpression are like p.Select(...) or p.Where(...) or p.DoSomething(...)
      var methodCallExpression = body as MethodCallExpression;
    
      // If it's not a methodcall, it can't be a select, so it's invalid
      if (methodCallExpression == null)
          return false;
    
      // Method contains the actual MethodInfo
      var method = methodCallExpression.Method;
    
      // Select is a generic method, so if it's not generic, it can't be valid
      if (!method.IsGenericMethod)
          return false;
    
      // Get the actual, parameterless methoddefinition of Enumerable.Select
      // NOTE: This is ugly as hell, but AFAIK there's no better way 
      // just query for the method whose name is 'Select' and has two parameters where the second one has two generic arguments (that's the Func argument)
      var selectMethod = typeof(Enumerable).GetMethods()
                        .Single(m => m.Name == nameof(Enumerable.Select) && m.GetParameters()[1].ParameterType.GetGenericArguments().Count() == 2);
    
      // If the method in the methodinfo is not the Select definition, it's not valid
      if (method.GetGenericMethodDefinition() != selectMethod)
          return false;
    
      // Otherwise the methodcall is in the form of p.Select(p=>'something else')
      // innerExpr gets the p=>'something else' part
      var innerExpr = methodCallExpression.Arguments[1];
    
      // If the expression really is a lambda expression, then recursively check the p=>'something else' part
      if (innerExpr is LambdaExpression lambda)
      {
        return IsValid(lambda);
      }
      else
      {
        // Otherwise it's invalid
        // NOTE: this is just in case, I'm not even sure if you can achieve this with regular C# code at compilation time
        return false;
      }                
    }