Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework: select property as Object

I'm running into some trouble when trying to retrieve property values as Objects instead of their respective types. The following code throws this exception:

Unable to cast the type 'System.DateTime' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.

This code works fine when selecting a string, but not when selecting DateTimes, Integers or Nullable types.

public class Customer
{
    public int Id { get; set; }

    public string Name { get; set; }

    public DateTime CreatedOn { get; set; }
}

public class Program
{
    public static void Main(string[] args)
    {
        using (var ctx = new MyContext())
        {
            // Property selector: select DateTime as Object
            Expression<Func<Customer, object>> selector = cust => cust.CreatedOn;

            // Get set to query
            IQueryable<Customer> customers = ctx.Set<Customer>();

            // Apply selector to set. This throws: 
            // 'Unable to cast the type 'System.DateTime' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.'
            IList<object> customerNames = customers.Select(selector).Distinct().ToList();

        }
    }
}

public class MyContext : DbContext
{

}

The end goal is a generic filtering to select distinct values from any of an object's properties.

like image 454
user2346738 Avatar asked Oct 20 '22 04:10

user2346738


1 Answers

I understand that you want to use the inline Expression declaration to select the property in a convenient way (without such as having to parse a dot-separated string representing the property path and using Reflection). However doing so will require the Expression to be declared explicitly and we have to use explicit type. Unfortunately that the object type cannot be used as the return type of the Expression because later on it cannot be converted to one of the supported types in the database.

I think there is a work-around here. The idea is we will convert the Expression<T,object> to another Expression<T,returnType> where returnType is the actual return type of the property (returned by selector). However Select always require an explicit type of Expression<T,returnType> meaning returnType should be known at design time. So that's impossible. We have no way to call Select directly. Instead we have to use Reflection to invoke the Select. The return result is expected as an IEnumerable<object> which you then can call ToList() to get a list of objects as what you want.

Now you can use this extension method for IQueryable<T>:

public static class QExtension
{
    public static IEnumerable<object> Select<T>(this IQueryable<T> source, 
                                               Expression<Func<T, object>> exp) where T : class
    {
        var u = exp.Body as UnaryExpression;
        if(u == null) throw new ArgumentException("exp Body should be a UnaryExpression.");            
        //convert the Func<T,object> to Func<T, actualReturnType>
        var funcType = typeof(Func<,>).MakeGenericType(source.ElementType, u.Operand.Type);
        //except the funcType, the new converted lambda expression 
        //is almost the same with the input lambda expression.
        var le = Expression.Lambda(funcType, u.Operand, exp.Parameters);            
        //try getting the Select method of the static class Queryable.
        var sl = Expression.Call(typeof(Queryable), "Select", 
                                 new[] { source.ElementType, u.Operand.Type }, 
                                 Expression.Constant(source), le).Method;
        //finally invoke the Select method and get the result 
        //in which each element type should be the return property type 
        //(returned by selector)
        return ((IEnumerable)sl.Invoke(null, new object[] { source, le })).Cast<object>();
    }        
}

Usage: (exactly as your code)

Expression<Func<Customer, object>> selector = cust => cust.CreatedOn;
IQueryable<Customer> customers = ctx.Set<Customer>();
IList<object> customerNames = customers.Select(selector).Distinct().ToList();

At first I tried accessing the exp.Body.Type and thought it was the actual return type of the inner expression. However somehow it's always System.Object except the special case of string (when the return type of property access is string). That means the info about the actual return type of the inner expression is totally lost (or at least hidden very carefully). That kind of design is fairly strange and totally unacceptable. I don't understand why they do so. The info about the actual return type of the expression should have been accessed easily.

like image 155
Hopeless Avatar answered Oct 30 '22 22:10

Hopeless