Home > Back-end >  How to select many from string in efcore?
How to select many from string in efcore?

Time:08-25

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)
        );
}
  • Related