Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Refactoring Func<T> into Expression<Func<T>>

I have a method that currently takes a Func<Product, string> as a parameter, but I need it to be an Expression<Func<Product, string>>. Using AdventureWorks, here's an example of what I'd like to do using the 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
        });
    }
}

I would like it to look something like this:

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

However, the problem I'm running into is that myExpression(product) is invalid (won't compile). After reading some other posts I understand why. And if it wasn't for the fact that I need the product variable for the second part of my key I could probably say something like this:

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

But I do need the product variable because I do need the second part of the key (ProductNumber). So I'm not really sure what to do now. I can't leave it as a Func because that causes problems. I can't figure out how to use an Expression because I don't see how I could pass it the product variable. Any ideas?

EDIT: Here's an example of how I would call the method:

DoSomethingWithFunc(product => product.ProductSubcategory.Name);
like image 740
Ecyrb Avatar asked Oct 20 '09 16:10

Ecyrb


2 Answers

There's no way to splice an expression tree that is represented as an Expression<T> object into a middle of a "tree literal" represented by lambda expression. You'll have to construct an expression tree to pass to GroupBy manually:

// 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);
    }
}
like image 177
Pavel Minaev Avatar answered Oct 04 '22 20:10

Pavel Minaev


On second thought, compiling the expression won't work.

You'll need to build your GroupBy expression by hand, which means you can't use anonymous type. I would suggest building out the rest of your expression and then decompiling to see the generated expression tree. The end result will look something like this, using parts of myExpression as appropriate:

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

        // ...
    }
}
like image 40
dahlbyk Avatar answered Oct 04 '22 19:10

dahlbyk