原文:http://www.codeproject.com/Articles/355513/Invent-your-own-Dynamic-LINQ-parser
LINQ Expression Tree
Dynamic LINQ
核心代码:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; namespace SimpleExpression { public abstract class PredicateParser { #region scanner /// <summary>tokenizer pattern: Optional-SpaceS...Token...Optional-Spaces</summary> private static readonly string _pattern = @"\s*(" + string.Join("|", new string[] { // operators and punctuation that are longer than one char: longest first string.Join("|", new string[] { "||", "&&", "==", "!=", "<=", ">=" }.Select(e => Regex.Escape(e))), @"""(?:\\.|[^""])*""", // string @"\d+(?:\.\d+)?", // number with optional decimal part @"\w+", // word @"\S", // other 1-char tokens (or eat up one character in case of an error) }) + @")\s*"; /// <summary>get 1st char of current token (or a Space if no 1st char is obtained)</summary> private char Ch { get { return string.IsNullOrEmpty(Curr) ? ' ' : Curr[0]; } } /// <summary>move one token ahead</summary><returns>true = moved ahead, false = end of stream</returns> private bool Move() { return _tokens.MoveNext(); } /// <summary>the token stream implemented as IEnumerator<string></summary> private IEnumerator<string> _tokens; /// <summary>constructs the scanner for the given input string</summary> protected PredicateParser(string s) { _tokens = Regex.Matches(s, _pattern, RegexOptions.Compiled).Cast<Match>() .Select(m => m.Groups[1].Value).GetEnumerator(); Move(); } protected bool IsNumber { get { return char.IsNumber(Ch); } } protected bool IsDouble { get { return IsNumber && Curr.Contains('.'); } } protected bool IsString { get { return Ch == '"'; } } protected bool IsIdent { get { char c = Ch; return char.IsLower(c) || char.IsUpper(c) || c == '_'; } } /// <summary>throw an argument exception</summary> protected void Abort(string msg) { throw new ArgumentException("Error: " + (msg ?? "unknown error")); } /// <summary>get the current item of the stream or an empty string after the end</summary> protected string Curr { get { return _tokens.Current ?? string.Empty; } } /// <summary>get current and move to the next token (error if at end of stream)</summary> protected string CurrAndNext { get { string s = Curr; if (!Move()) Abort("data expected"); return s; } } /// <summary>get current and move to the next token if available</summary> protected string CurrOptNext { get { string s = Curr; Move(); return s; } } /// <summary>moves forward if current token matches and returns that (next token must exist)</summary> protected string CurrOpAndNext(params string[] ops) { string s = ops.Contains(Curr) ? Curr : null; if (s != null && !Move()) Abort("data expected"); return s; } #endregion } public class PredicateParser<TData> : PredicateParser { #region code generator private static readonly Type _bool = typeof(bool); private static readonly Type[] _prom = new Type[] { typeof(decimal), typeof(double), typeof(float), typeof(ulong), typeof(long), typeof(uint), typeof(int), typeof(ushort), typeof(char), typeof(short), typeof(byte), typeof(sbyte) }; /// <summary>enforce the type on the expression (by a cast) if not already of that type</summary> private static Expression Coerce(Expression expr, Type type) { return expr.Type == type ? expr : Expression.Convert(expr, type); } /// <summary>casts if needed the expr to the "largest" type of both arguments</summary> private static Expression Coerce(Expression expr, Expression sibling) { if (expr.Type != sibling.Type) { Type maxType = MaxType(expr.Type, sibling.Type); if (maxType != expr.Type) expr = Expression.Convert(expr, maxType); } return expr; } /// <summary>returns the first if both are same, or the largest type of both (or the first)</summary> private static Type MaxType(Type a, Type b) { return a == b ? a : (_prom.FirstOrDefault(t => t == a || t == b) ?? a); } /// <summary> /// Code generation of binary and unary epressions, utilizing type coercion where needed /// </summary> private static readonly Dictionary<string, Func<Expression, Expression, Expression>> _binOp = new Dictionary<string, Func<Expression, Expression, Expression>>() { { "||", (a,b)=>Expression.OrElse(Coerce(a, _bool), Coerce(b, _bool)) }, { "&&", (a,b)=>Expression.AndAlso(Coerce(a, _bool), Coerce(b, _bool)) }, { "==", (a,b)=>Expression.Equal(Coerce(a,b), Coerce(b,a)) }, { "!=", (a,b)=>Expression.NotEqual(Coerce(a,b), Coerce(b,a)) }, { "<", (a,b)=>Expression.LessThan(Coerce(a,b), Coerce(b,a)) }, { "<=", (a,b)=>Expression.LessThanOrEqual(Coerce(a,b), Coerce(b,a)) }, { ">=", (a,b)=>Expression.GreaterThanOrEqual(Coerce(a,b), Coerce(b,a)) }, { ">", (a,b)=>Expression.GreaterThan(Coerce(a,b), Coerce(b,a)) }, }; private static readonly Dictionary<string, Func<Expression, Expression>> _unOp = new Dictionary<string, Func<Expression, Expression>>() { { "!", a=>Expression.Not(Coerce(a, _bool)) }, }; /// <summary>create a constant of a value</summary> private static ConstantExpression Const(object v) { return Expression.Constant(v); } /// <summary>create lambda parameter field or property access</summary> private MemberExpression ParameterMember(string s) { return Expression.PropertyOrField(_param, s); } /// <summary>create lambda expression</summary> private Expression<Func<TData, bool>> Lambda(Expression expr) { return Expression.Lambda<Func<TData, bool>>(expr, _param); } /// <summary>the lambda's parameter (all names are members of this)</summary> private readonly ParameterExpression _param = Expression.Parameter(typeof(TData), "_p_"); #endregion #region parser /// <summary>initialize the parser (and thus, the scanner)</summary> private PredicateParser(string s) : base(s) { } /// <summary>main entry point</summary> public static Expression<Func<TData, bool>> Parse(string s) { return new PredicateParser<TData>(s).Parse(); } private Expression<Func<TData, bool>> Parse() { return Lambda(ParseExpression()); } private Expression ParseExpression() { return ParseOr(); } private Expression ParseOr() { return ParseBinary(ParseAnd, "||"); } private Expression ParseAnd() { return ParseBinary(ParseEquality, "&&"); } private Expression ParseEquality() { return ParseBinary(ParseRelation, "==", "!="); } private Expression ParseRelation() { return ParseBinary(ParseUnary, "<", "<=", ">=", ">"); } private Expression ParseUnary() { return CurrOpAndNext("!") != null ? _unOp["!"](ParseUnary()) : ParsePrimary(); } //private Expression ParseIdent() { return ParameterMember(CurrOptNext); } //不支持嵌套 private Expression ParseIdent() //修改支持嵌套 { Expression expr = ParameterMember(CurrOptNext); while (CurrOpAndNext(".") != null && IsIdent) expr = Expression.PropertyOrField(expr, CurrOptNext); return expr; } private Expression ParseString() { return Const(Regex.Replace(CurrOptNext, "^\"(.*)\"$", m => m.Groups[1].Value)); } private Expression ParseNumber() { if (IsDouble) return Const(double.Parse(CurrOptNext)); return Const(int.Parse(CurrOptNext)); } private Expression ParsePrimary() { if (IsIdent) return ParseIdent(); if (IsString) return ParseString(); if (IsNumber) return ParseNumber(); return ParseNested(); } private Expression ParseNested() { if (CurrAndNext != "(") Abort("(...) expected"); Expression expr = ParseExpression(); if (CurrOptNext != ")") Abort("')' expected"); return expr; } /// <summary>generic parsing of binary expressions</summary> private Expression ParseBinary(Func<Expression> parse, params string[] ops) { Expression expr = parse(); string op; while ((op = CurrOpAndNext(ops)) != null) expr = _binOp[op](expr, parse()); return expr; } #endregion } }测试代码:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; namespace SimpleExpression { public abstract class PredicateParser { #region scanner /// <summary>tokenizer pattern: Optional-SpaceS...Token...Optional-Spaces</summary> private static readonly string _pattern = @"\s*(" + string.Join("|", new string[] { // operators and punctuation that are longer than one char: longest first string.Join("|", new string[] { "||", "&&", "==", "!=", "<=", ">=" }.Select(e => Regex.Escape(e))), @"""(?:\\.|[^""])*""", // string @"\d+(?:\.\d+)?", // number with optional decimal part @"\w+", // word @"\S", // other 1-char tokens (or eat up one character in case of an error) }) + @")\s*"; /// <summary>get 1st char of current token (or a Space if no 1st char is obtained)</summary> private char Ch { get { return string.IsNullOrEmpty(Curr) ? ' ' : Curr[0]; } } /// <summary>move one token ahead</summary><returns>true = moved ahead, false = end of stream</returns> private bool Move() { return _tokens.MoveNext(); } /// <summary>the token stream implemented as IEnumerator<string></summary> private IEnumerator<string> _tokens; /// <summary>constructs the scanner for the given input string</summary> protected PredicateParser(string s) { _tokens = Regex.Matches(s, _pattern, RegexOptions.Compiled) .Cast<Match>() .Select(m => m.Groups[1].Value) .GetEnumerator(); Move(); } protected bool IsNumber { get { return char.IsNumber(Ch); } } protected bool IsDouble { get { return IsNumber && Curr.Contains('.'); } } protected bool IsString { get { return Ch == '"'; } } protected bool IsIdent { get { char c = Ch; return char.IsLower(c) || char.IsUpper(c) || c == '_'; } } /// <summary>throw an argument exception</summary> protected void Abort(string msg) { throw new ArgumentException("Error: " + (msg ?? "unknown error")); } /// <summary>get the current item of the stream or an empty string after the end</summary> protected string Curr { get { return _tokens.Current ?? string.Empty; } } /// <summary>get current and move to the next token (error if at end of stream)</summary> protected string CurrAndNext { get { string s = Curr; if (!Move()) Abort("data expected"); return s; } } /// <summary>get current and move to the next token if available</summary> protected string CurrOptNext { get { string s = Curr; Move(); return s; } } /// <summary>moves forward if current token matches and returns that (next token must exist)</summary> protected string CurrOpAndNext(params string[] ops) { string s = ops.Contains(Curr) ? Curr : null; if (s != null && !Move()) Abort("data expected"); return s; } #endregion } public class PredicateParser<TData> : PredicateParser { #region code generator private static readonly Expression _zero = Expression.Constant(0); //add private static readonly Type _bool = typeof(bool); private static readonly Type[] _prom = new Type[] { typeof(decimal), typeof(double), typeof(float), typeof(ulong), typeof(long), typeof(uint), typeof(int), typeof(ushort), typeof(char), typeof(short), typeof(byte), typeof(sbyte) }; /// <summary>enforce the type on the expression (by a cast) if not already of that type</summary> private static Expression Coerce(Expression expr, Type type) { return expr.Type == type ? expr : Expression.Convert(expr, type); } /// <summary>casts if needed the expr to the "largest" type of both arguments</summary> private static Expression Coerce(Expression expr, Expression sibling) { if (expr.Type != sibling.Type) { Type maxType = MaxType(expr.Type, sibling.Type); if (maxType != expr.Type) expr = Expression.Convert(expr, maxType); } return expr; } /// <summary>returns the first if both are same, or the largest type of both (or the first)</summary> private static Type MaxType(Type a, Type b) { return a == b ? a : (_prom.FirstOrDefault(t => t == a || t == b) ?? a); } /// <summary>produce comparison based on IComparable types</summary> private static Expression CompareToExpression(Expression lhs, Expression rhs, Func<Expression, Expression> rel) { lhs = Coerce(lhs, rhs); rhs = Coerce(rhs, lhs); Expression cmp = Expression.Call( lhs, lhs.Type.GetMethod("CompareTo", new Type[] { rhs.Type }) ?? lhs.Type.GetMethod("CompareTo", new Type[] { typeof(object) }), rhs ); return rel(cmp); } /// <summary> /// Code generation of binary and unary epressions, utilizing type coercion where needed /// </summary> private static readonly Dictionary<string, Func<Expression, Expression, Expression>> _binOp = new Dictionary<string, Func<Expression, Expression, Expression>>() { { "||", (a,b)=>Expression.OrElse(Coerce(a, _bool), Coerce(b, _bool)) }, { "&&", (a,b)=>Expression.AndAlso(Coerce(a, _bool), Coerce(b, _bool)) }, //{ "==", (a,b)=>Expression.Equal(Coerce(a,b), Coerce(b,a)) }, //{ "!=", (a,b)=>Expression.NotEqual(Coerce(a,b), Coerce(b,a)) }, //{ "<", (a,b)=>Expression.LessThan(Coerce(a,b), Coerce(b,a)) }, //{ "<=", (a,b)=>Expression.LessThanOrEqual(Coerce(a,b), Coerce(b,a)) }, //{ ">=", (a,b)=>Expression.GreaterThanOrEqual(Coerce(a,b), Coerce(b,a)) }, //{ ">", (a,b)=>Expression.GreaterThan(Coerce(a,b), Coerce(b,a)) }, //Replace to=> { "==", (a,b)=>CompareToExpression(a, b, c=>Expression.Equal (c, _zero)) }, { "!=", (a,b)=>CompareToExpression(a, b, c=>Expression.NotEqual (c, _zero)) }, { "<", (a,b)=>CompareToExpression(a, b, c=>Expression.LessThan (c, _zero)) }, { "<=", (a,b)=>CompareToExpression(a, b, c=>Expression.LessThanOrEqual (c, _zero)) }, { ">=", (a,b)=>CompareToExpression(a, b, c=>Expression.GreaterThanOrEqual(c, _zero)) }, { ">", (a,b)=>CompareToExpression(a, b, c=>Expression.GreaterThan (c, _zero)) }, //To extend the parser=> { "+", (a,b)=>Expression.Add(Coerce(a,b), Coerce(b,a)) }, { "-", (a,b)=>Expression.Subtract(Coerce(a,b), Coerce(b,a)) }, { "*", (a,b)=>Expression.Multiply(Coerce(a,b), Coerce(b,a)) }, { "/", (a,b)=>Expression.Divide(Coerce(a,b), Coerce(b,a)) }, { "%", (a,b)=>Expression.Modulo(Coerce(a,b), Coerce(b,a)) }, }; private static readonly Dictionary<string, Func<Expression, Expression>> _unOp = new Dictionary<string, Func<Expression, Expression>>() { { "!", a=>Expression.Not(Coerce(a, _bool)) }, //To extend the parser=> { "-", a=>Expression.Negate(a) }, }; /// <summary>create a constant of a value</summary> private static ConstantExpression Const(object v) { return Expression.Constant(v); } /// <summary>create lambda parameter field or property access</summary> private MemberExpression ParameterMember(string s) { return Expression.PropertyOrField(_param, s); } /// <summary>create lambda expression</summary> private Expression<Func<TData, bool>> Lambda(Expression expr) { return Expression.Lambda<Func<TData, bool>>(expr, _param); } /// <summary>the lambda's parameter (all names are members of this)</summary> private readonly ParameterExpression _param = Expression.Parameter(typeof(TData), "_p_"); #endregion #region parser /// <summary>initialize the parser (and thus, the scanner)</summary> private PredicateParser(string s) : base(s) { } /// <summary>main entry point</summary> public static Expression<Func<TData, bool>> Parse(string s) { return new PredicateParser<TData>(s).Parse(); } private Expression<Func<TData, bool>> Parse() { return Lambda(ParseExpression()); } private Expression ParseExpression() { return ParseOr(); } private Expression ParseOr() { return ParseBinary(ParseAnd, "||"); } private Expression ParseAnd() { return ParseBinary(ParseEquality, "&&"); } private Expression ParseEquality() { return ParseBinary(ParseRelation, "==", "!="); } //private Expression ParseRelation() { return ParseBinary(ParseUnary, "<", "<=", ">=", ">"); } private Expression ParseRelation() { return ParseBinary(ParseSum, "<", "<=", ">=", ">"); } private Expression ParseSum() { return ParseBinary(ParseMul, "+", "-"); } private Expression ParseMul() { return ParseBinary(ParseUnary, "*", "/", "%"); } private Expression ParseUnary() { if (CurrOpAndNext("!") != null) return _unOp["!"](ParseUnary()); if (CurrOpAndNext("-") != null) return _unOp["-"](ParseUnary()); return ParsePrimary(); //return CurrOpAndNext("!") != null ? _unOp["!"](ParseUnary()) : ParsePrimary(); } //private Expression ParseIdent() { return ParameterMember(CurrOptNext); } //不支持嵌套 private Expression ParseIdent() //修改支持嵌套 { Expression expr = ParameterMember(CurrOptNext); while (CurrOpAndNext(".") != null && IsIdent) expr = Expression.PropertyOrField(expr, CurrOptNext); return expr; } private Expression ParseString() { return Const(Regex.Replace(CurrOptNext, "^\"(.*)\"$", m => m.Groups[1].Value)); } private Expression ParseNumber() { if (IsDouble) return Const(double.Parse(CurrOptNext)); return Const(int.Parse(CurrOptNext)); } private Expression ParsePrimary() { if (IsIdent) return ParseIdent(); if (IsString) return ParseString(); if (IsNumber) return ParseNumber(); return ParseNested(); } private Expression ParseNested() { if (CurrAndNext != "(") Abort("(...) expected"); Expression expr = ParseExpression(); if (CurrOptNext != ")") Abort("')' expected"); return expr; } /// <summary>generic parsing of binary expressions</summary> private Expression ParseBinary(Func<Expression> parse, params string[] ops) { Expression expr = parse(); string op; while ((op = CurrOpAndNext(ops)) != null) expr = _binOp[op](expr, parse()); return expr; } #endregion } }
测试代码:
using System; using System.Collections.Generic; using System.Linq; namespace LinqExpTest { /// <summary> /// Linq 表达式树 测试 /// </summary> class Program { static void Main(string[] args) { var items = new List<Element>() { #region //new Element("a", 1000,new List<CC>{new CC{email="fdsf@fd",age=12}}), //new Element("b", 900,new List<CC>{new CC{email="fdsf@fd",age=12}}), //new Element("c", 800,new List<CC>{new CC{email="fdsf@fd",age=12}}), //new Element("d", 700,new List<CC>{new CC{email="fdsf@fd",age=12}}), //new Element("e", 600,new List<CC>{new CC{email="fdsf@fd",age=12}}), //new Element("x", 500,new List<CC>{new CC{email="fdsf@fd",age=12}}), //new Element("y", 400,new List<CC>{new CC{email="fdsf@fd",age=12}}), //new Element("z", 300,new List<CC>{new CC{email="fdsf@fd",age=12}}) #endregion new Element("a", 1000,new CC{email="fdsf@fd",age=12}), new Element("b", 900,new CC{email="fdsf@fd",age=17}), new Element("c", 800,new CC{email="fdsf@fd",age=25}), new Element("d", 700,new CC{email="fdsf@fd",age=8}), new Element("e", 600,new CC{email="fdsf@fd",age=9}), new Element("x", 500,new CC{email="fdsf@fd",age=35}), new Element("y", 400,new CC{email="fdsf@fd",age=23}), new Element("z", 900,new CC{email="fdsf@fd",age=16}) }; //string s = "Name == \"x\" || Number >= 800 && PCC.age % 2==0 && PCC.age>15 && PCC.age*2<35"; string s = "Name == \"x\" || Number >= 800 && PCC.age>15 && PCC.age*2<35"; var pred = SimpleExpression.PredicateParser<Element>.Parse(s); Console.WriteLine("User Entry: {0}", s); Console.WriteLine("Expr Tree: {0}", pred.ToString()); var f = pred.Compile(); Console.WriteLine("\r\n==============mark affected items=============="); foreach (var item in items) { Console.WriteLine("{2} Name = {0}, Number = {1}, CC: email={3}, age={4}" , item.Name, item.Number, f(item) ? "x" : " ", item.PCC.email, item.PCC.age); } Console.WriteLine("==============where-select=============="); var q = from e in items where f(e) select e; foreach (var item in q) { Console.WriteLine(" Name = {0}, Number = {1},CC: email={2}, age={3}", item.Name, item.Number, item.PCC.email, item.PCC.age); } Console.ReadKey(); } } class Element { private string _name; private int _number; private CC _cc; //private IList<CC> _listcc; //public Element(string name, int number, List<CC> listcc) public Element(string name, int number, CC cc) { this._name = name; this._number = number; this._cc = cc; //this._listcc = listcc; } public string Name { get { return _name; } set { _name = value; } } public int Number { get { return _number; } set { _number = value; } } public CC PCC { get { return _cc; } set { _cc = value; } } //public IList<CC> ListCC { get { return _listcc; } set { _listcc = value; } } } class CC { public string email { get; set; } public int age { get; set; } } }
string s = "Name == \"x\" || Number >= 800 && PCC.age % 2==0 && PCC.age>15 && PCC.age*2<35";
原文下面提出了对此方法的扩展说明:
Add more operations
To extend the parser: You may easily add more to the expressions, especially new operators is a simple thing:
+
and -
binary operators, add them to the _binOp
dictionary (similar to ==
, e.g. , ("+":
Expression.Add(...)
, "-":
Expression.Subtract(...)
) create ParseSum()
as a copy ofParseRelation
, pass "+", "-"
as ops, pass ParseSum
to ParseRelation
(in place of the ParseUnary
), pass ParseUnary
to ParseSum
. That's it."*", "/", "%"
: make ParseMul
as copy of the above mentioned ParseSum
, pass the rightParseXXX
actions, add the respective Expression factories to the _binOps
dictionary. Done."-"
is to be added in the _unOps
dictionary (no coercion needed). The parsing is done in theParseUnary()
function, e.g.