简体   繁体   中英

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.

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.

The point of linq to entity is creation of sql query with use of .NET linq instructions. The linq to entities instructions are not meant to be executed ever, they are only translated to sql. So everything inside those linq statements needs to be convertible to sql. And sql has proper types for date, string, etc. Classes are understood as tables where each property mean a certain column. But there is no concept of object in sql as it is in .NET and this is the source of your problem. In your linq query you should focus on creating only the query to return proper data and do the cast in your program:

Expression<Func<Customer, DateTime>> selector = cust => cust.CreatedOn;

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

IList<object> customerNames = customers.Select(selector).Distinct().ToList().Cast<object>().ToList();

Every thing that you write in query up to first ToList is translated to sql query, the rest is executed in memory. So thanks to this we shift the cast part to memory where it makes sense to do it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM