2015-08-17 20 views
5

Sto riscontrando dei problemi durante il tentativo di recuperare i valori delle proprietà come Oggetti invece dei rispettivi tipi. Il seguente codice genera questa eccezione:Entity Framework: selezionare la proprietà come Oggetto

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

Questo codice funziona bene quando si seleziona una stringa, ma non quando si seleziona DateTimes, interi o tipi 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 
{ 

} 

L'obiettivo finale è un filtro generico per selezionare valori distinti da qualsiasi proprietà di un oggetto.

+4

Infine si chiama 'ToList', quindi perché non eseguire il casting di DateTime dopo questo? – Hopeless

+1

Poiché l'espressione del membro del selettore viene creata in fase di runtime, il tipo di proprietà non è noto al momento della compilazione. Potrei usare il reflection per determinare il tipo, ma sarebbe complicato per le proprietà nidificate – user2346738

risposta

3

Comprendo che si desidera utilizzare la dichiarazione in linea Expression per selezionare la proprietà in un modo conveniente (senza dover analizzare una stringa separata da punti che rappresenta il percorso della proprietà e utilizzando Reflection). Tuttavia, per fare ciò sarà necessario dichiarare esplicitamente lo Expression e utilizzare il tipo esplicito. Sfortunatamente il tipo object non può essere utilizzato come tipo di ritorno di Expression perché in seguito non può essere convertito in uno dei tipi supportati nel database.

Penso che ci sia un work-around qui. L'idea è di convertire lo Expression<T,object> in un altro Expression<T,returnType> dove returnType è il tipo di reso effettivo della proprietà (restituito da selector). Tuttavia, Select richiede sempre un tipo esplicito di Expression<T,returnType> che significa che returnType deve essere noto in fase di progettazione. Quindi è impossibile. Non abbiamo modo di chiamare direttamente Select. Invece dobbiamo usare Reflection per invocare lo Select. Il risultato di ritorno è previsto come IEnumerable<object> che è possibile chiamare ToList() per ottenere un elenco di oggetti come desiderato.

Ora è possibile utilizzare questo metodo di estensione per 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>(); 
    }   
} 

Uso: (esattamente come il codice)

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

In un primo momento ho provato l'accesso alla exp.Body.Type e ho pensato che era il tipo di ritorno effettivo dell'espressione interna. Tuttavia, in qualche modo è sempre System.Object tranne il caso speciale di string (quando il tipo di ritorno di accesso alla proprietà è string). Ciò significa che le informazioni sul tipo di ritorno effettivo dell'espressione interna sono completamente perse (o almeno nascoste con molta attenzione). Quel tipo di design è abbastanza strano e totalmente inaccettabile. Non capisco perché lo facciano. Le informazioni sul tipo di ritorno effettivo dell'espressione avrebbero dovuto essere facilmente accessibili.

0

Il punto di linq per entità è la creazione di query sql con l'uso delle istruzioni di linq .NET. Le istruzioni linq to entities non sono pensate per essere eseguite mai, sono tradotte solo in sql. Quindi tutto ciò che è all'interno di quelle istruzioni linq deve essere convertibile in sql. E SQL ha i tipi corretti per data, stringa, ecc. Le classi sono intese come tabelle in cui ogni proprietà indica una determinata colonna. Ma non c'è alcun concetto di oggetto in SQL come in .NET e questa è la fonte del tuo problema. Nella tua query LINQ si dovrebbe concentrarsi sulla creazione solo la query per restituire i dati corretti e fare il cast nel vostro programma:

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

Ogni cosa che si scrive nella query fino a prima ToList è tradotto in query SQL, il resto viene eseguito in memoria. Quindi grazie a questo spostiamo la parte del cast in memoria dove ha senso farlo.

+2

Sto lavorando con user2346738 su questo problema, abbiamo omesso alcune informazioni per brevità. Il selettore viene creato in fase di runtime creando un MemberExpression da una stringa (ad es. "Country.Name" diventa customer => customer.Country.Name), quindi il tipo di proprietà non è noto al momento della compilazione. Potremmo usare il reflection per determinare il tipo ma questo diventa un po 'fastidioso per i membri annidati. – chrisv