Created
September 29, 2021 20:06
-
-
Save prince272/e9e19de60484e9b07cd3afd4ba090d9b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using LinqKit; | |
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Diagnostics; | |
using System.Globalization; | |
using System.IO; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using System.Text; | |
namespace Rextester | |
{ | |
public class Person | |
{ | |
public string Name { get; set; } | |
public int Age { get; set; } | |
public static List<Person> People { get; } = new List<Person>() | |
{ | |
new Person { Name = "Prince", Age = 12 }, | |
new Person { Name = "Nadia", Age = 34 }, | |
new Person { Name = "Hilary", Age = 45 }, | |
new Person { Name = "Silkily", Age = 78 }, | |
new Person { Name = "Robby", Age = 56 }, | |
new Person { Name = "Johnson", Age = 83 }, | |
new Person { Name = "James", Age = 86 }, | |
}; | |
public override string ToString() | |
{ | |
return $"Name: {Name}, Age: {Age}"; | |
} | |
} | |
public class Program | |
{ | |
public static void Main(string[] args) | |
{ | |
var predicate = new RuleParser<Person>() | |
.AddProperty(_ => _.Name) | |
.Parse($"name!@{Uri.EscapeDataString("Prince")}"); | |
var filtedPeople = Person.People.Where(predicate.Compile()).ToList(); | |
} | |
} | |
// credit: https://stackoverflow.com/questions/38426175/extract-all-possible-paths-from-expression-tree-and-evaluate-them-to-hold-true | |
public class RuleParser<TSource> | |
{ | |
private static readonly Dictionary<string, RuleType> LogicMap = new Dictionary<string, RuleType>() { | |
{ "(" , RuleType.OpenParenthesis }, | |
{ ")" , RuleType.CloseParenthesis }, | |
{ "&&" , RuleType.And }, | |
{ "||" , RuleType.Or }, | |
}; | |
private static readonly Dictionary<string, RuleType> OperatorMap = new Dictionary<string, RuleType>() { | |
{ "==" , RuleType.Equal }, | |
{ "!=" , RuleType.NotEqual }, | |
{ "<" , RuleType.LessThan }, | |
{ "<=" , RuleType.LessThanOrEqual }, | |
{ ">" , RuleType.GreaterThan }, | |
{ ">=" , RuleType.GreaterThanOrEqual }, | |
{ "=@" , RuleType.Contains }, | |
{ "!@" , RuleType.Not | RuleType.Contains }, | |
{ "=^" , RuleType.StartsWith }, | |
{ "!^" , RuleType.Not | RuleType.StartsWith }, | |
{ "=$" , RuleType.EndsWith }, | |
{ "!$" , RuleType.Not | RuleType.EndsWith }, | |
}; | |
private readonly Dictionary<string, PropertyInfo> PropertyMap = new Dictionary<string, PropertyInfo>(); | |
// credit: https://stackoverflow.com/questions/671968/retrieving-property-name-from-lambda-expression | |
public virtual RuleParser<TSource> AddProperty<TLambda>(Expression<Func<TSource, TLambda>> propertyLambda, string propertyName = null) | |
{ | |
var body = propertyLambda.Body; | |
if (!(body is MemberExpression member) | |
&& !(body is UnaryExpression unary | |
&& (member = unary.Operand as MemberExpression) != null)) | |
throw new ArgumentException($"Expression '{propertyLambda}' " + | |
"does not refer to a property."); | |
if (!(member.Member is PropertyInfo propInfo)) | |
throw new ArgumentException($"Expression '{propertyLambda}' " + | |
"refers to a field, not a property."); | |
var type = typeof(TSource); | |
if (!propInfo.DeclaringType.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) | |
throw new ArgumentException($"Expresion '{propertyLambda}' " + | |
"refers to a property that is not from type '{type}'."); | |
PropertyMap.Add(propertyName ?? propInfo.Name, propInfo); | |
return this; | |
} | |
public Expression<Func<TSource, bool>> Parse(string ruleString) | |
{ | |
ruleString = CleanRuleString(ruleString); | |
var rule = ParseRuleString(ruleString); | |
if (rule != null) return BuildExpression(rule); | |
else return _ => true; | |
} | |
private Rule ParseRuleString(string ruleString) | |
{ | |
var rules = new List<Rule>(); | |
var reader = new StringReader(ruleString); | |
var logicChars = LogicMap.Keys.SelectMany(_ => _.ToCharArray()).ToArray(); | |
var logicString = string.Empty; | |
var operatorChars = OperatorMap.Keys.SelectMany(_ => _.ToCharArray()); | |
var operatorString = string.Empty; | |
while (reader.Peek() > -1) | |
{ | |
var charValue = (char)reader.Read(); | |
if (logicChars.Contains(charValue)) | |
{ | |
logicString += charValue; | |
} | |
else if (!logicChars.Any(_ => (logicString + charValue).EndsWith(_))) | |
{ | |
if (LogicMap.TryGetValue(logicString, out RuleType logicRuleType)) | |
{ | |
var operatorKey = new string(operatorString.Where(_ => operatorChars.Contains(_)).ToArray()); | |
var operatorRuleType = OperatorMap[operatorKey]; | |
var field = operatorString.Split(operatorKey); | |
rules.Add(new Rule { Name = field[0], Value = field[1], Type = operatorRuleType }); | |
rules.Add(new Rule { Type = logicRuleType }); | |
operatorString = string.Empty; | |
logicString = string.Empty; | |
} | |
operatorString += charValue; | |
} | |
if (reader.Peek() <= -1) | |
{ | |
if (!string.IsNullOrEmpty(operatorString)) | |
{ | |
var operatorKey = new string(operatorString.Where(_ => operatorChars.Contains(_)).ToArray()); | |
var operatorRuleType = OperatorMap[operatorKey]; | |
var field = operatorString.Split(operatorKey).Select(_ => Uri.UnescapeDataString(_)).ToArray(); | |
rules.Add(new Rule { Name = field[0], Value = field[1], Type = operatorRuleType }); | |
operatorString = string.Empty; | |
logicString = string.Empty; | |
} | |
} | |
} | |
rules = TransformToPolishNotation(rules); | |
var enumerator = rules.GetEnumerator(); | |
return enumerator.MoveNext() ? CreateRule(ref enumerator) : null; | |
} | |
private string CleanRuleString(string ruleString) | |
{ | |
return new string(ruleString.ToCharArray().Where(c => !char.IsWhiteSpace(c)).ToArray()); | |
} | |
private static Expression<Func<TSource, bool>> ComputeExpression(PropertyInfo propertyInfo, RuleType operatorRuleType, object value) | |
{ | |
static ConstantExpression ComputeConstantExpressionBasedOnPropertyExpressionTargetTypeAndValue(Type memberType, object value) | |
{ | |
var ce = Expression.Constant(value, memberType); | |
return ce; | |
} | |
var constantExpression = ComputeConstantExpressionBasedOnPropertyExpressionTargetTypeAndValue(propertyInfo.PropertyType, value); | |
var parameter = Expression.Parameter(propertyInfo.DeclaringType, "_"); | |
var propertyExpression = Expression.Property(parameter, propertyInfo); | |
Expression logicExpression = null; | |
if (operatorRuleType.HasFlag(RuleType.Equal)) | |
{ | |
logicExpression = Expression.Equal(propertyExpression, constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.NotEqual)) | |
{ | |
logicExpression = Expression.NotEqual(propertyExpression, constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.LessThan)) | |
{ | |
logicExpression = Expression.LessThan(propertyExpression, constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.LessThanOrEqual)) | |
{ | |
logicExpression = Expression.LessThanOrEqual(propertyExpression, constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.GreaterThan)) | |
{ | |
logicExpression = Expression.GreaterThan(propertyExpression, constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.GreaterThanOrEqual)) | |
{ | |
logicExpression = Expression.GreaterThanOrEqual(propertyExpression, constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.Equal)) | |
{ | |
logicExpression = Expression.NotEqual(propertyExpression, constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.Contains)) | |
{ | |
logicExpression = Expression.Call(propertyExpression, typeof(string).GetMethod("Contains", new[] { typeof(string) }), constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.StartsWith)) | |
{ | |
logicExpression = Expression.Call(propertyExpression, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }), constantExpression); | |
} | |
else if (operatorRuleType.HasFlag(RuleType.EndsWith)) | |
{ | |
logicExpression = Expression.Call(propertyExpression, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }), constantExpression); | |
} | |
else | |
{ | |
throw new InvalidOperationException($"Value '{operatorRuleType}' is not supported. (Parameter '{nameof(operatorRuleType)}')"); | |
} | |
if (operatorRuleType.HasFlag(RuleType.Not)) | |
{ | |
logicExpression = Expression.Not(logicExpression); | |
} | |
return Expression.Lambda<Func<TSource, bool>>(logicExpression, new[] { parameter }); | |
} | |
private Expression<Func<TSource, bool>> BuildExpression(Rule rule) | |
{ | |
var predicate = PredicateBuilder.New<TSource>(); | |
if (OperatorMap.Values.Contains(rule.Type)) | |
{ | |
var propertyInfo = PropertyMap.SingleOrDefault(_ => string.Equals(_.Key, rule.Name, StringComparison.CurrentCultureIgnoreCase)).Value; | |
if (propertyInfo != null) | |
{ | |
var propertyValue = TypeHelper.ConvertTo(rule.Value, propertyInfo.PropertyType); | |
predicate = predicate.Start(ComputeExpression(propertyInfo, rule.Type, propertyValue)); | |
} | |
} | |
foreach (var subRule in rule.SubRules) | |
{ | |
if (rule.Type == RuleType.And) | |
predicate = predicate.And(BuildExpression(subRule)); | |
else if (rule.Type == RuleType.Or) | |
predicate = predicate.Or(BuildExpression(subRule)); | |
} | |
return predicate; | |
} | |
private List<Rule> TransformToPolishNotation(List<Rule> infixRuleList) | |
{ | |
var outputQueue = new Queue<Rule>(); | |
var stack = new Stack<Rule>(); | |
foreach (var rule in infixRuleList) | |
{ | |
if (OperatorMap.Values.Contains(rule.Type)) | |
{ | |
outputQueue.Enqueue(rule); | |
} | |
else | |
{ | |
switch (rule.Type) | |
{ | |
case RuleType.And: | |
case RuleType.Or: | |
case RuleType.OpenParenthesis: | |
{ | |
stack.Push(rule); | |
} | |
break; | |
case RuleType.CloseParenthesis: | |
{ | |
while (stack.Peek().Type != RuleType.OpenParenthesis) | |
{ | |
outputQueue.Enqueue(stack.Pop()); | |
} | |
stack.Pop(); | |
} | |
break; | |
} | |
} | |
} | |
while (stack.Count > 0) | |
{ | |
outputQueue.Enqueue(stack.Pop()); | |
} | |
return outputQueue.Reverse().ToList(); | |
} | |
private Rule CreateRule(ref List<Rule>.Enumerator ruleEnumerator) | |
{ | |
var rule = ruleEnumerator.Current; | |
if (OperatorMap.Values.Contains(rule.Type)) | |
{ | |
ruleEnumerator.MoveNext(); | |
return rule; | |
} | |
else | |
{ | |
ruleEnumerator.MoveNext(); | |
if (rule.Type == RuleType.And || rule.Type == RuleType.Or) | |
{ | |
rule.SubRules.Insert(0, CreateRule(ref ruleEnumerator)); | |
rule.SubRules.Insert(0, CreateRule(ref ruleEnumerator)); | |
} | |
} | |
return rule; | |
} | |
} | |
[Flags] | |
public enum RuleType | |
{ | |
OpenParenthesis = 1 << 0, | |
CloseParenthesis = 1 << 1, | |
And = 1 << 2, | |
Or = 1 << 3, | |
Not = 1 << 4, | |
Equal = 1 << 5, | |
NotEqual = 1 << 6, | |
LessThan = 1 << 7, | |
LessThanOrEqual = 1 << 8, | |
GreaterThan = 1 << 9, | |
GreaterThanOrEqual = 1 << 10, | |
Contains = 1 << 11, | |
StartsWith = 1 << 12, | |
EndsWith = 1 << 13, | |
} | |
public class Rule | |
{ | |
public RuleType Type { get; set; } | |
public string Name { get; set; } | |
public string Value { get; set; } | |
public List<Rule> SubRules { get; } = new List<Rule>(); | |
} | |
public sealed class TypeHelper | |
{ | |
public static object ConvertTo(object value, Type destinationType) | |
{ | |
return ConvertTo(value, destinationType, CultureInfo.InvariantCulture); | |
} | |
public static object ConvertTo(object value, Type destinationType, CultureInfo culture) | |
{ | |
if (value == null) | |
return null; | |
var sourceType = value.GetType(); | |
var destinationConverter = TypeDescriptor.GetConverter(destinationType); | |
if (destinationConverter.CanConvertFrom(value.GetType())) | |
return destinationConverter.ConvertFrom(null, culture, value); | |
var sourceConverter = TypeDescriptor.GetConverter(sourceType); | |
if (sourceConverter.CanConvertTo(destinationType)) | |
return sourceConverter.ConvertTo(null, culture, value, destinationType); | |
if (destinationType.IsEnum && value is int) | |
return Enum.ToObject(destinationType, (int)value); | |
if (!destinationType.IsInstanceOfType(value)) | |
return Convert.ChangeType(value, destinationType, culture); | |
return value; | |
} | |
public static T ConvertTo<T>(object value) | |
{ | |
//return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); | |
return (T)ConvertTo(value, typeof(T)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment