¿Solución alternativa 'Contiene ()' usando Linq para entidades?

86

Estoy tratando de crear una consulta que usa una lista de identificadores en la cláusula where, usando la API del cliente Silverlight ADO.Net Data Services (y por lo tanto Linq To Entities). ¿Alguien sabe de alguna solución para que no se admita el contenido?

Quiero hacer algo como esto:

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

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

Intenté esto:

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

Pero obtuvo "El método 'Cualquiera' no es compatible".

James Bloomer
fuente
35
Nota: Entity Framework 4 (en .NET 4) tiene un método "Contiene", en caso de que alguien esté leyendo esto y no lo sepa. Sé que el OP estaba usando EF1 (.NET 3.5).
DarrellNorton
7
@Darrell Acabo de perder media hora porque me salté tu comentario. Me gustaría poder hacer que tu comentario parpadee y se marque en la pantalla.
Chris Dwyer

Respuestas:

97

Actualización: EF ≥ 4 admite Containsdirectamente (Checkout Any), por lo que no necesita ninguna solución.

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

USO:

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
fuente
6
Advertencia; cuando arg es una colección grande (la mía era 8500 item int list), desbordamiento de pila. Puede pensar que es una locura aprobar una lista así, pero creo que esto expone un defecto en este enfoque, no obstante.
dudeNumber4
2
Corrígeme si estoy equivocado. pero esto significa que cuando la colección pasada (filtro) es un conjunto vacío, básicamente dará como resultado todos los datos porque acaba de devolver el parámetro de consulta. Esperaba que filtrara todo el valor, ¿hay alguna manera de hacer esto?
Siesta
1
Si quiere decir que cuando la colección de verificación está vacía, no debería devolver ningún resultado, en el fragmento anterior reemplace la if (!collection.Any()) //action;acción - reemplazar con simplemente devolver una consulta vacía del tipo solicitado para obtener el mejor rendimiento - o simplemente elimine esta línea.
Shimmy Weitzhandler
1
return WhereIn (consulta, selector, colección); debe ser reemplazado por return WhereIn (consulta, selector, colección (IEnumerable <TValue>)); para evitar la recursividad no deseada.
Antoine Aubry
1
Creo que hay un error en el código. Si la lista de valores proporcionada está vacía, el comportamiento correcto debería ser no devolver resultados, es decir, / no existen objetos en la consulta en la colección. Sin embargo, el código hace exactamente lo contrario: se devuelven todos los valores, no ninguno. Creo que quieres "if (! Collection.Any ()) return query.Where (e => false)"
ShadowChaser
18

Puede recurrir a la codificación manual de algunos e-sql (tenga en cuenta la palabra clave "it"):

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

Aquí está el código que usé para generar algunos e-sql de una colección, YMMV:

string[] ids = orders.Select(x=>x.ProductID.ToString()).ToArray();
return CurrentDataSource.Products.Where("it.ID IN {" + string.Join(",", ids) + "}");
Rob Fonseca-Ensor
fuente
1
¿Tienes más información sobre "eso"? El prefijo "it" aparece en los ejemplos de MSDN, pero en ninguna parte puedo encontrar una explicación sobre cuándo / por qué se necesita "it".
Robert Claypool
1
Utilizado en la consulta dinámica de Entity Framework, eche un vistazo a geekswithblogs.net/thanigai/archive/2009/04/29/… , Thanigainathan Siranjeevi lo explica allí.
Shimmy Weitzhandler
13

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

y la consulta se convierte en:

var query2 = context.Entities.Where(BuildContainsExpression<Entity, int>(e => e.ID, ids));
James Bloomer
fuente
3
Si desea hacer un 'No contiene', simplemente realice las siguientes ediciones en el método BuildContainsExpression: - Expression.Equal se convierte en Expression.NotEqual - Expression.O se convierte en Expression.Y
Merritt
2

No estoy seguro acerca de Silverligth, pero en relación con los objetos, siempre uso any () para estas consultas.

var q = from t in svc.OpenTranaction
        where txnIds.Any(t.OpenTransactionId)
        select t;
AndreasN
fuente
5
Any no toma un objeto del tipo secuencia - no tiene parámetros (en cuyo caso es simplemente "está vacío o no") o toma un predicado.
Jon Skeet
Estoy muy contento de haber encontrado esta respuesta :) +1 Gracias AndreasN
SDReyes
1

Para completar el registro, aquí está el código que finalmente usé (se omitió la verificación de errores para mayor claridad) ...

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

Muchas gracias. WhereIn el método de extensión fue suficiente para mí. Lo perfilé y generé el mismo comando SQL en la base de datos que e-sql.

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

Generado esto:

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

Lo siento nuevo usuario, habría comentado la respuesta real, pero parece que todavía no puedo hacerlo.

De todos modos, con respecto a la respuesta con código de muestra para BuildContainsExpression (), tenga en cuenta que si usa ese método en Entidades de base de datos (es decir, no objetos en memoria) y está usando IQueryable, en realidad tiene que ir a la base de datos ya que básicamente hace muchas condiciones SQL "o" para verificar la cláusula "dónde en" (ejecútelo con SQL Profiler para ver).

Esto puede significar que, si está refinando un IQueryable con múltiples BuildContainsExpression (), no lo convertirá en una declaración SQL que se ejecuta al final como espera.

La solución para nosotros fue usar múltiples combinaciones LINQ para mantenerlo en una llamada SQL.

Shannon
fuente
0

Además de la respuesta seleccionada.

Reemplace Expression.Orcon Expression.OrElsepara usar con Nhibernate y corrija la Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseOr' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'excepción.

smg
fuente