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
仅适用于 double
和 decimal
。 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;
}
}
我有一个在 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
仅适用于 double
和 decimal
。 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;
}
}