2009-02-16 4 views
8

Disons que j'ai quelque chose appelé Stuff dans ma base de données, avec une propriété appelée Id. De l'utilisateur, je reçois une séquence d'objets Range sélectionnés (ou plutôt je les crée à partir de leur entrée) avec les Ids qu'ils veulent. Une version dépouillée de cette struct ressemble à ceci:C#, Linq2SQL: Création d'un prédicat pour trouver des éléments dans un certain nombre de plages

public struct Range<T> : IEquatable<Range<T>>, IEqualityComparer<Range<T>> 
{ 
    public T A; 
    public T B; 
    public Range(T a, T b) 
    { 
     A = a; 
     B = b; 
    } 
    ... 
} 

Donc, on pourrait par exemple avoir obtenu:

var selectedRange = new List<Range<int>> 
    { 
     new Range(1, 4), 
     new Range(7,11), 
    }; 

Je veux ensuite l'utiliser pour créer un prédicat de sélectionner uniquement les choses qui ont un valeur entre ceux-ci. Par exemple, en utilisant le PredicateBuilder, je peux par exemple faire de cette façon:

var predicate = PredicateBuilder.False<Stuff>(); 
foreach (Range<int> r in selectedRange) 
{ 
    int a = r.A; 
    int b = r.B; 
    predicate = predicate.Or(ø => ø.Id >= a && ø.Id <= b); 
} 

puis:

var stuff = datacontext.Stuffs.Where(predicate).ToList(); 

Ce qui fonctionne! Ce que je voudrais faire maintenant, est de créer une méthode d'extension générique pour créer ces prédicats pour moi. Un peu comme ceci:

public static Expression<Func<T,bool>> ToPredicate<T>(this IEnumerable<Range<int>> range, Func<T, int> selector) 
{ 
    Expression<Func<T, bool>> p = PredicateBuilder.False<T>(); 
    foreach (Range<int> r in range) 
    { 
     int a = r.A; 
     int b = r.B; 
     p = p.Or(ø => selector(ø) >= a && selector(ø) <= b); 
    } 
    return p; 
} 

problème ici, est qu'il tombe en panne avec un NotSupportedException en raison du sélecteur (ø) Appel: Method 'System.Object DynamicInvoke(System.Object[])' has no supported translation to SQL.

Je suppose que c'est compréhensible. Mais y a-t-il un moyen de contourner cela? Ce que je voudrais retrouver avec pour que je pouvais faire:

var stuff = datacontext.Stuffs.Where(selectedRange.ToPredicate<Stuff>(ø => ø.Id)); 

Ou mieux encore, créer quelque chose qui retourne un IQueryable pour que je pouvais faire:

var stuff = datacontext.Stuffs.WhereWithin<Stuff>(selectedRange, ø => ø.Id); // Possibly without having to specify Stuff as type there... 

Ainsi, tout des idées? Je voudrais vraiment obtenir ce travail, parce que si je ne vais obtenir beaucoup de ces blocs foreach de code, la création prédicats ...


Note 1: Bien sûr, serait bien si je pouvais expand à plus de int, comme DateTime et autres, mais je ne sais pas comment cela finit par utiliser les opérateurs 0 = = < = ... Est-ce que CompareTo fonctionne avec linq-to-sql? Sinon, il n'y a pas de problème à en créer deux. Un pour int et un pour DateTime, puisque ce sont principalement les types pour lesquels cela sera utilisé.

Note 2: Il sera utilisé pour les rapports, où l'utilisateur sera en mesure d'affiner ce qui sort, en fonction de choses différentes. Comme, je veux ce rapport pour ces personnes et ces dates.

Répondre

7

L'utilisation de génériques est problématique, car C# ne supporte pas les opérateurs sur les génériques - ce qui signifie que vous devez écrire l'expression manuellement. Et comme nous l'avons déjà vu, la chaîne fonctionne différemment. Mais pour le reste, que diriez-vous quelque chose comme (non testé):

(modifié pour plusieurs plages)

public static IQueryable<TSource> WhereBetween<TSource, TValue>(
     this IQueryable<TSource> source, 
     Expression<Func<TSource, TValue>> selector, 
     params Range<TValue>[] ranges) 
    { 
     return WhereBetween<TSource,TValue>(source, selector, 
      (IEnumerable<Range<TValue>>) ranges); 
    } 

    public static IQueryable<TSource> WhereBetween<TSource, TValue>(
     this IQueryable<TSource> source, 
     Expression<Func<TSource, TValue>> selector, 
     IEnumerable<Range<TValue>> ranges) 
    { 
     var param = Expression.Parameter(typeof(TSource), "x"); 
     var member = Expression.Invoke(selector, param); 
     Expression body = null; 
     foreach(var range in ranges) 
     { 
      var filter = Expression.AndAlso(
       Expression.GreaterThanOrEqual(member, 
        Expression.Constant(range.A, typeof(TValue))), 
       Expression.LessThanOrEqual(member, 
        Expression.Constant(range.B, typeof(TValue)))); 
      body = body == null ? filter : Expression.OrElse(body, filter); 
     }    
     return body == null ? source : source.Where(
      Expression.Lambda<Func<TSource, bool>>(body, param)); 
    } 

Remarque; l'utilisation d'Expression.Invoke signifie qu'il fonctionnera probablement sur LINQ-to-SQL mais pas sur EF (pour l'instant, nous l'espérons fixé en 4.0).

Avec l'utilisation (testé sur Northwind):

Range<decimal?> range1 = new Range<decimal?>(0,10), 
       range2 = new Range<decimal?>(15,20); 
var qry = ctx.Orders.WhereBetween(order => order.Freight, range1, range2); 

Génération TSQL (re-formaté):

SELECT -- (SNIP) 
FROM [dbo].[Orders] AS [t0] 
WHERE (([t0].[Freight] >= @p0) AND ([t0].[Freight] <= @p1)) 
OR (([t0].[Freight] >= @p2) AND ([t0].[Freight] <= @p3)) 

Juste ce que nous voulions ;-P

+0

Comment cela fonctionnerait-il avec toute une série d'objets Range ? – Svish

+0

Vous pourriez faire la même chose avec OrElse ... Je vais mettre à jour ... –

+0

Et quel est ce "x" dans votre param? – Svish

0

Vous obtenez cette erreur , car tout pour LINQ to SQL doit être sous la forme d'un Expression. Essayez ceci

public static Expression<Func<T,bool>> ToPredicate<T>(
    this IEnumerable<Range<int>> range, 
    Expression<Func<T, int>> selector 
) { 
    Expression<Func<T, bool>> p = PredicateBuilder.False<T>(); 
    Func<T, int> selectorFunc = selector.Compile(); 
    foreach (Range<int> r in range) 
    { 
     int a = r.A; 
     int b = r.B; 
     p = p.Or(ø => selectorFunc(ø) >= a && selectorFunc(ø) <= b); 
    } 
    return p; 
} 

Notez que je compile le sélecteur avant de l'utiliser. Cela devrait fonctionner sans accroc, j'ai déjà utilisé quelque chose comme ça dans le passé.

+0

Comment puis-je utiliser le sélecteur? En d'autres termes, qu'est-ce que je remplace 'selector (ø)' avec dans 'p = p.Or (ø => sélecteur (ø)> = a && sélecteur (ø) <= b); '? – Svish

+0

Non, je reçois le même' System 'System.Object DynamicInvoke (System.Object [])' n'a pas de traduction en SQL. ' – Svish

Questions connexes