Mashton
Mashton

Reputation: 6415

Refactoring two methods to use interface and/or generics

I have a program that passes some query criteria, for multiple types of class, that is used by EF to query data back from a db. There is a criteria class for each type that is being queried, but there is a lot of duplication and similarity between these methods which I'd hoped to refactor out, but without success.

These are example (simplified) service methods:

private static IQueryable<Class1> FilterClass1ResultsOnId(Class1QueryCriteria queryCriteria, IQueryable<Class1> queryResults)
    {
        if (!string.IsNullOrEmpty(queryCriteria.Id))
        {
            queryResults = from view in queryResults where view.Id==queryCriteria.Id select view;
        }
        return queryResults;
    }


private static IQueryable<Class2> FilterClass2ResultsOnId(Class2QueryCriteria queryCriteria, IQueryable<Class2> queryResults)
    {
        if (!string.IsNullOrEmpty(queryCriteria.Id))
        {
            queryResults = from view in queryResults where view.Id == queryCriteria.Id select view;
        }
        return queryResults;
    }

... which are called like this ...

queryResults = FilterClass1ResultsOnId(queryCriteria, queryResults);

The service has a series of these 'FilterClassXonSOMECRITERIA' methods to narrow down the IQueryable that is ultimately returned. There are a lot of these service methods that are essentially identical apart from the type of the input parameters and the type of the output.

I initially tried to refactor the methods to accept a queryResults argument that was an interface rather than a concrete class - a simple interface that just has an Id property - and then made Class1 and Class2 implement that interface. But then I need to make the return type of these methods non-concrete too if I want to use just the one method. This is where I got stuck.

I have been looking around and have seen covariance and contravariance information, but I can't seem to define a method/interface that uses these interfaces and a generic type without getting 'cannot resolve symbol T' or 'Argument type XXX is not assignable to parameter type YYY''.

At this point I wanted to sanity check with those more familiar with this problem: Is what I am trying to do possible? Can I have methods that take an argument specified only by interface, and return from that method an object also only specified by an interface?

EDIT: the criteria classes are pretty straightforward, and are actually complex types defined in the EF model. But here's a simplified example:

public class Class1QueryCriteria
{
  public string Id;
  public string Name;
}

public class Class2QueryCriteria
{
  public string Id;
  public string Category;
}

Upvotes: 2

Views: 661

Answers (2)

Lee
Lee

Reputation: 144136

You can build the filter expression manually:

private static Expression<Func<T, bool>> WhereById<T>(string id)
{
    var pExpr = Expression.Parameter(typeof(T));
    var idExpr = Expression.Property(pExpr, "Id");
    var eqExpr = Expression.Equal(idExpr, Expression.Constant(id));
    return Expression.Lambda<Func<T, bool>>(eqExpr, pExpr);
}

private static IQueryable<T> FilterById<T>(string id, IQueryable<T> source)
{
    if (!string.IsNullOrEmpty(queryCriteria.Id))
    {
        var whereExpr = WhereById<T>(string id);
        return source.Where(whereExpr);
    }
    return queryResults;
}

private static IQueryable<Class1> FilterClass1ResultsOnId(Class1QueryCriteria queryCriteria, IQueryable<Class1> queryResults)
{
    return FilterById(queryCriteria.Id, queryResults);
}

You can avoid the need for separate overloads if you create an base type with the string Id property.

Alternatively you could make the criteria class generic and include the query expression directly e.g.

public class QueryCriteria<T>
{
    public Expression<Func<T, bool>> FilterExpression { get; set; }
}

then you could write something like:

public static IQueryable<T> FilterResults<T>(QueryCriteria<T> criteria, IQueryable<T> source)
{
    return criteria.FilterExpression == null ? source : source.Where(criteria.FilterExpression);
}

Upvotes: 2

Simon Whitehead
Simon Whitehead

Reputation: 65079

You should be able to define an interface for the criteria:

interface IQueryCriteria {
    string Id { get; set; }
}

..and, if necessary, an interface for your "view" types:

interface IViewType {
    string Id { get; set; }
}

Then your method (non-plural) can be generic:

private static IQueryable<T> FilterClassResultsOnId<T>(IQueryCriteria queryCriteria, IQueryable<T> queryResults) 
    where T : IViewType
{
    if (!string.IsNullOrEmpty(queryCriteria.Id))
    {
        queryResults = from view in queryResults where view.Id == queryCriteria.Id select view;
    }
    return queryResults;
}

The key here is the generic type constraint where T : IViewType. This allows the use of view.Id in the body of the method, because the compiler can confirm that the type will at least implement the interface with the Id property.

Upvotes: 1

Related Questions