Rasmus-E
Rasmus-E

Reputation: 842

Conditional filtering an IQueryable<T> by expression and second object

What I am trying to achieve is to create a custom extension method on an IQueryable<T> where my expression selects a object of type AgentEntity, and the second parameter is of the same type, but used to do an conditional filtering.

Here is my code, which is not working but suggests what I would like to do.

public static IQueryable<T> Where<T>(this IQueryable<T> profiles, Func<AgentEntity, AgentEntity> agentSelector, AgentEntity agent)
{
    if (string.IsNullOrEmpty(agent.Mbox))
    {
        return profiles.Where(agentSelector.Mbox == agent.Mbox);
    }

    if (string.IsNullOrEmpty(agent.Mbox_SHA1SUM))
    {
        return profiles.Where(agentSelector.Mbox_SHA1SUM == agent.Mbox_SHA1SUM);
    }

    if (string.IsNullOrEmpty(agent.OpenId))
    {
        return profiles.Where(agentSelector.OpenId == agent.OpenId);
    }

    if (string.IsNullOrEmpty(agent.Account.HomePage))
    {
        return profiles.Where(agentSelector.Account.HomePage == agent.Account.HomePage && agentSelector.Account.Name == agent.Account.Name);
    }

    return profiles;
}

Usage

AgentEntity agent = new AgentEntity(){
  Mbox = "mailto:[email protected]"
}
_dbContext.OtherEntity.Where(x=> x.Agent, agent);
_dbContext.ThirdEntity.Where(x=> x.Object.Agent, agent);

How do I convert agentSelector to the following expression x=> x.Mbox == agent.Mbox or one of the other conditions, to use in Where clause to filter profiles.

The profiles.Where clause expects Expression<Func<T, bool>> predicate

Updae

After testing answer below I found EntityFramework cannot convert the following expressions into SQL. And throws the following error:

The LINQ expression 'Where<AgentEntity>(\r\n    source: DbSet<AgentEntity>, \r\n    predicate: (a) => (int)Invoke(__agentSelector_0, a[AgentEntity])\r\n    .ObjectType == (int)(Unhandled parameter: __agent_ObjectType_1))' could not be translated.
Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Summary:
### Explicit client evaluation
You may need to force into client evaluation explicitly in certain cases like following

The amount of data is small so that evaluating on the client doesn't incur a huge performance penalty.
The LINQ operator being used has no server-side translation.
In such cases, you can explicitly opt into client evaluation by calling methods like `AsEnumerable` or `ToList` (`AsAsyncEnumerable` or `ToListAsync` for async). 
By using `AsEnumerable` you would be streaming the results, but using ToList would cause buffering by creating a list, which also takes additional memory. Though if you're enumerating multiple times, then storing results in a list helps more since there's only one query to the database. Depending on the particular usage, you should evaluate which method is more useful for the case.

Upvotes: 1

Views: 1334

Answers (1)

Tim Hirst
Tim Hirst

Reputation: 614

An IQueryable can be filtered using the Where method. This method also returns an IQueryable, so if you want (and I often do) you can chain these together to filter multiple times - I find that leads to more readable code, and also you can branch code between these filters (to add conditions to whether you filter or not). That might look something like this (untested code):

IQueryable<Foo> foos = _dbContext.Foos;
foos = foos.Where(f => f.Bar == myBar);

if(!string.IsNullOrNothing(myBaz)){
    foos = foos.Where(f => f.Baz == myBaz)
}

So in this code, the set of Foo objects is always filtered for when their Bar property equals myBar, but the second filtering is only applied when myBar is not null and not nothing (note the ! making these not, which is one thing dotNet and both think looks like is missing in your original code)

Now let me try and apply that to the extension method you're trying to create. The complication is that there are different mappings to get from OtherEntity or ThirdEntity and the AgentEntity and we want to use a Func<T, AgentEntity> to define that mapping (note that we're mapping from generic type 'T')

public static IQueryable<T> Where<T>(this IQueryable<T> profiles, Func<T, AgentEntity> mapping, AgentEntity agent)
{
    if (!string.IsNullOrEmpty(agent.MBox))
    {
        profiles = profiles.Where(p => mapping(p).MBox == agent.MBox);
    }

    return profiles;
}

Note that we use the mapping function passed in to convert each profile to an agent that we use for the filter. It gets called just like in your original question:

_dbContext.OtherEntity.Where(x=> x.Agent, agent);

Also note that I don't return until the end of the function - that may or may not be what you're actually after - you may actually want to return as soon as you found one criteria you can filter on!

Upvotes: 0

Related Questions