简体   繁体   English

将IQueryable与IEnumerable连接成IQueryable

[英]Concatenating an IQueryable with an IEnumerable into an IQueryable

I have searched the internet for the last couple days for a solution to this, and haven't found what I've wanted. 最近几天我在互联网上搜索了解决方案,并没有找到我想要的东西。 Basically, here is my problem: 基本上,这是我的问题:

  1. I have an interface I need to implement that has a method that returns an IQueryable (I don't have access to the interface, so I cannot change this) 我有一个我需要实现的接口,它有一个返回IQueryable的方法(我没有访问接口,所以我不能改变它)
  2. I would like the method to return the concatenation of (a) an IQueryable that points to a very large database table, and (b) a large IEnumerable that has been computed in memory of the same Entity type 我希望该方法返回(a)指向非常大的数据库表的IQueryable和(b)在同一实体类型的内存中计算的大IEnumerable的串联
  3. I cannot do queryableA.Concat(enumerableB).Where(condition) because it will try to send the entire array to the server (and, aside from that, I get an exception that it only supports primitive types) 我不能做queryableA.Concat(enumerableB).Where(condition)因为它会尝试将整个数组发送到服务器(除此之外,我得到一个例外,它只支持基本类型)
  4. I cannot do enumerableB.Concat(queryableA).Where(condition) because it will pull the entirety of the table into memory and treat it as an IEnumerable 我不能做enumerableB.Concat(queryableA).Where(condition)因为它会将整个表拉入内存并将其视为IEnumerable

So, after some searching, I think I've decided a good way to approach this problem is to write my own ConcatenatingQueryable implementation of IQueryable that takes two IQueryable's and executes the Expression tree on each independently, and then concatenations the results. 因此,经过一些搜索,我认为我已经决定了解决这个问题的一个好方法是编写我自己的IQueryable的ConcatenatingQueryable实现,该实现需要两个IQueryable并在每个上独立执行Expression树,然后连接结果。 However, I seem to be having issues as it returns a stack overflow. 但是,我似乎遇到了问题,因为它返回堆栈溢出。 Based on http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx , this is what I've implemented so far: 基于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;
        }
    }
}

I don't have much experience with this interface, and am a bit lost as to what to do from here. 我对这个界面没有多少经验,而且从这里开始做什么有点遗失。 Does anyone have any suggestions on how to do this? 有没有人对如何做到这一点有任何建议? I'm also open to abandoning this approach entirely if need be. 如果需要,我也完全放弃这种方法。

Just to reiterate, I'm getting a StackOverflowException, and the stacktrace is simply a bunch of calls between the two commented lines above, with "[External Code]" in between each pair of calls. 重申一下,我得到一个StackOverflowException,堆栈跟踪只是上面两条注释行之间的一堆调用,每对调用之间都有“[External Code]”。 I have added an example Main method that uses two tiny enumerables, but you can imagine these were larger data sources that take a very long time to enumerate. 我添加了一个使用两个微小枚举的示例Main方法,但您可以想象这些是需要很长时间枚举的较大数据源。

Thank you very much in advance for your help! 非常感谢您的帮助!

When you break down the expression tree that gets passed into the IQueryProvider , you will see the call chain of LINQ methods. 当您分解传递给IQueryProvider的表达式树时,您将看到LINQ方法的调用链。 Remember that generally LINQ works by chaining extension methods, where the return value of the previous method is passed into the next method as the first argument. 请记住,LINQ通常通过链接扩展方法来工作,其中前一个方法的返回值作为第一个参数传递给下一个方法。

If we follow that logically, that means the very first LINQ method in the chain must have a source argument, and it's plain from the code that its source is, in fact, the very same IQueryable that kicked the whole thing off in the first place (your ConcatenatingQueryable ). 如果我们在逻辑上遵循这一点,那意味着链中的第一个LINQ方法必须具有源参数,并且从代码中可以看出它的来源实际上是完全相同的IQueryable (你的ConcatenatingQueryable )。

You pretty much got the idea right when you built this - you just need to go one small step further. 当你构建它时,你几乎已经有了这个想法 - 你只需要再向前迈一小步。 What we need to do is re-point that first LINQ method to use the actual source, then allow the execution to follow its natural path. 我们需要做的是重新指出第一个LINQ方法使用实际的源,然后允许执行遵循其自然路径。

Here is some example code that does this: 以下是一些示例代码:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM