C# 通用 .Contains() 方法在 Entity Framework 中实现 SqlFunctions.StringConvert

C# Generic .Contains() method implementing SqlFunctions.StringConvert in Entity Framework

我有一个在 Entity Framework 中动态创建查询的通用方法。 我将其用作数据 table headers.

的搜索功能

如果 Entity 属性 type/SQL 数据类型是字符串,该函数将完美运行。这是因为 .Contains() 扩展。

当数据类型不是字符串时就会出现问题。 这些数据类型没有 .Contains() 扩展名。

我希望能够在所有数据类型中使用此方法,并且发现我可以使用 SqlFunctions.StringConvert。我也知道它没有整数选项,必须将基于整数的属性转换为双精度。

我不确定如何一般地实现 SqlFunctions.StringConvert,请参阅我的以下方法(您会看到我已经排除了没有 .Contains() 扩展名的数据类型):

    public static IQueryable<T> Filter<T>(this IQueryable<T> query, List<SearchFilterDto> filters)
        where T : BaseEntity
    {
        if (filters != null && filters.Count > 0 && !filters.Any(f => string.IsNullOrEmpty(f.Filter)))
        {
            Expression filterExpression = null;

            ParameterExpression parameter = Expression.Parameter(query.ElementType, "item");

            filterExpression = filters.Select(f =>
            {
                Expression selector = parameter;
                Expression pred = Expression.Constant(f.Filter);

                foreach (var member in f.Column.Split('.'))
                {
                    PropertyInfo mi = selector.Type.GetProperty(member, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                    if (mi != null)
                    {
                        selector = Expression.Property(selector, mi);

                        if (selector.Type == typeof(Guid) ||
                        selector.Type == typeof(Guid?) ||
                        selector.Type == typeof(DateTime) ||
                        selector.Type == typeof(DateTime?) ||   
                        selector.Type == typeof(int) ||
                        selector.Type == typeof(int?)

                            )
                        {
                            return null;
                        }
                    }
                    else
                    {
                        return null;
                    }
                }

                Expression containsMethod = Expression.Call(selector, "Contains", null, pred);

                return containsMethod;
            }).Where(r => r != null).Aggregate(Expression.And);
            LambdaExpression where = Expression.Lambda(filterExpression, parameter);
            MethodInfo whereCall = (typeof(Queryable).GetMethods().First(mi => mi.Name == "Where" && mi.GetParameters().Length == 2).MakeGenericMethod(query.ElementType));
            MethodCallExpression call = Expression.Call(whereCall, new Expression[] { query.Expression, where });
            return query.Provider.CreateQuery<T>(call);
        }
        return query;
    }

The function works perfectly if the Entity property type/SQL data type is a string. This is because of the .Contains() extensions.

我想提一下,本例中的 Contains 不是扩展,而是常规的 string.Contains 方法。

I would like to be able to use this method across all data types

这不是一个好主意,因为非字符串值可以有不同的字符串表示形式,因此您不太清楚要搜索的内容。

但假设您无论如何都想要它。

and have found that I could possibly use SqlFunctions.StringConvert

有两个缺点 - 首先,SqlFunctions 是特定于 SqlServer 的(例如不像 DbFunctions),其次,StringConvert 仅适用于 doubledecimal。 IMO 更好的选择是使用 EF 中支持的 object.ToString 方法(至少在最新的 EF6 中)。

我将根据object.ToString()为您提供解决方案。但在此之前,让我在使用表达式时给您一些提示。任何时候您想使用 System.Linq.Expressions 构建表达式但不知道如何操作,您都可以构建一个类似的示例类型表达式并在调试器 Locals/Watch window 中检查它。例如:

public class Foo
{
    public int Bar { get; set; }
}

Expression<Func<Foo, bool>> e = item =>
    SqlFunctions.StringConvert((decimal?)item.Bar).Contains("1");

你可以设置一个断点并开始扩展 e 个成员,然后是它们的成员等等,你会看到编译器是如何构建表达式的,然后你只需要找到相应的Expression 方法。

最后,这是解决方案本身。我还包含了一些小技巧,可以避免在可能的情况下直接使用反射和字符串方法名称:

public static class QueryableUtils
{
    static Expression<Func<T, TResult>> Expr<T, TResult>(Expression<Func<T, TResult>> source) { return source; }

    static MethodInfo GetMethod(this LambdaExpression source) { return ((MethodCallExpression)source.Body).Method; }

    static readonly MethodInfo Object_ToString = Expr((object x) => x.ToString()).GetMethod();

    static readonly MethodInfo String_Contains = Expr((string x) => x.Contains("y")).GetMethod();

    public static IQueryable<T> Filter<T>(this IQueryable<T> query, List<SearchFilterDto> filters)
        // where T : BaseEntity
    {
        if (filters != null && filters.Count > 0 && !filters.Any(f => string.IsNullOrEmpty(f.Filter)))
        {
            var item = Expression.Parameter(query.ElementType, "item");
            var body = filters.Select(f =>
            {
                // Process the member path and build the final value selector
                Expression value = item;
                foreach (var memberName in f.Column.Split('.'))
                {
                    var member = item.Type.GetProperty(memberName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ??
                        (MemberInfo)item.Type.GetField(memberName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                    if (member == null) return null; // Should probably throw an error?
                    value = Expression.MakeMemberAccess(value, member);
                }
                // NOTE: "Safe" skipping invalid arguments is not a good practice.
                // Without that requirement, the above block will be simply
                // var value = f.Column.Split('.').Aggregate((Expression)item, Expression.PropertyOrField);
                // Convert value to string if needed
                if (value.Type != typeof(string))
                {
                    // Here you can use different conversions based on the value.Type
                    // I'll just use object.ToString()
                    value = Expression.Call(value, Object_ToString);
                }
                // Finally build and return a call to string.Contains method
                return (Expression)Expression.Call(value, String_Contains, Expression.Constant(f.Filter));
            })
            .Where(r => r != null)
            .Aggregate(Expression.AndAlso);

            var predicate = Expression.Lambda<Func<T, bool>>(body, item);
            query = query.Where(predicate);
        }
        return query;
    }
}