0

我有一个在实体框架中动态创建查询的通用方法。我将其用作数据表标题的搜索功能。

如果 Entity 属性类型/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;
    }
4

1 回答 1

2

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

我想提一下,Contains在这种情况下,不是扩展,而是常规string.Contains方法。

我希望能够在所有数据类型中使用此方法

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

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

并且发现我可以使用 SqlFunctions.StringConvert

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

我将为您提供基于object.ToString(). 但在此之前,让我在使用表达式时给您一些提示。每当您想使用System.Linq.Expressions但不知道如何构建表达式时,您可以构建一个类似的示例类型表达式并在调试器 Locals/Watch 窗口中检查它。例如:

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;
    }
}
于 2016-02-08T15:24:08.070 回答