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
?
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;
}
}