【问题标题】:How can you update a Linq Expression with additional parameters?如何使用附加参数更新 Linq 表达式?
【发布时间】:2012-03-14 00:35:54
【问题描述】:

我有一个 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 方法使用的是 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();
    }
}

更新
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);的返回类型和interlan是什么?
  • 什么是showAchieved?是否枚举projects变量?
  • showArchived 只是一个布尔值
  • 现在您已经添加了 Get,ToList() 很麻烦;我添加了一个表达式重写示例,您应该能够使用它来组合两个过滤器调用 Get

标签: c# linq


【解决方案1】:

如果我理解了这个问题,那么很可能是问题所在:

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

projects 上的任何工作都将使用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);
    }
}

【讨论】:

  • 这里的数据类型无所谓。
  • @Oybek Enumerable.WhereQueryable.Where 之间的区别非常重要 - 你能澄清你所说的无关紧要吗?
  • 有效,只是 ProjectRepository.Get(filter) 获取数据库的所有记录,然后 Where 再次命中数据库。我只想做一次数据库查询。第二段代码是我现在怎么做的
  • @SamWM IEnumberable&lt;T&gt;IQueryable&lt;T&gt; 之间有一个重要的区别。但是,如果返回查询,对Get 的调用可能根本不应该命中数据库;你能说明Get是怎么写的吗?
  • @Oybek 不,不是真的; 变量(不是对象)的类型在这里很重要;如果它只知道是IEnumerable&lt;T&gt;(即使它实际上是实现IQueryable&lt;T&gt;的东西),那么它将使用Enumerable.Where,即LINQ-to-Objects。它不会“组合”查询。对不起,你说的不对。
【解决方案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;
    }

【讨论】:

  • 这种方法并没有错误,但是很多LINQ引擎的支持很差; LINQ-to-SQL 可以使用 Expression.Invoke,但 EF 不喜欢它。因此,使用“访问者”方法直接组合谓词更可靠(且无需额外工作)。
  • 虽然看起来很有希望,但我希望尽可能通用,因为涉及到许多不同的类。使用实体框架,但希望将来证明是否会使用其他东西(如 NHibernate)。就像建立一个过滤器列表,然后在执行之前进行组合
  • @SamWM 理想情况下,您应该能够将我的简单方法签名与 Marc 的表达式访问者相结合,以获得可用的 EF 解决方案。
【解决方案3】:

这一切都取决于ProjectRepository.Get() 的行为方式以及它返回的内容。通常的方法(例如,LINQ to SQL 执行类似的操作)是它返回一个 IQueryable&lt;T&gt; 并允许您(除其他外)添加更多 Where() 子句,然后以一个 SQL 查询的形式将其发送到服务器,包括所有Where() 子句。如果是这种情况,Mark 的解决方案(使用 IQuerybale&lt;T&gt;)将为您工作。

但如果Get() 方法立即执行基于filter 的查询,则需要将表达式中的整个过滤器传递给它。为此,您可以使用PredicateBuilder

【讨论】:

    【解决方案4】:

    如果你 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,即发出数据库请求。

    【讨论】:

    • 这增加了额外的冗余代码。初始过滤器可能不仅仅是检查用户名。
    【解决方案5】:

    摆脱ToList(),你会没事的。

    【讨论】:

      猜你喜欢
      • 2021-04-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多