有没有办法在没有数据源的情况下创建一个 LINQ 查询变量?

7

前言:
我的核心问题与这个问题非常相似:如何编写一个干净的仓库而不将IQueryable暴露给我的应用程序的其余部分?,该问题一直未得到回答。我希望通过以不同的方式解决该问题,并提出略有不同的问题,可以得到结果。为了避免读者阅读上下文,我将重复一些来自其他问题的内容。

问题:
我正在使用POCO实体和Entity Framework 4。 我正在尝试在应用程序层允许对实体集进行复杂的即席过滤,同时试图避免将IQueryable<T>暴露给我的仓库边界以外的部分。 这让我面临了一些麻烦。

  • I do not want to create a single massive filter method on the repository that takes a huge list of parameters, such as:

    IEnumerable GetFilteredCustomers(string nameFilter, string addressFilter, bool isActive, int customerId, ...)
    

    Not only is this extremely cumbersome to use, but it's super ugly to look at, especially if it's mostly a bunch of nulls, etc. It's also not as maintainable as I would like.

  • I do not want to create a huge set of filter methods on the repository, such as:

    IEnumerable GetActiveCustomers()
    IEnumerable GetCustomersByName()
    

    There are a number of problems with this approach, including needing a huge list of methods which grows to n! where n is the number of available filter conditions if I want to be able to combine them in arbitrary ways. (i.e. all active customers with name George). Also highly difficult to maintain.

  • I do not want to create chainable methods (Fluent Interface) that manipulate IEnumerable<T>, because ultimately that involves bringing back a huge result set from the database and filtering it down in-memory which is not a scalable solution.

  • I can't create a Fluent Interface that manipulates IQueryable<T> because as I've already said, I don't want to expose the IQueryable<T> past the repositories.

  • I'd like to avoid simply rehashing the single massive filter method by passing in an object full of parameters instead of a large parameter list, although at this point this might be the least ugly solution.

思路:
我认为最理想的解决方案是发现一种方法来创建一个不知道源的完整查询,并将其存储为参数。然后,我可以将其传递到仓库中,在那里已知源,并将查询应用于源并返回结果。

澄清一下;与上面提到的简单创建参数对象相比,我想使用原始的LINQ查询,但以某种方式将它们存储在变量中,并稍后将它们应用于数据源。我怀疑返回类型必须事先知道,但我非常愿意定义它并事先知道。

从另一个角度来看,考虑以下内容:

IQueryable<Customer> filteredCustomers = customerRepository.GetAll()
    .Where(c => c.FirstName == "Dave")
    .Where(c => c.IsActive == true)
    .Where(c => c.HasAddress == true)
    ;

我希望将三个Where条件封装成一个查询对象,与customerRepository.GetAll()完全分离,将其作为参数传递并稍后应用。

3
我觉得你可能有点过早地着手处理事情,把事情弄得过于复杂化了。你是在进行重构练习,还是从头开始做呢? - O.O
3
你是否反对直接使用 Linq 表达式树进行工作?http://bartdesmet.net/blogs/bart/archive/2008/08/26/to-bind-or-not-to-bind-dynamic-expression-trees-part-1.aspx - M.Babcock
3
“使用原始的LINQ查询,但以某种方式将它们存储在变量中,并稍后将其应用于数据源”的问题是什么? 使用LINQ表达式:http://msdn.microsoft.com/en-us/library/system.linq.expressions.aspx - Sergey Sirotkin
我建议暴露IQueryable。如果不这样做,人们仍会尝试使用linq,但最终会使用Linq-to-objects,性能会非常差。 - Ivo
3个回答

12

当然可以。你可以编写一个像这样的方法:

public Expression<Func<Customer, bool>> GetDave()
{
    return c => c.FirstName == "Dave"
             && c.IsActive
             && c.HasAddress;
}

...以及类似以下这样的存储库方法:

public IEnumerable<Customer> GetOneGuy(Expression<Func<Customer, bool>> criteria)
{
    return Context.Customers.Where(criteria);
}

...然后调用:

var dave = Repository.GetOneGuy(this.GetDave()).Single();

这看起来很有前途。根据您的示例,我已经构建了一个简单版本,并完成了基本工作...但是:有没有办法可以组合表达式?我宁愿为名称比较、IsActive和HasAddress分别设置一个表达式,然后任意组合它们...否则,我仍然陷入一种情况中,在那里我必须为我想要执行的每个可能的过滤组合构建一个表达式...但至少现在我可以在应用程序级别构建这些表达式并仍然返回IEnumerable或List。 - Mir
1
组合表达式比你想象的要困难得多,但是来自LINQPad开发人员的LINQKit库可以大大减轻这种痛苦。 - Craig Stuntz
看起来我可以通过构建一个存储表达式列表(而不是将它们“连接”)的接口对象,并在存储库内逐个应用每个表达式的方式来解决问题。 - Mir
1
@Mir 如果你想要“and”表达式,那么它可以正常工作。如果你想要“or”它们(或者做一些更复杂的事情),你可能需要使用LINQKit。 - Craig Stuntz

1

如果您只想以可重用的方式捕获这些类型的查询,可以将其实现为 IQueryable<Customer>(或任何其他 POCO)上的扩展方法。类似于:

public static class ExtnensionsForIQueryableCustomer
{
    public static IEnumerable<Customer> WhereActiveWithAddressAndNamed (this IQueryable<Customer> queryable, string name)
    {
        return queryable.Where (c => c.FirstName == name)
                        .Where (c => c.IsActive)
                        .Where (c => c.HasAddress);
    }
}

然后你可以像这样使用:

customerRepository.GetAll ().WhereActiveWithAddressAndNamed ("Dave");

2
从他的问题中可以看出:“我无法创建一个能够操作 'IQueryable<T>' 的流畅接口,因为正如我已经说过的那样,我不想在仓储之外暴露 'IQueryable<T>'。” - Craig Stuntz

0

您可以构建一个表达式并稍后使用它

var isg = Guid.TryParse(id, out gLT);
Expression<Func<YourObjectResultType, bool>> query = w => w.Id == id;

if (isg) {
    query = w => w.Token == gLT;
}

var result = _context.TblYourTable.Where(query);

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