如何两次投影到相同的DTO类型?

时间:2019-06-01 02:48:06

标签: c# entity-framework linq

这里的目标是更新一个投影到dto中的IQueryable,以便它仅包含最初包含的属性的子集。 “子集”被提供为映射到dto属性的字符串列表。

我已经有将其转换为投影的代码,但是问题是我无法两次投影到相同的dto类型。幸运的是,我可以通过将2个选择放入相同的类型来重现该问题:

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 
    } );
}

按照本文所述,将返回错误"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."

我已经找到了一种解决方案,可以修改从getOrderProjection返回的表达式,但是这并不理想,因为在其他地方,我想修改的IQueryable远比这样的投影复杂得多。

因此,解决方案必须仅包含对applyPropertyList函数的更改。我在想某种ExpressionVisitor可以将这两者合并在一起,但我不知道从哪里开始,甚至不行。

1 个答案:

答案 0 :(得分:1)

我知道了。答案是使用ExpressionVisitor。这里的TrimProjection方法代替了问题中的applyPropertyList

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 );
        }
    }
}