Say there is a palette
---- --------
| id | name |
---- --------
| 1 | pa |
| 2 | pb |
---- --------
of colors.
---- ------ ------------
| id | name | palette_id |
---- ------ ------------
| 1 | ca | 1 |
| 2 | cb | 2 |
---- ------ ------------
To select and filter paletts I can use:
_dbContext.Palettes.Where(p => p.Colors.Any(x => x.Name.Contains("ca"))
However I would like to construct this from a string. Given a string like Colors.Name
and ca
, how can I create an efcore expression, that returns all palettes where color names match ca
?
The use case for this is, that I have a filter efcore extension that takes a string and converts that into an efcore expression.
_dbContext.Palettes.Filter("Colors.Name contains ca")...
CodePudding user response:
I have implemented two methods FilterContains
and FilterEquals
. I think it will be easy to extend them.
Function takes care about any nesting level and generates proper filter, but it requires to pass DbContext
for using Model information:
_dbContext.Palettes.FilterContains(_dbContext, "Colors.Name", "ca")
.ToList();
_dbContext.Palettes.FilterEquals(_dbContext, "Colors.Name", "ca")
.ToList();
But it also can handle something like this:
_dbContext.Users.FilterContains(_dbContext, "Country.Regions.SubRegions.Name", "ca")
.ToList();
And implementation:
public static class DynamicQueryableExtensions
{
public static IQueryable<T> FilterContains<T>(this IQueryable<T> query, DbContext context, string propPath, string value)
{
return FilterQuery(query, context.Model, propPath, propExpression =>
Expression.Call(EnsureString(propExpression), nameof(string.Contains), Type.EmptyTypes,
Expression.Constant(value)));
}
public static IQueryable<T> FilterEquals<T>(this IQueryable<T> query, DbContext context, string propPath, object value)
{
return FilterQuery(query, context.Model, propPath, propExpression =>
{
var valueType = value.GetType();
if (propExpression.Type != valueType)
{
propExpression = Expression.Convert(propExpression, valueType);
}
return Expression.Equal(propExpression, Expression.Constant(value));
});
}
private static IQueryable<T> FilterQuery<T>(IQueryable<T> query, IModel model, string propPath,
Func<Expression, Expression> filterFactory)
{
var propNames = propPath.Split('.');
var entityParameter = Expression.Parameter(typeof(T), "e");
var filter = BuildFilter(entityParameter, model, propNames, 0, filterFactory);
var filterLambda = Expression.Lambda<Func<T, bool>>(filter, entityParameter);
return query.Where(filterLambda);
}
private static Expression BuildFilter(Expression obj, IModel model, string[] propPath, int currentIndex, Func<Expression, Expression> predicateFactory)
{
var entityType = model.FindEntityType(obj.Type);
var propName = propPath[currentIndex];
var prop = entityType.FindProperty(propName);
Expression filter;
if (prop == null)
{
var navigation = entityType.GetNavigations().FirstOrDefault(n => n.Name == propName);
if (navigation == null)
throw new InvalidOperationException($"Property '{propName}' not found in type '{obj.Type}'");
var navigationAccess = Expression.MakeMemberAccess(obj, navigation.PropertyInfo);
if (navigation.IsCollection)
{
var targetType = navigation.TargetEntityType.ClrType;
var nParam = Expression.Parameter(targetType, "n");
var anyFilter = BuildFilter(nParam, model, propPath, currentIndex 1, predicateFactory);
filter = Expression.Call(typeof(Enumerable), nameof(Enumerable.Any), new[] { targetType }, navigationAccess, Expression.Lambda(anyFilter, nParam));
}
else
{
filter = BuildFilter(navigationAccess, model, propPath, currentIndex 1, predicateFactory);
}
}
else
{
var propAccess = Expression.MakeMemberAccess(obj, prop.PropertyInfo);
filter = predicateFactory(propAccess);
}
return filter;
}
// For safe conversion to string
private static Expression EnsureString(Expression expression)
{
if (expression.Type == typeof(string))
return expression;
if (expression.Type != typeof(object))
expression = Expression.Convert(expression, typeof(object));
expression = Expression.Call(_toStringMethod, expression);
return expression;
}
private static MethodInfo _toStringMethod = typeof(Convert).GetMethods()
.Single(m =>
m.Name == nameof(Convert.ToString) && m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == typeof(object)
);
}