将IQueryable与IEnumerable连接成IQueryable

时间:2014-09-15 19:09:43

标签: c# linq iqueryable

过去几天我在互联网上搜索了解决方案,并且找不到我想要的东西。基本上,这是我的问题:

  1. 我有一个我需要实现的接口,它有一个返回IQueryable的方法(我无法访问该接口,因此我无法更改)
  2. 我希望该方法返回(a)指向非常大的数据库表的IQueryable和(b)在同一实体类型的内存中计算的大型IEnumerable的串联
  3. 我不能做queryableA.Concat(enumerableB)。所以(条件)因为它会尝试将整个数组发送到服务器(除此之外,我得到一个它只支持基本类型的异常)
  4. 我无法执行enumerableB.Concat(queryableA).Where(condition)因为它会将整个表拉入内存并将其视为IEnumerable
  5. 所以,经过一些搜索,我认为我已经决定了解决这个问题的一个好方法是编写我自己的IQueryable的ConcatenatingQueryable实现,该实现需要两个IQueryable并在每个上独立执行Expression树,然后连接结果。但是,我似乎遇到了问题,因为它返回堆栈溢出。基于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,而堆栈跟踪只是上面两条评论行之间的一串调用,其中包含&#34; [外部代码]&#34;在每对电话之间。我添加了一个使用两个微小枚举的示例Main方法,但您可以想象这些是需要很长时间枚举的较大数据源。

    非常感谢您的帮助!

1 个答案:

答案 0 :(得分:2)

当你分解传递给IQueryProvider的表达式树时,你会看到LINQ方法的调用链。请记住,通常LINQ通过链接扩展方法来工作,其中前一个方法的返回值作为第一个参数传递给下一个方法。

如果我们逻辑地遵循这一点,那意味着链中的第一个LINQ方法必须有一个源参数,并且从代码中可以看出它的源实际上是同样的IQueryable。首先是你的ConcatenatingQueryable)。

当你构建它时,你几乎已经有了这个想法 - 你只需要再向前迈一小步。我们需要做的是重新指出第一个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;
    }