Amicable
Amicable

Reputation: 3101

Returning filtered IQueryable<T> with anonymous types

I have a set of Reports which I need to perform filtering on before returning the output. I would like to perform this with a single anonymous method to avoid duplicating the same code in different repositories. I'm using Entity Framework so the model types all related to the database and inherit from a base class called ReportBase.

This is what how I currently implement the filtering, each report type has to implement this method with a different context and returning a different IQueryable type.

private IQueryable<ReviewAgreement> GetFiltered(ReportFilter filter)
{
    IQueryable<ReviewAgreement> reviewAgreementQueryable = Context.ReviewAgreements.Where(p => p.ClientWorkflowId == filter.ClientWorkflowId);
    if (filter.AppraisalLevelId.HasValue)
    {
        reviewAgreementQueryable = reviewAgreementQueryable.Where(p => p.AppraisalLevelId == filter.AppraisalLevelId.Value);
    }
    return reviewAgreementQueryable;
}

I've been trying to implement this anonymously so I can reuse it, as in this non functional example.

public IQueryable<T> GetFiltered(ReportFilter filter)
{
    IQueryable<T> reportQueryable = Context.Set<T>();
    reportQueryable = reportQueryable.Where(p => p.ClientWorkflowId == filter.ClientWorkflowId);

    if (filter.AppraisalLevelId.HasValue)
    {
        reportQueryable = reportQueryable.Where(p => p.AppraisalLevelId == filter.AppraisalLevelId.Value);
    }

    return reportQueryable;
}

The issue I am having is of course that the use of Where is ambiguous, so it cannot resolve p.ClientWorkflowId.

I have tried using a Func<T, TResult> delegate to pass in the filtering options this but the Where operation seems to want to return a list.

Is there actually a method I can use to achieve the effect I want?

Upvotes: 2

Views: 2874

Answers (1)

Servy
Servy

Reputation: 203844

  1. Declare an interface that has the two ID properties that you need to perform this operation.
  2. Ensure that your entities implement that interface.
  3. Add a constraint to the generic argument that it implements that interface.

Note that if your base class defines both of the properties in question then you don't need an interface, and can simply constrain the type to that base class:

public IQueryable<T> GetFiltered<T>(ReportFilter filter) where T : ReportBase
{
    // body unchanged
}

If you want to go down the route of accepting parameters to represent these properties than it's also possible. The first thing is that you'll need to accept expressions, not Func objects, so that the query provider can analyze them. This means changing the function signature to:

public IQueryable<T> GetFiltered<T>(ReportFilter filter,
    Expression<Func<T, int>> clientIdSelector,
    Expression<Func<T, int>> appraisalIdSelector)
{

Next, to turn these selectors into predicates that compare the value to an ID that we have is a bit more involved for expressions than it is for regular delegates. What we really need here is a Compose method; for delegates it's simple enough, to compose one method with another you just invoke it with the parameter being the result of the first. With expressions this means taking the body of one expression and replacing all instance of the parameter with the body of another, and then wrapping the whole thing up in a new Lambda.

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

This itself is dependent on the ability to replace all instances of one expression with another. To do that we'll need to use the following:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal 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);
    }
}

Now that we have all of this in place we can actually compose our selectors with a comparison to the ID values of the filter:

IQueryable<T> reportQueryable = Context.Set<T>();
reportQueryable = reportQueryable
    .Where(clientIdSelector.Compose(id => id == filter.ClientWorkflowId));

if (filter.AppraisalLevelId.HasValue)
{
    reportQueryable = reportQueryable
        .Where(clientIdSelector.Compose(id => id == filter.AppraisalLevelId.Value));
}

return reportQueryable;

Upvotes: 5

Related Questions