Kleber Sales
Kleber Sales

Reputation: 13

How to select Item from an property list using Expression Tree in c#

I have a lambda expression as follows:

var source = new List<Entidade>();

var z = source.Select<Entidade, Resultado>(
                s =>
                new Resultado
                    {
                        Detalhes =
                            new List<DetalheResultado>(
                            s.Detalhes.Select<Detalhe, DetalheResultado>(
                                t => new DetalheResultado { Id = t.Id, Valor = t.Valor }))
                    });

I am trying to execute the same query with Expressions with the following code:

ParameterExpression sourceItem = Expression.Parameter(typeof(Entidade), "s");

var source3 = Expression.Parameter(typeof(Detalhe), "t");
var property3 = typeof(DetalheResultado).GetProperty("Id");
var member3 = Expression.Property(source3, "Id");
var itemResult3 = Expression.New(typeof(DetalheResultado).GetConstructor(Type.EmptyTypes));
var memberBinding3 = Expression.Bind(property3, member3);
var memberInit3 = Expression.MemberInit(itemResult3, memberBinding3);
var selector3 = Expression.Lambda(memberInit3, source3);

var detalhes = Expression.Property(sourceItem, "Detalhes");

// here you get an error
var lista3 = Expression.Call(
    typeof(Queryable), 
    "Select", 
    new Type[] { typeof(Detalhe), typeof(DetalheResultado) },
    detalhes, 
    selector3);

var listaResultado = typeof(DetalheResultado).GetProperty("Detalhes");
var memberBindigs4 = Expression.Bind(listaResultado, lista3);

...

but running this code I got the error:

No generic method 'Select ' on ' System.Linq.Queryable ' type is compatible with the arguments and the supplied type arguments. Any argument must be provided if the method is not generic.

I consulted the DebugView expression and implemented expressions as its return, but get the aforementioned error.

Any suggestions?

Upvotes: 1

Views: 1859

Answers (2)

Zev Spitz
Zev Spitz

Reputation: 15297

(Disclaimer: I am the author of the library in question.)

I've written a library that takes an expression tree and returns a string representation, also available via NuGet. The library allows you to see the factory method calls used to generate the expression.

For example, you could write the following code:

var source = new List<Entidade>();

Expression<Action> expr = () => source.Select<Entidade, Resultado>(
    s =>
        new Resultado {
            Detalhes = new List<DetalheResultado>(
                s.Detalhes.Select<Detalhe, DetalheResultado>(
                    t => new DetalheResultado { Id = t.Id, Valor = t.Valor }
                )
            )
        }
);

Console.WriteLine(expr.ToString("Factory methods"));

and get back the following output:

// using static System.Linq.Expressions.Expression

Lambda(
    Call(
        typeof(Enumerable).GetMethod("Select"),
        source,
        Lambda(
            MemberInit(
                New(
                    typeof(Resultado).GetConstructor()
                ),
                Bind(
                    typeof(Resultado).GetProperty("Detalhes"),
                    New(
                        typeof(List<DetalheResultado>).GetConstructor(),
                        Call(
                            typeof(Enumerable).GetMethod("Select"),
                            MakeMemberAccess(s,
                                typeof(Entidade).GetProperty("Detalhes")
                            ),
                            Lambda(
                                MemberInit(
                                    New(
                                        typeof(DetalheResultado).GetConstructor()
                                    ),
                                    Bind(
                                        typeof(DetalheResultado).GetProperty("Id"),
                                        MakeMemberAccess(t,
                                            typeof(Detalhe).GetProperty("Id")
                                        )
                                    ),
                                    Bind(
                                        typeof(DetalheResultado).GetProperty("Valor"),
                                        MakeMemberAccess(t,
                                            typeof(Detalhe).GetProperty("Valor")
                                        )
                                    )
                                ),
                                var t = Parameter(
                                    typeof(Detalhe),
                                    "t"
                                )
                            )
                        )
                    )
                )
            ),
            var s = Parameter(
                typeof(Entidade),
                "s"
            )
        )
    )
)

If you plug in your own classes, you'll probably get better results. The classes I used were all auto-generated by Visual Studio, as follows:

internal class Detalhe {
    public object Id { get; internal set; }
    public object Valor { get; internal set; }
}

internal class DetalheResultado {
    public object Id { get; internal set; }
    public object Valor { get; internal set; }
}

internal class Resultado {
    public List<DetalheResultado> Detalhes { get; internal set; }
}

internal class Entidade {
    public IEnumerable<Detalhe> Detalhes { get; internal set; }
}

Upvotes: 0

Shlomo
Shlomo

Reputation: 14350

I have never had luck with using that Expression.Call method on the LINQ generic methods. I always fetch it separately (see variables firstSelectMethod and secondSelectMethod). I don't know why, and if someone else knows why that won't work, I would be much obliged. The below code works, though I made some assumptions about what your classes look like.

Please note that I substituted Queryable for Enumerable.

var paramS = Expression.Parameter(typeof(Entidade), "s");
var paramT = Expression.Parameter(typeof(Detalhe), "t");

var firstSelectMethod = typeof(Enumerable).GetMethods().First(m => m.Name == "Select").MakeGenericMethod(typeof(Entidade), typeof(Resultado));
var secondSelectMethod = typeof(Enumerable).GetMethods().First(m => m.Name == "Select").MakeGenericMethod(typeof(Detalhe), typeof(DetalheResultado));

var lista4 = Expression.Call(
    firstSelectMethod,
    Expression.Constant(source),
    Expression.Lambda(
        Expression.MemberInit(
            Expression.New(typeof(Resultado).GetConstructor(Type.EmptyTypes)), 
            Expression.Bind(
                typeof(Resultado).GetProperty("Detalhes"), 
                Expression.New(
                    typeof(List<DetalheResultado>).GetConstructor(new Type[] {typeof(IEnumerable<DetalheResultado>)}),
                    Expression.Call(
                        secondSelectMethod,
                        Expression.Property(
                            paramS,
                            "Detalhes"
                        ),
                        Expression.Lambda(
                            Expression.MemberInit(
                                Expression.New(typeof(DetalheResultado).GetConstructor(Type.EmptyTypes)), 
                                Expression.Bind(
                                    typeof(DetalheResultado).GetProperty("Id"),
                                    Expression.Property(paramT, "Id")
                                ),
                                Expression.Bind(
                                    typeof(DetalheResultado).GetProperty("Valor"),
                                    Expression.Property(paramT, "Valor")
                                )
                            ),
                            paramT
                        )
                    )
                )
            )
        ), 
        paramS
    )
);

Upvotes: 1

Related Questions