'How can I project into the same DTO type twice?

The goal here is to update an IQueryable that projects into a dto so it only includes a subset of the properties that it originally included. The "subset" is being provided as a list of strings that maps to the properties of the dto.

I already have code that turns this into a projection but the problem is I cannot project into the same dto type twice. Luckily I can reproduce the problem by just having 2 selects into the same type:

public IEnumerable<OrderDto> GetOrderDtos( IEnumerable<string> properties ) {
    IQueryable<OrderDto> results = dbContext.Orders
        .Select( getOrderProjection( ) );

    if(properties != null && properties.Any()){
        results = results.applyPropertyList(properties);
    }

    return results.ToList( );
}

private Expression<Func<FixTradeRequest, OrderDto>> getOrderProjection(){
    return o => new OrderDto { 
        Id = o.Id, 
        AccountId = o.AccountId,
        AccountNumber = o.Account.Info.AccountNumber,
        Application = o.TradeRequestInstance.RequestType,
        WarningMessage = o.WarningMessage
        //trimmed for brevity, this has about 100 properties mapped
    };
}

private IQueryable<OrderDto> applyPropertyList( IQueryable<OrderDto> source, IEnumerable<string> properties ){
    /*in reality this is dynamically created from the provided list 
    of properties, but this static projection shows the problem*/
    return source.Select( o => new OrderDto { 
        Id = o.Id, 
        WarningMessage = o.WarningMessage 
    } );
}

As written this returns an error "The type 'OrderDto' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order."

I've figured out a solution that modifies the expression returned from getOrderProjection however this is not ideal because in other places the IQueryable I would like to modify is far more complex than just a projection like this one.

Therefore the solution must only include changes to the applyPropertyList function. I'm thinking some kind of ExpressionVisitor could merge these two together but I don't know where to start or if that would even work.



Solution 1:[1]

I figured it out. The answer was by using an ExpressionVisitor. The TrimProjection method here takes the place of the applyPropertyList in the question.

public static partial class QueryableExtensions {
    public static IQueryable<TResult> TrimProjection<TResult>( this IQueryable<TResult> source, IEnumerable<string> targetPropeties ) {
        var visitor = new ProjectionReducer<TResult>( targetPropeties );
        var expression = visitor.Visit( source.Expression );
        if( expression != source.Expression )
            return source.Provider.CreateQuery<TResult>( expression );
        return source;
    }

    private class ProjectionReducer<TResult> : ExpressionVisitor {
        private readonly List<string> propNames;
        public ProjectionReducer( IEnumerable<string> targetPropeties ) {
            if( targetPropeties == null || !targetPropeties.Any( ) ) {
                throw new ArgumentNullException( nameof( targetPropeties ) );
            }
            this.propNames = targetPropeties.ToList( );
        }

        protected override Expression VisitNew( NewExpression node ) {
            return base.VisitNew( node );
        }
        protected override Expression VisitLambda<T>( Expression<T> node ) {
            //if the node returns the type we are acting upon
            if( node.ReturnType == typeof( TResult ) ) {
                //create a new expression from this one that is the same thing with some of the bindings omitted
                var mie = (node.Body as MemberInitExpression);
                var currentBindings = mie.Bindings;

                var newBindings = new List<MemberBinding>( );

                foreach( var b in currentBindings ) {
                    if( propNames.Contains( b.Member.Name, StringComparer.CurrentCultureIgnoreCase ) ) {
                        newBindings.Add( b );
                    }
                }

                Expression testExpr = Expression.MemberInit(
                    mie.NewExpression,
                    newBindings
                );

                return Expression.Lambda( testExpr, node.Parameters );
            }
            return base.VisitLambda( node );
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 zlangner