将 Func<T> 重构为 Expression<Func<T>>

5

我有一个方法,目前它接受一个Func<Product, string>作为参数,但我需要它成为一个Expression<Func<Product, string>>。使用AdventureWorks,以下是我想使用Func完成的示例。

private static void DoSomethingWithFunc(Func<Product, string> myFunc)
{
    using (AdventureWorksDataContext db = new AdventureWorksDataContext())
    {
        var result = db.Products.GroupBy(product => new
        {
            SubCategoryName = myFunc(product),
            ProductNumber = product.ProductNumber
        });
    }
}

我希望它看起来像这样:

private static void DoSomethingWithExpression(Expression<Func<Product, string>> myExpression)
{
    using (AdventureWorksDataContext db = new AdventureWorksDataContext())
    {
        var result = db.Products.GroupBy(product => new
            {
                SubCategoryName = myExpression(product),
                ProductNumber = product.ProductNumber
            });
    }
}

然而,我遇到的问题是 myExpression(product) 无效(无法编译)。阅读了一些其他帖子后,我明白了其中的原因。如果不是因为我需要在密钥的第二部分使用 product 变量,我可能可以这样说:

var result = db.Products.GroupBy(myExpression);

但是我需要 product 变量,因为我需要键的第二部分 (ProductNumber)。所以我不确定该怎么办。我不能将它留作 Func,因为会引起问题。我也找不出如何使用 Expression,因为我不知道如何传递 product 变量。有什么建议吗?

编辑: 这里是我如何调用该方法的示例:

DoSomethingWithFunc(product => product.ProductSubcategory.Name);
2个回答

4

如果以Expression<T>对象表示的表达式树需要插入到由lambda表达式表示的“树形文字”中间,则没有办法进行切割。您必须手动构造一个表达式树来传递给GroupBy

// Need an explicitly named type to reference in typeof()
private class ResultType
{
     public string SubcategoryName { get; set; }
     public int ProductNumber { get; set; }|
}

private static void DoSomethingWithExpression(
    Expression<Func<Product,
    string>> myExpression)
{
    var productParam = Expression.Parameter(typeof(Product), "product");
    var groupExpr = (Expression<Func<Product, ResultType>>)Expression.Lambda(
        Expression.MemberInit(
           Expression.New(typeof(ResultType)),
           Expression.Bind(
               typeof(ResultType).GetProperty("SubcategoryName"),
               Expression.Invoke(myExpression, productParam)),
           Expression.Bind(
               typeof(ResultType).GetProperty("ProductNumber"),
               Expression.Property(productParam, "ProductNumber"))),
        productParam);
    using (AdventureWorksDataContext db = new AdventureWorksDataContext())
    {
        var result = db.Products.GroupBy(groupExpr);
    }
}

不错!但是最后一行编译不通过,因为无法从使用中推断出方法的类型参数。我有什么遗漏吗? - Ecyrb
1
Expression.Lambda的返回值应该转换为Expression<Func<Product, ResultType>> - Ben M
太好了!这比我预想的要复杂得多。我以前从未像这样制作过自己的表达式,所以我将研究这段代码,以确保我完全理解其中的内容。谢谢! - Ecyrb
是的,学习如何手动构建表达式树从来都不会太早;有许多常见情况几乎要求这样做... - Ben M
1
看起来你也可以这样说:Expression.Lambda>(...而不是强制转换。 - Ecyrb

3

仔细想想,编译表达式不起作用。

你需要手动构建GroupBy表达式,这意味着你不能使用匿名类型。我建议先构建出剩余的表达式,然后反编译以查看生成的表达式树。最终结果将类似于这样,使用myExpression的部分:

private static void DoSomethingWithExpression(Expression<Func<Product, string>> myExpression)
{
    var productParam = myExpression.Parameters[0];

    ConstructorInfo constructor = ...; // Get c'tor for return type

    var keySelector = Expression.Lambda(
                          Expression.New(constructor,
                              new Expression[] {
                                  productParam.Body,
                                  ... // Expressions to init other members
                              },
                              new MethodInfo[] { ... }), // Setters for your members
                          new [] { productParam });

    using (AdventureWorksDataContext db = new AdventureWorksDataContext())
    {
        var result = db.Products.GroupBy(keySelector);

        // ...
    }
}

你错过了他从Func转移到Expression的原因 - 他希望这在LINQ to SQL上下文中能够工作,而这不允许在查询中进行随机函数(或委托)调用。 - Pavel Minaev
你比Pavel更快地回答了问题,但他的回答对我来说更清晰。不过还是要感谢你的帮助!+1 - Ecyrb
同意Pavel的答案更好。我基于构建匿名类型生成的表达式编写了我的代码;对于命名类型,你需要使用MemberInit。 - dahlbyk

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