在嵌套对象的 IQueryable 上动态创建 lambda 搜索
Dynamically create lambda search on IQueryable on nested objects
我正在尝试对嵌套对象构建动态搜索,稍后会将其发送到 EF 和 SQL 服务器。到目前为止,我能够搜索第一个对象的所有属性。这是一个非常简化的版本:
public class User
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
}
public class MyClass<TEntity> where TEntity : class {
public IQueryable<TEntity> applySearch(IQueryable<TEntity> originalList, string propName, string valueToSearch) {
ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity), "p");
PropertyInfo propertyInfo = typeof(TEntity).GetProperty(propName);
MemberExpression member = Expression.MakeMemberAccess(parameterExpression, propertyInfo);
lambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(member, Expression.Constant(valueToSearch)), parameterExpression);
return originalList.Where(expression);
}
}
当 propName = "Name"
时一切正常,但是当 propName = "Address.City"
时,propertyInfo
为空,我在 member
赋值行出现此错误:
System.ArgumentNullException: Value cannot be null
我能够使用此 answer:
中的解决方案获得嵌套 属性 的 propertyInfo
PropertyInfo propertyInfo = GetPropertyRecursive(typeof(TEntity), propName);
...
private PropertyInfo GetPropertyRecursive(Type baseType, string propertyName)
{
string[] parts = propertyName.Split('.');
return (parts.Length > 1)
? GetPropertyRecursive(baseType.GetProperty(parts[0]).PropertyType, parts.Skip(1).Aggregate((a, i) => a + "." + i))
: baseType.GetProperty(propertyName);
}
但是我在 member
赋值时得到这个错误:
System.ArgumentException: Property 'System.String City' is not defined for type 'User'
这应该指向 Address
而不是 User
,但我不知道我是否在正确的轨道上,我的意思是,我现在应该更改 parameterExpression
吗?
如何对嵌套对象进行动态搜索,以便将其转换为 lambda 表达式,然后发送到 SQL?
可能值得看看 Linqkit 的谓词生成器...
http://www.albahari.com/nutshell/predicatebuilder.aspx
我还会看一下实体 SQL...
https://msdn.microsoft.com/en-us/library/vstudio/bb387145(v=vs.100).aspx
您可能正在用您正在编写的代码重新发明一个轮子。
我还应该评论,就 SQL 服务器计划缓存而言,除非您别无选择,否则我不会动态构建查询。您最好创建一个查询来处理 SQL 服务器可以为其缓存计划的所有情况,如果每次执行时都没有计划,您的查询将 运行 慢很多SQL 服务器的计划缓存。
提前警告 - 我不是在构建表达式,只是在检查它的结构。
当我需要动态创建表达式时,我发现检查表达式并复制其结构很有用:
Expression<Func<User, string>> getCity = user => user.Address.City;
现在你可以简单地调试它,例如在立即window (ctrlalt我这里):
getCity
{user => user.Address.City}
Body: {user.Address.City}
CanReduce: false
DebugView: ".Lambda #Lambda1<System.Func`2[ConsoleApplication1.User,System.String]>(ConsoleApplication1.User $user) {\r\n ($user.Address).City\r\n}"
Name: null
NodeType: Lambda
Parameters: Count = 1
ReturnType: {Name = "String" FullName = "System.String"}
TailCall: false
这里我们可以看到getCity
是一个只有一个参数的Lambda
。让我们检查一下它的主体:
getCity.Body
{user.Address.City}
CanReduce: false
DebugView: "($user.Address).City"
Expression: {user.Address}
Member: {System.String City}
NodeType: MemberAccess
Type: {Name = "String" FullName = "System.String"}
getCity.Body
是成员访问 - 它访问表达式 user.Address
的成员 City
。从技术上讲,这是一个 PropertyExpression
,它是一个内部 class,所以我们甚至不能转换成它,但没关系。
最后,让我们看看那个内部表达式:
((MemberExpression)getCity.Body).Expression
{user.Address}
CanReduce: false
DebugView: "$user.Address"
Expression: {user}
Member: {ConsoleApplication1.Address Address}
NodeType: MemberAccess
Type: {Name = "Address" FullName = "ConsoleApplication1.Address"}
那只是 user.Address
。
现在我们可以构建一个相同的表达式:
var addressProperty = typeof (User).GetProperty("Address");
var cityProperty = typeof(Address).GetProperty("City");
var userParameter = Expression.Parameter(typeof (User), "user");
var getCityFromUserParameter = Expression.Property(Expression.Property(userParameter, addressProperty), cityProperty);
var lambdaGetCity = Expression.Lambda<Func<User, string>>(getCityFromUserParameter, userParameter);
Expression.MakeMemberAccess
也可以,而不是 Expression.Property
.
显然,您需要在循环中更动态地构建表达式,但结构是相同的。
经过 Kobi 的建议和大量的反复试验,我终于成功了。这使用了Universal PredicateBuilder。这是:
public class MyClass<TEntity> where TEntity : class
{
public IQueryable<TEntity> ApplySearch(IQueryable<TEntity> originalList, string valueToSearch, string[] columnsToSearch)
{
Expression<Func<TEntity, bool>> expression = null;
foreach (var propName in columnsToSearch)
{
Expression<Func<TEntity, bool>> lambda = null;
ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity), "p");
string[] nestedProperties = propName.Split('.');
Expression member = parameterExpression;
foreach (string prop in nestedProperties)
{
member = Expression.PropertyOrField(member, prop);
}
var canConvert = CanConvertToType(valueToSearch, member.Type.FullName);
if (canConvert)
{
var value = ConvertToType(valueToSearch, member.Type.FullName);
if (member.Type.Name == "String")
{
ConstantExpression constant = Expression.Constant(value);
MethodInfo mi = typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) });
Expression call = Expression.Call(member, mi, constant);
lambda = Expression.Lambda<Func<TEntity, bool>>(call, parameterExpression);
}
else
{
lambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(member, Expression.Constant(value)), parameterExpression);
}
}
if (lambda != null)
{
if (expression == null)
{
expression = lambda;
}
else
{
expression = expression.Or(lambda);
}
}
}
if (expression != null)
{
return originalList.Where(expression);
}
return originalList;
}
}
private bool CanConvertToType(object value, string type)
{
bool canConvert;
try
{
var cValue = ConvertToType(value, type);
canConvert = true;
}
catch
{
canConvert = false;
}
return canConvert;
}
private dynamic ConvertToType(object value, string type)
{
return Convert.ChangeType(value, Type.GetType(type));
}
我正在尝试对嵌套对象构建动态搜索,稍后会将其发送到 EF 和 SQL 服务器。到目前为止,我能够搜索第一个对象的所有属性。这是一个非常简化的版本:
public class User
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
}
public class MyClass<TEntity> where TEntity : class {
public IQueryable<TEntity> applySearch(IQueryable<TEntity> originalList, string propName, string valueToSearch) {
ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity), "p");
PropertyInfo propertyInfo = typeof(TEntity).GetProperty(propName);
MemberExpression member = Expression.MakeMemberAccess(parameterExpression, propertyInfo);
lambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(member, Expression.Constant(valueToSearch)), parameterExpression);
return originalList.Where(expression);
}
}
当 propName = "Name"
时一切正常,但是当 propName = "Address.City"
时,propertyInfo
为空,我在 member
赋值行出现此错误:
System.ArgumentNullException: Value cannot be null
我能够使用此 answer:
中的解决方案获得嵌套 属性 的propertyInfo
PropertyInfo propertyInfo = GetPropertyRecursive(typeof(TEntity), propName);
...
private PropertyInfo GetPropertyRecursive(Type baseType, string propertyName)
{
string[] parts = propertyName.Split('.');
return (parts.Length > 1)
? GetPropertyRecursive(baseType.GetProperty(parts[0]).PropertyType, parts.Skip(1).Aggregate((a, i) => a + "." + i))
: baseType.GetProperty(propertyName);
}
但是我在 member
赋值时得到这个错误:
System.ArgumentException: Property 'System.String City' is not defined for type 'User'
这应该指向 Address
而不是 User
,但我不知道我是否在正确的轨道上,我的意思是,我现在应该更改 parameterExpression
吗?
如何对嵌套对象进行动态搜索,以便将其转换为 lambda 表达式,然后发送到 SQL?
可能值得看看 Linqkit 的谓词生成器...
http://www.albahari.com/nutshell/predicatebuilder.aspx
我还会看一下实体 SQL...
https://msdn.microsoft.com/en-us/library/vstudio/bb387145(v=vs.100).aspx
您可能正在用您正在编写的代码重新发明一个轮子。
我还应该评论,就 SQL 服务器计划缓存而言,除非您别无选择,否则我不会动态构建查询。您最好创建一个查询来处理 SQL 服务器可以为其缓存计划的所有情况,如果每次执行时都没有计划,您的查询将 运行 慢很多SQL 服务器的计划缓存。
提前警告 - 我不是在构建表达式,只是在检查它的结构。
当我需要动态创建表达式时,我发现检查表达式并复制其结构很有用:
Expression<Func<User, string>> getCity = user => user.Address.City;
现在你可以简单地调试它,例如在立即window (ctrlalt我这里):
getCity
{user => user.Address.City}
Body: {user.Address.City}
CanReduce: false
DebugView: ".Lambda #Lambda1<System.Func`2[ConsoleApplication1.User,System.String]>(ConsoleApplication1.User $user) {\r\n ($user.Address).City\r\n}"
Name: null
NodeType: Lambda
Parameters: Count = 1
ReturnType: {Name = "String" FullName = "System.String"}
TailCall: false
这里我们可以看到getCity
是一个只有一个参数的Lambda
。让我们检查一下它的主体:
getCity.Body
{user.Address.City}
CanReduce: false
DebugView: "($user.Address).City"
Expression: {user.Address}
Member: {System.String City}
NodeType: MemberAccess
Type: {Name = "String" FullName = "System.String"}
getCity.Body
是成员访问 - 它访问表达式 user.Address
的成员 City
。从技术上讲,这是一个 PropertyExpression
,它是一个内部 class,所以我们甚至不能转换成它,但没关系。
最后,让我们看看那个内部表达式:
((MemberExpression)getCity.Body).Expression
{user.Address}
CanReduce: false
DebugView: "$user.Address"
Expression: {user}
Member: {ConsoleApplication1.Address Address}
NodeType: MemberAccess
Type: {Name = "Address" FullName = "ConsoleApplication1.Address"}
那只是 user.Address
。
现在我们可以构建一个相同的表达式:
var addressProperty = typeof (User).GetProperty("Address");
var cityProperty = typeof(Address).GetProperty("City");
var userParameter = Expression.Parameter(typeof (User), "user");
var getCityFromUserParameter = Expression.Property(Expression.Property(userParameter, addressProperty), cityProperty);
var lambdaGetCity = Expression.Lambda<Func<User, string>>(getCityFromUserParameter, userParameter);
Expression.MakeMemberAccess
也可以,而不是 Expression.Property
.
显然,您需要在循环中更动态地构建表达式,但结构是相同的。
经过 Kobi 的建议和大量的反复试验,我终于成功了。这使用了Universal PredicateBuilder。这是:
public class MyClass<TEntity> where TEntity : class
{
public IQueryable<TEntity> ApplySearch(IQueryable<TEntity> originalList, string valueToSearch, string[] columnsToSearch)
{
Expression<Func<TEntity, bool>> expression = null;
foreach (var propName in columnsToSearch)
{
Expression<Func<TEntity, bool>> lambda = null;
ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity), "p");
string[] nestedProperties = propName.Split('.');
Expression member = parameterExpression;
foreach (string prop in nestedProperties)
{
member = Expression.PropertyOrField(member, prop);
}
var canConvert = CanConvertToType(valueToSearch, member.Type.FullName);
if (canConvert)
{
var value = ConvertToType(valueToSearch, member.Type.FullName);
if (member.Type.Name == "String")
{
ConstantExpression constant = Expression.Constant(value);
MethodInfo mi = typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) });
Expression call = Expression.Call(member, mi, constant);
lambda = Expression.Lambda<Func<TEntity, bool>>(call, parameterExpression);
}
else
{
lambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(member, Expression.Constant(value)), parameterExpression);
}
}
if (lambda != null)
{
if (expression == null)
{
expression = lambda;
}
else
{
expression = expression.Or(lambda);
}
}
}
if (expression != null)
{
return originalList.Where(expression);
}
return originalList;
}
}
private bool CanConvertToType(object value, string type)
{
bool canConvert;
try
{
var cValue = ConvertToType(value, type);
canConvert = true;
}
catch
{
canConvert = false;
}
return canConvert;
}
private dynamic ConvertToType(object value, string type)
{
return Convert.ChangeType(value, Type.GetType(type));
}