来自Dynamic Linq的Execution-Deferred IQueryable <T>?

Jax*_*ian 10 c# linq entity-framework dynamic-linq .net-4.5

我正在使用Dynamic Linq来执行一些查询(抱歉,这是我唯一的选择).结果,我得到了一个IQueryable而不是一个IQueryable<T>.就我而言,我希望有一个IQueryable<Thing>地方Thing是一个具体类型.

我的查询是这样的:

public IQueryable<Thing> Foo(MyContext db)
{
    var rootQuery = db.People.Where(x => x.City != null && x.State != null);
    var groupedQuery = rootQuery.GroupBy("new ( it.City, it.State )", "it", new []{"City", "State"});
    var finalLogicalQuery = groupedQuery.Select("new ( Count() as TotalNumber, Key.City as City, Key.State as State )");
    var executionDeferredResults = finalLogicalQuery.Take(10); // IQueryable

    IQueryable<Thing> executionDeferredTypedThings = ??; // <--- Help here!!!!

    return executionDeferredTypedThings;
}
Run Code Online (Sandbox Code Playgroud)

我的事情:

public class Thing
{
    public int TotalNumber { get; set; }
    public string City { get; set; }
    public string State { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

是的,我知道如果没有Dynamic Linq,上面的确切事情可以完成,但我有一些变量,我已经简化了这里.如果我的返回类型很简单IQueryable但我无法弄清楚如何转换为IQueryable<Thing>同时保持执行延迟并且同时保持实体框架满意,我可以使用我的变量.我确实有动态Select总是返回一些东西(使用正确的数据),看起来像一个Thing.但我根本无法想象如何返回IQueryable<Thing>并可以在那里使用一些帮助.谢谢!!

尝试失败1

根据Rex M的建议,我现在正在尝试使用AutoMapper来解决这个问题(尽管我并不致力于这种方法并愿意尝试其他方法).对于AutoMapper方法,我这样做:

IQueryable<Thing> executionDeferredTypedThings = executionDeferredResults.ProjectTo<Thing>(); // <--- Help here!!!!
Run Code Online (Sandbox Code Playgroud)

但是这会导致InvalidOperationException:

缺少从DynamicClass2到Thing的地图.使用Mapper.CreateMap创建.

问题是,虽然我已定义Thing,但我没有定义DynamicClass2,因此我无法映射它.

尝试失败2

IQueryable<Thing> executionDeferredTypedThings = db.People.Provider.CreateQuery<Thing>(executionDeferredResults.Expression);
Run Code Online (Sandbox Code Playgroud)

这会产生InvalidCastException,并且似乎与上述AutoMapper失败的基本问题相同:

无法将类型为'System.Data.Entity.Infrastructure.DbQuery'1 [DynamicClass2]'的对象强制转换为'System.Linq.IQueryable'1 [MyDtos.Thing]'.

Rex*_*x M 5

您可以使用AutoMapper 的 Queryable Extensions生成 IQueryable,它包装底层 IQueryable,从而保留原始 IQueryable 的 IQueryProvider 和延迟执行,但将映射/转换组件添加到管道中以从一种类型转换为另一种类型。

还有AutoMapper 的 UseAsDataSource,它使一些常见的查询扩展场景变得更容易。


Iva*_*oev 2

如果我理解正确,以下扩展方法应该可以为您完成工作

public static class DynamicQueryableEx
{
    public static IQueryable<TResult> Select<TResult>(this IQueryable source, string selector, params object[] values)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (selector == null) throw new ArgumentNullException("selector");
        var dynamicLambda = System.Linq.Dynamic.DynamicExpression.ParseLambda(source.ElementType, null, selector, values);
        var memberInit = dynamicLambda.Body as MemberInitExpression;
        if (memberInit == null) throw new NotSupportedException();
        var resultType = typeof(TResult);
        var bindings = memberInit.Bindings.Cast<MemberAssignment>()
            .Select(mb => Expression.Bind(
                (MemberInfo)resultType.GetProperty(mb.Member.Name) ?? resultType.GetField(mb.Member.Name),
                mb.Expression));
        var body = Expression.MemberInit(Expression.New(resultType), bindings);
        var lambda = Expression.Lambda(body, dynamicLambda.Parameters);
        return source.Provider.CreateQuery<TResult>(
            Expression.Call(
                typeof(Queryable), "Select",
                new Type[] { source.ElementType, lambda.Body.Type },
                source.Expression, Expression.Quote(lambda)));
    }
}
Run Code Online (Sandbox Code Playgroud)

(旁注:坦率地说,我不知道values参数的用途是什么,但添加它以匹配相应的DynamicQueryable.Select方法签名。)

所以你的例子会变成这样

public IQueryable<Thing> Foo(MyContext db)
{
    var rootQuery = db.People.Where(x => x.City != null && x.State != null);
    var groupedQuery = rootQuery.GroupBy("new ( it.City, it.State )", "it", new []{"City", "State"});
    var finalLogicalQuery = groupedQuery.Select<Thing>("new ( Count() as TotalNumber, Key.City as City, Key.State as State )");  // IQueryable<Thing>
    var executionDeferredTypedThings = finalLogicalQuery.Take(10);
    return executionDeferredTypedThings;
}
Run Code Online (Sandbox Code Playgroud)

怎么运行的

这个想法很简单。

里面的方法Select实现DynamicQueryable看起来像这样

public static IQueryable Select(this IQueryable source, string selector, params object[] values)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    LambdaExpression lambda = DynamicExpression.ParseLambda(source.ElementType, null, selector, values);
    return source.Provider.CreateQuery(
        Expression.Call(
            typeof(Queryable), "Select",
            new Type[] { source.ElementType, lambda.Body.Type },
            source.Expression, Expression.Quote(lambda)));
}
Run Code Online (Sandbox Code Playgroud)

它的作用是动态创建一个选择器表达式并将其绑定到源Select方法。我们采用完全相同的方法,但修改了调用创建的选择器表达式DynamicExpression.ParseLambda

唯一的要求是投影使用“new (...)”语法,并且投影属性的名称和类型 match,我认为这适合您的用例。

返回的表达式是这样的

(source) => new TargetClass
{
    TargetProperty1 = Expression1(source),
    TargetProperty2 = Expression2(source),
    ...
}
Run Code Online (Sandbox Code Playgroud)

其中TargetClass是动态生成的类。

我们想要的只是保留源部分,并将目标类/属性替换为所需的类/属性。

至于实现,首先将属性分配转换为

var bindings = memberInit.Bindings.Cast<MemberAssignment>()
    .Select(mb => Expression.Bind(
        (MemberInfo)resultType.GetProperty(mb.Member.Name) ?? resultType.GetField(mb.Member.Name),
        mb.Expression));
Run Code Online (Sandbox Code Playgroud)

然后new DynamicClassXXX { ... }将 替换为 with

var body = Expression.MemberInit(Expression.New(resultType), bindings);
Run Code Online (Sandbox Code Playgroud)