生成具有 运行 次指定 Return 类型的多参数 LINQ 搜索查询
Generate Multi-Parameter LINQ Search Queries with Run-time Specified Return Type
这个问题解决了好久,分享一下解决方法。
背景
我维护一个主要功能为管理订单的大型 Web 应用程序。它是一个 MVC over C# 应用程序,使用 EF6 处理数据。
有很多搜索屏幕。搜索屏幕都有多个参数和 return 不同的对象类型。
问题
每个搜索屏幕都有:
- 带有搜索参数的 ViewModel
- 处理搜索事件的控制器方法
- 为该屏幕提取正确数据的方法
- 一种将所有搜索过滤器应用于数据集的方法
- 一种将结果转换为新结果 ViewModel 的方法
- 结果视图模型
这加起来很快。我们有大约 14 个不同的搜索屏幕,这意味着大约有 84 个模型和方法来处理这些搜索。
我的目标
我希望能够创建一个 class,类似于当前的搜索参数 ViewModel,它将继承自基础 SearchQuery class,这样我的控制器就可以简单地触发对 运行 填充同一对象的结果字段。
我的理想状态的一个例子(因为它很难解释)
采用以下class结构:
public class Order
{
public int TxNumber;
public Customer OrderCustomer;
public DateTime TxDate;
}
public class Customer
{
public string Name;
public Address CustomerAddress;
}
public class Address
{
public int StreetNumber;
public string StreetName;
public int ZipCode;
}
假设我有很多可查询格式的记录——一个 EF DBContext 对象,一个 XML 对象,等等——我想搜索它们。首先,我创建了一个派生的 class 特定于我的 ResultType(在本例中为 Order)。
public class OrderSearchFilter : SearchQuery
{
//this type specifies that I want my query result to be List<Order>
public OrderSearchFilter() : base(typeof(Order)) { }
[LinkedField("TxDate")]
[Comparison(ExpressionType.GreaterThanOrEqual)]
public DateTime? TransactionDateFrom { get; set; }
[LinkedField("TxDate")]
[Comparison(ExpressionType.LessThanOrEqual)]
public DateTime? TransactionDateTo { get; set; }
[LinkedField("")]
[Comparison(ExpressionType.Equal)]
public int? TxNumber { get; set; }
[LinkedField("Order.OrderCustomer.Name")]
[Comparison(ExpressionType.Equal)]
public string CustomerName { get; set; }
[LinkedField("Order.OrderCustomer.CustomerAddress.ZipCode")]
[Comparison(ExpressionType.Equal)]
public int? CustomerZip { get; set; }
}
我使用属性来指定 field/property 任何给定搜索字段链接到的目标 ResultType,以及比较类型 (== < > <= >= !=)。空白的 LinkedField 表示搜索字段的名称与目标对象字段的名称相同。
通过此配置,对于给定的搜索我唯一需要的是:
- 像上面那样填充的搜索对象
- 一个数据源
不需要其他特定于场景的编码!
解决方案
首先,我们创建:
public abstract class SearchQuery
{
public Type ResultType { get; set; }
public SearchQuery(Type searchResultType)
{
ResultType = searchResultType;
}
}
我们还将创建上面用于定义搜索字段的属性:
protected class Comparison : Attribute
{
public ExpressionType Type;
public Comparison(ExpressionType type)
{
Type = type;
}
}
protected class LinkedField : Attribute
{
public string TargetField;
public LinkedField(string target)
{
TargetField = target;
}
}
对于每个搜索字段,我们不仅需要知道搜索的内容,还需要知道搜索是否完成。例如,如果 "TxNumber" 的值为 null,我们就不想 运行 该搜索。因此,我们创建了一个 SearchField object,除了实际的搜索值外,它还包含两个表达式:一个表示执行搜索,另一个验证是否应应用搜索。
private class SearchFilter<T>
{
public Expression<Func<object, bool>> ApplySearchCondition { get; set; }
public Expression<Func<T, bool>> SearchExpression { get; set; }
public object SearchValue { get; set; }
public IQueryable<T> Apply(IQueryable<T> query)
{
//if the search value meets the criteria (e.g. is not null), apply it; otherwise, just return the original query.
bool valid = ApplySearchCondition.Compile().Invoke(SearchValue);
return valid ? query.Where(SearchExpression) : query;
}
}
一旦我们创建了所有过滤器,我们需要做的就是遍历它们并在我们的数据集上调用 "Apply" 方法!简单!
下一步是创建验证表达式。我们将根据类型执行此操作;每个整数?被验证为与其他所有 int 相同?
private static Expression<Func<object, bool>> GetValidationExpression(Type type)
{
//throw exception for non-nullable types (strings are nullable, but is a reference type and thus has to be called out separately)
if (type != typeof(string) && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)))
throw new Exception("Non-nullable types not supported.");
//strings can't be blank, numbers can't be 0, and dates can't be minvalue
if (type == typeof(string )) return t => !string.IsNullOrWhiteSpace((string)t);
if (type == typeof(int? )) return t => t != null && (int)t >= 0;
if (type == typeof(decimal? )) return t => t != null && (decimal)t >= decimal.Zero;
if (type == typeof(DateTime?)) return t => t != null && (DateTime?)t != DateTime.MinValue;
//everything else just can't be null
return t => t != null;
}
这就是我的应用程序所需的全部内容,但肯定还有更多验证可以完成。
搜索表达式稍微复杂一些,需要一个解析器来 "De-qualify" Field/Property 名称(可能有更好的词,但如果是的话,我不知道)。基本上,如果我将 "Order.Customer.Name" 指定为链接字段并且我正在通过订单进行搜索,我需要将其转换为 "Customer.Name",因为订单 object 中没有订单字段。或者至少我希望不会。 :) 这不确定,但我认为接受和更正 fully-qualified object 名称比支持边缘情况更好。
public static List<string> DeQualifyFieldName(string targetField, Type targetType)
{
var r = targetField.Split('.').ToList();
foreach (var p in targetType.Name.Split('.'))
if (r.First() == p) r.RemoveAt(0);
return r;
}
这只是直接的文本解析,returns "levels" 中的字段名称(例如 "Customer"|"Name")。
好的,让我们把搜索表达式放在一起。
private Expression<Func<T, bool>> GetSearchExpression<T>(
string targetField, ExpressionType comparison, object value)
{
//get the property or field of the target object (ResultType)
//which will contain the value to be checked
var param = Expression.Parameter(ResultType, "t");
Expression left = null;
foreach (var part in DeQualifyFieldName(targetField, ResultType))
left = Expression.PropertyOrField(left == null ? param : left, part);
//Get the value against which the property/field will be compared
var right = Expression.Constant(value);
//join the expressions with the specified operator
var binaryExpression = Expression.MakeBinary(comparison, left, right);
return Expression.Lambda<Func<T, bool>>(binaryExpression, param);
}
还不错!例如,我们要创建的是:
t => t.Customer.Name == "Searched Name"
其中 t 是我们的 ReturnType——在本例中为订单。首先我们创建参数 t。然后,我们遍历 property/field 名称的各个部分,直到我们获得目标 object 的完整标题(将其命名为 "left" 因为它在我们比较的左侧)。我们比较的 "right" 方面很简单:用户提供的常量。
然后我们创建二进制表达式并将其转换为 lambda。就像从木头上掉下来一样容易!无论如何,如果从原木上掉下来需要无数个小时的挫折和失败的方法。但是我跑题了。
我们现在已经准备好了所有的零件;我们只需要一种方法来 assemble 我们的查询:
protected IQueryable<T> ApplyFilters<T>(IQueryable<T> data)
{
if (data == null) return null;
IQueryable<T> retVal = data.AsQueryable();
//get all the fields and properties that have search attributes specified
var fields = GetType().GetFields().Cast<MemberInfo>()
.Concat(GetType().GetProperties())
.Where(f => f.GetCustomAttribute(typeof(LinkedField)) != null)
.Where(f => f.GetCustomAttribute(typeof(Comparison)) != null);
//loop through them and generate expressions for validation and searching
try
{
foreach (var f in fields)
{
var value = f.MemberType == MemberTypes.Property ? ((PropertyInfo)f).GetValue(this) : ((FieldInfo)f).GetValue(this);
if (value == null) continue;
Type t = f.MemberType == MemberTypes.Property ? ((PropertyInfo)f).PropertyType : ((FieldInfo)f).FieldType;
retVal = new SearchFilter<T>
{
SearchValue = value,
ApplySearchCondition = GetValidationExpression(t),
SearchExpression = GetSearchExpression<T>(GetTargetField(f), ((Comparison)f.GetCustomAttribute(typeof(Comparison))).Type, value)
}.Apply(retVal); //once the expressions are generated, go ahead and (try to) apply it
}
}
catch (Exception ex) { throw (ErrorInfo = ex); }
return retVal;
}
基本上,我们只是在派生的 class(已链接)中获取 fields/properties 的列表,从中创建一个 SearchFilter object,然后应用它们。
Clean-Up
当然还有更多。例如,我们用字符串指定 object 链接。如果有错字怎么办?
在我的例子中,每当它启动派生 class 的实例时,我都会进行 class 检查,如下所示:
private bool ValidateLinkedField(string fieldName)
{
//loop through the "levels" (e.g. Order / Customer / Name) validating that the fields/properties all exist
Type currentType = ResultType;
foreach (string currentLevel in DeQualifyFieldName(fieldName, ResultType))
{
MemberInfo match = (MemberInfo)currentType.GetField(currentLevel) ?? currentType.GetProperty(currentLevel);
if (match == null) return false;
currentType = match.MemberType == MemberTypes.Property ? ((PropertyInfo)match).PropertyType
: ((FieldInfo)match).FieldType;
}
return true; //if we checked all levels and found matches, exit
}
其余的都是实施细节。如果您有兴趣查看,包含完整实现(包括测试数据)的项目是 here。这是一个 VS 2015 项目,但如果这是一个问题,只需获取 Program.cs 和 Search.cs 文件并将它们放入您选择的 IDE 中的新项目中。
感谢 Whosebug 上所有提出问题并写下帮助我整理这些答案的人!
这个问题解决了好久,分享一下解决方法。
背景
我维护一个主要功能为管理订单的大型 Web 应用程序。它是一个 MVC over C# 应用程序,使用 EF6 处理数据。
有很多搜索屏幕。搜索屏幕都有多个参数和 return 不同的对象类型。
问题
每个搜索屏幕都有:
- 带有搜索参数的 ViewModel
- 处理搜索事件的控制器方法
- 为该屏幕提取正确数据的方法
- 一种将所有搜索过滤器应用于数据集的方法
- 一种将结果转换为新结果 ViewModel 的方法
- 结果视图模型
这加起来很快。我们有大约 14 个不同的搜索屏幕,这意味着大约有 84 个模型和方法来处理这些搜索。
我的目标
我希望能够创建一个 class,类似于当前的搜索参数 ViewModel,它将继承自基础 SearchQuery class,这样我的控制器就可以简单地触发对 运行 填充同一对象的结果字段。
我的理想状态的一个例子(因为它很难解释)
采用以下class结构:
public class Order
{
public int TxNumber;
public Customer OrderCustomer;
public DateTime TxDate;
}
public class Customer
{
public string Name;
public Address CustomerAddress;
}
public class Address
{
public int StreetNumber;
public string StreetName;
public int ZipCode;
}
假设我有很多可查询格式的记录——一个 EF DBContext 对象,一个 XML 对象,等等——我想搜索它们。首先,我创建了一个派生的 class 特定于我的 ResultType(在本例中为 Order)。
public class OrderSearchFilter : SearchQuery
{
//this type specifies that I want my query result to be List<Order>
public OrderSearchFilter() : base(typeof(Order)) { }
[LinkedField("TxDate")]
[Comparison(ExpressionType.GreaterThanOrEqual)]
public DateTime? TransactionDateFrom { get; set; }
[LinkedField("TxDate")]
[Comparison(ExpressionType.LessThanOrEqual)]
public DateTime? TransactionDateTo { get; set; }
[LinkedField("")]
[Comparison(ExpressionType.Equal)]
public int? TxNumber { get; set; }
[LinkedField("Order.OrderCustomer.Name")]
[Comparison(ExpressionType.Equal)]
public string CustomerName { get; set; }
[LinkedField("Order.OrderCustomer.CustomerAddress.ZipCode")]
[Comparison(ExpressionType.Equal)]
public int? CustomerZip { get; set; }
}
我使用属性来指定 field/property 任何给定搜索字段链接到的目标 ResultType,以及比较类型 (== < > <= >= !=)。空白的 LinkedField 表示搜索字段的名称与目标对象字段的名称相同。
通过此配置,对于给定的搜索我唯一需要的是:
- 像上面那样填充的搜索对象
- 一个数据源
不需要其他特定于场景的编码!
解决方案
首先,我们创建:
public abstract class SearchQuery
{
public Type ResultType { get; set; }
public SearchQuery(Type searchResultType)
{
ResultType = searchResultType;
}
}
我们还将创建上面用于定义搜索字段的属性:
protected class Comparison : Attribute
{
public ExpressionType Type;
public Comparison(ExpressionType type)
{
Type = type;
}
}
protected class LinkedField : Attribute
{
public string TargetField;
public LinkedField(string target)
{
TargetField = target;
}
}
对于每个搜索字段,我们不仅需要知道搜索的内容,还需要知道搜索是否完成。例如,如果 "TxNumber" 的值为 null,我们就不想 运行 该搜索。因此,我们创建了一个 SearchField object,除了实际的搜索值外,它还包含两个表达式:一个表示执行搜索,另一个验证是否应应用搜索。
private class SearchFilter<T>
{
public Expression<Func<object, bool>> ApplySearchCondition { get; set; }
public Expression<Func<T, bool>> SearchExpression { get; set; }
public object SearchValue { get; set; }
public IQueryable<T> Apply(IQueryable<T> query)
{
//if the search value meets the criteria (e.g. is not null), apply it; otherwise, just return the original query.
bool valid = ApplySearchCondition.Compile().Invoke(SearchValue);
return valid ? query.Where(SearchExpression) : query;
}
}
一旦我们创建了所有过滤器,我们需要做的就是遍历它们并在我们的数据集上调用 "Apply" 方法!简单!
下一步是创建验证表达式。我们将根据类型执行此操作;每个整数?被验证为与其他所有 int 相同?
private static Expression<Func<object, bool>> GetValidationExpression(Type type)
{
//throw exception for non-nullable types (strings are nullable, but is a reference type and thus has to be called out separately)
if (type != typeof(string) && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)))
throw new Exception("Non-nullable types not supported.");
//strings can't be blank, numbers can't be 0, and dates can't be minvalue
if (type == typeof(string )) return t => !string.IsNullOrWhiteSpace((string)t);
if (type == typeof(int? )) return t => t != null && (int)t >= 0;
if (type == typeof(decimal? )) return t => t != null && (decimal)t >= decimal.Zero;
if (type == typeof(DateTime?)) return t => t != null && (DateTime?)t != DateTime.MinValue;
//everything else just can't be null
return t => t != null;
}
这就是我的应用程序所需的全部内容,但肯定还有更多验证可以完成。
搜索表达式稍微复杂一些,需要一个解析器来 "De-qualify" Field/Property 名称(可能有更好的词,但如果是的话,我不知道)。基本上,如果我将 "Order.Customer.Name" 指定为链接字段并且我正在通过订单进行搜索,我需要将其转换为 "Customer.Name",因为订单 object 中没有订单字段。或者至少我希望不会。 :) 这不确定,但我认为接受和更正 fully-qualified object 名称比支持边缘情况更好。
public static List<string> DeQualifyFieldName(string targetField, Type targetType)
{
var r = targetField.Split('.').ToList();
foreach (var p in targetType.Name.Split('.'))
if (r.First() == p) r.RemoveAt(0);
return r;
}
这只是直接的文本解析,returns "levels" 中的字段名称(例如 "Customer"|"Name")。
好的,让我们把搜索表达式放在一起。
private Expression<Func<T, bool>> GetSearchExpression<T>(
string targetField, ExpressionType comparison, object value)
{
//get the property or field of the target object (ResultType)
//which will contain the value to be checked
var param = Expression.Parameter(ResultType, "t");
Expression left = null;
foreach (var part in DeQualifyFieldName(targetField, ResultType))
left = Expression.PropertyOrField(left == null ? param : left, part);
//Get the value against which the property/field will be compared
var right = Expression.Constant(value);
//join the expressions with the specified operator
var binaryExpression = Expression.MakeBinary(comparison, left, right);
return Expression.Lambda<Func<T, bool>>(binaryExpression, param);
}
还不错!例如,我们要创建的是:
t => t.Customer.Name == "Searched Name"
其中 t 是我们的 ReturnType——在本例中为订单。首先我们创建参数 t。然后,我们遍历 property/field 名称的各个部分,直到我们获得目标 object 的完整标题(将其命名为 "left" 因为它在我们比较的左侧)。我们比较的 "right" 方面很简单:用户提供的常量。
然后我们创建二进制表达式并将其转换为 lambda。就像从木头上掉下来一样容易!无论如何,如果从原木上掉下来需要无数个小时的挫折和失败的方法。但是我跑题了。
我们现在已经准备好了所有的零件;我们只需要一种方法来 assemble 我们的查询:
protected IQueryable<T> ApplyFilters<T>(IQueryable<T> data)
{
if (data == null) return null;
IQueryable<T> retVal = data.AsQueryable();
//get all the fields and properties that have search attributes specified
var fields = GetType().GetFields().Cast<MemberInfo>()
.Concat(GetType().GetProperties())
.Where(f => f.GetCustomAttribute(typeof(LinkedField)) != null)
.Where(f => f.GetCustomAttribute(typeof(Comparison)) != null);
//loop through them and generate expressions for validation and searching
try
{
foreach (var f in fields)
{
var value = f.MemberType == MemberTypes.Property ? ((PropertyInfo)f).GetValue(this) : ((FieldInfo)f).GetValue(this);
if (value == null) continue;
Type t = f.MemberType == MemberTypes.Property ? ((PropertyInfo)f).PropertyType : ((FieldInfo)f).FieldType;
retVal = new SearchFilter<T>
{
SearchValue = value,
ApplySearchCondition = GetValidationExpression(t),
SearchExpression = GetSearchExpression<T>(GetTargetField(f), ((Comparison)f.GetCustomAttribute(typeof(Comparison))).Type, value)
}.Apply(retVal); //once the expressions are generated, go ahead and (try to) apply it
}
}
catch (Exception ex) { throw (ErrorInfo = ex); }
return retVal;
}
基本上,我们只是在派生的 class(已链接)中获取 fields/properties 的列表,从中创建一个 SearchFilter object,然后应用它们。
Clean-Up
当然还有更多。例如,我们用字符串指定 object 链接。如果有错字怎么办?
在我的例子中,每当它启动派生 class 的实例时,我都会进行 class 检查,如下所示:
private bool ValidateLinkedField(string fieldName)
{
//loop through the "levels" (e.g. Order / Customer / Name) validating that the fields/properties all exist
Type currentType = ResultType;
foreach (string currentLevel in DeQualifyFieldName(fieldName, ResultType))
{
MemberInfo match = (MemberInfo)currentType.GetField(currentLevel) ?? currentType.GetProperty(currentLevel);
if (match == null) return false;
currentType = match.MemberType == MemberTypes.Property ? ((PropertyInfo)match).PropertyType
: ((FieldInfo)match).FieldType;
}
return true; //if we checked all levels and found matches, exit
}
其余的都是实施细节。如果您有兴趣查看,包含完整实现(包括测试数据)的项目是 here。这是一个 VS 2015 项目,但如果这是一个问题,只需获取 Program.cs 和 Search.cs 文件并将它们放入您选择的 IDE 中的新项目中。
感谢 Whosebug 上所有提出问题并写下帮助我整理这些答案的人!