Solution de contournement 'Contains ()' à l'aide de Linq to Entities?

86

J'essaie de créer une requête qui utilise une liste d'identifiants dans la clause where, en utilisant l'api du client Silverlight ADO.Net Data Services (et donc Linq To Entities). Quelqu'un connaît-il une solution de contournement pour Contains qui n'est pas prise en charge?

Je veux faire quelque chose comme ça:

List<long?> txnIds = new List<long?>();
// Fill list 

var q = from t in svc.OpenTransaction
        where txnIds.Contains(t.OpenTransactionId)
        select t;

J'ai essayé ceci:

var q = from t in svc.OpenTransaction
where txnIds.Any<long>(tt => tt == t.OpenTransactionId)
select t;

Mais obtenu "La méthode 'Any' n'est pas supportée".

James Bloomer
la source
35
Remarque: Entity Framework 4 (dans .NET 4) a une méthode "Contains", juste au cas où quelqu'un lirait ceci sans le savoir. Je sais que l'OP utilisait EF1 (.NET 3.5).
DarrellNorton
7
@Darrell Je viens de perdre une demi-heure parce que j'ai sauté votre commentaire. Je souhaite que je pourrais faire clignoter votre commentaire et marquer sur l'écran.
Chris Dwyer

Réponses:

97

Mise à jour: EF ≥ 4 prend Containsdirectement en charge (Checkout Any), vous n'avez donc besoin d'aucune solution de contournement.

public static IQueryable<TEntity> WhereIn<TEntity, TValue>
  (
    this ObjectQuery<TEntity> query,
    Expression<Func<TEntity, TValue>> selector,
    IEnumerable<TValue> collection
  )
{
  if (selector == null) throw new ArgumentNullException("selector");
  if (collection == null) throw new ArgumentNullException("collection");
  if (!collection.Any()) 
    return query.Where(t => false);

  ParameterExpression p = selector.Parameters.Single();

  IEnumerable<Expression> equals = collection.Select(value =>
     (Expression)Expression.Equal(selector.Body,
          Expression.Constant(value, typeof(TValue))));

  Expression body = equals.Aggregate((accumulate, equal) =>
      Expression.Or(accumulate, equal));

  return query.Where(Expression.Lambda<Func<TEntity, bool>>(body, p));
}

//Optional - to allow static collection:
public static IQueryable<TEntity> WhereIn<TEntity, TValue>
  (
    this ObjectQuery<TEntity> query,
    Expression<Func<TEntity, TValue>> selector,
    params TValue[] collection
  )
{
  return WhereIn(query, selector, (IEnumerable<TValue>)collection);
}

USAGE:

public static void Main()
{
  using (MyObjectContext context = new MyObjectContext())
  {
    //Using method 1 - collection provided as collection
    var contacts1 =
      context.Contacts.WhereIn(c => c.Name, GetContactNames());

    //Using method 2 - collection provided statically
    var contacts2 = context.Contacts.WhereIn(c => c.Name,
      "Contact1",
      "Contact2",
      "Contact3",
      "Contact4"
      );
  }
}
Shimmy Weitzhandler
la source
6
Attention; quand arg est une grande collection (la mienne était 8500 item int list), débordement de pile. Vous pensez peut-être fou de passer une telle liste, mais je pense que cela expose néanmoins une faille dans cette approche.
dudeNumber4
2
Corrigez-moi si je me trompe. mais cela signifie que lorsque la collection (filtre) passée est un ensemble vide, il en résultera essentiellement toutes les données car il vient de renvoyer le paramètre de requête. Je m'attendais à ce qu'il filtre toutes les valeurs, y a-t-il un moyen de le faire?
Nap
1
Si vous voulez dire que lorsque la collection de contrôle est vide, elle ne doit renvoyer aucun résultat, le dans l'extrait de code ci-dessus remplace l' if (!collection.Any()) //action;action - replace par simplement renvoyer une requête vide du type demandé pour de meilleures performances - ou supprimez simplement cette ligne.
Shimmy Weitzhandler
1
return WhereIn (requête, sélecteur, collection); doit être remplacé par return WhereIn (requête, sélecteur, (IEnumerable <TValue>) collection); pour éviter les récursions indésirables.
Antoine Aubry
1
Je pense qu'il y a un bogue dans le code. Si la liste de valeurs fournie est vide, le comportement correct devrait être de ne renvoyer aucun résultat - c'est-à-dire qu'aucun objet de la requête n'existe dans la collection. Cependant, le code fait exactement le contraire - toutes les valeurs sont renvoyées, pas aucune. Je crois que vous voulez "if (! Collection.Any ()) return query.Where (e => false)"
ShadowChaser
18

Vous pouvez vous rabattre sur le codage de certains e-sql (notez le mot-clé "it"):

return CurrentDataSource.Product.Where("it.ID IN {4,5,6}"); 

Voici le code que j'ai utilisé pour générer du e-sql à partir d'une collection, YMMV:

string[] ids = orders.Select(x=>x.ProductID.ToString()).ToArray();
return CurrentDataSource.Products.Where("it.ID IN {" + string.Join(",", ids) + "}");
Rob Fonseca-Ensor
la source
1
Avez-vous plus d'informations sur "it"? Le préfixe «it» apparaît dans les échantillons MSDN, mais je ne peux trouver nulle part d'explication sur quand / pourquoi «il» est nécessaire.
Robert Claypool
1
Utilisé dans la requête dynamique Entity Framework, jetez un œil à geekswithblogs.net/thanigai/archive/2009/04/29/… , Thanigainathan Siranjeevi l'explique ici.
Shimmy Weitzhandler
13

Depuis MSDN :

static Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue>(
    Expression<Func<TElement, TValue>> valueSelector, IEnumerable<TValue> values)
{
    if (null == valueSelector) { throw new ArgumentNullException("valueSelector"); }
    if (null == values) { throw new ArgumentNullException("values"); }
    ParameterExpression p = valueSelector.Parameters.Single();

    // p => valueSelector(p) == values[0] || valueSelector(p) == ...
    if (!values.Any())
    {
        return e => false;
    }

    var equals = values.Select(
             value => (Expression)Expression.Equal(valueSelector.Body, Expression.Constant(value, typeof(TValue))));

    var body = equals.Aggregate<Expression>((accumulate, equal) => Expression.Or(accumulate, equal));

    return Expression.Lambda<Func<TElement, bool>>(body, p);
} 

et la requête devient:

var query2 = context.Entities.Where(BuildContainsExpression<Entity, int>(e => e.ID, ids));
James Bloomer
la source
3
Si vous voulez faire un 'Ne contient pas', apportez simplement les modifications suivantes dans la méthode BuildContainsExpression: - Expression.Equal devient Expression.NotEqual - Expression.Or devient Expression.And
Merritt
2

Je ne suis pas sûr de Silverligth, mais dans linq aux objets, j'utilise toujours any () pour ces requêtes.

var q = from t in svc.OpenTranaction
        where txnIds.Any(t.OpenTransactionId)
        select t;
AndreasN
la source
5
Any ne prend pas un objet du type séquence - il n'a pas de paramètres (auquel cas c'est juste "est-ce vide ou non") ou il prend un prédicat.
Jon Skeet
Je suis terriblement heureux d'avoir trouvé cette réponse :) +1 Merci AndreasN
SDReyes
1

Pour compléter l'enregistrement, voici le code que j'ai finalement utilisé (vérification des erreurs omise pour plus de clarté) ...

// How the function is called
var q = (from t in svc.OpenTransaction.Expand("Currency,LineItem")
         select t)
         .Where(BuildContainsExpression<OpenTransaction, long>(tt => tt.OpenTransactionId, txnIds));



 // The function to build the contains expression
   static System.Linq.Expressions.Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue>(
                System.Linq.Expressions.Expression<Func<TElement, TValue>> valueSelector, 
                IEnumerable<TValue> values)
        {
            if (null == valueSelector) { throw new ArgumentNullException("valueSelector"); }
            if (null == values) { throw new ArgumentNullException("values"); }
            System.Linq.Expressions.ParameterExpression p = valueSelector.Parameters.Single();

            // p => valueSelector(p) == values[0] || valueSelector(p) == ...
            if (!values.Any())
            {
                return e => false;
            }

            var equals = values.Select(value => (System.Linq.Expressions.Expression)System.Linq.Expressions.Expression.Equal(valueSelector.Body, System.Linq.Expressions.Expression.Constant(value, typeof(TValue))));
            var body = equals.Aggregate<System.Linq.Expressions.Expression>((accumulate, equal) => System.Linq.Expressions.Expression.Or(accumulate, equal));
            return System.Linq.Expressions.Expression.Lambda<Func<TElement, bool>>(body, p);
        }
James Bloomer
la source
0

Merci beaucoup. La méthode d'extension WhereIn me suffisait. Je l'ai profilé et généré la même commande SQL dans la base de données que e-sql.

public Estado[] GetSomeOtherMore(int[] values)
{
    var result = _context.Estados.WhereIn(args => args.Id, values) ;
    return result.ToArray();
}

Généré ceci:

SELECT 
[Extent1].[intIdFRLEstado] AS [intIdFRLEstado], 
[Extent1].[varDescripcion] AS [varDescripcion]
FROM [dbo].[PVN_FRLEstados] AS [Extent1]
WHERE (2 = [Extent1].[intIdFRLEstado]) OR (4 = [Extent1].[intIdFRLEstado]) OR (8 = [Extent1].[intIdFRLEstado])
jrojo
la source
0

Désolé nouvel utilisateur, j'aurais commenté la réponse réelle, mais il semble que je ne peux pas encore le faire?

Quoi qu'il en soit, en ce qui concerne la réponse avec un exemple de code pour BuildContainsExpression (), sachez que si vous utilisez cette méthode sur des entités de base de données (c'est-à-dire pas des objets en mémoire) et que vous utilisez IQueryable, il doit en fait aller à la base de données car il fait essentiellement beaucoup de conditions SQL "ou" pour vérifier la clause "where in" (exécutez-le avec SQL Profiler pour voir).

Cela peut signifier que si vous affinez un IQueryable avec plusieurs BuildContainsExpression (), il ne le transformera pas en une instruction SQL qui sera exécutée à la fin comme prévu.

La solution de contournement pour nous était d'utiliser plusieurs jointures LINQ pour le conserver à un seul appel SQL.

Shannon
la source
0

En plus de la réponse sélectionnée.

Remplacez Expression.Orpar Expression.OrElsepour utiliser par Nhibernate et corrigez l' Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseOr' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'exception.

smg
la source