我实现了一个搜索页面,用户可以使用一堆不同的过滤器过滤结果。我需要将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);
答案 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; }
}
注意基类DbPerson
和DbPersonFilter
的属性名称是如何相同的。这很重要,因为上面的许多代码要求命名约定是一致的。但是,属性类型并不相同。这是因为对于过滤器,我们可以设置搜索范围,而不仅仅是一个值。稍后会有一些样本看看它是如何工作的。
现在,让我们填写我们的数据库&#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);
}
}