Soluzione alternativa "Contains ()" utilizzando Linq to Entities?


86

Sto cercando di creare una query che utilizza un elenco di ID nella clausola where, utilizzando l'API del client Silverlight ADO.Net Data Services (e quindi Linq To Entities). Qualcuno sa di una soluzione alternativa a Contains non supportata?

Voglio fare qualcosa del genere:

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

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

Ho provato questo:

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

Ma ha ottenuto "Il metodo" Qualsiasi "non è supportato".


35
Nota: Entity Framework 4 (in .NET 4) ha un metodo "Contains", nel caso in cui qualcuno legga questo e non lo sappia. So che l'OP stava usando EF1 (.NET 3.5).
DarrellNorton

7
@Darrell ho perso solo mezz'ora perché ho saltato il tuo commento. Vorrei poter far lampeggiare il tuo commento e apparire sullo schermo.
Chris Dwyer

Risposte:


97

Aggiornamento: EF ≥ 4 supporta Containsdirettamente (Checkout Any), quindi non è necessaria alcuna soluzione alternativa.

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

UTILIZZO:

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

6
Avvertimento; quando arg è una raccolta di grandi dimensioni (il mio era 8500 item int list), stack overflow. Potresti pensare che sia pazzesco passare un elenco del genere, ma penso che questo esponga comunque un difetto in questo approccio.
dudeNumber4

2
Correggimi se sbaglio. ma questo significa che quando la raccolta passata (filtro) è un set vuoto, sostanzialmente risulterà in tutti i dati perché ha appena restituito il parametro della query. Mi aspettavo che filtrasse tutto il valore, c'è un modo per farlo?
Nap

1
Se intendi che quando la raccolta di controllo è vuota non dovrebbe restituire alcun risultato, nello snippet precedente sostituisci l' if (!collection.Any()) //action;azione - sostituisci con la restituzione di una query vuota del tipo richiesto per ottenere le migliori prestazioni - o rimuovi semplicemente questa riga.
Shimmy Weitzhandler

1
return WhereIn (query, selector, collection); dovrebbe essere sostituito da return WhereIn (query, selector, (IEnumerable <TValue>) collection); per evitare ricorsioni indesiderate.
Antoine Aubry

1
Credo ci sia un bug nel codice. Se l'elenco di valori fornito è vuoto, il comportamento corretto dovrebbe essere quello di non restituire risultati, ovvero / nessun oggetto nella query esiste nella raccolta. Tuttavia, il codice fa l'esatto opposto: vengono restituiti tutti i valori, non nessuno. Credo che tu voglia "if (! Collection.Any ()) return query.Where (e => false)"
ShadowChaser

18

Puoi ricorrere alla codifica manuale di alcuni e-sql (nota la parola chiave "it"):

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

Ecco il codice che ho usato per generare alcuni e-sql da una raccolta, YMMV:

string[] ids = orders.Select(x=>x.ProductID.ToString()).ToArray();
return CurrentDataSource.Products.Where("it.ID IN {" + string.Join(",", ids) + "}");

1
Hai altre info su "it"? Il prefisso "it" compare negli esempi MSDN, ma da nessuna parte posso trovare una spiegazione su quando / perché "esso" è necessario.
Robert Claypool,

1
Utilizzato nella query dinamica di Entity Framework, dai un'occhiata a geekswithblogs.net/thanigai/archive/2009/04/29/… , Thanigainathan Siranjeevi lo spiega lì.
Shimmy Weitzhandler

13

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

e la query diventa:

var query2 = context.Entities.Where(BuildContainsExpression<Entity, int>(e => e.ID, ids));

3
Se vuoi fare un "Non contiene", apporta le seguenti modifiche nel metodo BuildContainsExpression: - Expression.Equal diventa Expression.NotEqual - Expression.O diventa Expression.E
Merritt

2

Non sono sicuro di Silverligth, ma in linq to objects uso sempre any () per queste query.

var q = from t in svc.OpenTranaction
        where txnIds.Any(t.OpenTransactionId)
        select t;

5
Any non accetta un oggetto del tipo di sequenza: non ha parametri (nel qual caso è solo "è vuoto o no") o richiede un predicato.
Jon Skeet

Sono terribilmente felice di aver trovato questa risposta :) +1 Grazie AndreasN
SDReyes

1

Per completare il record, ecco il codice che ho finalmente usato (controllo degli errori omesso per chiarezza) ...

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


0

Grazie mille. Il metodo di estensione WhereIn era sufficiente per me. L'ho profilato e ho generato lo stesso comando SQL nel DataBase di e-sql.

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

Generato questo:

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])


0

Scusa nuovo utente, avrei commentato la risposta effettiva, ma sembra che non riesca ancora a farlo?

Ad ogni modo, per quanto riguarda la risposta con codice di esempio per BuildContainsExpression (), tieni presente che se usi quel metodo su Entità di database (cioè non oggetti in memoria) e stai usando IQueryable, che in realtà deve andare al database dal momento che fondamentalmente esegue molte condizioni SQL "o" per controllare la clausola "where in" (eseguirlo con SQL Profiler per vedere).

Ciò può significare che, se stai perfezionando un IQueryable con più BuildContainsExpression (), non lo trasformerà in un'istruzione SQL che viene eseguita alla fine come previsto.

La soluzione alternativa per noi consisteva nell'usare più join LINQ per mantenerlo a una chiamata SQL.


0

Oltre alla risposta selezionata.

Sostituisci Expression.Orcon Expression.OrElseda usare con Nhibernate e correggi l' Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseOr' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'eccezione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.