简体   繁体   中英

LINQ with dynamic where clause in EFCore

I'm using net5.0 and EntityFrameworkCore 5.0.4.

I have a search method that has optional strings to search for on a DataContext in EFCore.

I want to check if each of the strings is not null or white space.

I could do this:

var query = context.Model.AsQueryable();

if (!string.IsNullOrWhiteSpace(parameters.Id))
{
  query = query.Where(x => x.Id.ToLower().Contains(parameters.Id.ToLower()));
}

but that is just horrible to maintain. What I started trying to get going is this:

public static IQueryable<T> FilterBy<T>(this IQueryable<T> query, string? searchValue,
    Expression<Func<T, string>> getValueExpression)
{
    if (string.IsNullOrWhiteSpace(searchValue?.Trim()))
    {
        return query;
    }

    // return with a where clause
}

I got this working but it does not support accessing joins that I need and I get the feeling this is not a good way:

var searchLower = searchValue.Trim().ToLower();
var propertyName = getValueExpression.GetMemberAccess().Name;
return query.Where(x => EF.Property<string?>(x!, propertyName)!.ToLower().Contains(searchLower));

I want to use it like so:

query                        
.FilterBy(parameters.Id, x => x.Id)
.FilterBy(parameters.Name, x => x.Name)
.FilterBy(parameters.CompanyName, x => x.Company.Name) // access via include/join

Solution

    private static readonly MethodInfo StringContainsMethod =
        typeof(string).GetMethod(nameof(string.Contains), new[] {typeof(string)})!;
    private static readonly MethodInfo StringToLowerMethod =
        typeof(string).GetMethod(nameof(string.ToLower), Type.EmptyTypes)!;

    public static IQueryable<T> FilterBy<T>(this IQueryable<T> query, string? searchValue,
        Expression<Func<T, string?>> memberExpression)
    {
        if (string.IsNullOrWhiteSpace(searchValue))
            return query;

        var valueExpression = Expression.Constant(searchValue.ToLower());
        var toLower = Expression.Call(memberExpression.Body, StringToLowerMethod);
        var call = Expression.Call(toLower, StringContainsMethod, valueExpression);
        var sourceParam = memberExpression.Parameters.First();
        Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(call, sourceParam);

        return query.Where(predicate);
    }

Integer

    public static IQueryable<T> FilterBy<T>(this IQueryable<T> query, int? filterInteger,
        Expression<Func<T, int?>> memberExpression)
    {
        if (filterInteger == null)
            return query;

        var valueExpression = Expression.Constant(filterInteger);
        var call = Expression.Equal(memberExpression.Body, valueExpression);
        var sourceParam = memberExpression.Parameters.First();
        Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(call, sourceParam);
        return query.Where(predicate);
    }

Boolean

    public static IQueryable<T> FilterBy<T>(this IQueryable<T> query, bool? filterBoolean,
        Expression<Func<T, bool?>> memberExpression)
    {
        if (filterBoolean == null)
            return query;

        var valueExpression = Expression.Constant(filterBoolean);
        var call = Expression.Equal(memberExpression.Body, valueExpression);
        var sourceParam = memberExpression.Parameters.First();
        Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(call, sourceParam);
        return query.Where(predicate);
    }

I've not tested this but I believe you'd need to use expressions to achieve this. The following will build an expression to use as the predicate in the Where method:

    public static IQueryable<T> FilterBy<T>(
        this IQueryable<T> query, 
        string searchValue,
        Expression<Func<T, string>> memberExpression)
    {
        if (string.IsNullOrWhiteSpace(searchValue))
            return query;

        // must be a lambda expression
        LambdaExpression lambdaExpression = memberExpression as LambdaExpression;
        if (lambdaExpression == null)
            throw new ArgumentException($"Expression '{memberExpression}' is not a lambda expression.");

        // get the member
        Func<ParameterExpression, Expression> sourceExpression = source => Expression.Invoke(lambdaExpression, source);
        ParameterExpression sourceParameter = Expression.Parameter(typeof(T), "source");
        Expression sourceMember = sourceExpression(sourceParameter);

        // expression for the search value
        ConstantExpression valueExpression = Expression.Constant(searchValue);

        // expression to call the Contains method
        MethodInfo containsMethod = GetContainsMethod();
        MethodCallExpression callExpression = Expression.Call(null, containsMethod, sourceMember, valueExpression);

        // predicate expression
        Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(callExpression, sourceParameter);

        return query.Where(predicate);
    }

    private static MethodInfo GetContainsMethod()
    {
        // get method
        MethodInfo genericContainsMethod = typeof(Queryable)
            .GetMethods(BindingFlags.Static | BindingFlags.Public)
            .First(m => m.Name == "Contains"
                     && m.IsGenericMethod
                     && m.GetParameters().Count() == 2);

        // apply generic types
        MethodInfo containsMethod = genericContainsMethod
            .MakeGenericMethod(new Type[] { typeof(string) });

        return containsMethod;
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM