2012-03-13 35 views
5

Tengo una expresión de Linq, que puede verse alterada dependiendo de ciertas condiciones. Un ejemplo de lo que me gustaría hacer (izquierda en blanco el bit No estoy seguro acerca):¿Cómo se puede actualizar una expresión Linq con parámetros adicionales?

Expression<Func<Project, bool>> filter = (Project p) => p.UserName == "Bob"; 
if(showArchived) 
{ 
    // update filter to add && p.Archived 
} 
// query the database when the filter is built 
IEnumerable<Project> projects = unitOfWork.ProjectRepository.Get(filter); 

¿Cómo se actualiza el filtro de añadir ningún parámetro extra?

En este momento se recuperan todos los registros, entonces utilizo un Where para filtrar los resultados. Sin embargo, eso resulta en más consultas a la base de datos de lo estrictamente necesario.

IEnumerable<Project> projects = unitOfWork.ProjectRepository.Get(filter); 
if(showArchived) 
{ 
    projects = projects.Where(p => p.Archived); 
} 

método GET está utilizando el patrón GenericRepository:

public class GenericRepository<TEntity> where TEntity : class 
{ 
    internal ProgrammeDBContext context; 
    internal DbSet<TEntity> dbSet; 

    public GenericRepository(ProgrammeDBContext context) 
    { 
     this.context = context; 
     this.dbSet = context.Set<TEntity>(); 
    } 

    public virtual IEnumerable<TEntity> Get(
     Expression<Func<TEntity, bool>> filter = null, 
     Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, 
     string includeProperties = "") 
    { 
     IQueryable<TEntity> query = dbSet; 

     if (filter != null) 
     { 
      query = query.Where(filter); 
     } 

     foreach (var includeProperty in includeProperties.Split 
      (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) 
     { 
      query = query.Include(includeProperty); 
     } 

     if (orderBy != null) 
     { 
      return orderBy(query).ToList(); 
     } 
     else 
     { 
      return query.ToList(); 
     } 
    } 

    public virtual TEntity GetByID(object id) 
    { 
     return dbSet.Find(id); 
    } 

    public virtual void Insert(TEntity entity) 
    { 
     dbSet.Add(entity); 
    } 

    public virtual void Delete(object id) 
    { 
     TEntity entityToDelete = dbSet.Find(id); 
     Delete(entityToDelete); 
    } 

    public virtual void Delete(TEntity entityToDelete) 
    { 
     if (context.Entry(entityToDelete).State == EntityState.Detached) 
     { 
      dbSet.Attach(entityToDelete); 
     } 
     dbSet.Remove(entityToDelete); 
    } 

    public virtual void Update(TEntity entityToUpdate) 
    { 
     dbSet.Attach(entityToUpdate); 
     context.Entry(entityToUpdate).State = EntityState.Modified; 
    } 

    public virtual IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters) 
    { 
     return dbSet.SqlQuery(query, parameters).ToList(); 
    } 
} 

actualización
creado algunos métodos de extensión basado en el código de abajo por Marc Gravell y David B, resuelve el problema para mí

public static class LinqExtensionMethods 
{ 
    public static Expression<Func<T, bool>> CombineOr<T>(params Expression<Func<T, bool>>[] filters) 
    { 
     return filters.CombineOr(); 
    } 

    public static Expression<Func<T, bool>> CombineOr<T>(this IEnumerable<Expression<Func<T, bool>>> filters) 
    { 
     if (!filters.Any()) 
     { 
      Expression<Func<T, bool>> alwaysTrue = x => true; 
      return alwaysTrue; 
     } 
     Expression<Func<T, bool>> firstFilter = filters.First(); 

     var lastFilter = firstFilter; 
     Expression<Func<T, bool>> result = null; 
     foreach (var nextFilter in filters.Skip(1)) 
     { 
      var nextExpression = new ReplaceVisitor(lastFilter.Parameters[0], nextFilter.Parameters[0]).Visit(lastFilter.Body); 
      result = Expression.Lambda<Func<T, bool>>(Expression.OrElse(nextExpression, nextFilter.Body), nextFilter.Parameters); 
      lastFilter = nextFilter; 
     } 
     return result; 
    } 

    public static Expression<Func<T, bool>> CombineAnd<T>(params Expression<Func<T, bool>>[] filters) 
    { 
     return filters.CombineAnd(); 
    } 

    public static Expression<Func<T, bool>> CombineAnd<T>(this IEnumerable<Expression<Func<T, bool>>> filters) 
    { 
     if (!filters.Any()) 
     { 
      Expression<Func<T, bool>> alwaysTrue = x => true; 
      return alwaysTrue; 
     } 
     Expression<Func<T, bool>> firstFilter = filters.First(); 

     var lastFilter = firstFilter; 
     Expression<Func<T, bool>> result = null; 
     foreach (var nextFilter in filters.Skip(1)) 
     { 
      var nextExpression = new ReplaceVisitor(lastFilter.Parameters[0], nextFilter.Parameters[0]).Visit(lastFilter.Body); 
      result = Expression.Lambda<Func<T, bool>>(Expression.AndAlso(nextExpression, nextFilter.Body), nextFilter.Parameters); 
      lastFilter = nextFilter; 
     } 
     return result; 
    } 

    class ReplaceVisitor : ExpressionVisitor 
    { 
     private readonly Expression from, to; 
     public ReplaceVisitor(Expression from, Expression to) 
     { 
      this.from = from; 
      this.to = to; 
     } 
     public override Expression Visit(Expression node) 
     { 
      return node == from ? to : base.Visit(node); 
     } 
    } 
} 
+0

¿Cuál es el tipo de retorno y las intercalaciones de 'ProjectRepository.Get (filter);'? – Oybek

+0

¿Qué es 'showAchieved'? ¿Enumera la variable 'projects'? – Oybek

+0

showArchived es solo un booleano – SamWM

Respuesta

11

Si entiendo la pregunta, lo más probable es que aquí esté el problema:

IEnumerable<Project> projects = unitOfWork.ProjectRepository.Get(filter); 

Cualquier trabajo en projects va a utilizar Enumerable, no Queryable; probablemente debería ser:

IQueryable<Project> projects = unitOfWork.ProjectRepository.Get(filter); 
if(showArchived) 
{ 
    projects = projects.Where(p => p.Archived); 
} 

Este último es componibles y .Where debe funcionar como se esperaba, la construcción de una consulta más restrictiva antes de enviarlo al servidor.

Su otra opción es volver a escribir el filtro para combinar antes de enviar:

using System; 
using System.Linq.Expressions; 

static class Program 
{ 
    static void Main() 
    { 
     Expression<Func<Foo, bool>> filter1 = x => x.A > 1; 
     Expression<Func<Foo, bool>> filter2 = x => x.B > 2.5; 

     // combine two predicates: 
     // need to rewrite one of the lambdas, swapping in the parameter from the other 
     var rewrittenBody1 = new ReplaceVisitor(
      filter1.Parameters[0], filter2.Parameters[0]).Visit(filter1.Body); 
     var newFilter = Expression.Lambda<Func<Foo, bool>>(
      Expression.AndAlso(rewrittenBody1, filter2.Body), filter2.Parameters); 
     // newFilter is equivalent to: x => x.A > 1 && x.B > 2.5 
    } 
} 
class Foo 
{ 
    public int A { get; set; } 
    public float B { get; set; } 
} 
class ReplaceVisitor : ExpressionVisitor 
{ 
    private readonly Expression from, to; 
    public ReplaceVisitor(Expression from, Expression to) 
    { 
     this.from = from; 
     this.to = to; 
    } 
    public override Expression Visit(Expression node) 
    { 
     return node == from ? to : base.Visit(node); 
    } 
} 

o re-escrito en una forma de permitir el uso conveniente:

using System; 
using System.Linq.Expressions; 

static class Program 
{ 
    static void Main() 
    { 
     Expression<Func<Foo, bool>> filter = x => x.A > 1; 

     bool applySecondFilter = true; 
     if(applySecondFilter) 
     { 
      filter = Combine(filter, x => x.B > 2.5); 
     } 
     var data = repo.Get(filter); 
    } 
    static Expression<Func<T,bool>> Combine<T>(Expression<Func<T,bool>> filter1, Expression<Func<T,bool>> filter2) 
    { 
     // combine two predicates: 
     // need to rewrite one of the lambdas, swapping in the parameter from the other 
     var rewrittenBody1 = new ReplaceVisitor(
      filter1.Parameters[0], filter2.Parameters[0]).Visit(filter1.Body); 
     var newFilter = Expression.Lambda<Func<T, bool>>(
      Expression.AndAlso(rewrittenBody1, filter2.Body), filter2.Parameters); 
     return newFilter; 
    } 
} 
class Foo 
{ 
    public int A { get; set; } 
    public float B { get; set; } 
} 
class ReplaceVisitor : ExpressionVisitor 
{ 
    private readonly Expression from, to; 
    public ReplaceVisitor(Expression from, Expression to) 
    { 
     this.from = from; 
     this.to = to; 
    } 
    public override Expression Visit(Expression node) 
    { 
     return node == from ? to : base.Visit(node); 
    } 
} 
+0

El tipo de datos aquí no importa. – Oybek

+1

@Oybek la diferencia entre 'Enumerable.Where' y' Queryable.Where' importa ** mucho ** - ¿Puedes aclarar que lo que dices no importa? –

+1

Funciona, es solo que ProjectRepository.Get (filter) obtiene todos los registros de la base de datos, y luego Where vuelve a la base de datos. Quiero hacer la consulta de la base de datos solo una vez. El segundo bit del código es cómo lo hago ahora – SamWM

0

Si Get método retrives los datos y devuelve en objetos de memoria lo que podría hacer

Expression<Func<Project, bool>> filter = (Project p) => p.UserName == "Bob"; 
if(showArchived) { 
    filter = (Project p) => p.UserName == "Bob" && p.Archived; 
} 
IEnumerable<Project> projects = unitOfWork.ProjectRepository.Get(filter); 

EDITAR

Solo para señalar. Cuando utiliza el método .ToList(), enumera Queryable, es decir, realiza una solicitud de base de datos.

+0

Eso agrega código extra redundante. El filtro inicial puede ser más que simplemente verificar el nombre de usuario. – SamWM

0

Todo esto depende de cómo se comporta ProjectRepository.Get() y lo que devuelve. La forma habitual (por ejemplo, LINQ to SQL hace algo como esto) es que devuelve un IQueryable<T> y le permite (entre otras cosas) agregar más cláusulas Where() antes de enviarlo al servidor en forma de una consulta SQL, con todas las Where() cláusulas incluidas. Si este es el caso, la solución de Mark (use IQuerybale<T>) funcionará para usted.

Pero si el método Get() ejecuta la consulta basada en el filter inmediatamente, debe pasarle el filtro completo en la expresión. Para hacer eso, puede usar PredicateBuilder.

0

Deshágase de ToList() y estará bien.

1

creo que desea combinar filtros de esta manera:

var myFilters = new List<Expression<Func<Customer, bool>>>(); 
myFilters.Add(c => c.Name.StartsWith("B")); 
myFilters.Add(c => c.Orders.Count() == 3); 
if (stranded) 
{ 
    myFilters.Add(c => c.Friends.Any(f => f.Cars.Any())); //friend has car 
} 
Expression<Func<Customer, bool>> filter = myFilters.AndTheseFiltersTogether(); 
IEnumerable<Customer> thoseCustomers = Data.Get(filter); 

Este código le permitirá combinar sus filtros.

public static Expression<Func<T, bool>> OrTheseFiltersTogether<T>(params Expression<Func<T, bool>>[] filters) 
    { 
     return filters.OrTheseFiltersTogether(); 
    } 

    public static Expression<Func<T, bool>> OrTheseFiltersTogether<T>(this IEnumerable<Expression<Func<T, bool>>> filters) 
    { 
     if (!filters.Any()) 
     { 
      Expression<Func<T, bool>> alwaysTrue = x => true; 
      return alwaysTrue; 
     } 

     Expression<Func<T, bool>> firstFilter = filters.First(); 

     var body = firstFilter.Body; 
     var param = firstFilter.Parameters.ToArray(); 
     foreach (var nextFilter in filters.Skip(1)) 
     { 
      var nextBody = Expression.Invoke(nextFilter, param); 
      body = Expression.OrElse(body, nextBody); 
     } 
     Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(body, param); 
     return result; 
    } 


    public static Expression<Func<T, bool>> AndTheseFiltersTogether<T>(params Expression<Func<T, bool>>[] filters) 
    { 
     return filters.AndTheseFiltersTogether(); 
    } 

    public static Expression<Func<T, bool>> AndTheseFiltersTogether<T>(this IEnumerable<Expression<Func<T, bool>>> filters) 
    { 
     if (!filters.Any()) 
     { 
      Expression<Func<T, bool>> alwaysTrue = x => true; 
      return alwaysTrue; 
     } 
     Expression<Func<T, bool>> firstFilter = filters.First(); 

     var body = firstFilter.Body; 
     var param = firstFilter.Parameters.ToArray(); 
     foreach (var nextFilter in filters.Skip(1)) 
     { 
      var nextBody = Expression.Invoke(nextFilter, param); 
      body = Expression.AndAlso(body, nextBody); 
     } 
     Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(body, param); 
     return result; 
    } 
+1

Este enfoque no es * incorrecto *, pero es mal soportado por muchos motores LINQ; LINQ-to-SQL está bien con Expression.Invoke, sin embargo EF no le gusta. Como tal, es más confiable (y no hay trabajo adicional) utilizar el enfoque de "visitante" para combinar los predicados ** directamente **. –

+0

<3 LinqToSql. Algún día EF se pondrá al día ... –

+0

Aunque parecía prometedor, quiero ser tan genérico como pueda ya que hay muchas clases diferentes involucradas. Utilizando Entity Framework, pero con la esperanza de una prueba futura si algo más alguna vez sería utilizado (como NHibernate). Al igual que la idea de crear una lista de filtros, simplemente combinar antes de ejecutar – SamWM

Cuestiones relacionadas