2017-09-07 9 views
0

J'ai un système qui permet de stocker différents critères relatifs aux ventes dans la base de données. Lorsque les critères sont chargés, ils sont utilisés pour générer une requête et renvoyer toutes les ventes applicables. Les objets de critères ressemblent à ceci:Création d'une requête dynamique dans une boucle à l'aide d'arbres d'expression

ReferenceColumn (La colonne dans la table de vente, ils appliquent à)

MinValue (valeur minimale de la colonne de référence doit être)

MaxValue (valeur maximale de la colonne de référence doit être

Une recherche de Ventes est effectuée en utilisant une collection des critères susmentionnés. Les ReferenceColumns du même type sont OR'd ensemble, et les ReferenceColumns de différents types sont ET'd ensemble. Ainsi, par exemple si j'avais trois critères:

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

ReferenceColumn: 'Prix', MinValue: '80', MaxValue: « 100 '

ReferenceColumn: 'âge', MINVALUE: '2', MaxValue: '3'

la requête doit retourner toutes les ventes où le prix était entre 10-20 ou entre 80-100, mais seulement si les L'âge des ventes est entre 2 et 3 ans.

Je l'ai implémenté en utilisant une chaîne de requête SQL et l'exécution en utilisant .FromSql:

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

Et c'est fait fonctionne très bien avec notre base de données, mais il ne joue pas bien avec d'autres collections, particulièrement la Base de données InMemory que nous utilisons pour UnitTesting, j'essaie donc de le réécrire en utilisant des arborescences Expression, que je n'ai jamais utilisées auparavant. Jusqu'à présent, j'ai obtenu ceci:

public IEnumerable<Sale> GetByCriteria(ICollection<SaleCriteria> criteria) 
{ 
var referenceFields = criteria.GroupBy(c => c.ReferenceColumn); 

Expression masterExpression = Expression.Equal(Expression.Constant(1), Expression.Constant(1)); 
List<ParameterExpression> parameters = new List<ParameterExpression>(); 

// AND these... 
foreach (IGrouping<string, SaleCriteria> criteriaGrouping in referenceFields) 
{ 
    Expression innerExpression = Expression.Equal(Expression.Constant(1), Expression.Constant(0)); 
    ParameterExpression referenceColumn = Expression.Parameter(typeof(Decimal), criteriaGrouping.Key); 
    parameters.Add(referenceColumn); 

    // OR these... 
    foreach (SaleCriteria sc in criteriaGrouping) 
    { 
     Expression low = Expression.Constant(Decimal.Parse(sc.MinValue)); 
     Expression high = Expression.Constant(Decimal.Parse(sc.MaxValue)); 
     Expression rangeExpression = Expression.GreaterThanOrEqual(referenceColumn, low); 
     rangeExpression = Expression.AndAlso(rangeExpression, Expression.LessThanOrEqual(referenceColumn, high)); 
     innerExpression = Expression.OrElse(masterExpression, rangeExpression); 
    } 

    masterExpression = Expression.AndAlso(masterExpression, innerExpression); 
} 

var lamda = Expression.Lambda<Func<Sale, bool>>(masterExpression, parameters); 

return _context.Sale.Where(lamda.Compile()); 
} 

Il est actuellement jeter un ArgumentException quand je l'appelle Expression.Lamda. Decimal ne peut pas être utilisé là-bas et il dit qu'il veut le type Sale, mais je ne sais pas quoi mettre là pour les ventes, et je ne suis pas sûr que je suis même sur la bonne voie ici. Je suis également préoccupé par le fait que mon masterExpression se duplique à chaque fois au lieu de l'ajouter comme je l'ai fait avec le générateur de chaînes, mais peut-être que cela fonctionnera de toute façon.

Je cherche de l'aide sur la façon de convertir cette requête dynamique en arbre d'expression, et je suis ouvert à une approche totalement différente si je suis hors de la base ici.

+0

Dose votre travail de code d'origine? Cela ne devrait pas fonctionner et pourquoi utilisez-vous 1 = 1 et 1 = 0? –

+0

Oui cela fonctionne si la collection fait partie d'un DbContext utilisant SQL Server. 1 = 1 et 1 = 0 sont là donc je peux toujours ajouter 'AND'/'OU' à la chaîne de requête sans avoir à traiter le premier cas spécial d'itération, etc. – Valuator

+0

Essayez d'utiliser LINQKit (http: //www.albahari .com/nutshell/linqkit.aspx), cela le rend beaucoup plus facile. La page dit: Avec LINQKit, vous pouvez: ... Construire dynamiquement des prédicats – Tom

Répondre

1

Je pense que cela fonctionnera pour vous

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

Ça a bien fonctionné.Intéressé que vous avez inclus une condition Type de plage. J'ai exactement la même chose mais je l'ai omis pour garder l'exemple plus court. – Valuator

+0

@Valuator Je pensais que c'est pourquoi je l'ai ajouté. C'est pourquoi je pensais que votre SQL ne fonctionnait pas il manquait un (je construis quelque chose comme ça tellement de fois que c'était évident. –