I'm trying to query my DB-Entities with a Business-Class-Linq-Query. I know I need to transform the LINQ-Query so that it can run against the Entity-Class.
My Problem is, that I can't figure out, why I get the following exception:
This is the complete (test)-code, as you can see Person and PersonEntity are nearly identical.
using System.Linq.Expressions;
namespace ConsoleApp1
{
class Person
{
public int Age { get; set; }
public string? Name { get; set; }
}
class PersonEntity
{
public int Id { get; set; }
public int Age { get; set; }
public string? Name { get; set; }
}
static class ExpressionTranslator
{
public static Expression<Func<PersonEntity, bool>> Translate(Expression<Func<Person, bool>> expression)
{
ParameterExpression parameter = Expression.Parameter(typeof(PersonEntity), "e");
Expression body = new ReplaceVisitor(expression.Parameters[0], parameter).Visit(expression.Body);
return Expression.Lambda<Func<PersonEntity, bool>>(body, parameter);
}
private class ReplaceVisitor : ExpressionVisitor
{
private readonly ParameterExpression _oldParameter;
private readonly ParameterExpression _newParameter;
public ReplaceVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
{
_oldParameter = oldParameter;
_newParameter = newParameter;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node == _oldParameter ? _newParameter : base.VisitParameter(node);
}
}
}
internal class Program
{
static void Main(string[] args)
{
Expression<Func<Person, bool>> actualQuery = x => x.Age > 18;
IEnumerable<Person> result = QueryPersons(actualQuery);
foreach (var p in result)
{
Console.WriteLine($"{p.Name}, {p.Age}");
}
}
private static IEnumerable<Person> QueryPersons(Expression<Func<Person, bool>> query)
{
var db = new List<PersonEntity>(new[] {
new PersonEntity() { Id= 1, Age=15, Name="John" },
new PersonEntity() { Id= 2, Age=25, Name="Sally" },
new PersonEntity() { Id= 3, Age=35, Name="Jack" },
});
Expression<Func<PersonEntity, bool>> entityQuery = ExpressionTranslator.Translate(query);
return db.AsQueryable().Where(entityQuery).Select(x => new Person() { Age = x.Age, Name = x.Name }).ToList();
}
}
}
My Problem is, that I can't figure out, why I get the following exception:
System.ArgumentException: 'Property 'Int32 Age' is not defined for type 'ConsoleApp1.PersonEntity' (Parameter 'property')'
Well, while replacing parts of an expression tree with other expressions has similarities with string.Replace
, it's not exactly the same. It usually is used to emulate a "call" or member access of expression with another expression of the same type. If you indeed replace it with expression of different type as here, there is a lot of additional work to be done. If it is argument of a generic method, the whole method call (including other arguments) must be rebind to the new type. Same for members (methods, properties, fields) of the original type.
In your case, the problem is this expression
x.Age
which in expression terms is MemberExpression with Expression being ParameterExpression
with name x
(unimportant) and type Person
(very important), and (also very important) Member is reflection MemberInfo
(PropertyInfo
in this case) with DeclaringType
equal to Person
. Note that the last is not a name. So when you replace x
with e
of type PersonEntity
, the member info is no more valid, hence the exception.
So as I said before, you need to do much more work in order to support such scenario. Some third party libraries provide such functionality, for instance AutoMapper along with "auto" mapping properties by name (what essentially are you trying to do here) also allow specifying the replacing expression via configuration.
In order to support at least this simplified member mapping by name, parameter replacing visitor is not enough, you need at least visitor which also overrides VisitMember and does the mapping there.
For instance, if we generalize your example with generics, it could be something like this
static class ExpressionTranslator
{
public static Expression<Func<TDestination, bool>> Translate<TSource, TDestination>(
this Expression<Func<TSource, bool>> expression)
{
var parameter = Expression.Parameter(typeof(TDestination), "e");
var body = new TranslateVisitor(expression.Parameters[0], parameter).Visit(expression.Body);
return Expression.Lambda<Func<TDestination, bool>>(body, parameter);
}
private class TranslateVisitor : ExpressionVisitor
{
private readonly ParameterExpression _oldParameter;
private readonly ParameterExpression _newParameter;
public TranslateVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
{
_oldParameter = oldParameter;
_newParameter = newParameter;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node == _oldParameter ? _newParameter : base.VisitParameter(node);
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression?.Type == _oldParameter.Type)
{
// Map source member by name
var expression = Visit(node.Expression);
var member = Expression.PropertyOrField(expression, node.Member.Name);
return member;
}
return base.VisitMember(node);
}
}
}
with (working) usage in your sample:
var entityQuery = query.Translate<Person, PersonEntity>();
As a side note, you can totally eliminate the need of all this if you apply the filter after projection (Select
) which does the object mapping, e.g.
db.AsQueryable()
//.Where(entityQuery)
.Select(x => new Person() { Age = x.Age, Name = x.Name })
.Where(query)
.ToList();
The replacing/remapping technique is really needed when you want to generate the body of the the above Select
.