Is there any way to create a LINQ query as a varia

2020-07-16 09:35发布

问题:

Preamble:
My core question is very similar to this one: How can I write a clean Repository without exposing IQueryable to the rest of my application? which has remained unanswered. I am hoping that if I approach the problem in a different way, and ask a slightly different question, I may get a result. I will repeat some of the content from this other question to avoid requiring readers to read it for context.

Problem:
I'm working with POCO entities, and Entity Framework 4. I am trying to allow for sophisticated ad-hoc filtering of entity sets at the application layer, while simultaneously trying to avoid exposing IQueryable<T> past my repository boundary. This is leaving me with some complications.

  • 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.

Ideas:
Ultimately, I think an ideal solution would be discovering some way to create a full query that doesn't know the source, and store it as a parameter. I could then pass that into the repository, where the source is known, and apply the query to the source and return the results.

To clarify; in contrast to simply creating an object of parameters as mentioned above, I'd like to use the raw LINQ queries, but store them in a variable somehow, and apply them to a data source later. I suspect the return type would have to be known ahead of time, but I'm perfectly fine with defining that and having it known in advance.

To view it from yet another perspective, consider the following:

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

I want to package up the three Where clauses as a query object, completely separate from the customerRepository.GetAll(), pass it around as a parameter and apply it later.

回答1:

Sure. You can write a method like:

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

...and repository methods like:

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

...and call:

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


回答2:

If you just want to capture these kinds of queries in a re-usable way, you might implement this as extension methods on IQueryable<Customer> (or whatever POCO you want). Something like:

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

You could then consume this like:

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