Generate multi-parameter LINQ search queries with a given return time

After spending a long time solving this problem, I wanted to share a solution.

Background

I support a large web application with a basic order management function. This is an MVC application on top of C # using EF6 for data.

There are many search screens. All search screens have several parameters and return different types of objects.

Problem

On each search screen was:

  • ViewModel with search options
  • Controller Method for Processing a Search Event
  • Method to pull the correct data for this screen
  • Method for applying all search filters to a dataset
  • Method to Convert Results to NEW ViewModel Results
  • Results ViewModel Results

. 14 , 84 .

, ViewModel, SearchQuery, , Results .

( )

:

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

, / ResultType - , (== < > <= > =! =). LinkedField , .

, , :

  • ,

- !

+1
1

:

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

WHAT-, WHUTHER, . , "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;
        }
    }

, , , "" ! !

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

, , , .

"De-qualify" Field/Property names (, , , ). , "Order.Customer.Name" Orders, "Customer.Name", Order Order. , , , .:) , , , .

    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"

t - ReturnType - , . t. /, , ( "", ). "" : , .

. , ! , , . .

; , , - :

    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 .

Clean-Up

, . , . , ?

, , :

    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 .

, StackOverflow, , !

+2

Source: https://habr.com/ru/post/1628909/


All Articles