实体框架:选择属性作为对象

时间:2015-08-17 14:15:53

标签: c# linq entity-framework linq-to-entities

尝试将属性值检索为对象而不是各自的类型时,我遇到了一些麻烦。以下代码抛出此异常:

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

此代码在选择字符串时工作正常,但在选择DateTimes,Integers或Nullable类型时则不行。

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
{

}

最终目标是通用过滤,以从任何对象的属性中选择不同的值。

2 个答案:

答案 0 :(得分:3)

我知道您希望使用内联Expression声明以方便的方式选择属性(无需解析表示属性路径和使用Reflection的以点分隔的字符串)。但是,这样做需要显式声明Expression,我们必须使用显式类型。遗憾的是,object类型不能用作Expression的返回类型,因为稍后它不能转换为数据库中支持的类型之一。

我认为这里有一个解决方法。我们的想法是将Expression<T,object>转换为另一个Expression<T,returnType>,其中returnType是属性的实际返回类型(由selector返回)。但是Select始终需要明确的Expression<T,returnType>类型,因此{EL}应该在设计时知道returnType。所以这是不可能的。我们无法直接致电Select。相反,我们必须使用Reflection来调用Select。返回结果应为IEnumerable<object>,然后您可以调用ToList()来获取所需的对象列表。

现在,您可以将此扩展方法用于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>();
    }        
}

用法 :(与您的代码完全相同)

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

起初我尝试访问exp.Body.Type并认为它是内部表达式的实际返回类型。但是,除了System.Object的特殊情况(当属性访问的返回类型为string时),它总是string。这意味着关于内部表达式的实际返回类型的信息完全丢失(或者至少非常小心地隐藏)。这种设计相当奇怪,完全不可接受。我不明白为什么他们这样做。应该可以轻松访问有关表达式实际返回类型的信息。

答案 1 :(得分:0)

linq to entity的要点是使用.NET linq指令创建sql查询。 linq to entities指令不是要执行,它们只被转换为sql。所以这些linq语句中的所有内容都需要转换为sql。并且sql具有适当的日期,字符串等类型。类被理解为表,其中每个属性表示某个列。但是在.NET中没有对象的概念,因为它在.NET中,这是你的问题的根源。在您的linq查询中,您应该专注于仅创建查询以返回正确的数据并在程序中执行转换:

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();

您在查询到第一个ToList时编写的所有内容都将转换为sql查询,其余内容在内存中执行。所以,多亏了这一点,我们将演员部分转移到记忆中,这样才有意义。