Jonas Olesen
Jonas Olesen

Reputation: 568

Iterate through property and creating an Expression<Func<>> with the result

Im not used to working with expression funcs, but my problem is: I get the name of a property as a string, i then need to convert this to the appropriate expression.

Currently im doing something like this:

if (string.Equals(propertyString, "customerNo", StringComparison.InvariantCultureIgnoreCase))
{
    return _repo.DoSomething(x=>x.CustomerNo);
}
if (string.Equals(propertyString, "customerName", StringComparison.InvariantCultureIgnoreCase))
{
    return _repo.DoSomething(x => x.CustomerName);
}

With the repo function something like this:

public IEnumerable<ICustomer> DoSomething(Expression<Func<IObjectWithProperties, object>> express)
{
    //Do stuff
}

What i would like to do, is use reflection like this:

var type = typeof(IObjectWithProperties);
PropertyInfo[] properties = type.GetProperties();

foreach (PropertyInfo property in properties)
{
    if(string.Equals(property.Name,propertyString,StringComparison.InvariantCultureIgnoreCase))
        return _repo.DoSomething(x => x.PROPERTY);
}

But I can't figure out a way to generate the expression func from the propertyinfo

EDIT: Mong Zhu's answer, i am able to create an expression using the property.

The reason i need this expression, is that im trying to dynamically set the orderby in an iqueryable.

public IEnumerable<Customer> List(Expression<Func<IObjectWithProperties, object>> sortColumn)
{
    using (var context = _contextFactory.CreateReadOnly())
    {
        return context.Customers.OrderBy(sortColumn).ToList();
    }
}

Using the answer like this:

public Customer Test(string sortColumn){

        var type = typeof(IObjectWithProperties);
        PropertyInfo[] properties = type.GetProperties();

        foreach (PropertyInfo property in properties)
        {
            if (string.Equals(property.Name, sortColumn, StringComparison.InvariantCultureIgnoreCase))
            {
                Expression<Func<IObjectWithProperties, object>> exp = u =>
                (
                    u.GetType().InvokeMember(property.Name, BindingFlags.GetProperty, null, u, null)
                );

                return _customerRepository.List(exp);
            }
        }
}

I get an error:

System.InvalidOperationException : variable 'u' of type 'IObjectWithProperties' referenced from scope '', but it is not defined

EDIT:

The Customer return type inherits the IObjectWithProperties:

public class Customer: IObjectWithProperties
{
     //properties
}

Upvotes: 1

Views: 538

Answers (1)

Mong Zhu
Mong Zhu

Reputation: 23732

OK, after digging around I found a working solution for EF in this answer.

It has to be slightly modified

private static Expression<Func<T, object>> ToLambda<T>(string propertyName)
{
    var parameter = Expression.Parameter(typeof(T));
    var property = Expression.Property(parameter, propertyName);
    return Expression.Lambda<Func<T, object>>(property, parameter);
}

The call would look like this:

var type = typeof(IObjectWithProperties);
PropertyInfo[] properties = type.GetProperties();

foreach (PropertyInfo property in properties)
{
    if (string.Equals(property.Name, propertyString, StringComparison.InvariantCultureIgnoreCase))
    {
        var result = DoSomething(ToLambda<IObjectWithProperties>(property.Name));
    }
}

I will assume that Customer is an partial class which implements the interface IObjectWithProperties and an extension to a existing database table. So your orderby method should look like this:

public IEnumerable<Customer> DoSomething(Expression<Func<IObjectWithProperties, object>> sortColumn)
{
    using (var context = _contextFactory.CreateReadOnly())
    {
        return context.Customers.OrderBy(sortColumn).Cast<Customer>().ToList(); 
    }          
}

The important thing that you need to do here is to call Compile() which will allow to be translated into an sql statement and send to the server for querying. Since you are using an Interface as input parameter for the Func the compiler seems not be able to deduce that you partial class implements this interface. So a further explicit Cast<Customer>() call is necessary to establish the correct return type.

I hope this is understandable and helps you to solve also the second problem

This solution translates also the OrderBy clause to SQL.

Disclaimer:

Unfortunately it works with string properties, but up to now NOT with Int32. I am still trying to figure out why.

EDIT:

Meanwhile I found another solution in this answer by David Specht

This extension class can really be used as copy paste and it works on either type. Here is the important code that you need:

public static class IQueryableExtensions
{
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
    {
        return CallOrderedQueryable(query, "OrderBy", propertyName, comparer);
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
    {
        return CallOrderedQueryable(query, "OrderByDescending", propertyName, comparer);
    }

    /// <summary>
    /// Builds the Queryable functions using a TSource property name.
    /// </summary>
    public static IOrderedQueryable<T> CallOrderedQueryable<T>(this IQueryable<T> query, string methodName, string propertyName,
            IComparer<object> comparer = null)
    {
        var param = Expression.Parameter(typeof(T), "x");

        var body = propertyName.Split('.').Aggregate<string, Expression>(param, Expression.PropertyOrField);

        return comparer != null
            ? (IOrderedQueryable<T>)query.Provider.CreateQuery(
                Expression.Call(
                    typeof(Queryable),
                    methodName,
                    new[] { typeof(T), body.Type },
                    query.Expression,
                    Expression.Lambda(body, param),
                    Expression.Constant(comparer)
                )
            )
            : (IOrderedQueryable<T>)query.Provider.CreateQuery(
                Expression.Call(
                    typeof(Queryable),
                    methodName,
                    new[] { typeof(T), body.Type },
                    query.Expression,
                    Expression.Lambda(body, param)
                )
            );
    }
}

And your order method would look simply like this:

public IEnumerable<Customer> DoSomething(string propertyName)
{
    using (var context = _contextFactory.CreateReadOnly())
    {                
        return context.Customers.OrderBy(propertyName).ToList();
    }
}

Upvotes: 1

Related Questions