2013-04-12 2 views
17

Dato un valore di base age Io so come creare un'espressione come questa:Creazione di un'espressione LINQ in cui il parametro è uguale oggetto

//assuming: age is an int or some other primitive type 
employee => employee.Age == age 

In questo modo:

var entityType = typeof(Employee); 
var propertyName = "Age"; 
int age = 30; 
var parameter = Expression.Parameter(entityType, "entity"); 

var lambda = Expression.Lambda(
     Expression.Equal(
      Expression.Property(parameter, propertyName), 
      Expression.Constant(age) 
     )      
    , parameter); 

Questo funziona bene tranne che in scenari dove la proprietà e la costante in questione non sono tipi primitivi.

Come dovrei costruire una simile espressione se il confronto è tra gli oggetti?

Con EF posso solo scrivere:

Location location = GetCurrentLocation(); 
employees = DataContext.Employees.Where(e => e.Location == location); 

che funziona anche, ma se cerco di creare la stessa espressione:

var entityType = typeof(Employee); 
var propertyName = "Location"; 
var location = GetCurrentLocation(); 
var parameter = Expression.Parameter(entityType, "entity"); 

var lambda = Expression.Lambda(
     Expression.Equal(
      Expression.Property(parameter, propertyName), 
      Expression.Constant(location) 
     )      
    , parameter); 

ottengo un errore che dice:

Unable to create a constant value of type 'Location'. Only primitive types or enumeration types are supported in this context.

My sospetto è che Expression.Constant() aspetta solo tipi primitivi, quindi ho bisogno di utilizzare un diverso metodo di espressione di fabbrica. (maype Expression.Object? - So che non esiste)

C'è un modo per creare un'espressione che mette a confronto gli oggetti? Perchè è che EF è in grado di interpretare correttamente se un compilato dichiarazione LINQ, ma non quando è espressione?

+1

Proprio come una nota a margine, l'uso di Expression.Constant impone al server di database di generare un nuovo piano di esecuzione SQL ogni volta che la costante cambia. Questo può avere un grande impatto sulle prestazioni, vedere https://stackoverflow.com/questions/34845097/rewriting-a-linq-expression-query-to-enable-caching-sql-execution-plan –

risposta

4

In aggiunta a ciò che è stato menzionato nelle precedenti risposte. Una soluzione più specifica sarebbe andato come tale:

public static Expression CreateExpression<T>(string propertyName, object valueToCompare) 
{ 
    // get the type of entity 
    var entityType = typeof(T); 
    // get the type of the value object 
    var valueType = valueToCompare.GetType(); 
    var entityProperty = entityType.GetProperty(propertyName); 
    var propertyType = entityProperty.PropertyType; 


    // Expression: "entity" 
    var parameter = Expression.Parameter(entityType, "entity"); 

    // check if the property type is a value type 
    // only value types work 
    if (propertyType.IsValueType || propertyType.Equals(typeof(string))) 
    { 
     // Expression: entity.Property == value 
     return Expression.Equal(
      Expression.Property(parameter, entityProperty), 
      Expression.Constant(valueToCompare) 
     ); 
    } 
    // if not, then use the key 
    else 
    { 
     // get the key property 
     var keyProperty = propertyType.GetProperties().FirstOrDefault(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Length > 0); 

     // Expression: entity.Property.Key == value.Key 
     return Expression.Equal(
      Expression.Property(
       Expression.Property(parameter, entityProperty), 
       keyProperty 
      ), 
      Expression.Constant(
       keyProperty.GetValue(valueToCompare), 
       keyProperty.PropertyType 
      ) 
     ); 
    } 
} 

PUNTI IMPORTANTI:

  1. Assicurarsi di verificare la presenza di valori nulli
  2. Assicurarsi propertyType e valueType sono compatibili (sia che siano dello stesso tipo o sono convertibili)
  3. Qui vengono fatte diverse ipotesi (ad esempio, si assegna un KeyAttribute)
  4. Questo codice non è testato, quindi non è esattamente pronto per la copia/incolla.

Spero che questo aiuti.

+2

cos'è 'otherProperty' – Sinaesthetic

3

Non puoi farlo perché EF non sa come tradurre confronti di uguaglianza su Location in un'espressione SQL.

Tuttavia, se si sa quali proprietà di Location si desidera confrontare, si può fare questo con i tipi anonimi:

var location = GetCurrentLocation(); 
var locationObj = new { location.LocationName, location.LocationDescription }; 
employees = DataContext.Employees.Where(e => new { e.Location.LocationName, e.Location.Description } == locationObj); 

Certo che è equivalente a:

var location = GetCurrentLocation(); 
employees = DataContext.Employees.Where(e => e.Location.LocationName == location.Name && 
              e.Location.Description == location.Description); 
+0

Ciò che mi confonde è che 'dipendenti = DataContext.Employees.Where (e => e.Location == posizione); '** funziona ** se lo scrivo direttamente in quel modo, ma se provo a creare un'espressione identica, non funziona. Il compilatore fa qualcosa in più che non può essere fatto in fase di runtime? –

+1

Il compilatore sta facendo molto meno del tempo di esecuzione. Sta solo controllando se i tipi sono compatibili in C# per questa espressione. In fase di esecuzione, l'EF deve effettivamente convertire l'espressione in una query SQL, che è molto più difficile. Stai vedendo l'errore in fase di esecuzione e non in fase di compilazione perché il compilatore non sa che l'espressione non può essere convertita. –

+0

Non sono nemmeno sicuro di essermi spiegato, ma ho aggiornato la domanda nella speranza di fornire ulteriore chiarezza. –

2

Dare il codice qui sotto una corsa. Ho voluto mettere alla prova la vostra ipotesi che e => e.Location == posizione sta compilando in qualcosa che può essere costruito con Expression.Equal, Expression.Property e Expression.Constant.

class Program { 
     static void Main(string[] args) { 
      var location = new Location(); 
      Expression<Func<Employee, bool>> expression = e => e.Location == location; 

      var untypedBody = expression.Body; 

      //The untyped body is a BinaryExpression 
      Debug.Assert(
       typeof(BinaryExpression).IsAssignableFrom(untypedBody.GetType()), 
       "Not Expression.Equal"); 

      var body = (BinaryExpression)untypedBody; 
      var untypedLeft = body.Left; 
      var untypedRight = body.Right; 

      //The untyped left expression is a MemberExpression 
      Debug.Assert(
       typeof(MemberExpression).IsAssignableFrom(untypedLeft.GetType()), 
       "Not Expression.Property"); 

      ////The untyped right expression is a ConstantExpression 
      //Debug.Assert(
      // typeof(ConstantExpression).IsAssignableFrom(untypedRight.GetType()),     
      // "Not Expression.Constant"); 

      //The untyped right expression is a MemberExpression? 
      Debug.Assert(
       typeof(MemberExpression).IsAssignableFrom(untypedRight.GetType()))); 
    } 
} 

public class Employee 
{ 
    public Location Location { get; set; } 
} 

public class Location { } 

Sembra che non lo sia, ed è perché l'espressione corretta non è una costante. Per vedere questo, decommentare il codice commentato.

Quello che non capisco è il motivo per cui l'espressione giusta è un MemberExpression. Forse qualcuno che conosce il compilatore di espressioni linq può fare più luce su questo, quindi posso farlo.

Modifica: questo può avere a che fare con la chiusura in lambda: una classe viene creata dietro le quinte che contiene le variabili chiuse. La posizione potrebbe quindi essere un membro di quella classe. Non ne sono sicuro, ma è quello che sospetto.

This post potrebbe far luce supplementare sulla situazione.

+0

Ottimo! Lei ha capito il mio problema! Penso che stiamo andando nella giusta direzione. –

+0

Felice di sentirlo. Il link che ho appena aggiunto suggerito 1) che le variabili catturate non possono essere aggiunte alle espressioni linq nel runtime, e 2) il LinqKit potrebbe essere in grado di aiutare. Non ho letto una lettura approfondita, ma penso che potresti apprezzare la discussione. – Doug