2012-03-13 13 views
5

Mam wyrażenie Linq, które może być zmieniane w zależności od pewnych warunków. Przykładem tego, co chciałbym zrobić (po lewej pusty bitu nie jestem pewien):Jak zaktualizować wyrażenie Linq o dodatkowe parametry?

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

Jak zaktualizować filtr dodawać żadnych dodatkowych parametrów?

W tej chwili wszystkie rekordy są pobierane, a następnie używam Where do dalszego filtrowania wyników. Jednak powoduje to więcej zapytań do bazy danych, niż jest to absolutnie konieczne.

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

metody GET używa GenericRepository wzoru:

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

Aktualizacja
Utworzono kilka metod rozszerzeń opartych na kodzie poniżej Marc Gravell i David B, rozwiązuje problem dla mnie

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

Jaki jest typ zwracany i interlany z 'ProjectRepository.Get (filter);'? – Oybek

+0

Co to jest "showAchieved"? Czy wylicza zmienną 'projects'? – Oybek

+0

showArchived jest po prostu boolean – SamWM

Odpowiedz

11

Jeśli rozumiem pytanie, to najprawdopodobniej tutaj jest problem:

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

Wszelkie prace na projects będzie za pomocą Enumerable, nie Queryable; Należy to prawdopodobnie:

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

Ten ostatni jest composable i .Where powinien działać zgodnie z oczekiwaniami, budując bardziej restrykcyjne zapytanie przed wysłaniem go do serwera.

Twój Inną opcją jest przepisać filtr połączyć przed wysłaniem:

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

lub ponownie napisany w taki sposób, aby umożliwić wygodne użytkowanie:

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

Typ danych tutaj nie ma znaczenia. – Oybek

+1

@Obek różnica między "Enumerable.Where" i "Queryable.Where" ma znaczenie ** bardzo dużo ** - czy możesz wyjaśnić, co mówisz, nie ma znaczenia? –

+1

To działa, po prostu ProjectRepository.Get (filter) pobiera wszystkie rekordy dla bazy danych, a następnie gdzie trafia ponownie w bazę danych. Chcę wykonać kwerendę bazy danych tylko raz. Drugi kawałek kodu jest jak robię to teraz – SamWM

0

Jeśli Get metoda retrives dane i wraca do obiektów pamięci, które można było zrobić, aby można było wykonać

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

EDYTOWANIE

Tylko zwrócić uwagę. Podczas korzystania z metody .ToList() wylicza ona Queryable, tj. Tworzy żądanie bazy danych.

+0

który dodaje dodatkowy kod nadmiarowy. Początkowy filtr może być czymś więcej niż tylko sprawdzaniem nazwy użytkownika. – SamWM

0

Wszystko zależy od tego, jak zachowuje się ProjectRepository.Get() i co powraca. Zwykle (na przykład LINQ to SQL robi coś takiego) jest to, że zwraca on IQueryable<T> i pozwala (między innymi) dodać więcej klauzul Where() przed wysłaniem go do serwera w postaci jednego zapytania SQL, z wszystkimi Where() zawarte klauzule. W takim przypadku rozwiązanie Mark (użyj IQuerybale<T>) będzie działać.

Ale jeśli metoda Get() natychmiast wykonuje zapytanie oparte na, należy przekazać cały filtr w wyrażeniu. Aby to zrobić, możesz użyć PredicateBuilder.

1

myślę chcesz połączyć filtry w ten sposób:

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

Ten kod pozwoli Ci łączyć filtry.

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

To podejście nie jest * błędne *, ale jest słabo obsługiwane przez wiele silników LINQ; LINQ-to-SQL jest w porządku z Expression.Invoke, jednak EF go nie lubi. Jako takie, bardziej wiarygodne (i bez dodatkowej pracy) jest stosowanie podejścia "odwiedzającego" do łączenia predykatów ** bezpośrednio **. –

+0

<3 LinqToSql. Któregoś dnia EF nadgoni ... –

+0

Wygląda obiecująco, ale chcę być tak ogólny jak tylko potrafię, ponieważ zaangażowanych jest wiele różnych klas. Korzystanie z Entity Framework, ale mając nadzieję na przyszły dowód, jeśli kiedykolwiek zostanie użyty inny element (np. NHibernate). Podobnie jak przy tworzeniu listy filtrów, to po prostu łączenie przed wykonaniem – SamWM

Powiązane problemy