Entity Framework: select 属性 作为对象

Entity Framework: select property as Object

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

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

此代码在 select 字符串时工作正常,但在 select 日期时间、整数或可空类型时无效。

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
{

}

最终目标是对来自任何对象属性的 select 不同值进行通用过滤。

实体 linq 的重点是使用 .NET linq 指令创建 sql 查询。 linq to entities 指令并不意味着永远执行,它们只被翻译成 sql。因此,这些 linq 语句中的所有内容都需要转换为 sql。 sql 具有日期、字符串等适当的类型。类 被理解为表,其中每个 属性 表示特定的列。但是 sql 中没有 .NET 中的对象概念,这就是问题的根源。在您的 linq 查询中,您应该专注于仅创建对 return 正确数据的查询并在您的程序中进行转换:

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 查询,其余部分在内存中执行。因此,多亏了这一点,我们将演员转移到内存中,这是有意义的。

我知道您想使用内联 Expression 声明来 select 和 属性 以一种方便的方式(无需解析代表属性 路径和使用反射)。但是这样做需要显式声明 Expression 并且我们必须使用显式类型。不幸的是,object 类型不能用作表达式的 return 类型,因为稍后它无法转换为数据库中支持的类型之一。

我认为这里有一个解决方法。我们的想法是将 Expression<T,object> 转换为另一个 Expression<T,returnType>,其中 returnType 是 属性 的实际 return 类型(return 由 selector).但是 Select 总是需要一个显式的 Expression<T,returnType> 类型,这意味着 returnType 应该在设计时就知道了。所以那是不可能的。我们没有办法直接调用Select。相反,我们必须使用反射来调用 Select。 return 结果预期为 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 并认为它是内部表达式的实际 return 类型。然而不知何故它总是 System.Object 除了 string 的特殊情况(当 属性 访问的 return 类型是 string 时)。这意味着有关内部表达式的实际 return 类型的信息完全丢失(或至少非常小心地隐藏)。这种设计相当奇怪,完全不能接受。我不明白他们为什么这样做。有关表达式实际 return 类型的信息应该很容易访问。