关于c#:将IQueryable与IEnumerable连接成IQueryable

Concatenating an IQueryable with an IEnumerable into an IQueryable

最近几天,我在网上搜索了一个解决方案,但还没有找到我想要的。基本上,我的问题是:

  • 我有一个我需要实现的接口,它有一个返回iQueryable的方法(我没有访问该接口的权限,因此我无法更改此接口)
  • 我希望该方法返回(a)指向非常大的数据库表的iQueryable和(b)在相同实体类型的内存中计算的大型IEnumerable的串联
  • 我不能执行queryableA.concat(EnumerableB).where(condition),因为它将尝试将整个数组发送到服务器(除此之外,我还得到一个异常,它只支持基元类型)
  • 我不能执行EnumerableB.Concat(QueryableA).Where(Condition),因为它会将整个表拖到内存中,并将其视为IEnumerable
  • 因此,在进行了一些搜索之后,我认为我已经决定了一个解决这个问题的好方法,那就是编写我自己的IQueryable的可连接查询实现,它接受两个IQueryable,并独立地对每个IQueryable执行表达式树,然后连接结果。但是,当它返回堆栈溢出时,我似乎遇到了一些问题。基于http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx,这是我迄今为止实现的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    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,stackTrace只是上面两行注释之间的一系列调用,每对调用之间都有"[外部代码]"。我添加了一个使用两个小的可枚举项的主方法示例,但是您可以想象这些是需要很长时间才能枚举的较大的数据源。

    非常感谢您的帮助!


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

    如果从逻辑上讲,这意味着链中的第一个linq方法必须有一个源参数,而且从代码中可以清楚地看到,它的源实际上与启动整个过程的IQueryable(您的ConcatenatingQueryable)完全相同。

    当你建造这个的时候,你的想法是正确的-你只需要再往前走一小步。我们需要做的是重新指出,第一个LINQ方法使用实际的源,然后允许执行遵循其自然路径。

    下面是一些执行此操作的示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
        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;
        }