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

本文是小编为大家收集整理的关于使用运行时指定的返回类型生成多参数Linq搜索查询的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

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

背景

我维护一个具有管理订单的主要功能的大型Web应用程序.它是使用EF6的C#应用​​程序上的MVC.

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

问题

每个搜索屏幕都有:

  • 带有搜索参数的ViewModel
  • 处理搜索事件的控制器方法
  • 一种删除该屏幕正确数据的方法
  • 一种将所有搜索过滤器应用于数据集的方法
  • 一种将结果转换为新结果ViewModel
  • 的方法
  • 结果ViewModel

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

我的目标

我希望能够创建类似于当前搜索参数ViewModel的类,该类将从基本搜索程序类中继承,以便我的控制器可以简单地触发搜索以运行以填充同一对象的结果字段.

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

进行以下类结构:

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

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

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

使用此配置,给定搜索我唯一需要的东西是:

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

不需要其他方案的编码!

推荐答案

解决方案

对于初学者,我们创建:

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,我们不想运行该搜索.因此,我们创建一个搜索域对象,除了实际搜索值外,还包含两个表达式:一个表示执行搜索的对象,一个验证是否应应用搜索.

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

我们创建了所有过滤器后,我们需要做的就是循环循环它们,并在数据集中调用"应用"方法!简单!

下一步是创建验证表达式.我们将基于类型进行此操作;每个int?被验证与其他所有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;
    }

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

搜索表达式稍微复杂一些,需要解析器才能"取消Qualify"字段/属性名称(可能有一个更好的单词,但是如果是的话,我不知道).基本上,如果我将" order.customer.name"指定为链接字段,并且我正在通过订单进行搜索,则我需要将其转换为" customer.name",因为订单对象中没有订单字段.或至少我希望不要. :)这不确定,但是我认为接受和纠正完全符合条件的对象名称比支持该边缘案例更好.

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

这只是直接解析,并在"级别"中返回字段名称(例如"客户" |" 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.然后,我们循环浏览属性/字段名称的各个部分,直到我们具有我们针对的对象的完整标题(将其命名为"左",因为它是我们比较的左侧).我们比较的"右"一侧很简单:用户提供的常数.

然后我们创建二进制表达式并将其变成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;
    }

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

清理

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

就我而言,每当启用派生类的实例时,我都会检查一下:

:

    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 and Search.cs文件,然后将它们扔进您选择的IDE中.

感谢stackoverflow上的每个人都问了问题,并写了帮助我将其放在一起的答案!

本文地址:https://www.itbaoku.cn/post/1557069.html

问题描述

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

Background

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.

The Problem

Every search screen had:

  • A ViewModel with the search parameters
  • A Controller method to handle the Search event
  • A method to pull the correct data for that screen
  • A method to apply all the search filters to the dataset
  • A method to convert the results into a NEW results ViewModel
  • The Results ViewModel

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

My Goal

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.

An Example of My Ideal State (Because It's a Bear To Explain)

Take the following class structure:

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

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

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:

  • A populated search object like the one above
  • A data source

No other scenario-specific coding should be required!

推荐答案

The Solution

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

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

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!

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.

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"

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.

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

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.

Clean-Up

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
    }

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.

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