2017-09-07 2 views
0

Ich habe ein System, das verschiedene Kriterien für Sales in der Datenbank gespeichert werden kann. Wenn die Kriterien geladen sind, werden sie verwendet, um eine Abfrage zu erstellen und alle zutreffenden Verkäufe zurückzugeben. Die Kriterien Objekte wie folgt aussehen:Erstellen dynamischer Abfrage in einer Schleife mit Ausdrucksbäumen

ReferenceColumn (die Spalte in der Sale Tabelle sie gelten)

MinValue (Minimalwert der Referenzspalte sein muss)

MaxValue (Maximalwert der Referenzspalte muss sein)

Eine Suche nach Sales erfolgt anhand einer Sammlung der oben genannten Kriterien. ReferenceColumns des gleichen Typs werden OR-verknüpft, und ReferenceColumns verschiedener Typen werden UND-verknüpft. So zum Beispiel, wenn ich hatte drei Kriterien:

ReferenceColumn: 'Preis', MinValue: '10', MaxValue: '20'

ReferenceColumn: 'Preis', MinValue: '80', MaxValue: ‚100 '

ReferenceColumn: 'Alter', MinValue: '2', MaxValue: '3'

die Abfrage sollte alle Verkäufe zurück, wo der Preis war zwischen 10-20 oder zwischen 80-100, aber nur, wenn diejenigen, Das Verkaufsalter liegt zwischen 2 und 3 Jahren.

Ich habe es implementiert, um eine SQL-Abfrage-String und unter Verwendung .FromSql Ausführung:

public IEnumerable<Sale> GetByCriteria(ICollection<SaleCriteria> criteria) 
{ 
StringBuilder sb = new StringBuilder("SELECT * FROM Sale"); 

var referenceFields = criteria.GroupBy(c => c.ReferenceColumn); 

// Adding this at the start so we can always append " AND..." to each outer iteration 
if (referenceFields.Count() > 0) 
{ 
    sb.Append(" WHERE 1 = 1"); 
} 

// AND all iterations here together 
foreach (IGrouping<string, SaleCriteria> criteriaGrouping in referenceFields) 
{ 
    // So we can always use " OR..." 
    sb.Append(" AND (1 = 0"); 

    // OR all iterations here together 
    foreach (SaleCriteria sc in criteriaGrouping) 
    { 
     sb.Append($" OR {sc.ReferenceColumn} BETWEEN '{sc.MinValue}' AND '{sc.MaxValue}'"); 
    } 

    sb.Append(")"); 
} 

return _context.Sale.FromSql(sb.ToString(); 
} 

Und das ist tatsächlich ganz gut mit unserer Datenbank funktioniert, aber es ist nicht schön spielen mit anderen Sammlungen, besonders die InMemory-Datenbank verwenden wir für UnitTesting, also versuche ich es mit Ausdrucksbäumen neu zu schreiben, die ich noch nie zuvor benutzt habe. Bisher habe ich das bekommen:

Es wirft derzeit eine ArgumentException, wenn ich Expression.Lamda aufrufen. Dezimal kann dort nicht verwendet werden und es sagt, dass es Typ Sale will, aber ich weiß nicht, was ich dort für den Vertrieb setzen soll, und ich bin mir nicht sicher, ob ich hier überhaupt auf dem richtigen Weg bin. Ich bin auch besorgt, dass mein MasterExpression jedes Mal mit sich selbst dupliziert, anstatt anzuhängen, wie ich es mit dem String Builder getan habe, aber vielleicht wird das trotzdem funktionieren.

Ich suche nach Hilfe, wie diese dynamische Abfrage in eine Ausdrucksbaumstruktur umgewandelt wird, und ich bin für einen völlig anderen Ansatz offen, wenn ich hier aus bin.

+0

Dose ursprünglichen Code Arbeit für Sie arbeiten? Das sollte nicht funktionieren und warum benutzt du 1 = 1 und 1 = 0? –

+0

Ja, es funktioniert, wenn die Sammlung Teil eines DbContext mit SQL Server ist. 1 = 1 und 1 = 0 sind da, also kann ich AND '/' OR 'immer an den Query-String anhängen, ohne sich mit dem ersten Iterations-Spezialfall usw. befassen zu müssen. – Valuator

+0

Versuchen Sie es mit LINQKit (http://www.albahari.org) .com/nutshell/linqkit.aspx), macht es viel einfacher. Die Seite sagt: Mit LINQKit können Sie: ... Prädikate dynamisch erstellen – Tom

Antwort

1

Ich denke, das

public class Sale 
      { 
       public int A { get; set; } 

       public int B { get; set; } 

       public int C { get; set; } 
      } 

      //I used a similar condition structure but my guess is you simplified the code to show in example anyway 
      public class Condition 
      { 
       public string ColumnName { get; set; } 

       public ConditionType Type { get; set; } 

       public object[] Values { get; set; } 

       public enum ConditionType 
       { 
        Range 
       } 

       //This method creates the expression for the query 
       public static Expression<Func<T, bool>> CreateExpression<T>(IEnumerable<Condition> query) 
       { 
        var groups = query.GroupBy(c => c.ColumnName); 

        Expression exp = null; 
        //This is the parametar that will be used in you lambda function 
        var param = Expression.Parameter(typeof(T)); 

        foreach (var group in groups) 
        { 
         // I start from a null expression so you don't use the silly 1 = 1 if this is a requirement for some reason you can make the 1 = 1 expression instead of null 
         Expression groupExp = null; 

         foreach (var condition in group) 
         { 
          Expression con; 
          //Just a simple type selector and remember switch is evil so you can do it another way 
          switch (condition.Type) 
          { 
//this creates the between NOTE if data types are not the same this can throw exceptions 
           case ConditionType.Range: 
            con = Expression.AndAlso(
             Expression.GreaterThanOrEqual(Expression.Property(param, condition.ColumnName), Expression.Constant(condition.Values[0])), 
             Expression.LessThanOrEqual(Expression.Property(param, condition.ColumnName), Expression.Constant(condition.Values[1]))); 
            break; 
           default: 
            con = Expression.Constant(true); 
            break; 
          } 
          // Builds an or if you need one so you dont use the 1 = 1 
          groupExp = groupExp == null ? con : Expression.OrElse(groupExp, con); 
         } 

         exp = exp == null ? groupExp : Expression.AndAlso(groupExp, exp); 
        } 

        return Expression.Lambda<Func<T, bool>>(exp,param); 
       } 
      } 

      static void Main(string[] args) 
      { 
       //Simple test data as an IQueriable same as EF or any ORM that supports linq. 
       var sales = new[] 
       { 
        new Sale{ A = 1, B = 2 , C = 1 }, 
        new Sale{ A = 4, B = 2 , C = 1 }, 
        new Sale{ A = 8, B = 4 , C = 1 }, 
        new Sale{ A = 16, B = 4 , C = 1 }, 
        new Sale{ A = 32, B = 2 , C = 1 }, 
        new Sale{ A = 64, B = 2 , C = 1 }, 
       }.AsQueryable(); 

       var conditions = new[] 
       { 
        new Condition { ColumnName = "A", Type = Condition.ConditionType.Range, Values= new object[]{ 0, 2 } }, 
        new Condition { ColumnName = "A", Type = Condition.ConditionType.Range, Values= new object[]{ 5, 60 } }, 
        new Condition { ColumnName = "B", Type = Condition.ConditionType.Range, Values= new object[]{ 1, 3 } }, 
        new Condition { ColumnName = "C", Type = Condition.ConditionType.Range, Values= new object[]{ 0, 3 } }, 
       }; 

       var exp = Condition.CreateExpression<Sale>(conditions); 
       //Under no circumstances compile the expression if you do you start using the IEnumerable and they are not converted to SQL but done in memory 
       var items = sales.Where(exp).ToArray(); 

       foreach (var sale in items) 
       { 
        Console.WriteLine($"new Sale{{ A = {sale.A}, B = {sale.B} , C = {sale.C} }}"); 
       } 

       Console.ReadLine(); 
      } 
+0

Das hat super funktioniert.Interessiert, dass Sie eine Bedingung enthalten Typ des Bereichs. Ich habe genau das gleiche, habe es aber weggelassen, um das Beispiel kürzer zu halten. – Valuator

+0

@Valuator Ich dachte, deshalb habe ich es hinzugefügt. Deshalb habe ich deine SQL nicht funktionierte, es fehlte ein (. Ich baue so etwas so oft, dass es offensichtlich war. –

Verwandte Themen