来自 Dynamic Linq 的执行延迟 IQueryable<T>?

Execution-Deferred IQueryable<T> from Dynamic Linq?

我正在使用 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;
}

我的Thing.cs:

public class Thing
{
    public int TotalNumber { get; set; }
    public string City { get; set; }
    public string State { get; set; }
}

是的,我知道没有 Dynamic Linq 也可以完成上面的事情,但是我有一些可变性,我已经从这里简化了。如果我的 return 类型只是 IQueryable,我可以让它与我的可变性一起工作,但我无法弄清楚如何转换为 IQueryable<Thing>,同时保持它的执行延迟,同时还保持Entity Framework 开心。我确实有动态 Select 总是 returning 一些 看起来像 Thing 的东西(使用正确的数据)。但我根本不知道如何 return 和 IQueryable<Thing> 并且可以在那里使用一些帮助。谢谢!!

失败尝试 1

根据Rex M的建议,我现在正在尝试使用AutoMapper来解决这个问题(虽然我不打算采用这种方法,也愿意尝试其他方法).对于 AutoMapper 方法,我是这样做的:

IQueryable<Thing> executionDeferredTypedThings = executionDeferredResults.ProjectTo<Thing>(); // <--- Help here!!!!

但这会导致 InvalidOperationException:

Missing map from DynamicClass2 to Thing. Create using Mapper.CreateMap.

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

第二次尝试失败

IQueryable<Thing> executionDeferredTypedThings = db.People.Provider.CreateQuery<Thing>(executionDeferredResults.Expression);

这给出了一个 InvalidCastException 并且似乎是与上述 AutoMapper 失败命中相同的潜在问题:

Unable to cast object of type 'System.Data.Entity.Infrastructure.DbQuery'1[DynamicClass2]' to type 'System.Linq.IQueryable'1[MyDtos.Thing]'.

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

还有 AutoMapper's UseAsDataSource 可以简化一些常见的查询扩展方案。

这个不需要 Dynamic Linq。

var groupedQuery = from p in db.People
    where p.City != null && p.State != null
    group p by new {p.City, p.State}
    into gp
    select new Thing {
        TotalNumber = gp.Count(),
        City = gp.Key.City,
        State = gp.Key.State
    };

IQueryable<Thing> retQuery = groupedQuery.AsQueryable(); 
retQuery= retQuery.Take(10);
return retQuery;

这样的事情对您有帮助吗?

public static IQueryable<TEntity> GetQuery<TEntity>(this DbContext db, bool includeReferences = false) where TEntity : class
    {
        try
        {
            if (db == null)
            {
                return null;
            }

            var key = typeof(TEntity).Name;
            var metaWorkspace = db.ToObjectContext().MetadataWorkspace;
            var workspaceItems = metaWorkspace.GetItems<EntityType>(DataSpace.OSpace);
            var workspaceItem = workspaceItems.First(f => f.FullName.Contains(key));
            var navProperties = workspaceItem.NavigationProperties;

            return !includeReferences
                    ? db.Set<TEntity>()
                    : navProperties.Aggregate((IQueryable<TEntity>)db.Set<TEntity>(), (current, navProperty) => current.Include(navProperty.Name));
        }
        catch (Exception ex)
        {
            throw new ArgumentException("Invalid Entity Type supplied for Lookup", ex);
        }
    }

您可能想查看位于此处的 Github 上的通用搜索项目: https://github.com/danielpalme/GenericSearch

如果我没理解错的话,下面的扩展方法应该可以帮你完成工作

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

(旁注:坦率地说,我不知道 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;
}

工作原理

这个想法很简单。

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

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

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

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

返回的表达式是这样的

(source) => new TargetClass
{
    TargetProperty1 = Expression1(source),
    TargetProperty2 = Expression2(source),
    ...
}

其中 TargetClass 是动态生成的 class。

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

至于实现,首先将 属性 赋值转换为

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

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

var body = Expression.MemberInit(Expression.New(resultType), bindings);