如何在Linq表达式中添加参数进行更新?

10

我有一个Linq表达式,根据某些条件可能需要进行修改。以下是我想要做的示例(未填写我不确定的部分):

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

如何更新筛选器以添加任何额外的参数?

目前,所有记录都被检索出来,然后使用 Where 进一步过滤结果。但是,这会导致比严格必要的更多的查询到数据库。

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

Get方法使用通用仓储模式:

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

更新
根据Marc Gravell和David B的以下代码创建了一些扩展方法,为我解决了这个问题。

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

ProjectRepository.Get(filter); 的返回类型和中间语言(interlans)是什么? - Oybek
showAchieved是什么?它是否枚举了projects变量? - Oybek
showArchived 只是一个布尔值。 - SamWM
现在你已经添加了Get,ToList()就很麻烦了;我添加了一个表达式重写示例,你应该能够使用它来在调用Get之前组合这两个过滤器。 - Marc Gravell
5个回答

16

如果我理解问题正确的话,那么最有可能的问题是:

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

任何涉及项目的工作都将使用Enumerable,而不是Queryable;因此应该这样处理:

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

后者是可组合的,.Where 应该按照您的期望工作,构建出一个更为严格的查询内容,在将其发送到服务器之前。

您的另一个选项是在发送之前重写筛选器以进行组合:

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

或者改写为一种方便使用的方式:

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

2
@Oybek,“Enumerable.Where”和“Queryable.Where”的区别非常重要,您能澄清一下您所说的不重要吗? - Marc Gravell
1
它是可行的,只是ProjectRepository.Get(filter)会从数据库中获取所有记录,然后Where语句再次访问数据库。我想只执行一次数据库查询。第二段代码是我现在的做法。 - SamWM
2
@SamWM, IEnumberable<T>IQueryable<T> 之间有一个重要的区别。但是,如果返回查询,则对Get方法的调用可能根本不应该访问数据库;你可以展示一下Get方法是如何编写的吗? - Marc Gravell
2
@Oybek 不,不是真的;这里关键是变量的类型(而不是对象);如果它只被认为是 IEnumerable<T>(即使它实际上是实现了 IQueryable<T> 的东西),那么它将使用 Enumerable.Where,这是LINQ-to-Objects。它不会“组合”查询。抱歉,但你说的是错误的。 - Marc Gravell
@SamWM 我添加了第二种方法,即表达式树重写;您可以在调用 Get 之前使用它来组合两个过滤器。有什么用处吗? - Marc Gravell
显示剩余4条评论

2
我认为您希望以这种方式组合筛选器:
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);

这段代码将允许您组合多个过滤器。

    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
这种方法并不“错误”,但是很多LINQ引擎很少支持;虽然LINQ-to-SQL可以使用Expression.Invoke,但EF不喜欢它。因此,更可靠(而且没有额外的工作)的方法是直接使用“访问者”方法来合并谓词。 - Marc Gravell
看起来很有前途,希望尽可能通用,因为涉及到许多不同的类。使用Entity Framework,但希望未来可以使用其他东西(如NHibernate)来保证兼容性。喜欢构建过滤器列表的想法,然后在执行之前将它们组合起来。 - SamWM
@SamWM 理想情况下,您应该能够将我的简单方法签名与Marc的表达式访问器相结合,以获得可用的EF解决方案。 - Amy B

0
如果您的Get方法检索数据并将其返回到内存对象中,则可以这样做。
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);

编辑

需要指出的是,当您使用.ToList()方法时,它会枚举Queryable,即发出数据库请求。


这会添加额外的冗余代码。初始过滤器可能不仅仅是检查用户名。 - SamWM

0

这完全取决于 ProjectRepository.Get() 的行为和返回值。通常的方式(例如,LINQ to SQL 就是这样做的)是返回一个 IQueryable<T>,让你(除了其他操作)在将其作为一个 SQL 查询发送到服务器之前添加更多的 Where() 子句,将所有的 Where() 子句都包括进去。如果是这种情况,那么 Mark 的解决方案(使用 IQuerybale<T>)就适合你。

但如果 Get() 方法立即基于 filter 执行查询,那么你需要在表达式中传递整个过滤器。为此,可以使用 PredicateBuilder


0

摆脱ToList(),你就没问题了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接