So, first off, why doesn't your code work. The first snippet:
p.Addresses.Select(AddressModel.FullSelector);
This doesn't work because the navigational properties do not implement IQueryable
, they implement ICollection
. ICollection
of course doesn't have a Select
method that accepts an Expression
parameter.
The second snippet:
p.Addresses.Select(AddressModel.FullSelector.Compile());
This doesn't work because FullSelector
is being compiled. Since it's being compiled, the query provider can't look into the body of the method and translate the code into SQL code.
The third snippet has the exact same problem as the second. Wrapping it in a lambda doesn't change that fact.
So, now that we know why your code doesn't work, what to do now?
This is going to be a bit mind boggling, and I'm not a huge fan of the design of this method, but here we go. We'll write a method that accepts an expression representing a function with one argument, then it'll accept another that accepts some unrelated type, then a function of the same type as the delegate in our first parameter, and then returns an unrelated type.
The implementation of this method can simply replace all instances of the delegate parameter used with the expression that we have, and then wrap it all up in a new lambda:
public static Expression<Func<T1, T2>> Use<T1, T2, T3, T4>(
this Expression<Func<T3, T4>> expression,
Expression<Func<T1, Func<T3, T4>, T2>> other)
{
return Expression.Lambda<Func<T1, T2>>(
other.Body.Replace(other.Parameters[1], expression),
other.Parameters[0]);
}
//another overload if there are two selectors
public static Expression<Func<T1, T2>> Use<T1, T2, T3, T4, T5, T6>(
this Expression<Func<T3, T4>> firstExpression,
Expression<Func<T5, T6>> secondExpression,
Expression<Func<T1, Func<T3, T4>, Func<T5, T6>, T2>> other)
{
return Expression.Lambda<Func<T1, T2>>(
other.Body.Replace(other.Parameters[1], firstExpression)
.Replace(other.Parameters[2], secondExpression),
other.Parameters[0]);
}
The idea is kinda mind boggling, but the code is actually quite short. It relies on this method to replace all instances of one expression with another:
public static Expression Replace(this Expression expression,
Expression searchEx, Expression replaceEx)
{
return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public ReplaceVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
}
Now to call it we can call Use
on our address selector and then write a method that accepts both our normal parameter as well as the delegate for the address selector:
public static Expression<Func<Project, ProjectModel>> FullSelector =
AddressModel.FullSelector.Use((Project project,
Func<Address, AddressModel> selector) => new ProjectModel
{
ProjectName = project.ProjectName,
ProjectNumber = project.ProjectNumber,
Addresses = project.Addresses.Select(selector),
});
And now this will work exactly as desired.