Obejście „Contains ()” przy użyciu Linq to Entities?

86

Próbuję utworzyć kwerendę, która używa listy identyfikatorów w klauzuli Where, przy użyciu interfejsu API klienta usług danych Silverlight ADO.Net (a zatem Linq To Entities). Czy ktoś wie o obejściu problemu Zawartość nie jest obsługiwany?

Chcę zrobić coś takiego:

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

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

Próbowałem tego:

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

Ale otrzymano „Metoda„ Dowolna ”nie jest obsługiwana”.

James Bloomer
źródło
35
Uwaga: Entity Framework 4 (w .NET 4) ma metodę „Contains”, na wypadek gdyby ktoś to czytał i nie wiedział o tym. Wiem, że OP korzystał z EF1 (.NET 3.5).
DarrellNorton
7
@Darrell Właśnie zmarnowałem pół godziny, ponieważ pominąłem twój komentarz. Żałuję, że nie mogę sprawić, by Twój komentarz mrugnął i pojawił się na ekranie.
Chris Dwyer,

Odpowiedzi:

97

Aktualizacja: EF ≥ 4 obsługuje Containsbezpośrednio (Checkout Any), więc nie potrzebujesz żadnego obejścia.

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

STOSOWANIE:

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
źródło
6
Ostrzeżenie; kiedy arg jest dużą kolekcją (u mnie było 8500 pozycji na liście int), przepełnienie stosu. Możesz pomyśleć, że przekazanie takiej listy jest szalone, ale myślę, że to ujawnia wadę tego podejścia.
dudeNumber4
2
Popraw mnie, jeśli się mylę. ale to oznacza, że ​​kiedy przekazana kolekcja (filtr) jest pustym zestawem, w zasadzie zwróci wszystkie dane, ponieważ po prostu zwrócił parametr zapytania. Spodziewałem się, że przefiltruje wszystkie wartości, czy istnieje sposób, aby to zrobić?
Drzemka
1
Jeśli masz na myśli, że gdy kolekcja sprawdzająca jest pusta, nie powinna zwracać żadnych wyników, w powyższym fragmencie if (!collection.Any()) //action;kodu zastępuje akcję - zamień po prostu zwracając puste zapytanie żądanego typu w celu uzyskania najlepszej wydajności - lub po prostu usuń tę linię.
Shimmy Weitzhandler,
1
return WhereIn (zapytanie, selektor, kolekcja); należy zastąpić zwrotem WhereIn (zapytanie, selektor, (IEnumerable <TValue>) kolekcja); aby uniknąć niechcianej rekursji.
Antoine Aubry
1
Uważam, że w kodzie jest błąd. Jeśli podana lista wartości jest pusta, poprawnym zachowaniem powinno być zwracanie żadnych wyników - tj. / Żadne obiekty w zapytaniu nie istnieją w kolekcji. Jednak kod działa dokładnie odwrotnie - zwracane są wszystkie wartości, a nie żadna z nich. Wierzę, że chcesz "if (! Collection.Any ()) return query.Where (e => false)"
ShadowChaser,
18

Możesz wrócić do kodowania e-sql (zwróć uwagę na słowo kluczowe „it”):

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

Oto kod, którego użyłem do wygenerowania e-sql z kolekcji YMMV:

string[] ids = orders.Select(x=>x.ProductID.ToString()).ToArray();
return CurrentDataSource.Products.Where("it.ID IN {" + string.Join(",", ids) + "}");
Rob Fonseca-Ensor
źródło
1
Czy masz więcej informacji na temat „tego”? Prefiks „to” pojawia się w próbkach MSDN, ale nigdzie nie mogę znaleźć wyjaśnienia, kiedy / dlaczego „to” jest potrzebne.
Robert Claypool
1
Używane w dynamicznym zapytaniu Entity Framework, spójrz na geekswithblogs.net/thanigai/archive/2009/04/29/… , wyjaśnia to Thanigainathan Siranjeevi.
Shimmy Weitzhandler
13

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

a zapytanie staje się:

var query2 = context.Entities.Where(BuildContainsExpression<Entity, int>(e => e.ID, ids));
James Bloomer
źródło
3
Jeśli chcesz wykonać `` Nie zawiera '', po prostu wprowadź następujące zmiany w metodzie BuildContainsExpression: - Expression.Equal staje się Expression.NotEqual - Expression.Lub staje się Expression.I
Merritt
2

Nie jestem pewien co do Silverligth, ale w linq do obiektów zawsze używam any () do tych zapytań.

var q = from t in svc.OpenTranaction
        where txnIds.Any(t.OpenTransactionId)
        select t;
AndreasN
źródło
5
Any nie przyjmuje obiektu typu sekwencji - albo nie ma parametrów (w takim przypadku jest to po prostu „czy to jest puste, czy nie”) lub przyjmuje predykat.
Jon Skeet,
Bardzo się cieszę, że znalazłem tę odpowiedź :) +1 Dzięki AndreasN
SDReyes
1

Aby uzupełnić rekord, oto kod, którego ostatecznie użyłem (sprawdzanie błędów pominięto dla przejrzystości) ...

// 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
źródło
0

Dziękuję bardzo. Wystarczyła mi metoda WhereIn extension. Sprofilowałem go i wygenerowałem to samo polecenie SQL do bazy danych, co e-sql.

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

Wygenerowano to:

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
źródło
0

Przepraszam nowy użytkowniku, skomentowałbym właściwą odpowiedź, ale wygląda na to, że jeszcze nie mogę tego zrobić?

W każdym razie, jeśli chodzi o odpowiedź z przykładowym kodem dla BuildContainsExpression (), pamiętaj, że jeśli używasz tej metody na jednostkach bazy danych (tj. Nie obiektach w pamięci) i używasz IQueryable, to faktycznie musi przejść do bazy danych ponieważ w zasadzie wykonuje wiele warunków SQL, aby sprawdzić klauzulę „where in” (uruchom go z SQL Profiler, aby zobaczyć).

Może to oznaczać, że jeśli udoskonalasz IQueryable za pomocą wielu BuildContainsExpression (), nie zmieni to w jedną instrukcję SQL, która zostanie uruchomiona na końcu zgodnie z oczekiwaniami.

Rozwiązaniem dla nas było użycie wielu sprzężeń LINQ, aby zachować je w jednym wywołaniu SQL.

Shannon
źródło
0

Oprócz wybranej odpowiedzi.

Wymień Expression.Orsię Expression.OrElsedo korzystania z NHibernate i naprawić Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseOr' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'wyjątku.

smg
źródło