#pragma warning disable CS8509
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
public static class ExpressionParser
private const string c_lhs = "lhs";
private const string c_rhs = "rhs";
private const string c_bop = "bop";
private const string c_luop = "luop";
private const string c_ruop = "ruop";
private static Regex s_parser = new(@$"^((?<{c_luop}>[+\-!~]?)(?<{c_lhs}>([a-z]\w*\.?(?(?<=\.)[a-z]\w*))+|\d*\.?(?(?<=\.)\d+)[fdmul]?(?(?<=u)l?)|"".+""|'(.|\\[ux](?(?<=x)(?=0{0,4}[1-9A-F]{0,4}).{1,4}|[0-9A-F]{4}))'|)(?<{c_bop}>[+\-/%^]|[*&|><]{{1,2}}|[=!><]=|\?\?)(?<{c_ruop}>[+\-!~]?)(?<{c_rhs}>([a-z]\w*\.?(?(?<=\.)[a-z]\w*))+|\d*\.?(?(?<=\.)\d+)[fdmul]?(?(?<=u)l?)|"".+""|'(.|\\[ux](?(?<=x)(?=0{0,4}[1-9A-F]{0,4}).{1,4}|[0-9A-F]{4}))'|)|(?<paren>[(])|(?<-paren>[)]))*(?(paren)(?!))$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(5));
public static LambdaExpression Parse(string expression, params ParameterExpression[] parameters)
ArgumentNullException.ThrowIfNull(parameters, nameof(parameters));
expression = Regex.Replace(expression, @"\s+", String.Empty);
Match parsed = s_parser.Match(expression);
Expression expr = Expression.Empty();
for (int n = -1; n < parsed.Groups[c_bop].Captures.Count - 1;)
string lhs = parsed.Groups[c_lhs].Captures[++n].Value;
string rhs = parsed.Groups[c_rhs].Captures[n].Value;
string bop = parsed.Groups[c_bop].Captures[n].Value;
Expression lhsExpr = String.IsNullOrEmpty(lhs)
Expression rhsExpr = String.IsNullOrEmpty(rhs)
ParseUnaryOperation(parsed.Groups[c_luop].Captures[n].Value, ref lhsExpr);
ParseUnaryOperation(parsed.Groups[c_ruop].Captures[n].Value, ref rhsExpr);
if (rhsExpr.Type != lhsExpr.Type)
if (lhsExpr.Type == typeof(string))
rhsExpr = Expression.Convert(rhsExpr, lhsExpr.Type, lhsExpr.Type.GetMethod(nameof(Object.ToString), BindingFlags.Instance | BindingFlags.Public, Type.EmptyTypes));
else if (rhsExpr.Type == typeof(string))
rhsExpr = Expression.Convert(rhsExpr, lhsExpr.Type, lhsExpr.Type.GetMethod(nameof(Int32.Parse), BindingFlags.Static | BindingFlags.Public, [typeof(string)]));
rhsExpr = Expression.Convert(rhsExpr, lhsExpr.Type);
"+" => Expression.Add(lhsExpr, rhsExpr),
"-" => Expression.Subtract(lhsExpr, rhsExpr),
"*" => Expression.Multiply(lhsExpr, rhsExpr),
"/" => Expression.Divide(lhsExpr, rhsExpr),
"%" => Expression.Modulo(lhsExpr, rhsExpr),
"^" => Expression.ExclusiveOr(lhsExpr, rhsExpr),
"&" => Expression.And(lhsExpr, rhsExpr),
"|" => Expression.Or(lhsExpr, rhsExpr),
">" => Expression.GreaterThan(lhsExpr, rhsExpr),
"<" => Expression.LessThan(lhsExpr, rhsExpr),
"&&" => Expression.AndAlso(lhsExpr, rhsExpr),
"||" => Expression.OrElse(lhsExpr, rhsExpr),
">>" => Expression.RightShift(lhsExpr, rhsExpr),
"<<" => Expression.LeftShift(lhsExpr, rhsExpr),
">=" => Expression.GreaterThanOrEqual(lhsExpr, rhsExpr),
"<=" => Expression.LessThanOrEqual(lhsExpr, rhsExpr),
"==" => Expression.Equal(lhsExpr, rhsExpr),
"!=" => Expression.NotEqual(lhsExpr, rhsExpr),
"??" => Expression.Coalesce(lhsExpr, rhsExpr),
static void ParseUnaryOperation(string @operator, ref Expression expr)
if (String.IsNullOrEmpty(@operator))
expr = @operator switch {
"+" => Expression.UnaryPlus(expr),
"-" => Expression.Negate(expr),
"~" => Expression.OnesComplement(expr),
"!" => Expression.Not(expr),
Expression ParseOperand(string operand)
if (StringComparer.Ordinal.Equals(operand, true.ToString()))
return Expression.Constant(true, typeof(bool));
if (StringComparer.Ordinal.Equals(operand, false.ToString()))
return Expression.Constant(false, typeof(bool));
if (StringComparer.Ordinal.Equals(operand, "null"))
return Expression.Constant(null);
return Expression.Constant(operand.Substring(1, operand.Length - 2), typeof(string));
return Expression.Constant(Convert.ChangeType(operand.Substring(1, operand.Length - 2), typeof(char)), typeof(char));
if (Regex.IsMatch(operand, @"^[\d.]", RegexOptions.Singleline))
const string valueGroup = "val";
const string typeGroup = "type";
Match constant = Regex.Match(operand, @$"(?<{valueGroup}>[\d.]+)(?<{typeGroup}>[fdmul]{{1,2}})?", RegexOptions.IgnoreCase);
Type type = constant.Groups[typeGroup].ToString().ToUpperInvariant() switch {
_ => operand.Contains('.')
return Expression.Constant(Convert.ChangeType(constant.Groups[valueGroup].Captures[0].Value, type), type);
string[] members = operand.Split('.');
if (Array.Find(parameters, p => StringComparer.Ordinal.Equals(p.Name, members[0])) is not Expression expr)
i = Convert.ToInt32(StringComparer.Ordinal.Equals(members[0], "this"));
expr = parameters.Single();
for (; i < members.Length; ++i)
expr = Expression.PropertyOrField(expr, members[i]);
if (lhsExpr.Type != typeof(double))
lhsExpr = Expression.Convert(lhsExpr, typeof(double));
if (rhsExpr.Type != typeof(double))
rhsExpr = Expression.Convert(rhsExpr, typeof(double));
return Expression.Power(lhsExpr, rhsExpr);
return Expression.Lambda(expr, parameters);
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
public class ExpressionValidationAttribute<T>(string expression, string errorMessage) : Attribute
public Func<T, bool> Validation { get; private init; } = ((Expression<Func<T, bool>>)ExpressionParser.Parse(expression, Expression.Parameter(typeof(T), "this"))).Compile();
public string ErrorMessage { get; init; } = errorMessage;
public bool Validate(T obj) =>
[ExpressionValidation<Foo>(@"Bar != null", "Bar can't be null.")]
[ExpressionValidation<Foo>(@"Bar >= 0", "Bar must be positive.")]
[ExpressionValidation<Foo>(@"Bar % 2 == 0", "Bar must be even.")]
[property: ExpressionValidation<int?>("this < 10", "Bar must be smaller than 10.")] int? Bar,
[property: ExpressionValidation<DateTime>(@"this > ""2025-01-01""", "Baz must lie after 01-01-2025.")] DateTime Baz);
public static class Program
public static T Print<T>(this T obj)
public static void Validate<T>(this T obj)
IEnumerable<string> errors = typeof(T).GetCustomAttributes<ExpressionValidationAttribute<T>>().Where(a => !a.Validate(obj)).Select(a => a.ErrorMessage)
.Concat(typeof(T).GetMembers()
.Where(m => m.MemberType == MemberTypes.Property || m.MemberType == MemberTypes.Field)
.SelectMany(m => m.GetCustomAttributes(typeof(ExpressionValidationAttribute<>).MakeGenericType((m as PropertyInfo)?.PropertyType ?? (m as FieldInfo)?.FieldType!))
.Where(a => !(bool)a.GetType().GetMethod("Validate")!.Invoke(a, [(m as PropertyInfo)?.GetValue(obj) ?? (m as FieldInfo)?.GetValue(obj)])!))
.Select(a => (string)a.GetType().GetProperty("ErrorMessage")!.GetValue(a)!));
foreach (string error in errors)
public static void Main()
ParameterExpression a = Expression.Parameter(typeof(int), "a");
ParameterExpression b = Expression.Parameter(typeof(int), "b");
LambdaExpression expr = ExpressionParser.Parse(@"5 * (a + b) - ""7""", a, b);
expr.Compile().DynamicInvoke(3, 5).Dump();
new Foo(null, DateTime.Today).Validate();