2012-05-15 8 views
8

Sono in procinto di creare un sistema di filtro più elaborato per questo nostro enorme progetto. Uno dei principali predicati è essere in grado di passare confronti attraverso un parametro stringa. Questo esprime nella forma seguente: "> 50" o "5-10" o "< 123,2"Metodo di estensione che restituisce espressione lambda tramite confronto

Quello che ho (come esempio per illustrare)

ViewModel:

TotalCost (string) (value: "<50") 
Required (string) (value: "5-10") 

EF Modello:

TotalCost (double) 
Required(double) 

Espressione che vorrei utilizzare:

model => model.Where(field => field.TotalCost.Compare(viewModel.TotalCost) && field.Required.Compare(viewModel.Required)); 

Espressione che vorrei ricevere:

model => model.Where(field => field.TotalCost < 50 && field.Required > 5 && field.Required < 10); 

O qualcosa di simile a quello

Tuttavia ... non ho idea da dove cominciare. L'ho ristretto a

public static Expression Compare<T>(this Expression<Func<T, bool>> value, string compare) 

Potrebbe non essere nemmeno corretto, ma questo è tutto ciò che ho. Il builder di confronto non è il problema, è facile. La parte difficile sta effettivamente restituendo l'espressione. Non ho mai provato a restituire espressioni come valori di funzione. Quindi in pratica quello che devo mantenere, è il campo e restituisco un'espressione di confronto, praticamente.

Qualsiasi aiuto? : X

Aggiornamento:

Purtroppo questo non risolve il problema. Può essere perché sono stato nelle ultime 23 ore, ma non ho il minimo indizio su come trasformarlo in un metodo di estensione. Come ho detto, quello che vorrei ... è fondamentalmente un modo di scrivere:

var ex = new ExTest(); 
var items = ex.Repo.Items.Where(x => x.Cost.Compare("<50")); 

Il modo in cui ho sagomato che la funzione (probabilmente completamente sbagliata) è

public static Expression<Func<decimal, bool>> Compare(string arg) 
{ 
    if (arg.Contains("<")) 
     return d => d < int.Parse(arg); 

    return d => d > int.Parse(arg); 
} 

manca il " questo "valore-qualcosa" da confrontare al primo posto, e non sono ancora riuscito a capire come poter essere in grado di ottenere un input di espressione ... come per ReSharper, mi suggerisce di convertirlo in booleano invece ...

La mia testa è piena di pelucchi al momento ...

Aggiornamento 2:

sono riuscito a trovare un modo per avere un pezzo di codice che funziona in un repository di memoria su un applicazione console. Devo ancora provarlo con Entity Framework però.

public static bool Compare(this double val, string arg) 
    { 
     var arg2 = arg.Replace("<", "").Replace(">", ""); 
     if (arg.Contains("<")) 
      return val < double.Parse(arg2); 

     return val > double.Parse(arg2); 
    } 

Tuttavia, dubito fortemente che è quello che sto dopo

Update 3:

Destra, dopo seduto e guardando attraverso le espressioni lambda di nuovo, prima che l'ultima risposta, mi è venuto con qualcosa di simile al seguente, non soddisfa i requisiti esatti di "Compare()" ma è un "overload-ish" Dove metodo:

public static IQueryable<T> WhereExpression<T>(this IQueryable<T> queryable, Expression<Func<T, double>> predicate, string arg) 
    { 
     var lambda = 
      Expression.Lambda<Func<T, bool>>(Expression.LessThan(predicate.Body, Expression.Constant(double.Parse(50.ToString())))); 

     return queryable.Where(lambda); 
    } 

Tuttavia, nonostante ai miei occhi, tutto sembrava logico, ottengo un'eccezione di runtime di:

System.ArgumentException was unhandled 
    Message=Incorrect number of parameters supplied for lambda declaration 
    Source=System.Core 
    StackTrace: 
     at System.Linq.Expressions.Expression.ValidateLambdaArgs(Type delegateType, Expression& body, ReadOnlyCollection`1 parameters) 
     at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable`1 parameters) 
     at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, Boolean tailCall, IEnumerable`1 parameters) 
     at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, ParameterExpression[] parameters) 

Questa è la linea colpevole, ovviamente:

var lambda = 
       Expression.Lambda<Func<T, bool>>(Expression.LessThan(predicate.Body, Expression.Constant(double.Parse(50.ToString())))); 

Sono molto vicino alla soluzione. Se riesco a togliermi quell'errore dalle spalle, credo che EF dovrebbe essere in grado di tradurlo in SQL. Altrimenti ... beh, probabilmente arriverà l'ultima risposta.

+0

credo, che la vostra parte Update2 non verrà eseguito in SQL Server (EF). Hai provato? –

+0

Sì, l'ho fatto. Come avrei pensato tbh. – NeroS

risposta

6

Per generare l'espressione, che verrebbe tradotto in SQL (eSQL), dovresti generare manualmente Expression. Ecco un esempio per la creazione del filtro GreaterThan, altri filtri possono essere realizzati con tecnica simile.

static Expression<Func<T, bool>> CreateGreaterThanExpression<T>(Expression<Func<T, decimal>> fieldExtractor, decimal value) 
{ 
    var xPar = Expression.Parameter(typeof(T), "x"); 
    var x = new ParameterRebinder(xPar); 
    var getter = (MemberExpression)x.Visit(fieldExtractor.Body); 
    var resultBody = Expression.GreaterThan(getter, Expression.Constant(value, typeof(decimal))); 
    return Expression.Lambda<Func<T, bool>>(resultBody, xPar); 
} 

private sealed class ParameterRebinder : ExpressionVisitor 
{ 
    private readonly ParameterExpression _parameter; 

    public ParameterRebinder(ParameterExpression parameter) 
    { this._parameter = parameter; } 

    protected override Expression VisitParameter(ParameterExpression p) 
    { return base.VisitParameter(this._parameter); } 
} 

Ecco l'esempio di utilizzo.(Assumere, che abbiamo StackEntites contesto EF con TestEnitities insieme di entità di TestEntity entità)

static void Main(string[] args) 
{ 
    using (var ents = new StackEntities()) 
    { 
     var filter = CreateGreaterThanExpression<TestEnitity>(x => x.SortProperty, 3); 
     var items = ents.TestEnitities.Where(filter).ToArray(); 
    } 
} 

Update: Per la vostra creazione di un'espressione complessa è possibile utilizzare il codice come questo: (Si supponga hanno già fatto CreateLessThanExpression e CreateBetweenExpression funzioni)

static Expression<Func<T, bool>> CreateFilterFromString<T>(Expression<Func<T, decimal>> fieldExtractor, string text) 
{ 
    var greaterOrLessRegex = new Regex(@"^\s*(?<sign>\>|\<)\s*(?<number>\d+(\.\d+){0,1})\s*$"); 
    var match = greaterOrLessRegex.Match(text); 
    if (match.Success) 
    { 
     var number = decimal.Parse(match.Result("${number}")); 
     var sign = match.Result("${sign}"); 
     switch (sign) 
     { 
      case ">": 
       return CreateGreaterThanExpression(fieldExtractor, number); 
      case "<": 
       return CreateLessThanExpression(fieldExtractor, number); 
      default: 
       throw new Exception("Bad Sign!"); 
     } 
    } 

    var betweenRegex = new Regex(@"^\s*(?<number1>\d+(\.\d+){0,1})\s*-\s*(?<number2>\d+(\.\d+){0,1})\s*$"); 
    match = betweenRegex.Match(text); 
    if (match.Success) 
    { 
     var number1 = decimal.Parse(match.Result("${number1}")); 
     var number2 = decimal.Parse(match.Result("${number2}")); 
     return CreateBetweenExpression(fieldExtractor, number1, number2); 
    } 
    throw new Exception("Bad filter Format!"); 
} 
+0

Questo sembra più utile. Ho aggiornato nuovamente la domanda originale, con la mia soluzione parziale che ... beh, non funziona ancora. – NeroS

+0

Ho implementato la funzione ora nel metodo di filtraggio. Ci sono alcuni problemi con EF anche se, prima di tutto, "getter" non deve avere (MemberExpression) il cast di fronte ad esso. Sarà solo un'eccezione. In secondo luogo, la regex del numero (che è la cosa più facile da risolvere) è leggermente off, dovrebbe essere (? \ d + (\. \ D *)?), Quello nel tuo post lo dice ** richiede ** un " "qui. Non si accontenta di un valore non decimale. Ma a parte questo (y) – NeroS

+0

Informazioni su Regex: giusto sei. Risposta aggiornata con quantificatore * {0,1} * (uguale a *? *). –

4

Una delle caratteristiche magiche a prima vista del compilatore C# può fare il duro lavoro per voi. Probabilmente sapete si può fare questo:

Func<decimal, bool> totalCostIsUnder50 = d => d < 50m; 

che è, utilizzare un'espressione lambda per assegnare un Func. Ma lo sapevate che si può anche fare questo:

Expression<Func<decimal, bool>> totalCostIsUnder50Expression = d => d < 50m; 

che è, usare un'espressione lambda per assegnare un Expression che esprime una Func? È abbastanza pulito.

Dato che dici costruttore

Il paragone non è il problema, che è la parte più semplice. La parte rigida restituisce effettivamente l'espressione

Suppongo che tu possa compilare gli spazi vuoti qui; supponiamo passiamo in `" < 50" per:

Expression<Func<decimal, bool>> TotalCostCheckerBuilder(string criterion) 
{ 
    // Split criterion into operator and value 

    // when operator is < do this: 
    return d => d < value; 

    // when operator is > do this: 
    return d => d > value; 

    // and so on 
} 

Infine, per comporre il Expression s insieme a && (e hanno un Expression ancora), fare questo:

var andExpression = Expression.And(firstExpression, secondExpression); 
+0

Non sono sicuro che sia applicabile alla situazione. Post originale aggiornato con commenti. – NeroS

0

la parte difficile è realmente restituendo l'espressione.

Traduci stringhe in costruzioni più strutturati come enumerazioni e classi per definire proprietà, operatori e filtri:

Enum Parameter 
    TotalCost 
    Required 
End Enum 

Enum Comparator 
    Less 
    More 
    Equals 
End Enum 

Class Criterion 
    Public ReadOnly Parameter As Parameter 
    Public ReadOnly Comparator As Comparator 
    Public ReadOnly Value As Double 

    Public Sub New(Parameter As Parameter, Comparator As Comparator, Value As Double) 
     Me.Parameter = Parameter 
     Me.Comparator = Comparator 
     Me.Value = Value 
    End Sub 
End Class 

Poi una funzione per creare espressione viene definita:

Function CreateExpression(Criteria As IEnumerable(Of Criterion)) As Expression(Of Func(Of Field, Boolean)) 
    Dim FullExpression = PredicateBuilder.True(Of Field)() 

    For Each Criterion In Criteria 
     Dim Value = Criterion.Value 

     Dim TotalCostExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From { 
      {Comparator.Less, Function(Field) Field.TotalCost < Value}, 
      {Comparator.More, Function(Field) Field.TotalCost > Value}, 
      {Comparator.Equals, Function(Field) Field.TotalCost = Value} 
     } 

     Dim RequiredExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From { 
      {Comparator.Less, Function(Field) Field.Required < Value}, 
      {Comparator.More, Function(Field) Field.Required > Value}, 
      {Comparator.Equals, Function(Field) Field.Required = Value} 
     } 

     Dim Expressions As New Dictionary(Of Parameter, IDictionary(Of Comparator, Expression(Of Func(Of Field, Boolean)))) From { 
      {Parameter.TotalCost, TotalCostExpressions}, 
      {Parameter.Required, RequiredExpressions}} 

     Dim Expression = Expressions(Criterion.Parameter)(Criterion.Comparator) 

     FullExpression = Expression.And(Expression) 
    Next 

    Return FullExpression 
End Function 

PredicateBuilder taken here è necessario per combinare due espressioni con l'operatore AND.

Usage:

Function Usage() As Integer 

    Dim Criteria = { 
     New Criterion(Parameter.TotalCost, Comparator.Less, 50), 
     New Criterion(Parameter.Required, Comparator.More, 5), 
     New Criterion(Parameter.Required, Comparator.Less, 10)} 

    Dim Expression = CreateExpression(Criteria) 
End Function 

Si creerà espressione esattamente come previsto in un esempio

field => field.TotalCost < 50 && field.Required > 5 && field.Required < 10