【问题标题】:Entity Framework: select property as Object实体框架:选择属性作为对象
【发布时间】:2015-08-17 14:15:53
【问题描述】:

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

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
{

}

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

【问题讨论】:

  • 你终于调用了ToList,那为什么不在那之后将 DateTime 转换为对象呢?
  • 因为选择器成员表达式是在运行时创建的,所以在编译时属性类型是未知的。我可以使用反射来确定类型,但这对于嵌套属性会很复杂

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


【解决方案1】:

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

我认为这里有一个解决方法。我们的想法是将Expression&lt;T,object&gt; 转换为另一个Expression&lt;T,returnType&gt;,其中returnType 是属性的实际返回类型(由selector 返回)。然而Select 总是需要Expression&lt;T,returnType&gt; 的显式类型,这意味着returnType 在设计时应该是已知的。所以这是不可能的。我们无法直接拨打Select。相反,我们必须使用反射来调用Select。返回结果应为IEnumerable&lt;object&gt;,然后您可以调用ToList() 来获取所需的对象列表。

现在您可以将这种扩展方法用于IQueryable&lt;T&gt;

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 时)。这意味着有关内部表达式的实际返回类型的信息完全丢失了(或者至少非常小心地隐藏了)。这种设计相当奇怪,完全不能接受。我不明白他们为什么这样做。应该可以轻松访问有关表达式的实际返回类型的信息。

【讨论】:

    【解决方案2】:

    linq to entity 的重点是使用 .NET linq 指令创建 sql 查询。 linq to entity 指令并不意味着永远执行,它们只被翻译成 sql。因此,这些 linq 语句中的所有内容都需要转换为 sql。并且 sql 具有日期、字符串等的适当类型。类被理解为每个属性都表示特定列的表。但是 sql 中没有对象的概念,因为它在 .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 查询,其余的在内存中执行。因此,由于这一点,我们将铸件转移到记忆中有意义的地方。

    【讨论】:

    • 我正在与 user2346738 一起解决这个问题,为简洁起见,我们省略了一些信息。选择器是在运行时通过从字符串创建 MemberExpression 来创建的(例如,'Country.Name' 变为 customer => customer.Country.Name),因此在编译时不知道属性类型。我们可以使用反射来确定类型,但这对于嵌套成员来说有点讨厌。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多