Is there any way to create a LINQ query as a variable without having the data source (yet)?
Asked Answered
C

3

7

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.

Clutter answered 30/12, 2011 at 19:46 Comment(4)
I think you may be jumping the gun a little bit and overcomplicating things. Are you doing a refactoring exercise or are you starting from scratch?Carlist
Are you opposed to working directly with Linq Expression Trees? bartdesmet.net/blogs/bart/archive/2008/08/26/…Canada
So, what is the problem in "using the raw LINQ queries, but store them in a variable somehow, and apply them to a data source later"? Use LINQ Expressions: msdn.microsoft.com/en-us/library/system.linq.expressions.aspxKafir
I would suggest to expose IQueryable. If not, people will try to use linq anyway and they will end using Linq-to-objects and the performance will be really bad.Banquette
N
12

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();
Nathalia answered 30/12, 2011 at 19:56 Comment(4)
This looks promising. Working from your example, I've built out a simple version of this, and it get's the basic job done... however: Is there any way that I can combine expressions? I'd rather have one expression for Name comparison, one for IsActive, one for HasAddress, and then combine them arbitrarily... otherwise I'm still stuck in a situation where I have to construct an expression for each possible combination of filtering I want to do... but at least now I can build out those expressions at the application level and still pass back IEnumerable or ListClutter
Combining expressions is more difficult than you might expect, but the LINQKit library from the LINQPad guy takes much of the pain away.Nathalia
It looks like I might be able to get away with building out an interface that stores lists of expressions (as opposed to 'joining' them) in an object, and then inside the repository applies each sequentially.Clutter
@Clutter That works fine so long as you want to "and" the expressions. If you want to "or" them (or do something more complicated), you probably want LINQKit.Nathalia
D
1

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");
Deluxe answered 30/12, 2011 at 20:2 Comment(1)
From his question: "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.Nathalia
I
0

You can build an expression and use it later

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);
Incommunicative answered 10/3, 2022 at 15:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.