Search code examples
c#linqlambdaexpression

How to convert type Expression to work on different type in C#


How to convert Expression<Func<A, bool>> to Expression<Func<B, bool>> in C#?

The A and B can be really different classes (e.g. no common parent).

I would want to do something like: hi expression working on type A. The property you use from type A are e.g. SomeProperty from type B. So run the expression on type B.

I have read that I could use ExpressionVisitor class, but I really even don't imagine how could I use it because I don't know how it works.

This is example in code:

public async Task FindAsync(Expression<Func<A, bool>> where, CancellationToken cancellationToken = default)
{
    // _dbContext.B.Where is here for B type, like .Where<B>()
    // But the method parameter where is for type A
    return _dbContext.B.Where(where).SingleOrDefaultAsync(cancellationToken);
}

So I want run expression of type A on type B, but don't know how to convert it.

The types could looks like this:

public class A
{
    public string AProperty1 { get; set; }
    public string AProperty2 { get; set; }
}

public class B
{
    public string BProperty1 { get; set; }
    public string BProperty2 { get; set; }
}

And this is example mapping between these 2 types:

var a = new A
{
    AProperty1 = "x",
    AProperty2 = "2"
};
var b = new B
{
    BProperty1 = a.AProperty1,
    BProperty2 = a.AProperty2
};

And it's result I would want to achieve:

Expression<Func<A, bool>> where = p => p.AProperty1 == "x" && p.AProperty2 == "2";
// Convert result will be as below:
Expression<Func<B, bool>> result = p => p.BProperty1 == "x" && p.BProperty2 == "2";

I hope I expressed my problem well. Please ask if you have any questions.


Solution

  • Given a Dictionary<string, string> that maps all referenced properties from the original parameter type to the new parameter type:

    var mapping = new Dictionary<string, string> {
        { nameof(A.AProperty1), nameof(B.BProperty1) },
        { nameof(A.AProperty2), nameof(B.BProperty2) }
    };
    

    You can use an ExpressionVisitor subclass to replace the lambda parameter with a new parameter of the new type and all member expressions with new member expressions referencing the new parameter and the mapped replacing property:

    var resExpr = whereExpr.ReplaceLambdaParameter<A, B>(mapping);
    

    Here is the extension method and ExpressionVisitor subclass:

    public static class ExpressionExt {
        public static Expression<Func<T2, bool>> ReplaceLambdaParameter<T1, T2>(this Expression<Func<T1, bool>> orig, Dictionary<string, string> pMap) =>
            (Expression<Func<T2, bool>>)(new LambdaParameterReplacer<T1, T2>(pMap).Visit(orig));
    }
    
    public class LambdaParameterReplacer<T1, T2> : ExpressionVisitor {
        Dictionary<string, string> ParameterMap;
        ParameterExpression oldP, newP = Expression.Parameter(typeof(T2), "t2");
    
        public LambdaParameterReplacer(Dictionary<string, string> pMap) => ParameterMap = pMap;
    
        [return: NotNullIfNotNull("node")]
        public override Expression Visit(Expression node) {
            if (node is Expression<Func<T1, bool>> lambdaExpr)
                oldP = lambdaExpr.Parameters[0]; // save original parameter to replace later
            return base.Visit(node);
        }
    
        protected override Expression VisitLambda<T>(Expression<T> lambdaExpr)
            => Expression.Lambda(Visit(lambdaExpr.Body), newP);
    
        protected override Expression VisitParameter(ParameterExpression parmExpr) {
            if (parmExpr == oldP)
                return newP;
            else
                return base.VisitParameter(parmExpr);
        }
        protected override Expression VisitMember(MemberExpression memberExpr) {
            if (memberExpr.Expression == oldP)
                return Expression.Property(newP, ParameterMap[memberExpr.Member.Name]);
            else
                return base.Visit(memberExpr);
        }
    }