Reputation: 6415
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
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
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