Skip to content

Instantly share code, notes, and snippets.

@forgetaboutit
Last active October 6, 2021 20:34
Show Gist options
  • Save forgetaboutit/183dcec83f6635bad09b0919e84faa69 to your computer and use it in GitHub Desktop.
Save forgetaboutit/183dcec83f6635bad09b0919e84faa69 to your computer and use it in GitHub Desktop.
json-filter-to-expression
public enum Operation
{
Gt,
Gte,
Lt,
Lte,
Eq,
Neq
}
public static class Functions
{
public static TOut? Chain<TIn, TOut>(
this TIn? input,
Func<TIn, TOut?> fn)
{
if (input is null)
{
return default;
}
return fn(input);
}
public static Func<Expression, Expression, Expression>
OperationToCombinatorExpression(
Operation operation)
=> operation switch
{
Operation.Gt => Expression.GreaterThan,
Operation.Gte => Expression.GreaterThanOrEqual,
Operation.Lt => Expression.LessThan,
Operation.Lte => Expression.LessThanOrEqual,
Operation.Eq => Expression.Equal,
Operation.Neq => Expression.NotEqual,
_ => throw new ArgumentOutOfRangeException(nameof(operation))
};
public static TOut? AggregateNonEmptyOrDefault<TIn, TOut>(
IReadOnlyCollection<TIn> source,
TOut defaultValue,
Func<TIn?, TOut?> selector,
Func<TOut, TOut, TOut?> aggregator)
{
if (!source.Any())
{
return defaultValue;
}
return selector(source.First()).
Chain(
firstResult => source.
Skip(1).
Aggregate<TIn?, TOut?>(
firstResult,
(acc, cur) => acc.
Chain(
accumulated => selector(cur).
Chain(nextResult => aggregator(accumulated, nextResult)))));
}
public static Expression? AggregateGenExprs(
IReadOnlyList<IOperator> ops,
GenExprContext context,
Func<Expression, Expression, Expression> combinator)
=> AggregateNonEmptyOrDefault(
ops,
Expression.Constant(true),
op => op?.GenExpr(context),
combinator);
public static Expression? GenStringComparisonExpr(
GenExprContext context,
string member,
string value,
Func<Expression, Expression, Expression> mkExpr)
=> context.
GenStringMemberAccess(member).
Chain(
t => mkExpr(
t,
Expression.Constant(value)));
public static Expression? GenNumericComparisonExpr(
GenExprContext context,
string member,
double value,
Func<Expression, Expression, Expression> mkExpr)
=> context.
GenNumericMemberAccess(member).
Chain(
t => mkExpr(
t.Item2,
Expression.Constant(
Convert.ChangeType(
value,
t.Item1))));
public static Expression<Func<T, bool>>? CompileFilter<T>(
IOperator op)
{
var parameter = Expression.Parameter(
typeof(T),
"t");
var ctx = new GenExprContext(
parameter,
typeof(T));
return op.
GenExpr(ctx).
Chain(expr =>
Expression.Lambda(
expr,
false,
parameter) as Expression<Func<T, bool>>);
}
}
public sealed record GenExprContext(
ParameterExpression InputParameter,
Type Type)
{
private static readonly Type[] NumericTypes = new[]
{
typeof(double),
typeof(float),
typeof(long),
typeof(int),
typeof(short),
typeof(sbyte),
typeof(ulong),
typeof(uint),
typeof(ushort),
typeof(byte),
};
MemberInfo? FindMember(
string memberName,
Type memberType)
=> Type.
GetMember(memberName).
FirstOrDefault(
member => (member is PropertyInfo pi
&& pi.PropertyType == memberType
&& pi.CanRead)
|| (member is FieldInfo fi
&& fi.FieldType == memberType));
public IReadOnlyCollection<MemberInfo> FindStringMembers()
=> Type.
GetMembers().
Where(member => (member is PropertyInfo pi
&& pi.PropertyType == typeof(string)
&& pi.CanRead)
|| (member is FieldInfo fi
&& fi.FieldType == typeof(string))).
ToList();
Tuple<Type, MemberInfo>? FindNumericMember(
string memberName)
=> Type.
GetMember(memberName).
Select(
member => (member is PropertyInfo pi
&& NumericTypes.Contains(pi.PropertyType)
&& pi.CanRead)
? Tuple.Create(pi.PropertyType, (MemberInfo)pi)
: ((member is FieldInfo fi
&& NumericTypes.Contains(fi.FieldType))
? Tuple.Create(fi.FieldType, (MemberInfo)fi)
: null)).
FirstOrDefault(t => t is not null);
public Expression? GenStringMemberAccess(
string memberName)
=> FindMember(
memberName,
typeof(string)).
Chain(
member => Expression.MakeMemberAccess(
InputParameter,
member));
public Tuple<Type, Expression>? GenNumericMemberAccess(
string memberName)
=> FindNumericMember(
memberName).
Chain(
t => Tuple.Create(
t.Item1,
(Expression)Expression.MakeMemberAccess(
InputParameter,
t.Item2)));
}
[JsonConverter(typeof(OperatorConverter))]
public interface IOperator
{
public Expression? GenExpr(
GenExprContext context);
}
public sealed record OrOperator(
IReadOnlyList<IOperator> SubOps)
: IOperator
{
public Expression? GenExpr(
GenExprContext context)
=> Functions.AggregateGenExprs(
SubOps,
context,
Expression.OrElse);
}
public sealed record AndOperator(
IReadOnlyList<IOperator> SubOps)
: IOperator
{
public Expression? GenExpr(
GenExprContext context)
=> Functions.AggregateGenExprs(
SubOps,
context,
Expression.AndAlso);
}
public sealed record StringOperator(
string Member,
string Value,
Operation Operation)
: IOperator
{
public Expression? GenExpr(
GenExprContext context)
=> Functions.GenStringComparisonExpr(
context,
Member,
Value,
Functions.OperationToCombinatorExpression(Operation));
}
public sealed record NumOperator(
string Member,
double Value,
Operation Operation)
: IOperator
{
public Expression? GenExpr(
GenExprContext context)
=> Functions.GenNumericComparisonExpr(
context,
Member,
Value,
Functions.OperationToCombinatorExpression(Operation));
}
public sealed record LikeOperator(
string Member,
string Needle)
: IOperator
{
public static readonly MethodInfo ContainsMethod =
typeof(string).GetMethod(
"Contains",
new[] { typeof(string) })!;
public Expression? GenExpr(
GenExprContext context)
=> context.
GenStringMemberAccess(Member).
Chain(
memberAccess => Expression.Call(
memberAccess,
ContainsMethod,
Expression.Constant(Needle)));
}
public sealed record GoogleOperator(
string Needle)
: IOperator
{
public Expression? GenExpr(
GenExprContext context)
=> Functions.AggregateNonEmptyOrDefault(
context.FindStringMembers(),
Expression.Constant(true),
mi => (Expression) Expression.Call(
Expression.MakeMemberAccess(
context.InputParameter,
mi),
LikeOperator.ContainsMethod,
Expression.Constant(Needle)),
Expression.OrElse);
}
public sealed class OperatorConverter
: JsonConverter
{
public override bool CanConvert(
Type objectType)
{
throw new NotImplementedException();
}
public override object? ReadJson(
JsonReader reader,
Type objectType,
object? existingValue,
JsonSerializer serializer)
{
var @object = JObject.Load(reader);
return ParseOperator(@object);
}
private static IOperator ParseOperator(
JObject @object)
{
if (ParseAny(
@object,
o => ParseCombinator(
o,
"and",
ops => new AndOperator(ops)),
o => ParseCombinator(
o,
"or",
ops => new OrOperator(ops)),
ParseLike,
ParseGoogle,
o => ParseComparator(
o,
"eq",
Operation.Eq),
o => ParseComparator(
o,
"neq",
Operation.Neq),
o => ParseComparator(
o,
"gt",
Operation.Gt),
o => ParseComparator(
o,
"gte",
Operation.Gte),
o => ParseComparator(
o,
"lt",
Operation.Lt),
o => ParseComparator(
o,
"lte",
Operation.Lte)) is IOperator op)
{
return op;
}
throw new NotImplementedException();
}
private static string? ParseString(
JToken? token)
=> token is JValue { Type: JTokenType.String, Value: string s }
? s
: null;
private static double? ParseDouble(
JToken? token)
=> token is JValue { Type: JTokenType.Float, Value: double d }
? d
: null;
private static long? ParseLong(
JToken? token)
=> token is JValue { Type: JTokenType.Integer, Value: long l }
? l
: null;
private static IOperator? ParseAny(
JObject @object,
params Func<
JObject,
IOperator?>[] parsers)
=> parsers.
Select(p => p(@object)).
FirstOrDefault(result => result is not null);
private static IOperator? ParseGoogle(
JObject @object)
{
if (@object["google"] is JToken google
&& ParseString(google) is string needle)
{
return new GoogleOperator(needle);
}
return null;
}
private static IOperator? ParseCombinator(
JObject @object,
string combinatorName,
Func<IReadOnlyList<IOperator>, IOperator?> mkCombinator)
{
if (@object[combinatorName] is JArray array
&& array.
Children<JObject>().
Select(ParseOperator).
ToList() is { Count: > 0 } ops)
{
return mkCombinator(ops);
}
return null;
}
private static IOperator? ParseLike(
JObject @object)
{
if (@object["like"] is JObject likeObject
&& ParseString(likeObject["path"]) is string path
&& ParseString(likeObject["value"]) is string needle)
{
return new LikeOperator(path, needle);
}
return null;
}
private static IOperator? ParseComparator(
JObject @object,
string comparatorName,
Operation operation)
{
if (@object[comparatorName] is JObject { } eq
&& ParseString(eq["path"]) is string path)
{
var value = eq["value"];
if (ParseDouble(value) is double d)
{
return new NumOperator(
path,
(double) d,
operation);
}
else if (ParseLong(value) is long l)
{
return new NumOperator(
path,
(double) l,
operation);
}
else if (ParseString(value) is string s)
{
return new StringOperator(
path,
s,
operation);
}
}
return null;
}
public override void WriteJson(
JsonWriter writer,
object? value,
JsonSerializer serializer)
=> throw new NotImplementedException();
}
public sealed class Model
{
public Model() { }
public string? S { get; set; }
public double D { get; set; }
public DateTime? Dt { get; set; }
public int I { get; set; }
public string? S1 { get; set; }
public string? S2 { get; set; }
public string? S3 { get; set; }
}
public void Main()
{
var json = @"
{
""or"": [
{ ""like"": { ""path"": ""S"", ""value"": ""needle"" }},
{ ""eq"": { ""path"": ""S"", ""value"": ""5"" }},
{ ""eq"": { ""path"": ""I"", ""value"": 5.12 }},
{ ""eq"": { ""path"": ""D"", ""value"": 5.12 }}
]
}";
var googleJson = @"
{
""google"": ""foobar""
}";
// { ""eq"": { ""path"": ""Dt"", ""value"": ""2021 - 01 - 05"" } },
var filter = JsonConvert.
DeserializeObject<IOperator>(googleJson).
Chain(op => Functions.CompileFilter<Model>(op));
filter.Dump();
var filter2 = new GoogleOperator("banana").
Chain(op => Functions.CompileFilter<Model>(op));
filter2.Dump();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment