2015-07-08 22 views
8

Supponiamo ho la seguente classe:Come creare dinamicamente un'espressione lambda

public class Show 
{ 
    public string Language { get; set; } 
    public string Name { get; set; } 
} 

Sulla base di queste informazioni, il mio obiettivo è quello di creare un'espressione lambda come questo:

g => g.Language == lang && g.Name == name 

lang e name sono variabili locali che vorrei aggiungere come valori costanti durante la creazione dell'espressione.

Come si può vedere, la funzione compilata sarebbe di tipo Func<Genre, bool>

per aiutare a comprendere in modo più chiaro, vorrei realizzare qualcosa di simile a questo:

string lang = "en"; 
string name = "comedy"; 
Genre genre = new Genre { Language = "en", Name = "comedy" }; 
Expression<Func<Genre, bool>> expression = CreateExpression(genre, lang, name); 
// expression = (g => g.Language == "en" && g.Name == "comedy") 

Sono consapevole del esistenza di alberi di espressione ma sono praticamente nuovo su questo argomento, quindi non so nemmeno come iniziare.

Questo problema può essere risolto? Come posso creare tale espressione dinamicamente?

+0

sto indovinando il problema è mor e complicato nella pratica di quanto tu abbia descritto, giusto? Se conosci le proprietà che stai testando prima del tempo, non puoi scrivere una funzione che restituisce una funzione? – kaveman

+0

@kaveman In realtà, è vero, hai ragione. È troppo complicato per me spiegare senza probabilmente confondere te e persino me stesso. Ho un'interfaccia repository che può interrogare le mie entità usando un'espressione. Il problema è che questa espressione deve essere nel formato sopra indicato: condizioni chiave separate da '&&'. –

+0

@kaveman Sto ricevendo il tipo (nel mio esempio, Genere) come parametro generico, insieme ai valori chiave, quindi ho bisogno di recuperare le mie proprietà chiave tramite riflessione (penso di sapere come farlo) e creare l'espressione quindi posso interrogare le entità di genere –

risposta

1
public Expression<Func<TValue, bool>> CreateExpression<TValue, TCompare>(TValue value, TCompare compare) 
{ 
    var pv = Expression.Parameter(typeof(TValue), "data"); 
    var compareProps = typeof(TCompare).GetProperties(); 

    // First statement of the expression 
    Expression exp = Expression.Constant(true); 

    foreach (var prop in typeof(TValue).GetProperties()) 
    { 
     // Check if the compare type has the same property 
     if (!compareProps.Any(i => i.Name == prop.Name)) 
      continue; 

     // Build the expression: value.PropertyA == "A" 
     // which "A" come from compare.PropertyA 
     var eq = Expression.Equal(
      Expression.Property(pv, prop.Name), 
      Expression.Constant(compareProps 
       .Single(i => i.Name == prop.Name) 
       .GetValue(compare))); 

     // Append with the first (previous) statement 
     exp = Expression.AndAlso(exp, eq); 
    } 

    return Expression.Lambda<Func<TValue, bool>>(exp, pv); 
} 

Usage:

var value = new { Lang = "en", Name = "comedy"}; 

// The compareValue should have the same property name as the value, 
// or the expression will just ignore the property 
var compareValue = new { Lang = "en", Name = "comedy", Other = "xyz" }; 

// The create expression content is 
// {data => ((True AndAlso (data.Lang == "en")) AndAlso (data.Name == "comedy"))} 
bool isMatch = CreateExpression(value, compareValue).Compile()(value); // true 
0

È possibile utilizzare le interfacce per creare un Func rispetto all'interfaccia che ha definito le proprietà necessarie.

Esempio:

interface IExpressionable { 
    string Language { get;set; } 
    string Name { get;set; } 
} 
class Genre : IExpressionable { 
    string Language {get;set;} 
    string Name {get;set;} 
} 
Genre genre = new Genre(); 
Expression<Func<IExpressionable, bool>> expression = CreateExpression(genre, lang, name); 
expression = (g => g.Language == "en" && g.Name == "comedy") 

Un'implementazione alternativa di questo concetto può essere avuto fornendo un metodo GetLanguage e GetName sulla vostra interfaccia, che costringerebbe abbonati di implementare i metodi di base al fine di restituire una "lingua" e "Nome" basato sui loro metodi interni.

interface IExpressionable { 
    string GetExpressionOne(); 
    string GetExpressionTwo(); 
} 
class Genre : IExpressionable { 
    string Language {get;set;} 
    string Name {get;set;} 
    public string GetExpressionOne() { 
     return Language; 
    } 
    public string GetExpressionOne() { 
     return Name; 
    } 
} 
class SomethingElse { 

    string Orange {get;set;} 
    string BananaPeel {get;set;} 
    public string GetExpressionOne() { 
     return Orange; 
    } 
    public string GetExpressionOne() { 
     return BananaPeel; 
    } 
} 
Genre genre = new Genre(); 
SomethingElse else = new SomethingElse(); 
Expression<Func<IExpressionable, bool>> expression = CreateExpression(genre, lang, name); 
Expression<Func<IExpressionable, bool>> expression2 = CreateExpression(else, lang, name); 

expression = (g => g.GetExpressionOne() == "en" && g.GetExpressionTwo() == "comedy"); 

Modifica dal vostro commento sopra: @kaveman I only have the key values, but I can fetch the key properties via reflection using some custom attributes that I defined. In this example, Language and Name would be decorated with an attribute that defines them as key properties

La mia risposta è di evitare questo pensiero completamente. Crea un'interfaccia che definisca i metodi necessari per eseguire qualsiasi espressione tu stia cercando e che le entità ereditino da essa. Potresti avere i tuoi metodi sull'interfaccia "GetKeyOne" e "GetKeyTwo". Quindi la tua espressione non deve essere dinamica, devi solo definire cosa fa l'espressione quando interagisce con KeyOne e KeyTwo, che è definita in ogni implementatore.

+0

Questa è una buona soluzione, ma l'esistenza delle proprietà 'Language' e' Name' dipende molto dal tipo di corrente entità, quindi avrei bisogno di creare un'interfaccia diversa per tutti i diversi tipi di entità che ho (possono avere proprietà completamente diverse) –

+0

Questo è un esempio e puoi dare un nome alle proprietà come vuoi, anche "ExpressionArgument1" e "ExpressionArgument2" . Vedi la modifica. –

+0

Posso vedere il corpo di 'CreateExpression'? –

0

Supponendo di avere gli attributi su ciascuna delle proprietà di ciascuno dei soggetti che si sono interessati a trattare in questa sede (vedi comments), e si ha il parametro di tipo (lo chiamano T) una firma possibile per CreateExpression è:

Expression<Func<T, bool>> CreateExpression(T entity, IOrderedDictionary<string, string> propertyTests); 

In questo modo, i vostri CreateExpression opere con il tipo generico noto, e il chiamante può specificare cosa prova a fare contro le proprietà dell'entità. Qui assumiamo che il key sia il nome della proprietà su cui riflettere (assicurarsi che abbia l'attributo conosciuto) mentre lo value è il valore richiesto di detta proprietà. IOrderedDictionary è un modo per forzare il cortocircuito dei test &&, se questo è il tuo caso.

È possibile aggiungere ulteriori vincoli allo T (ad es.T deve attuare alcune interfaccia) tramite generic type constraints using where

Vedi anche Check if property has attribute

E Reflection - get attribute name and value on property

0

Questo lo farà, e dovrebbe rendere abbastanza facile da usare per costruire

private Expression<Func<Genre,bool>> CreateExpression(params Expression<Func<Object>>[] selectors) 
{ 
    //We are working on a Genre type, and make a parameter of it 
    //Query so far looks like g => 
    var param = Expression.Parameter(typeof(Genre),"g"); 

    //Set the base expression to make it easy to build 
    //Query so far looks like g => true 
    Expression expression = Expression.Constant(true); 

    foreach(var selector in selectors) { 
     //Find out the name of the variable was passed 
     var selectorname = TestExtension.nameof(selector); 
     //Get the value 
     var selectorValue = selector.Compile()(); 

     //Create an accessor to the genre (g.Language for example) 
     var accessMethod = Expression.PropertyOrField(param, selectorname); 

     //Check if it equals the value (g.Language == "en") 
     var equalExpr = Expression.Equal(accessMethod, Expression.Constant(selectorValue)); 

     //Make it an And expression 
     //Query so far looks like g => true && g.Language == "en" 
     expression = Expression.AndAlso(expression, equalExpr); 

     //Second pass through the loop would build: 
     //g => true && g.Language == "en" && g.Name == "comedy" 
    } 

    //Turn it into a lambda func and cast it 
    var result = Expression.Lambda(expression, param) as Expression<Func<Genre, bool>>; 
    return result; 
} 

public class Genre 
{ 
    public string Language { get; set; } 
    public string Name { get; set; } 
} 

//Taken from my answer at http://stackoverflow.com/a/31262225/563532 

public static class TestExtension 
{ 
    public static String nameof<T>(Expression<Func<T>> accessor) 
    { 
     return nameof(accessor.Body); 
    } 

    public static String nameof<T, TT>(this T obj, Expression<Func<T, TT>> propertyAccessor) 
    { 
     return nameof(propertyAccessor.Body); 
    } 

    private static String nameof(Expression expression) 
    { 
     if (expression.NodeType == ExpressionType.MemberAccess) 
     { 
      var memberExpression = expression as MemberExpression; 
      if (memberExpression == null) 
       return null; 
      return memberExpression.Member.Name; 
     } 
     return null; 
    } 
} 

Quindi si utilizza così:

string language = "en"; 
string name = "comedy"; 
var genre = new Genre { Language = "en", Name="comedy" }; 

var query = CreateExpression(() => language,() => name); 

Si noti che le variabili devono corrispondere ai nomi di proprietà. In caso contrario, sarà necessario un qualche tipo di mappatura [lang => lingua] ecc

per valutarla:

var matches = query.Compile()(genre); 

In alternativa, è possibile passare in EF ad esempio:

dtx.Genre.Where(query); 
Problemi correlati