使用自定义运算符组合Lambda表达式

时间:2016-09-05 06:58:10

标签: c# elasticsearch lambda expression

我实现了一个搜索页面,用户可以使用一堆不同的过滤器过滤结果。我需要将Lambda Expression传递给搜索层,以便进行过滤。 我的问题是我不知道如何动态构建Lambda表达式。

我尝试使用AndAlso()组合表达式,但由于我的Lambda表达式没有返回bool,因此无法正常工作。 所以我猜我需要实现一个ExpressionVisitor以及我现在迷失的地方。

// The custom operator for AFilterType
public static AFilterType operator &(AFilterType first, AFilterType second)
{
    return new AndFilter(AFilterType.GetFilters<AndFilter>(first, second));
}

// Here's a simplified version of what I'm trying to do
var filterInput = new FilterInput() { FirstName = "John", LastName = "Doe" };

// Using Match() which is a AFilterType method
Expression<Func<Person, AFilterType>> firstNameFilterExpression = x => x.firstName.Match(filterInput.FirstName);
Expression<Func<Person, AFilterType>> lastNameFilterExpression = x => x.LastName.Match(filterInput.LastName);

// How can I combine those 2 expressions into 1 single Expression at runtime using the custom operator '&' (not the bool '&&').
// Combined Expression should be like this.
Expression<Func<Person, AFilterType>> combinedFilterExpression = x => x.firstName.Match(filterInput.FirstName) & x.LastName.Match(filterInput.LastName);

2 个答案:

答案 0 :(得分:2)

我曾经遇到过同样的问题,我使用LinqKit和一些反思来解决它(我已经在EntityFramework项目中使用了它,但如果需要,它可以适应其他类型)。我将尝试在下面发布我的剥离代码(希望它不会太久)。

易感性:包含LinqKit(https://www.nuget.org/packages/LinqKit或通过NuGet)版本1.1.7.2或更高版本。

代码由子目录中的几个文件组成:

  • 接口
  • 框架
  • 扩展
  

接口\ IPredicateParser.cs

using System;
using System.Collections.Generic;

namespace LambdaSample.Interfaces
{
    // Used to defined IPredicateParser for parsing predicates
    public interface IPredicateParser
    {
        bool Parse(string text, bool rangesAllowed, Type definedType);

        List<IPredicateItem> Items { get; }
    }
}
  

接口\ IPredicateItem.cs

namespace LambdaSample.Interfaces
{
    public interface IPredicateItem
    {
        bool IsValid { get; }
    }
}
  

框架\ PredicateItemSingle.cs

using LambdaSample.Interfaces;

namespace LambdaSample.Framework
{
    /// <summary>
    /// Item for single predicate (e.g. "44")
    /// </summary>
    public class PredicateItemSingle : IPredicateItem
    {
        public PredicateItemSingle()
        {

        }

        public bool IsValid => Value != null;


        public object Value { get; set; }
    }
}
  

框架\ PredicateItemRange.cs

using LambdaSample.Interfaces;

namespace LambdaSample.Framework
{
    /// <summary>
    /// Item for range predicates (e.g. "1-5")
    /// </summary>
    public class PredicateItemRange : IPredicateItem
    {
        public PredicateItemRange()
        {

        }

        public bool IsValid => Value1 != null && Value2 != null;

        public object Value1 { get; set; }

        public object Value2 { get; set; }
    }
}
  

框架\ PredicateParser.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using LambdaSample.Extensions;
using LambdaSample.Interfaces;

namespace LambdaSample.Framework
{
    /// <summary>
    /// Simple parser for text used in search fields for
    /// searching through records or any values
    /// </summary>
    public class PredicateParser : IPredicateParser
    {
        private enum RangeType
        {
            None,
            From,
            To
        }

        public PredicateParser()
        {
            Items = new List<IPredicateItem>();
        }

        public bool Parse(string text, bool rangesAllowed, Type definedType)
        {
            Items.Clear();

            if (string.IsNullOrWhiteSpace(text))
                return true;

            var result = true;

            var items = text.Split(',');
            foreach (var item in items)
            {
                object val1, val2;
                bool isRange;
                var ranges = item.Split('-');
                if (rangesAllowed && ranges.Length == 2) // Range is only when ranges are allowed and length is 2, otherwise its single value.
                {
                    object val1Temp, val2Temp;
                    if (ParseValue(ranges[0], definedType, RangeType.From, out isRange, out val1, out val1Temp) &&
                        ParseValue(ranges[1], definedType, RangeType.To, out isRange, out val2, out val2Temp))
                    {
                        Items.Add(new PredicateItemRange { Value1 = val1, Value2 = val2, });
                    }
                    else
                    {
                        result = false;
                    }
                }
                else
                {
                    if (ParseValue(item, definedType, RangeType.None, out isRange, out val1, out val2))
                    {
                        if (isRange)
                        {
                            Items.Add(new PredicateItemRange { Value1 = val1, Value2 = val2, });
                        }
                        else
                        {
                            Items.Add(new PredicateItemSingle { Value = val1, });
                        }
                    }
                    else
                    {
                        result = false;
                    }
                }
            }

            return result;
        }

        private bool ParseValue(string value, Type definedType, RangeType rangeType, out bool isRange, out object result, out object result2)
        {
            result = null;
            result2 = null;
            isRange = false;
            if (string.IsNullOrWhiteSpace(value))
                return false;

            // Enums are also treated like ints!
            if (definedType == typeof(int) || definedType.IsEnum)
            {
                int val;
                if (!int.TryParse(value, out val))
                    return false;
                result = val;
                return true;
            }

            if (definedType == typeof(long))
            {
                long val;
                if (!long.TryParse(value, out val))
                    return false;
                result = val;
                return true;
            }

            if (definedType == typeof(decimal))
            {
                decimal val;
                if (!decimal.TryParse(value, NumberStyles.Number ^ NumberStyles.AllowThousands, new CultureInfo("sl-SI"), out val))
                    return false;
                result = val;
                return true;
            }

            if (definedType == typeof(DateTime))
            {
                int year, month, yearMonth;
                if (value.Length == 4 && int.TryParse(value, out year) && year >= 1000 && year <= 9999) // If only year, we set whole year's range (e.g. 2015 ==> 2015-01-01 00:00:00.0000000 - 2015-12-31 23:59:59.9999999
                {
                    // Default datetime for From range and if no range
                    result = new DateTime(year, 1, 1);
                    switch (rangeType)
                    {
                        case RangeType.None:
                            result2 = ((DateTime)result).AddYears(1).AddMilliseconds(-1);
                            isRange = true;
                            break;
                        case RangeType.To:
                            result = ((DateTime)result).AddYears(1).AddMilliseconds(-1);
                            break;
                    }
                    return true;
                }
                if (value.Length == 6 && int.TryParse(value, out yearMonth) && yearMonth >= 100001 && yearMonth <= 999912) // If only year and month, we set whole year's range (e.g. 201502 ==> 2015-02-01 00:00:00.0000000 - 2015-02-28 23:59:59.9999999
                {
                    year = Convert.ToInt32(yearMonth.ToString().Substring(0, 4));
                    month = Convert.ToInt32(yearMonth.ToString().Substring(4, 2));

                    // Default datetime for From range and if no range
                    result = new DateTime(year, month, 1);
                    switch (rangeType)
                    {
                        case RangeType.None:
                            result2 = ((DateTime)result).AddMonths(1).AddMilliseconds(-1);
                            isRange = true;
                            break;
                        case RangeType.To:
                            result = ((DateTime)result).AddMonths(1).AddMilliseconds(-1);
                            break;
                    }
                    return true;
                }

                DateTime val;
                if (!value.ParseDateTimeEx(CultureInfo.InvariantCulture, out val))
                {
                    return false;
                }

                if (val.Hour == 0 && val.Minute == 0)
                {
                    // No hours and minutes specified, searching whole day or to the end of the day.
                    // If this is no range, we make it a range
                    result = new DateTime(val.Year, val.Month, val.Day);
                    switch (rangeType)
                    {
                        case RangeType.None:
                            result2 = ((DateTime)result).AddDays(1).AddMilliseconds(-1);
                            isRange = true;
                            break;
                        case RangeType.To:
                            result = ((DateTime)result).AddDays(1).AddMilliseconds(-1);
                            break;
                    }
                    return true;
                }

                result = val;
                return true;
            }

            if (definedType == typeof(string))
            {
                result = value;
                return true;
            }

            return false;
        }

        public List<IPredicateItem> Items { get; private set; }
    }
}
  

扩展\ StringExtensions.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace LambdaSample.Extensions
{
    public static class StringExtensions
    {
        private static List<string> GetValidDateTimeFormats()
        {
            var dateFormats = new[]
            {
                "dd.MM.yyyy",
                "yyyy-MM-dd",
                "yyyyMMdd",
            }.ToList();
            var timeFormats = new[]
            {
                "HH:mm:ss.fff",
                "HH:mm:ss",
                "HH:mm",
            }.ToList();

            var result = (from dateFormat in dateFormats
                          from timeFormat in timeFormats
                          select $"{dateFormat} {timeFormat}").ToList();

            return result;
        }

        public static bool ParseDateTimeEx(this string @this, CultureInfo culture, out DateTime dateTime)
        {
            if (culture == null)
            {
                culture = CultureInfo.InvariantCulture;
            }

            if (DateTime.TryParse(@this, culture, DateTimeStyles.None, out dateTime))
                return true;

            var dateTimeFormats = GetValidDateTimeFormats();

            if (DateTime.TryParseExact(@this, dateTimeFormats.ToArray(), culture, DateTimeStyles.None, out dateTime))
                return true;

            return false;
        }
    }
}
  

扩展\ ObjectExtensions.cs

using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace LambdaSample.Extensions
{
    public static class ObjectExtensions
    {
        /// <summary>
        /// Build Filter Dictionary<string,string> used in ExpressionExtensions.BuildPredicate to build
        /// predicates for Predicate Builder based on class's properties values. Filters are then used
        /// by PredicateParser, which converts them to appropriate types (DateTime, int, decimal, etc.)
        /// </summary>
        /// <param name="this">Object to build dictionary from</param>
        /// <param name="includeNullValues">Includes null values in dictionary</param>
        /// <returns>Dictionary with string keys and string values</returns>
        public static Dictionary<string, string> ToFilterDictionary(this object @this, bool includeNullValues)
        {
            var result = new Dictionary<string, string>();
            if (@this == null || !@this.GetType().IsClass)
                return result;

            // First, generate Dictionary<string, string> from @this by using reflection
            var props = @this.GetType().GetProperties();
            foreach (var prop in props)
            {
                var value = prop.GetValue(@this);
                if (value == null && !includeNullValues)
                    continue;

                // If value already is a dictionary add items from this dictionary
                var dictValue = value as IDictionary;
                if (dictValue != null)
                {
                    foreach (var key in dictValue.Keys)
                    {
                        var valueTemp = dictValue[key];
                        if (valueTemp == null && !includeNullValues)
                            continue;
                        result.Add(key.ToString(), valueTemp != null ? valueTemp.ToString() : null);
                    }
                    continue;
                }

                // If property ends with list, check if list of generics
                if (prop.Name.EndsWith("List", false, CultureInfo.InvariantCulture))
                {
                    var propName = prop.Name.Remove(prop.Name.Length - 4, 4);
                    var sb = new StringBuilder();
                    var list = value as IEnumerable;
                    if (list != null)
                    {
                        foreach (var item in list)
                        {
                            if (item == null)
                                continue;
                            if (sb.Length > 0)
                                sb.Append(",");
                            sb.Append(item.ToString());
                        }
                        result.Add(propName, sb.ToString());
                    }
                    continue;
                }

                var str = value != null ? value.ToString() : null;
                result.Add(prop.Name, str);
            }

            return result;
        }
    }
}
  

扩展\ ExpressionExtensions.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using LambdaSample.Framework;
using LambdaSample.Interfaces;
using LinqKit;

namespace LambdaSample.Extensions
{
    public static class ExpressionExtensions
    {
        private static readonly MethodInfo StringContainsMethod = typeof(string).GetMethod(@"Contains", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, null);
        private static readonly MethodInfo StringStartsWithMethod = typeof(string).GetMethod(@"StartsWith", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, null);
        private static readonly MethodInfo StringEndsWithMethod = typeof(string).GetMethod(@"EndsWith", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, null);
        private static readonly MethodInfo ObjectEquals = typeof(object).GetMethod(@"Equals", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(object) }, null);
        //private static readonly MethodInfo BooleanEqualsMethod = typeof(bool).GetMethod(@"Equals", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(bool) }, null);

        /// <summary>
        /// Build a predicate with linq clauses, taking searchCriteria object's properties to define where conditions.
        /// </summary>
        /// <typeparam name="TDbType">Type of entity to build predicate for</typeparam>
        /// <param name="searchCriteria">Object which contains criteria for predicate</param>
        /// <param name="predicateParser">Implementation of predicate parser that will parse predicates as string</param>
        /// <param name="includeNullValues">Determines whether null values are included when constructing query</param>
        /// <returns></returns>
        public static Expression<Func<TDbType, bool>> BuildPredicate<TDbType>(object searchCriteria, IPredicateParser predicateParser, bool includeNullValues)
        {
            var filterDictionary = searchCriteria.ToFilterDictionary(includeNullValues);
            return BuildPredicate<TDbType>(filterDictionary, predicateParser);
        }

        public static Expression<Func<TDbType, bool>> BuildPredicate<TDbType>(Dictionary<string, string> searchCriteria, IPredicateParser predicateParser)
        {
            var predicateOuter = PredicateBuilder.New<TDbType>(true);
            var predicateErrorFields = new List<string>();

            var dict = searchCriteria;// as Dictionary<string, string>;
            if (dict == null || !dict.Any())
                return predicateOuter;

            var searchFields = typeof(TDbType).GetProperties();
            foreach (var searchField in searchFields)
            {
                // Get the name of the DB field, which may not be the same as the property name.
                var dbFieldName = GetDbFieldName(searchField);

                var dbType = typeof(TDbType);

                var dbFieldMemberInfo = dbType.GetMember(dbFieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).SingleOrDefault();
                if (dbFieldMemberInfo == null || !dict.ContainsKey(dbFieldMemberInfo.Name))
                    continue;

                var predicateValue = dict[dbFieldMemberInfo.Name];
                if (predicateValue == null)
                    continue;

                var rangesAllowed = searchField.PropertyType != typeof(string);
                if (!predicateParser.Parse(predicateValue, rangesAllowed, searchField.PropertyType))
                {
                    predicateErrorFields.Add(dbFieldMemberInfo.Name);
                    continue;
                }
                if (!predicateParser.Items.Any())
                    continue;

                var predicateInner = BuildInnerPredicate<TDbType>(predicateParser, searchField, dbFieldMemberInfo);
                if (predicateInner == null)
                    continue;

                predicateOuter = predicateOuter.And(predicateInner);
            }

            return predicateOuter;
        }

        private static Expression<Func<TDbType, bool>> BuildInnerPredicate<TDbType>(IPredicateParser predicateParser, PropertyInfo searchField, MemberInfo dbFieldMemberInfo)
        {
            var dbType = typeof(TDbType);

            // Create an "x" as TDbType
            var dbTypeParameter = Expression.Parameter(dbType, @"x");

            // Get at x.firstName
            var dbFieldMember = Expression.MakeMemberAccess(dbTypeParameter, dbFieldMemberInfo);

            Expression<Func<TDbType, bool>> predicateInner = null;

            foreach (var predicateItem in predicateParser.Items)
            {
                var predicateItemSingle = predicateItem as PredicateItemSingle;
                var predicateItemRange = predicateItem as PredicateItemRange;

                if (predicateItemSingle != null)
                {
                    // Create the MethodCallExpression like x.firstName.Contains(criterion)
                    if (searchField.PropertyType == typeof(string))
                    {
                        var str = predicateItemSingle.Value as string ?? "";
                        var startsWithAsterisk = str.StartsWith("*");
                        var endsWithAsterisk = str.EndsWith("*");
                        str = str.Trim('*').Trim();

                        MethodCallExpression callExpression;
                        if (startsWithAsterisk && !endsWithAsterisk)
                        {
                            callExpression = Expression.Call(dbFieldMember, StringEndsWithMethod, new Expression[] { Expression.Constant(str) });
                        }
                        else if (!startsWithAsterisk && endsWithAsterisk)
                        {
                            callExpression = Expression.Call(dbFieldMember, StringStartsWithMethod, new Expression[] { Expression.Constant(str) });
                        }
                        else
                        {
                            callExpression = Expression.Call(dbFieldMember, StringContainsMethod, new Expression[] { Expression.Constant(str) });
                        }
                        predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(Expression.Lambda(callExpression, dbTypeParameter) as Expression<Func<TDbType, bool>>);
                    }
                    else
                    {
                        if (dbFieldMember.Type.IsEnum)
                        {
                            if (!dbFieldMember.Type.IsEnumDefined(predicateItemSingle.Value))
                                continue;

                            var enumValue = (int)predicateItemSingle.Value;
                            if (enumValue <= 0)
                                continue;

                            var enumObj = Enum.ToObject(dbFieldMember.Type, (int)predicateItemSingle.Value);
                            predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(Expression.Lambda<Func<TDbType, bool>>(Expression.Equal(dbFieldMember, Expression.Constant(enumObj)), new[] { dbTypeParameter }));
                        }
                        else
                        {
                            predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(Expression.Lambda<Func<TDbType, bool>>(Expression.Equal(dbFieldMember, Expression.Constant(predicateItemSingle.Value)), new[] { dbTypeParameter }));
                        }
                    }
                }
                else if (predicateItemRange != null)
                {
                    var predicateRange = PredicateBuilder.New<TDbType>(true);
                    predicateRange = predicateRange.And(Expression.Lambda<Func<TDbType, bool>>(Expression.GreaterThanOrEqual(dbFieldMember, Expression.Constant(predicateItemRange.Value1)), new[] { dbTypeParameter }));
                    predicateRange = predicateRange.And(Expression.Lambda<Func<TDbType, bool>>(Expression.LessThanOrEqual(dbFieldMember, Expression.Constant(predicateItemRange.Value2)), new[] { dbTypeParameter }));
                    predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(predicateRange);
                }
            }

            return predicateInner;
        }

        private static string GetDbFieldName(PropertyInfo propertyInfo)
        {
            var dbFieldName = propertyInfo.Name;
            // TODO: Can put custom logic here, to obtain another field name if desired.
            return dbFieldName;
        }
    }
}
  

<强>用法

我们假设我们拥有保存数据的DbPerson类:

public class DbPerson
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime BirthDate { get; set; }

    public int Age { get; set; }
}

除了那个DbPerson类之外,我们还有一个代表DbPerson对象过滤器的类:

public class DbPersonFilter
{
    public string Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string BirthDate { get; set; }

    public string Age { get; set; }
}

注意基类DbPersonDbPersonFilter的属性名称是如何相同的。这很重要,因为上面的许多代码要求命名约定是一致的。但是,属性类型并不相同。这是因为对于过滤器,我们可以设置搜索范围,而不仅仅是一个值。稍后会有一些样本看看它是如何工作的。

现在,让我们填写我们的数据库&#34;简单的数据。我们使用这种方法:

private List<DbPerson> GenerateTestDb()
{
    var result = new List<DbPerson>
    {
        new DbPerson { Id = 1,FirstName = "John", LastName = "Doe", BirthDate = new DateTime(1963, 6, 14), Age = 53 },
        new DbPerson { Id = 2,FirstName = "Jane", LastName = "Hunt", BirthDate = new DateTime(1972, 1, 16), Age = 44 },
        new DbPerson { Id = 3,FirstName = "Aaron", LastName = "Pitch", BirthDate = new DateTime(1966, 7, 31), Age = 50 },
    };

    return result;
}

我们的示例应用程序的子句如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using LambdaSample.Extensions;
using LambdaSample.Framework;
using LinqKit;

现在,让我们在WinForms应用程序中创建一些btnTest(当然,你会在你的应用程序中使用它,无论是什么):

private void btnTest_Click(object sender, EventArgs e)
{
    // Load sample database into db (db is actually List<DbPerson>)
    var db = GenerateTestDb();

    // Create filter looking for FirstName is "John"
    var filterValues = new DbPersonFilter
    {
        FirstName = "John",
    };

    // Build PredicateParser which it used to parse predicates inside ExpressionExtensions. 
    var predicateParser = new PredicateParser();

    // Build predicate...
    var predicate1 = PredicateBuilder.New(ExpressionExtensions.BuildPredicate<DbPerson>(filterValues, predicateParser, true));
    // And search for items...
    var items1 = db.AsQueryable().AsExpandable().Where(predicate1).ToList();

    // Create filter to look for items where Id is between 1 and 2
    filterValues = new DbPersonFilter
    {
        Id = "1-2",
    };
    // Build predicate...
    var predicate2 = PredicateBuilder.New(ExpressionExtensions.BuildPredicate<DbPerson>(filterValues, predicateParser, true));
    // And search for items...
    var items2 = db.AsQueryable().AsExpandable().Where(predicate2).ToList();

    // Create filter to look for items where Age is 44
    filterValues = new DbPersonFilter
    {
        Age = "44",
    };
    // Build predicate...
    var predicate3 = PredicateBuilder.New(ExpressionExtensions.BuildPredicate<DbPerson>(filterValues, predicateParser, true));
    // And search for items...
    var items3 = db.AsQueryable().AsExpandable().Where(predicate3).ToList();
}

希望这会有所帮助。代码应该是不言自明的,因为评论不包含在任何地方。如果您还有其他问题,请询问。

注意: .AsExpandable()是LinqKit的扩展方法,在Where扩展方法中使用PredicateBuilder。

答案 1 :(得分:1)

我粗略地遗漏了有关我正在工作的实际域名的详细信息。这是为了使我的问题更加通用并专注于表达式。 但事实证明,我正在使用的API(Episerver FIND)中有一个FilterExpressionParser可以派上用场。

所以这是一个构建和应用复合滤波器的函数。

private void MechanicalPropertiesFilter(SteelNavigatorForm form, ref ITypeSearch<SteelGradeVariantPage> search)
    {
        FilterExpressionParser filterExpressionParser = new FilterExpressionParser(SearchClient.Instance.Conventions);
        Filter combinedFilter = null;

        // Dimension
        if (form.DimensionThickness > 0)
        {
            var dimensionFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.DimensionInMillimeterMin.LessThan(form.DimensionThickness)
            & m.DimensionInMillimeterMax.GreaterThan(form.DimensionThickness));

            combinedFilter = (combinedFilter == null) ? dimensionFilter : combinedFilter & dimensionFilter;
        }

        // Yield strength
        if (form.YieldStrengthMin > 0)
        {
            var yieldStrengthFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.YieldStrengh.GreaterThan(form.YieldStrengthMin));
            combinedFilter = (combinedFilter == null) ? yieldStrengthFilter : combinedFilter & yieldStrengthFilter;
        }

        // Tensile strength
        if (form.TensileStrengthMin > 0 | form.TensileStrengthMax > 0)
        {
            var tensileStrengthMin = (form.TensileStrengthMin == 0) ? double.MinValue : form.TensileStrengthMin;
            var tensileStrengthMax = (form.TensileStrengthMax == 0) ? double.MaxValue : form.TensileStrengthMax;
            var tensileStrengthFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.TensileStrengthMin.InRangeInclusive(tensileStrengthMin, tensileStrengthMax) | m.TensileStrengthMax.InRangeInclusive(tensileStrengthMin, tensileStrengthMax));

            combinedFilter = (combinedFilter == null) ? tensileStrengthFilter : combinedFilter & tensileStrengthFilter;
        }

        // Elongation
        if (form.Elongation > 0)
        {
            var elongationFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.ElongationA5Percentage.GreaterThan(form.Elongation));

            combinedFilter = (combinedFilter == null) ? elongationFilter : combinedFilter & elongationFilter;
        }

        // Hardness
        if (form.HardnessMin > 0 || form.HardnessMax > 0)
        {
            var max = (form.HardnessMax == 0) ? double.MaxValue : form.HardnessMax;

            var hardnessFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.HardnessScaleGuid.Match(form.HardnessMethod) & (
                m.HardnessMin.InRangeInclusive(form.HardnessMin, max)
                | m.HardnessMax.InRangeInclusive(form.HardnessMin, max)));

            combinedFilter = (combinedFilter == null) ? hardnessFilter : combinedFilter & hardnessFilter;
        }

        if (combinedFilter != null)
        {
            NestedFilterExpression<SteelGradeVariantPage, MechanicalProperties> mechanicalFilterExpression = new NestedFilterExpression<SteelGradeVariantPage, MechanicalProperties>(v => v.MechanicalProperties, ((MechanicalProperties item) => combinedFilter), search.Client.Conventions);

            search = search.Filter(mechanicalFilterExpression.NestedFilter);
        }
    }