I have a C# Service that takes a filter string and a FilterMap
class and generates an Expression<Func<T, bool>>
for use within Linq to * Scenarios (currently it is in use to create dynamic filtering for documentdb queries).
Filters have the form *parameter*_*operation*_*value*
, for example: firstName_eq_Joe
. Multiple filters can be contained within the same string if they are space delimited, for example: firstName_eq_Joe lastName_eq_Bloggs
.
The FilterMap
class is a simple POCO that states what filters are available and where they exist on the target object. Each property on the FilterMap
is a string, where the property name is the property part of a filter and its value is where that property lives on T
.
Example FilterMap
class:
public class FilterMap
{
public string Reputation => "Reputation";
public string FirstName => "User.FirstName";
public string LastName => "User.LastName";
}
In the example above Reputation
is mapped to the property Reputation
. FirstName
is mapped to an object property on T
called User
and then a property on that User
object called FirstName
.
The code for the service can be found below:
public class FilterService<TModel> : IFilterService<TModel> where TModel : class
{
private readonly FilterTypeFactory _filterTypeFactory;
private readonly Lazy<IEnumerable<PropertyInfo>> _modelPropertiesLazy;
public FilterService()
{
_filterTypeFactory = new FilterTypeFactory();
_modelPropertiesLazy = new Lazy<IEnumerable<PropertyInfo>>(() => typeof(TModel).GetTypeInfo().GetProperties(BindingFlags.Instance | BindingFlags.Public));
}
public Expression<Func<TModel, bool>> GenerateFilterFromFilterMapSync<TFilterMap>(string filterString) where TFilterMap : class, new()
{
IEnumerable<RawFilterParts> filters = ParseFilterString(filterString);
if (!filters.Any())
{
return null;
}
IEnumerable<PropertyInfo> filterMapProperties = typeof(TFilterMap).GetTypeInfo().GetProperties(BindingFlags.Instance | BindingFlags.Public);
TFilterMap filterMap = new TFilterMap();
ParameterExpression modelParameterExpression = Expression.Parameter(typeof(TModel));
Expression filterExpression = null;
foreach (RawFilterParts filterParts in filters)
{
string propertyPath = GetPropertyPath(filterParts, filterMap, filterMapProperties);
FilterInfo filterInfo = GetFilterInfoFromFilterMap(propertyPath, _modelPropertiesLazy.Value, modelParameterExpression);
filterExpression = GetFilterExpression(filterExpression, filterParts, filterInfo);
}
return Expression.Lambda<Func<TModel, bool>>(filterExpression, modelParameterExpression);
}
private Expression GetFilterExpression(Expression filterExpression, RawFilterParts filterParts, FilterInfo filterInfo)
{
filterInfo.Operation = filterParts.Operation;
filterInfo.Value = filterParts.Value;
IFilterType filterType = _filterTypeFactory.GetFilterType(filterInfo.Property);
Expression propertyExpression = filterType.GetPropertyExpression(filterInfo);
if (filterExpression == null)
{
filterExpression = propertyExpression;
}
else
{
filterExpression = Expression.AndAlso(filterExpression, propertyExpression);
}
return filterExpression;
}
private string GetPropertyPath<TFilterMap>(RawFilterParts filterParts, TFilterMap filterMap, IEnumerable<PropertyInfo> filterMapProperties)
{
PropertyInfo property = filterMapProperties.FirstOrDefault(p => p.Name.ToLowerInvariant() == filterParts.Property.ToLowerInvariant());
return (string)property.GetValue(filterMap);
}
private FilterInfo GetFilterInfoFromFilterMap(string propertyPath, IEnumerable<PropertyInfo> modelProperties, ParameterExpression parameterExpression)
{
Expression currentPropertyExpression = parameterExpression;
PropertyInfo currentProperty;
int dotIndex;
do
{
dotIndex = propertyPath.IndexOf(".", StringComparison.Ordinal);
string propertyName = dotIndex < 0 ? propertyPath : propertyPath.Substring(0, dotIndex);
currentProperty = modelProperties.FirstOrDefault(pi => pi.Name.ToLowerInvariant() == propertyName.ToLowerInvariant());
currentPropertyExpression = Expression.Property(currentPropertyExpression, currentProperty);
if (dotIndex < 0)
{
break;
}
IEnumerable<PropertyInfo> subModelProperties = currentProperty.PropertyType.GetTypeInfo().GetProperties(BindingFlags.Instance | BindingFlags.Public);
modelProperties = subModelProperties;
propertyPath = propertyPath.Substring(dotIndex + 1);
} while (dotIndex >= 0);
return new FilterInfo()
{
AccessExpression = currentPropertyExpression,
Property = currentProperty
};
}
private IEnumerable<RawFilterParts> ParseFilterString(string filterString)
{
if (filterString == null)
{
throw new ArgumentNullException(nameof(filterString));
}
IEnumerable<string> filterStringSegments = filterString.Split(' ');
List<RawFilterParts> filters = new List<RawFilterParts>(filterStringSegments.Count());
foreach (string filterStringSegment in filterStringSegments)
{
string[] filterParts = filterStringSegment.Split('_');
if (filterParts.Length != 3)
{
throw new InvalidOperationException($"Invalid Filter String Segment: {filterStringSegment}");
}
filters.Add(new RawFilterParts()
{
Property = filterParts[0],
Operation = filterParts[1],
Value = filterParts[2]
});
}
return filters;
}
}
The service uses the following internal
DTOs to pass information between methods:
internal class RawFilterParts
{
public string Property { get; set; }
public string Operation { get; set; }
public string Value { get; set; }
}
internal class FilterInfo
{
public Expression AccessExpression { get; set; }
public PropertyInfo Property { get; set; }
public string Operation { get; set; }
public string Value { get; set; }
}
The service also makes use of a IFilterType
object which is created using a FilterTypeFactory
. The FilterTypeFactory
creates a different IFilterType
depending on the type of the property that the filter corresponds to. The IFilterType
defines what filter operations are available for that type (it throws if the operation is not valid) and generates the expression that performs the filter comparison.
FilterTypeFactory:
internal class FilterTypeFactory
{
public IFilterType GetFilterType(PropertyInfo property)
{
TypeCode typeCode = Type.GetTypeCode(property.PropertyType);
switch (typeCode)
{
case TypeCode.String:
return new StringFilterType();
case TypeCode.Boolean:
return new BoolFilterType();
case TypeCode.Int32:
return new IntFilterType();
case TypeCode.Double:
return new DoubleFilterType();
case TypeCode.DateTime:
return new DateTimeFilterType();
case TypeCode.Object:
if (property.PropertyType == typeof(Guid))
{
return new GuidFilterType();
}
if (property.PropertyType == typeof(Guid?))
{
return new NullableFilterType<Guid>(new GuidFilterType());
}
if (property.PropertyType == typeof(DateTime?))
{
return new NullableFilterType<DateTime>(new DateTimeFilterType());
}
if (property.PropertyType == typeof(int?))
{
return new NullableFilterType<int>(new IntFilterType());
}
if (property.PropertyType == typeof(double?))
{
return new NullableFilterType<double>(new DoubleFilterType());
}
if (property.PropertyType == typeof(bool?))
{
return new NullableFilterType<bool>(new BoolFilterType());
}
break;
}
throw new InvalidOperationException($"The type {typeCode} is not supported for filtering for the property {property.Name}");
}
}
IFilterType:
internal interface IFilterType
{
Expression GetPropertyExpression(FilterInfo filterInfo);
}
The implementations of IFilterType
share an abstract BaseFilterType
which handles creating the Expression to return. The implementations of BaseFilterType
simply defines the operations available for that Filter Type and handles converting the string value provided in the filter string into the required type for the filter.
internal abstract class BaseFilterType<T> : IFilterType
{
internal readonly IEnumerable<string> _allowedOperations;
protected BaseFilterType(IEnumerable<string> allowedOperations)
{
_allowedOperations = allowedOperations;
}
public Expression GetPropertyExpression(FilterInfo filterInfo)
{
if (!IsOperationAllowed(filterInfo.Operation))
{
throw new InvalidOperationException($"Filter string is invalid, operation {filterInfo.Operation} is not valid for {filterInfo.Property.Name}");
}
Expression valueExpression = Expression.Convert(Expression.Constant(GetFilterValue(filterInfo.Value)), filterInfo.Property.PropertyType);
switch (filterInfo.Operation.ToLowerInvariant())
{
case "eq":
return Expression.Equal(filterInfo.AccessExpression, valueExpression);
case "ne":
return Expression.NotEqual(filterInfo.AccessExpression, valueExpression);
case "lt":
return Expression.LessThan(filterInfo.AccessExpression, valueExpression);
case "le":
return Expression.LessThanOrEqual(filterInfo.AccessExpression, valueExpression);
case "gt":
return Expression.GreaterThan(filterInfo.AccessExpression, valueExpression);
case "ge":
return Expression.GreaterThanOrEqual(filterInfo.AccessExpression, valueExpression);
case "ct":
return Expression.Call(filterInfo.AccessExpression, typeof(string).GetRuntimeMethods().FirstOrDefault(mi => mi.Name == "Contains"), valueExpression);
default:
throw new InvalidOperationException($"Error getting expression for filter type, the operation {filterInfo.Operation} is not supported.");
}
}
protected abstract T GetFilterValue(string value);
private bool IsOperationAllowed(string operation)
{
return _allowedOperations.Contains(operation);
}
}
Example FilterType Implementation:
internal class IntFilterType : BaseFilterType<int>
{
public IntFilterType() : base(FilterOperations.NumericOperations)
{
}
protected override int GetFilterValue(string value)
{
int integer;
bool success = Int32.TryParse(value, out integer);
if (success)
{
return integer;
}
throw new ArgumentException("Value is not a valid integer");
}
}
FilterOperations
is a static class that provides preconfigured arrays of available operations for the types.
I'm looking for design and clean code advice, but also any tips on obvious performance issues would be helpful.