Problemumgehung 'Enthält ()' mit Linq to Entities?

86

Ich versuche, eine Abfrage zu erstellen, die eine Liste von IDs in der where-Klausel verwendet, wobei ich die Silverlight ADO.Net Data Services-Client-API (und damit Linq To Entities) verwende. Kennt jemand eine Problemumgehung für Contains, die nicht unterstützt wird?

Ich möchte so etwas machen:

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

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

Versuchte dies:

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

Aber bekam "Die Methode 'Any' wird nicht unterstützt".

James Bloomer
quelle
35
Hinweis: Entity Framework 4 (in .NET 4) verfügt über eine "Enthält" -Methode, nur für den Fall, dass jemand dies liest, der nichts davon weiß. Ich weiß, dass das OP EF1 (.NET 3.5) verwendet hat.
DarrellNorton
7
@Darrell Ich habe gerade eine halbe Stunde verschwendet, weil ich deinen Kommentar übersprungen habe. Ich wünschte, ich könnte Ihren Kommentar über den Bildschirm blinken lassen.
Chris Dwyer

Antworten:

97

Update: EF ≥ 4 unterstützt Containsdirekt (Checkout Any), sodass Sie keine Problemumgehung benötigen.

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

VERWENDUNG:

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
quelle
6
Warnung; Wenn arg eine große Sammlung ist (meine war 8500 item int list), Stapelüberlauf. Sie mögen es für verrückt halten, eine solche Liste zu bestehen, aber ich denke, dies enthüllt dennoch einen Fehler in diesem Ansatz.
DudeNumber4
2
Korrigieren Sie mich, wenn ich falsch liege. Dies bedeutet jedoch, dass die übergebene Sammlung (Filter), wenn sie leer ist, im Grunde genommen zu allen Daten führt, da sie gerade den Abfrageparameter zurückgegeben hat. Ich hatte erwartet, dass es alle Werte filtert. Gibt es eine Möglichkeit, dies zu tun?
Nap
1
Wenn Sie meinen, dass die Prüfsammlung, wenn sie leer ist, keine Ergebnisse zurückgeben soll, ersetzen Sie im obigen Snippet die if (!collection.Any()) //action;Aktion "Ersetzen" durch die einfache Rückgabe einer leeren Abfrage des angeforderten Typs, um die beste Leistung zu erzielen - oder entfernen Sie einfach diese Zeile.
Shimmy Weitzhandler
1
return WhereIn (Abfrage, Selektor, Sammlung); sollte durch return WhereIn (Abfrage, Selektor, (IEnumerable <TValue>) Sammlung) ersetzt werden; um unerwünschte Rekursionen zu vermeiden.
Antoine Aubry
1
Ich glaube, es gibt einen Fehler im Code. Wenn die angegebene Werteliste leer ist, sollte das richtige Verhalten darin bestehen, keine Ergebnisse zurückzugeben - dh / keine Objekte in der Abfrage sind in der Auflistung vorhanden. Der Code macht jedoch genau das Gegenteil - alle Werte werden zurückgegeben, keiner von ihnen. Ich glaube, Sie wollen "if (! Collection.Any ()) return query.Where (e => false)"
ShadowChaser
18

Sie können auf die Codierung von E-SQL zurückgreifen (beachten Sie das Schlüsselwort "it"):

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

Hier ist der Code, mit dem ich aus einer Sammlung, YMMV, E-SQL generiert habe:

string[] ids = orders.Select(x=>x.ProductID.ToString()).ToArray();
return CurrentDataSource.Products.Where("it.ID IN {" + string.Join(",", ids) + "}");
Rob Fonseca-Ensor
quelle
1
Haben Sie weitere Informationen zu "it"? Das Präfix "it" wird in MSDN-Beispielen angezeigt, aber nirgends kann ich erklären, wann / warum "it" benötigt wird.
Robert Claypool
1
Schauen Sie sich geekswithblogs.net/thanigai/archive/2009/04/29/… an , das in der dynamischen Abfrage von Entity Framework verwendet wird. Thanigainathan Siranjeevi erklärt es dort.
Shimmy Weitzhandler
13

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

und die Abfrage wird:

var query2 = context.Entities.Where(BuildContainsExpression<Entity, int>(e => e.ID, ids));
James Bloomer
quelle
3
Wenn Sie ein 'Nicht enthält' ausführen möchten, nehmen Sie einfach die folgenden Änderungen in der BuildContainsExpression-Methode vor: - Expression.Equal wird zu Expression.NotEqual - Expression.Or wird zu Expression.And
Merritt
2

Ich bin mir bei Silverligth nicht sicher, aber in Linq to Objects verwende ich immer any () für diese Abfragen.

var q = from t in svc.OpenTranaction
        where txnIds.Any(t.OpenTransactionId)
        select t;
AndreasN
quelle
5
Jeder nimmt kein Objekt vom Sequenztyp - es hat entweder keine Parameter (in diesem Fall ist es nur "ist das leer oder nicht") oder es nimmt ein Prädikat.
Jon Skeet
Ich bin furchtbar froh, diese Antwort gefunden zu haben :) +1 Danke AndreasN
SDReyes
1

Um den Datensatz zu vervollständigen, ist hier der Code, den ich schließlich verwendet habe (Fehlerprüfung aus Gründen der Klarheit weggelassen) ...

// 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
quelle
0

Vielen Dank. Wobei mir die Erweiterungsmethode ausreichte. Ich habe es profiliert und den gleichen SQL-Befehl für die Datenbank wie e-sql generiert.

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

Generierte dies:

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
quelle
0

Sorry neuer Benutzer, ich hätte die eigentliche Antwort kommentiert, aber anscheinend kann ich das noch nicht?

Beachten Sie in Bezug auf die Antwort mit Beispielcode für BuildContainsExpression (), dass, wenn Sie diese Methode für Datenbankentitäten (dh keine speicherinternen Objekte) verwenden und IQueryable verwenden, diese tatsächlich in die Datenbank verschoben werden muss da es im Grunde viele SQL "- oder" Bedingungen ausführt, um die "where in" -Klausel zu überprüfen (führen Sie sie mit SQL Profiler aus, um zu sehen).

Dies kann bedeuten, dass wenn Sie ein IQueryable mit mehreren BuildContainsExpression () verfeinern, es nicht in eine SQL-Anweisung umgewandelt wird, die am Ende wie erwartet ausgeführt wird.

Die Problemumgehung bestand für uns darin, mehrere LINQ-Joins zu verwenden, um einen SQL-Aufruf zu erhalten.

Shannon
quelle
0

Neben der ausgewählten Antwort.

Ersetzen Sie Expression.Ordurch Expression.OrElse, um es mit Nhibernate zu verwenden, und beheben Sie die Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseOr' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'Ausnahme.

smg
quelle