1

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
}
Mong Zhu
  • 23,309
  • 10
  • 44
  • 76
Jonas Olesen
  • 568
  • 5
  • 25
  • "I get the name of a property as a string" where do you get it from? user input? can you influence the source ? – Mong Zhu Aug 22 '18 at 10:44
  • from a controller, sadly i have no control over this – Jonas Olesen Aug 22 '18 at 10:44
  • might it be that Customer is a `partial class` which serves as an extension for an existing database model? – Mong Zhu Aug 23 '18 at 06:25
  • that is correct yes – Jonas Olesen Aug 23 '18 at 06:37
  • ok I learned a lot about Expression trees. From the point of applicability and especially readabilty I would advise you to stay with the if/else approach. I guess it can be solved but the effort is high and I guess the readability will suffer – Mong Zhu Aug 23 '18 at 09:13

1 Answers1

1

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();
    }
}
Mong Zhu
  • 23,309
  • 10
  • 44
  • 76
  • This appears to work if im using a class, but my IObjectWithProperties is an interface – Jonas Olesen Aug 22 '18 at 11:18
  • Ignore my comment, the error is elsewhere in the code – Jonas Olesen Aug 22 '18 at 11:25
  • Let me extend my question a bit, since this now results in a problem where i use the expression – Jonas Olesen Aug 22 '18 at 11:26
  • No problem, i really appreciate any help you can offer – Jonas Olesen Aug 22 '18 at 12:57
  • @JonasOlesen basically you need to do something like `.OrderBy(x => express.Compile().Invoke...` but I don't get which role the `IObjectWithProperties` plays in selecting a sorting column. does Customer implement `IObjectWithProperties` ? otherwise I cannot make sense of it – Mong Zhu Aug 22 '18 at 13:01
  • Yes it does, my bad, missed that when i cleaned up the example for SO – Jonas Olesen Aug 22 '18 at 13:04
  • @JonasOlesen. ok then I got your answer, give me a minute to reedit my post. Can you please update your question with this necessary information. so we make it coherent – Mong Zhu Aug 22 '18 at 13:05
  • @JonasOlesen ok I made an edit, using your newest information. Especially related to the interface. Have a look – Mong Zhu Aug 22 '18 at 13:31
  • This only works if i call .ToList() beforehand, meaning i load all the data into memory beforehand, if i try to do it before .ToList() i get the error: LINQ to Entities does not recognize the method 'System.Object Invoke(Customer)' method, and this method cannot be translated into a store expression. – Jonas Olesen Aug 22 '18 at 13:38
  • @JonasOlesen ok I tested it on a database table with an extended class. This solution works at my machine. Have a look. – Mong Zhu Aug 22 '18 at 14:35
  • 1
    This method still transforms it into an ienumrable, if you look at the sql generated by your suggestion vs if i just do .OrderBy(x=>x.customerNo), i can see that your suggestion omits the ORDER BY from the sql, and performs the ordering in memory instead – Jonas Olesen Aug 23 '18 at 06:36
  • @JonasOlesen crap. It always feels like *almost there* but it ain't :D that's a tough nut – Mong Zhu Aug 23 '18 at 06:46
  • Ye :p at this point im tempted to just stick to the if statement hell – Jonas Olesen Aug 23 '18 at 06:51
  • @JonasOlesen ok, I found one solution. It translates the orderby into sql. But `int32` cannot be used apparently. Haven't figured out why yet – Mong Zhu Aug 23 '18 at 08:34
  • @JonasOlesen found a better solution. It works on either type. Learned a lot though. If it helped you you might mark it as accepted ;) cheers mate – Mong Zhu Aug 23 '18 at 09:31
  • 1
    Thanks a lot. Ill give this a try and if it doesnt work, ill stick to if / else :) – Jonas Olesen Aug 23 '18 at 09:43
  • @JonasOlesen you are welcome, thanx for the opportunity to dig again in expression trees ;) – Mong Zhu Aug 23 '18 at 10:00