C# 使用运行时指定的返回类型生成多参数LINQ搜索查询

C# 使用运行时指定的返回类型生成多参数LINQ搜索查询,c#,entity-framework,linq,lambda,expression-trees,C#,Entity Framework,Linq,Lambda,Expression Trees,在花了很长时间解决这个问题后,我想与大家分享解决方案 背景 我维护一个大型web应用程序,其主要功能是管理订单。它是一个MVC over C#应用程序,使用EF6处理数据 有很多搜索屏幕。搜索屏幕都有多个参数,并返回不同的对象类型 问题 每个搜索屏幕都有: 带有搜索参数的ViewModel 处理搜索事件的控制器方法 为该屏幕提取正确数据的方法 将所有搜索筛选器应用于数据集的方法 将结果转换为新结果视图模型的方法 结果视图模型 这加起来很快。我们有大约14个不同的搜索屏幕,这意味着大约84个模

在花了很长时间解决这个问题后,我想与大家分享解决方案

背景

我维护一个大型web应用程序,其主要功能是管理订单。它是一个MVC over C#应用程序,使用EF6处理数据

有很多搜索屏幕。搜索屏幕都有多个参数,并返回不同的对象类型

问题

每个搜索屏幕都有:

  • 带有搜索参数的ViewModel
  • 处理搜索事件的控制器方法
  • 为该屏幕提取正确数据的方法
  • 将所有搜索筛选器应用于数据集的方法
  • 将结果转换为新结果视图模型的方法
  • 结果视图模型
这加起来很快。我们有大约14个不同的搜索屏幕,这意味着大约84个模型和方法来处理这些搜索

我的目标

我希望能够创建一个类,类似于当前的搜索参数ViewModel,它将从一个基本的SearchQuery类继承,这样我的控制器就可以简单地触发搜索运行,以填充同一对象的结果字段

我理想状态的一个例子(因为它是一个熊来解释)

以以下班级结构为例:

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对象等等——我想搜索它们。首先,我创建一个特定于我的ResultType的派生类(在本例中为Order)

公共类OrderSearchFilter:SearchQuery { //此类型指定我希望查询结果是列表 public OrderSearchFilter():base(typeof(Order)){} [链接字段(“TxDate”)] [比较(表达式类型.大于或等于)] 公共日期时间?TransactionDateFrom{get;set;} [链接字段(“TxDate”)] [比较(ExpressionType.lessthanRequal)] 公共日期时间?TransactionDateTo{get;set;} [链接字段(“”) [比较(ExpressionType.Equal)] 公共整数?TxNumber{get;set;} [LinkedField(“Order.OrderCustomer.Name”)] [比较(ExpressionType.Equal)] 公共字符串CustomerName{get;set;} [LinkedField(“Order.OrderCustomer.CustomerAddress.ZipCode”)] [比较(ExpressionType.Equal)] public int?CustomerZip{get;set;} } 我使用属性指定任何给定搜索字段链接到的目标结果类型的字段/属性,以及比较类型(=<>=!=)。空白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对象,它除了包含实际的搜索值外,还包含两个表达式:一个表示执行搜索,另一个验证是否应应用搜索

    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;
        }
    }
其中t是我们的返回类型——在本例中是订单。首先我们创建参数t。然后,我们循环遍历属性/字段名的各个部分,直到得到目标对象的完整标题(将其命名为“左”,因为它是比较的左侧)。我们比较的“正确”方面很简单:用户提供的常量

然后我们创建二进制表达式并将其转换为lambda。像从木头上掉下来一样容易!不管怎么说,从日志上掉下来需要无数个小时的挫折和失败的方法。但我离题了

我们现在已经拥有了所有的碎片;我们只需要一个方法来组合查询:

    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;
    }
其余的都是实现细节。如果您对签出感兴趣,那么一个包含完整实现(包括测试数据)的项目是非常有用的。这是一个VS 2015项目,但如果这是一个问题,只需抓取Program.cs和Search.cs文件,并将它们放入您选择的IDE中的新项目中即可

感谢StackOverflow上的每个人,他们提出了问题,并写下了答案,帮助我把这些放在一起

    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;
    }
    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;
    }
    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"
    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;
    }
    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
    }