生成具有运行时指定的返回类型的多参数LINQ搜索查询 [英] Generate Multi-Parameter LINQ Search Queries with Run-time Specified Return Type

查看:60
本文介绍了生成具有运行时指定的返回类型的多参数LINQ搜索查询的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

花了很长时间解决这个问题,我想分享解决方案.

Having spent a long time solving this problem, I wanted to share the solution.

背景

我维护着一个大型Web应用程序,其主要功能是管理订单.这是一个使用EF6的C#MVC应用程序.

I maintain a large web application with the primary function of managing orders. It is an MVC over C# application using EF6 for data.

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

There are LOTS of search screens. The search screens all have multiple parameters and return different object types.

问题

每个搜索屏幕都有:

  • 具有搜索参数的ViewModel
  • 用于处理Search事件的Controller方法
  • 一种为该屏幕提取正确数据的方法
  • 一种将所有搜索过滤器应用于数据集的方法
  • 将结果转换为新结果ViewModel的方法
  • 结果ViewModel

这很快就加起来了.我们大约有14种不同的搜索屏幕,这意味着大约有84种型号和这些搜索的方法.

This adds up quickly. We have about 14 different search screens, which means about 84 models & methods to handle these searches.

我的目标

我希望能够创建一个类似于当前搜索参数ViewModel的类,该类将从基本SearchQuery类继承,以便我的Controller只需触发搜索即可运行以填充同一对象的Results字段.

I wanted to be able to create a class, analogous to the current search parameter ViewModel, that would inherit from a base SearchQuery class such that my Controller could simply trigger the search to run to populate a Results field of the same object.

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

采用以下类结构:

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).

Let's assume I have lots of those records in a queryable format--an EF DBContext object, an XML object, whatever--and I want to search them. First, I create a derived class specific to my ResultType(in this case, 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; }
}

我使用属性来指定任何给定搜索字段所链接到的目标ResultType的字段/属性以及比较类型(==<>< => =!=).空白的LinkedField表示搜索字段的名称与目标对象字段的名称相同.

I use attributes to specify what field/property of the target ResultType any given search field is linked to, as well as the comparison type (== < > <= >= !=). A blank LinkedField means that the name of the search field is the same as the name of the target object field.

配置此后,对于给定的搜索,我唯一需要做的是:

With this configured, the only things I should need for a given search are:

  • 一个填充的搜索对象,如上面的一个
  • 数据源

不需要其他特定于场景的编码!

No other scenario-specific coding should be required!

推荐答案

解决方案

对于初学者,我们创建:

For starters, we create:

public abstract class SearchQuery 
{
    public Type ResultType { get; set; }
    public SearchQuery(Type searchResultType)
    {
        ResultType = searchResultType;
    }
}

我们还将创建上面用于定义搜索字段的属性:

We'll also create the attributes we used above to define the search field:

    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对象,该对象除了实际的搜索值外,还包含两个表达式:一个表示执行搜索,另一个用于验证是否应应用搜索.

For each search field, we'll need to know not only WHAT search is done, but also WHETHER the search is done. For example, if the value of "TxNumber" is null, we wouldn't want to run that search. So we create a SearchField object that contains, in addition to the actual search value, two expressions: one that represents performing the search, and one that validates whether the search should be applied.

    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"方法!容易!

Once we have created all our filters, all we need to do is loop through them and call the "Apply" method on our dataset! Easy!

下一步是创建验证表达式.我们将基于Type进行此操作;每个int?是否与其他所有int相同?

The next step is creating the validation expressions. We'll do this based on the Type; every int? is validated the same as every other 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;
    }

这是我应用程序所需的全部,但是肯定可以做更多的验证.

This was all I needed for my application, but there is definitely more validation that could be done.

搜索表达式稍微复杂些,并且需要解析器来取消限定"字段/属性名称(可能有一个更好的词,但如果这样,我不知道).基本上,如果我将"Order.Customer.Name"指定为链接字段并且正在搜索Orders,则需要将其转换为"Customer.Name",因为Order对象中没有Order Field.或者至少我希望不会. :)不确定,但是我认为接受并更正完全合格的对象名称比支持这种边缘情况更好.

The search expression is slightly more complicated and required a parser to "De-qualify" Field/Property names (there's probably a better word, but if so, I don't know it). Basically, if I specified "Order.Customer.Name" as a linked field and I'm searching through Orders, I need to turn that into "Customer.Name" because there is no Order Field inside an Order object. Or at least I hope not. :) This isn't certain, but I considered it better to accept and correct fully-qualified object names than to support that edge case.

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

这只是直接的文本解析,并以级别"(例如客户" |名称")返回字段名称.

This is just straight text parsing, and returns the Field name in "levels" (e.g. "Customer"|"Name").

好的,让我们一起搜索表达式.

All right, let's get our search expression together.

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

还不错!例如,我们试图创建的是

Not so bad! What we're trying to create is, for example:

t => t.Customer.Name == "Searched Name"

在这里t是我们的ReturnType,在这种情况下是订单.首先,我们创建参数t.然后,我们遍历属性/字段名称的各个部分,直到获得我们要定位的对象的完整标题(将其命名为"left",因为它是我们比较的左侧).我们进行比较的右边"很简单:用户提供的常数.

Where t is our ReturnType--an Order, in this case. First we create the parameter, t. Then, we loop through the parts of the property/field name until we have the full title of the object we're targeting (naming it "left" because it's the left side of our comparison). The "right" side of our comparison is simple: the constant provided by the user.

然后,我们创建二进制表达式并将其转换为lambda.就像掉落日志一样容易!无论如何,如果要掉落日志,则需要数小时的挫败感和失败的方法论.但是我离题了.

Then we create the binary expression and turn it into a lambda. Easy as falling off a log! If falling off a log required countless hours of frustration and failed methodologies, anyway. But I digress.

我们现在已经掌握了一切;我们需要的只是一种组装查询的方法:

We've got all the pieces now; all we need is a method to assemble our query:

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

基本上,我们只获取派生类(链接的)中的字段/属性的列表,从它们中创建SearchFilter对象,然后应用它们.

Basically, we just grab a list of fields/properties in the derived class (that are linked), create a SearchFilter object from them, and apply them.

清理

当然还有更多.例如,我们使用字符串指定对象链接.如果有错字怎么办?

There's a bit more, of course. For example, we're specifying object links with strings. What if there's a typo?

就我而言,每当它旋转派生类的实例时,我都要进行类检查,如下所示:

In my case, I have the class check whenever it spins up an instance of a derived class, like this:

    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
    }

剩下的就是所有实现细节.如果您有兴趣将其签出,那么一个包含完整实施(包括测试数据)的项目为这里.这是一个VS 2015项目,但是如果有问题,只需获取Program.cs和Search.cs文件,然后将其放入您选择的IDE中的新项目中即可.

The rest is all implementation minutia. If you're interested in checking it out, a project that includes a full implementation, including test data, is here. It's a VS 2015 project, but if that's an issue, just grab the Program.cs and Search.cs files and throw them into a new project in your IDE of choice.

感谢StackOverflow上的每个人都提出了问题并写下了答案,这有助于我将这些问题汇总在一起!

Thanks to everyone on StackOverflow who asked the questions and wrote the answers that helped me put this together!

这篇关于生成具有运行时指定的返回类型的多参数LINQ搜索查询的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆