score:1

Accepted answer

You are very close with your 2nd approach!

Let's say you define the projection of the product entity to the DTO (the mapper as you call it) like you did:

Expression<Func<Product, ProductDTO>> productProjection = prod => new ProductDTO
{
    ID = prod.ID,
    DateOfTransaction = prod.Date
    // ...
};

and the projection of the client entity to it's DTO like this (slightly simpler, but logically equivalent to what you did):

Expression<Func<Client, ClientDTO>> clientProjection = client => new ClientDTO
{
    ID = client.ClientID,
    FirstName = client.FirstName,
    // ...
    Products = client.Products.Select(productProjection.Compile()).ToList(),
    // ...
};

The compiler let's you do so, but the queryable will not understand that. However what you have achieved is that the productProjection is somehow contained in the expression tree. All you have to do is some expression manipulation.

If you look at the subtree the compiler builds for the argument to .Select you'll find a MethodCallExpression - the call to .Compile(). It's .Object expression - the thing that is to be compiled - is a MemberExpression accessing a field named productProjection(!) on an ConstantExpression containing an instance of an oddly named compiler generated closure class.

So: Find .Compile() calls and replace them with what would be compiled, ending up with the very expression tree you had in your original version.

I'm maintaining a helper class for expression stuff called Express. (See another answer that deals with .Compile().Invoke(...) for a similar situation).

clientProjection = Express.Uncompile(clientProjection);
var clientList = dbClients.Select(clientProjection).ToList();

Here's the relevant snipped of the Express class.

public static class Express
{
    /// <summary>
    /// Replace .Compile() calls to lambdas with the lambdas themselves.
    /// </summary>
    public static Expression<TDelegate> Uncompile<TDelegate>(Expression<TDelegate> lambda)
    => (Expression<TDelegate>)UncompileVisitor.Singleton.Visit(lambda);

    /// <summary>
    /// Evaluate an expression to a value.
    /// </summary>
    private static object GetValue(Expression x)
    {
        switch (x.NodeType)
        {
            case ExpressionType.Constant:
                return ((ConstantExpression)x).Value;
            case ExpressionType.MemberAccess:
                var xMember = (MemberExpression)x;
                var instance = xMember.Expression == null ? null : GetValue(xMember.Expression);
                switch (xMember.Member.MemberType)
                {
                    case MemberTypes.Field:
                        return ((FieldInfo)xMember.Member).GetValue(instance);
                    case MemberTypes.Property:
                        return ((PropertyInfo)xMember.Member).GetValue(instance);
                    default:
                        throw new Exception(xMember.Member.MemberType + "???");
                }
            default:
                // NOTE: it would be easy to compile and invoke the expression, but it's intentionally not done. Callers can always pre-evaluate and pass a member of a closure.
                throw new NotSupportedException("Only constant, field or property supported.");
        }
    }

    private sealed class UncompileVisitor : ExpressionVisitor
    {
        public static UncompileVisitor Singleton { get; } = new UncompileVisitor();
        private UncompileVisitor() { }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.Name != "Compile" || node.Arguments.Count != 0 || node.Object == null || !typeof(LambdaExpression).IsAssignableFrom(node.Object.Type))
                return base.VisitMethodCall(node);
            var lambda = (LambdaExpression)GetValue(node.Object);
            return lambda;

            // alternatively recurse on the lambda if it possibly could contain .Compile()s
            // return Visit(lambda); // recurse on the lambda
        }
    }
}

score:1

Use LINQKit to expand user-defined lambda functions into the lambdas needed in the query:

https://github.com/scottksmith95/LINQKit


Related Query

More Query from same tag