如何创建 IEnumerable 扩展方法的 IQueryable 版本?

How to create the IQueryable version of IEnumerable extension method?

最近,我对 IEnumerableIQueryable 接口之间的差异更感兴趣,因此,我发现 IQueryable 在许多情况下比 [=12= 更有效],虽然我还没有完全掌握它们。既不将表达式树与 IQueryable 结合使用,但我想提高我创建的扩展方法的性能:

    public static IEnumerable<TSource> In<TSource, TMember>(this IEnumerable<TSource> source,
        Func<TSource, TMember> identifier, params TMember[] values) =>
     source.Where(m => values.Contains(identifier(m)));

据我所知,我想做 IQueryable 版本,所以,与其从服务器获取所有记录并在内存中过滤它们,我只想从中获取过滤后的记录服务器,例如 运行 服务器上的这个查询:SELECT * FROM Books WHERE Id IN (1, 2, 3) 当调用这个 books.In(x => x.Id, 1, 2, 3) 时,所以这就是我想出的:

 public static IQueryable<TSource> In<TSource, TMember>(this IQueryable<TSource> source,
      Expression<Func<TSource, TMember>> identifier, params TMember[] values) =>
    source.Where(m => values.Contains(identifier.Compile()(m)));

老实说,我在尝试了一些错误之后想出了这段代码,它确实有效,但我不确定这是否是我制作 IQueryable 扩展方法的方式?

编辑

正如xanatos的回答所建议的那样,我在VS中对其进行了测试并且它也有效, 但我有一些问题想了解发生了什么:

  1. 你怎么知道它是正确的,如果他们都给出 IQueryable 结果,我的尝试和你的有什么区别(当然我知道我的不正确!)?
  2. 我可以自己测试一下,一个正确另一个不正确吗? (你提到你用 AsQueryable 测试过它,怎么样?)

我注意到您的代码的结果类型为:

{System.Collections.Generic.List'1[NewNS.Book].Where(x => value(System.Int32[]).Contains(x.Id))}

我的在哪里: {System.Collections.Generic.List'1[NewNS.Book].Where(m => value(NewNS.Linqs+<>c__DisplayClass0_0'2[NewNS.Book,System.Int32]).values.Contains(Invoke(value(NewNS.Linqs+<>c__DisplayClass0_0'2[NewNS.Book,System.Int32]).identifier.Compile(), m)))}

  1. 它们是什么意思,我能从这些差异中看出什么?

如果您回答这些问题以帮助我了解 IQueryable 的工作原理,我将不胜感激..

有点复杂。我用 AsQueryable() 测试过它。我还没有用 Entity Framework 测试过它,但它应该可以工作。代码有大量注释。

// The Enumerable.Contains method
private static readonly MethodInfo Contains = (from x in typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
                                                where x.Name == nameof(Enumerable.Contains)
                                                let args = x.GetGenericArguments()
                                                where args.Length == 1
                                                let pars = x.GetParameters()
                                                where pars.Length == 2 &&
                                                    pars[0].ParameterType == typeof(IEnumerable<>).MakeGenericType(args[0]) &&
                                                    pars[1].ParameterType == args[0]
                                                select x).Single();

public static IQueryable<TSource> In<TSource, TMember>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, TMember>> identifier, params TMember[] values)
{
    // Some argument checks
    if (source == null)
    {
        throw new NullReferenceException(nameof(source));
    }

    if (identifier == null)
    {
        throw new NullReferenceException(nameof(identifier));
    }

    if (values == null)
    {
        throw new NullReferenceException(nameof(values));
    }

    // We only accept expressions of type x => x.Something
    // member wil be the x.Something
    var member = identifier.Body as MemberExpression;

    if (member == null)
    {
        throw new ArgumentException(nameof(identifier));
    }

    // Enumerable.Contains<TMember>
    var contains = Contains.MakeGenericMethod(typeof(TMember));

    // Enumerable.Contains<TMember>(values, x.Something)
    var call = Expression.Call(contains, Expression.Constant(values), member);

    // x => Enumerable.Contains<TMember>(values, x.Something)
    var lambda = Expression.Lambda<Func<TSource, bool>>(call, identifier.Parameters);

    return source.Where(lambda);
}

不缓存 MethodInfo 的较短版本(见评论):

public static IQueryable<TSource> In<TSource, TMember>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, TMember>> identifier, params TMember[] values)
{
    // Some argument checks
    if (source == null)
    {
        throw new NullReferenceException(nameof(source));
    }

    if (identifier == null)
    {
        throw new NullReferenceException(nameof(identifier));
    }

    if (values == null)
    {
        throw new NullReferenceException(nameof(values));
    }

    // We only accept expressions of type x => x.Something
    // member wil be the x.Something
    var member = identifier.Body as MemberExpression;

    if (member == null)
    {
        throw new ArgumentException(nameof(identifier));
    }

    // Enumerable.Contains<TMember>(values, x.Something)
    var call = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[] { typeof(TMember) }, Expression.Constant(values), member);

    // x => Enumerable.Contains<TMember>(values, x.Something)
    var lambda = Expression.Lambda<Func<TSource, bool>>(call, identifier.Parameters);

    return source.Where(lambda);
}

只是为了好玩,"simple" 在不使用反射的情况下找到 Enumerable.Contains<T> 的方法(与第一个示例一起使用):

private static readonly MethodInfo Contains = ((MethodCallExpression)((Expression<Func<bool>>)(() => new object[0].Contains(new object()))).Body).Method.GetGenericMethodDefinition();
  1. How do you know that it works right, what does differentiate my try from your's if they both give IQueryable result(off course I know mine is not correct!)?

因为我 运行 它 :-) 你的不能工作,因为中间有一个 .Compile()。我知道 Entity Framework 库和 LINQ-to-SQL 库不支持 .Compile()(也不支持 .Invoke()),所以我知道你的不支持工作。

  1. Could I test it to see my self if one is correct and other not? (you mentioned you tested it with AsQueryable, how?)

有一项 "smell" 测试,但您的测试也可能通过了。

new[] { new { ID = 1 }, new { ID = 2 } }.AsQueryable().In(x => x.ID, 2, 4).ToArray()

唯一真正的测试是用它来对抗 Entity Framework。

  1. what do they mean, and what can I tell from these differences?

它是一个"textual representation"的表达式树。光是看一眼,就能看到.Compile().Invoke()了。您必须记住,使用 IQueryable<> "normally"(因此不包括在本地执行的 AsQueryable()),您的查询 t运行 被指定为 "language" "server"(通常是SQL服务器)可以理解,可以远程执行。这个 "translator" 非常有限,只知道一些方法(最常见的)。因此,如果您尝试使用任何不是 "one of the most common methods" 的内容,那么您的查询将在执行时中断。