将IQueryable和IEnumerable连接起来,形成一个IQueryable。

4
我在过去的几天里搜索了互联网,寻找解决方案,但没有找到我想要的。基本上,这是我的问题:
1. 我需要实现一个接口,该接口具有返回IQueryable的方法(我无法访问接口,因此无法更改此内容)
2. 我希望该方法返回一个连接到非常大的数据库表的IQueryable和在内存中计算的相同实体类型的大型IEnumerable的连接
3. 我不能使用queryableA.Concat(enumerableB).Where(condition),因为它会尝试将整个数组发送到服务器(除此之外,我还会收到只支持原始类型的异常)
4. 我不能使用enumerableB.Concat(queryableA).Where(condition),因为它会将整个表作为IEnumerable加载到内存中处理

所以经过一些搜索,我认为处理这个问题的好方法是编写自己的ConcatenatingQueryable IQueryable实现,它接受两个IQueryable并在每个上独立执行表达式树,然后连接结果。然而,我好像遇到了问题,因为它返回了一个堆栈溢出。根据http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx,这是我目前实现的东西:

class Program
{
    static void Main(string[] args)
    {
        var source1 = new[] {  1, 2 }.AsQueryable();
        var source2 = new[] { -1, 1 }.AsQueryable();
        var matches = new ConcatenatingQueryable<int>(source1, source2).Where(x => x <= 1).ToArray();
        Console.WriteLine(string.Join(",", matches));
        Console.ReadKey();
    }

    public class ConcatenatingQueryable<T> : IQueryable<T>
    {
        private readonly ConcatenatingQueryableProvider<T> provider;
        private readonly Expression expression;

        public ConcatenatingQueryable(IQueryable<T> source1, IQueryable<T> source2)
            : this(new ConcatenatingQueryableProvider<T>(source1, source2))
        {}

        public ConcatenatingQueryable(ConcatenatingQueryableProvider<T> provider)
        {
            this.provider = provider;
            this.expression = Expression.Constant(this);
        }

        public ConcatenatingQueryable(ConcatenatingQueryableProvider<T> provider, Expression expression)
        {
            this.provider = provider;
            this.expression = expression;
        }

        Expression IQueryable.Expression
        {
            get { return expression; }
        }

        Type IQueryable.ElementType
        {
            get { return typeof(T); }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return provider; }
        }

        public IEnumerator<T> GetEnumerator()
        {
            // This line is calling Execute below
            return ((IEnumerable<T>)provider.Execute(expression)).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)provider.Execute(expression)).GetEnumerator();
        }
    }

    public class ConcatenatingQueryableProvider<T> : IQueryProvider
    {
        private readonly IQueryable<T> source1;
        private readonly IQueryable<T> source2;

        public ConcatenatingQueryableProvider(IQueryable<T> source1, IQueryable<T> source2)
        {
            this.source1 = source1;
            this.source2 = source2;
        }

        IQueryable<TS> IQueryProvider.CreateQuery<TS>(Expression expression)
        {
            var elementType = TypeSystem.GetElementType(expression.Type);
            try
            {
                return (IQueryable<TS>)Activator.CreateInstance(typeof(ConcatenatingQueryable<>).MakeGenericType(elementType), new object[] { this, expression });
            }
            catch (TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
        }

        IQueryable IQueryProvider.CreateQuery(Expression expression)
        {
            var elementType = TypeSystem.GetElementType(expression.Type);
            try
            {
                return (IQueryable)Activator.CreateInstance(typeof(ConcatenatingQueryable<>).MakeGenericType(elementType), new object[] { this, expression });
            }
            catch (TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
        }

        TS IQueryProvider.Execute<TS>(Expression expression)
        {
            return (TS)Execute(expression);
        }

        object IQueryProvider.Execute(Expression expression)
        {
            return Execute(expression);
        }

        public object Execute(Expression expression)
        {
            // This is where I suspect the problem lies, as executing the 
            // Expression.Constant from above here will call Enumerate again,
            // which then calls this, and... you get the point
            dynamic results1 = source1.Provider.Execute(expression);
            dynamic results2 = source2.Provider.Execute(expression);
            return results1.Concat(results2);
        }
    }

    internal static class TypeSystem
    {
        internal static Type GetElementType(Type seqType)
        {
            var ienum = FindIEnumerable(seqType);
            if (ienum == null)
                return seqType;
            return ienum.GetGenericArguments()[0];
        }

        private static Type FindIEnumerable(Type seqType)
        {
            if (seqType == null || seqType == typeof(string))
                return null;
            if (seqType.IsArray)
                return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
            if (seqType.IsGenericType)
            {
                foreach (var arg in seqType.GetGenericArguments())
                {
                    var ienum = typeof(IEnumerable<>).MakeGenericType(arg);
                    if (ienum.IsAssignableFrom(seqType))
                    {
                        return ienum;
                    }
                }
            }
            var ifaces = seqType.GetInterfaces();
            if (ifaces.Length > 0)
            {
                foreach (var iface in ifaces)
                {
                    var ienum = FindIEnumerable(iface);
                    if (ienum != null)
                        return ienum;
                }
            }
            if (seqType.BaseType != null && seqType.BaseType != typeof(object))
            {
                return FindIEnumerable(seqType.BaseType);
            }
            return null;
        }
    }
}

我对这个接口没有太多经验,有点不知所措。请问有什么建议吗?如果需要的话,我也可以完全放弃这种方法。
再次说明一下,我遇到了StackOverflowException,堆栈跟踪只是在上面两个注释之间进行了一堆调用,并在每对调用之间添加了“[External Code]”。我已经添加了一个使用两个小枚举类型的示例Main方法,但您可以想象这些数据源是更大的数据源,需要很长时间来枚举。
非常感谢您的帮助!

听起来如果我理解正确的话,你可能需要使用一些抽象类。如果是这种情况,请看一下这篇文章:https://dev59.com/AXA75IYBdhLWcg3waIQJ - MethodMan
谢谢您的回复。您有关于我应该使用哪些类的建议吗?我认为需要实现IQueryable是因为我正在使用的接口要求这样做,而且我不知道任何抽象(或具体)类可以给我所需的连接属性。 - cfred
说真的,我一时半会儿想不出别的,除了我在这个MSDN帖子中看到过的内容:http://msdn.microsoft.com/en-us/library/vstudio/bb534644(v=vs.100).aspx - MethodMan
这个必须在一般情况下工作吗?我的意思是,对于这种特殊情况,你不能只做 enumerableB.Where(condition).Concat(queryableA.Where(condition)) 吗? - Lucas Trzesniewski
很遗憾,是的,它需要成为一个通用解决方案,因为IQueryable在其他代码中被使用。 - cfred
1个回答

2
当您分解传递到中的表达式树时,您将看到LINQ方法的调用链。请记住,通常情况下,LINQ通过链接扩展方法来工作,其中上一个方法的返回值作为第一个参数传递到下一个方法中。
如果我们按照逻辑进行,那么这意味着链中的第一个LINQ方法必须具有源参数,并且从代码中可以明显看出,它的源实际上是一开始启动整个过程的同一个(即您的)。
当您构建这个时,您已经基本掌握了这个想法 - 您只需要再走一小步。我们需要做的是重新指向第一个LINQ方法以使用实际源,然后允许执行按其自然路径进行。
以下是执行此操作的示例代码:
    public object Execute(Expression expression)
    {
        var query1 = ChangeQuerySource(expression, Expression.Constant(source1));
        var query2 = ChangeQuerySource(expression, Expression.Constant(source2));
        dynamic results1 = source1.Provider.Execute(query1);
        dynamic results2 = source2.Provider.Execute(query2);
        return Enumerable.Concat(results1, results2);
    }

    private static Expression ChangeQuerySource(Expression query, Expression newSource)
    {
        // step 1: cast the Expression as a MethodCallExpression.
        // This will usually work, since a chain of LINQ statements
        // is generally a chain of method calls, but I would not
        // make such a blind assumption in production code.
        var methodCallExpression = (MethodCallExpression)query;

        // step 2: Create a new MethodCallExpression, passing in
        // the existing one's MethodInfo so we're calling the same
        // method, but just changing the parameters. Remember LINQ
        // methods are extension methods, so the first argument is
        // always the source. We carry over any additional arguments.
        query = Expression.Call(
            methodCallExpression.Method,
            new Expression[] { newSource }.Concat(methodCallExpression.Arguments.Skip(1)));

        // step 3: We call .AsEnumerable() at the end, to get an
        // ultimate return type of IEnumerable<T> instead of
        // IQueryable<T>, so we can safely use this new expression
        // tree in any IEnumerable statement.
        query = Expression.Call(
            typeof(Enumerable).GetMethod("AsEnumerable", BindingFlags.Static | BindingFlags.Public)
            .MakeGenericMethod(
                TypeSystem.GetElementType(methodCallExpression.Arguments[0].Type)
            ),
            query);
        return query;
    }

谢谢大家的评论。看来我可能需要检测表达式树的结尾并以某种方式结束递归,因为我只需要连接一次,然后就不再需要了。在Execute()方法中,我可以使用以下代码检测是否已经到达ConstantExpression:"if (expression.GetType() == typeof(ConstantExpression) && expression.Type.IsGenericType && expression.Type.GetGenericTypeDefinition() == typeof(ConcatenatedQueryable<>))",但是此时我实际上想调用.Where(expression)而不是.Provider.Execute(),但是expression不是正确的类型。 - cfred
谢谢 - 就这些了!漂亮的代码片段。尽管它似乎不能与LINQ-to-Entities一起使用。例如,如果我将IQueryable<TableObject>作为source1传入,我会得到错误:**从实例化的 'TableObject' 类型到 'System.Linq.IQueryable1[TableObject]' 类型的指定转换无效**,出现在source1.Provider.Execute(query1)上。有任何想法吗?如果我从表中提取一个子集到内存中,它就能工作。你可以用以下代码重现:对于任何EntitySet q,q.Provider.Execute(q.Expression)`,但对于任何内存中的IQueryable都能正常工作。谢谢。 - cfred
@cfred,你尝试运行什么确切的查询? - Rex M
@Rex M 只是在其中一个属性上使用了简单的 .Where() 子句。如果你不知道的话,我可以将其标记为已回答并自己查看,因为你已经回答了原问题...只是想试一下。我有 q = context.Entities.Where(x => x.Property = Value),如果我执行 q.Provider.Execute(q.Expression),我会得到上面的异常。 - cfred
@cfred,我不是立即知道答案,但我建议你调试Execute方法并且密切检查表达式树... 因为有些表达式一开始看起来是有效的,但仔细检查过后可能会发现问题。 - Rex M
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接