Using Entity Framework we have a number of entities, which we want to hide from the rest of the calling code to remove the direct dependency on the database. We're doing so by using DTOs, which for the most part are identical to the entities. We also have an abstract generic provider and persister pattern which handle the conversion between the dtos and the entities, and all of the database interaction. The general loading method of the base abstract provider uses the following signature:
protected virtual IEnumerable<TDto> Load(Expression<Func<TEntity, bool>> filter)
So any provider that extends the abstract can look like this:
public IEnumerable<FooDto> GetSomeFilteredData(IEnumerable<string> identifiers)
{
return base.Load(entity => identifiers.Contains(entity.SomeProperty));
}
Therefore hiding the entities from the calling code.
This pattern works well for us, but I have been investigating how to manipulate expressions so that I could put the following in the base provider:
public virtual IEnumerable<TDto> Load(Expression<Func<TDto, bool>> filter)
The base provider would then convert the Expression<Func<TDto, bool>>
into the equivalent entity expression, and load the data as before, therefore removing the need to write tedious methods and give us more control in the calling code.
The end result has been the following ExpressionConverter
public class ExpressionConverter<Dto, Entity>
where Dto : class, new() where Entity : class, new()
{
private MappedConverter<Dto, Entity> converter;
public ExpressionConverter(MappedConverter<Dto, Entity> converter)
{
this.converter = converter;
}
public Expression<Func<Entity, bool>> ConvertExpr(Expression<Func<Dto, bool>> expr)
{
ParameterExpression entityParam = Expression.Parameter(typeof(Entity), "e");
Expression entityExpression = this.ConvertExpression(expr.Body, entityParam);
var result = Expression.Lambda<Func<Entity, bool>>(entityExpression, entityParam);
return result;
}
private Expression ConvertExpression(Expression expression, ParameterExpression entityParam)
{
if (expression is BinaryExpression binary)
{
return this.ConvertBinaryExpression(binary, entityParam);
}
if (expression is MemberExpression member)
{
return this.ConvertMemberExpression(member, entityParam);
}
if (expression is MethodCallExpression method)
{
return this.ConvertMethodCallExpression(method, entityParam);
}
return expression;
}
private Expression ConvertBinaryExpression(BinaryExpression binary, ParameterExpression param)
{
Expression left = this.ConvertExpression(binary.Left, param);
Expression right = this.ConvertExpression(binary.Right, param);
ExpressionType nodeType = binary.NodeType;
return this.CombineExpressions(left, right, nodeType);
}
private Expression ConvertMemberExpression(Expression expression, ParameterExpression entityParam)
{
if (this.IsParseableMemberExpression(expression))
{
MemberExpression memberExpr = expression as MemberExpression;
var value = Expression.Lambda(memberExpr).Compile().DynamicInvoke();
return Expression.Constant(value);
}
else if (this.IsConvertibleMember(expression))
{
MemberInfo dtoMember = (expression as MemberExpression).Member;
Mapping<Dto, Entity> mapping = this.converter.GetMappingFromMemberName<Dto>(dtoMember.Name);
MemberExpression entityMemberExpr = Expression.MakeMemberAccess(entityParam, mapping.EntityProperty);
return entityMemberExpr;
}
return expression;
}
private Expression ConvertMethodCallExpression(MethodCallExpression expression, ParameterExpression entityParam)
{
Expression objectExpr = this.ConvertMemberExpression(expression.Object, entityParam);
IEnumerable<Expression> argumentExpressions = expression.Arguments.Select(x => this.ConvertMemberExpression(x, entityParam));
MethodInfo method = expression.Method;
Expression result = Expression.Call(objectExpr, method, argumentExpressions);
return result;
}
private bool IsConvertibleMember(Expression expression)
{
return expression.NodeType == ExpressionType.MemberAccess;
}
private bool IsParseableMemberExpression(Expression expression)
{
if (expression is MemberExpression memberExpression)
{
return Regex.IsMatch(expression.ToString(), @"value[(].*[)]")
|| this.IsDateTimeExpression(memberExpression);
}
return false;
}
private bool IsDateTimeExpression(MemberExpression expression)
{
return Regex.IsMatch(expression.ToString(), @"DateTime\.(Today|Now)");
}
private Expression CombineExpressions(Expression left, Expression right, ExpressionType type)
{
switch (type)
{
case ExpressionType.And: return Expression.And(left, right);
case ExpressionType.AndAlso: return Expression.AndAlso(left, right);
case ExpressionType.Equal: return Expression.Equal(left, right);
case ExpressionType.ExclusiveOr: return Expression.ExclusiveOr(left, right);
case ExpressionType.GreaterThan: return Expression.GreaterThan(left, right);
case ExpressionType.GreaterThanOrEqual: return Expression.GreaterThanOrEqual(left, right);
case ExpressionType.LessThan: return Expression.LessThan(left, right);
case ExpressionType.LessThanOrEqual: return Expression.LessThanOrEqual(left, right);
case ExpressionType.NotEqual: return Expression.NotEqual(left, right);
case ExpressionType.Or: return Expression.Or(left, right);
case ExpressionType.OrElse: return Expression.OrElse(left, right);
default:
throw new Exception($"Unsupported expression type: {type.ToString()}");
}
}
}
Where MappedConverter<Dto, Entity>
is the following:
public class MappedConverter<Dto, Entity> where Dto : class, new() where Entity : class, new()
{
public List<Mapping<Dto, Entity>> Mappings { get; set; }
public MappedConverter(params Mapping<Dto, Entity>[] maps)
{
this.Mappings = maps.ToList();
}
public Mapping<Dto, Entity> GetMappingFromMemberName<T>(string name)
{
if (typeof(T) == typeof(Dto))
{
return this.Mappings.SingleOrDefault(x => x.DtoProperty.Name == name);
}
else if (typeof(T) == typeof(Entity))
{
return this.Mappings.SingleOrDefault(x => x.EntityProperty.Name == name);
}
throw new Exception($"Cannot get mapping for {typeof(T).Name} from MappedConverter<{typeof(Dto).Name}, {typeof(Entity).Name}>");
}
}
And Mapping<Dto, Entity>
is the following:
public class Mapping<Dto, Entity>
{
public PropertyInfo DtoProperty { get; }
public PropertyInfo EntityProperty { get; }
}
Notes:
The conversion only has to handle Expressions of the form Expression<Func<Dto, bool>>
I am specifically looking for ways to improve the IsParseableMemberExpression
and IsDateTimeExpression
methods
IsParseableMemberExpression
uses Regex
to see if the expression is representing a variable in the full expression. e.g. variable
in x => x.Value == variable
TEntity
is compromising your otherwise quite extensive separation of layers. However, the issue in only usingTDto
is that there is no guarantee that its relatedTEntity
has a similar property with which the expression would work. I'm not saying it can't technically be done, but is the effort required worth the payoff? It's counterintuitive to want direct control over the entity properties you're forcibly trying to abstract. You're essentially asking for zero type safety for your filters, which seems like it can become a source of future issues. \$\endgroup\$