Click here to Skip to main content
15,891,431 members
Articles / Programming Languages / C#
Alternative
Tip/Trick

Invent your own Dynamic LINQ parser

Rate me:
Please Sign up or sign in to vote.
4.95/5 (28 votes)
27 Mar 2012MIT4 min read 78.4K   243   56  
This is an alternative for "Dynamically generate a LINQ query with a custom property"
This is an old version of the currently published tip/trick.

Introduction

This is an alternative to the original Tip Dynamically generate a LINQ query with a custom property.

The original post follows the concept of tweaking with the client of of a query to allow dynamic queries. I find that quite intrusive and not re-usable for future use as I commented on the original post.

I outline here how you can re-invent your own Dynamic Linq version. I present some parsing techniques that might be helpful for other simple parser tasks. For "real" parser works, I strongly recommend using a parser generator like Coco/R or like ANTLR.

The functionality:

create from an expression that is given in a string a function that can be used in a LINQ query, e.g.

C#
IEnumerable<MySourceElement> items = ...;
...
string s = GetUserEntry(); // e.g. Name == "x" || Number >= 800
...
var pred = SimpleExpression.PredicateParser<MySourceElement>.Parse(s);
var f = pred.Compile();
var query = from e in items where f(e) select e;
...

Using the code

This is a very dense code that shows how you could write your own Dynamic LINQ parser:

  • the scanner is from line 11 to line 53
  • the code generator is from line 57 to line 108
  • the parser from is line 109 to line 152

The implemented features:

  • names as properties or fields of the lambda parameter
  • double or int numbers
  • strings
  • nested expressions
  • numeric, string, boolean type system with numeric type promotion
  • operators ||, &&, ==, !=, <, <=, >=, >, !
C#
  1  using System;
  2  using System.Collections.Generic;
  3  using System.Linq;
  4  using System.Linq.Expressions;
  5  using System.Text.RegularExpressions;
  6   
  7  namespace SimpleExpression
  8  {
  9      public abstract class PredicateParser
 10      {
 11          #region scanner
 12          /// <summary>tokenizer pattern: Optional-SpaceS...Token...Optional-Spaces</summary>
 13          private static readonly string _pattern = @"\s*(" + string.Join("|", new string[]
 14          {
 15              // operators and punctuation that are longer than one char: longest first
 16              string.Join("|", new string[] { "||", "&&", "==", "!=", "<=", ">=" }.Select(e => Regex.Escape(e))),
 17              @"""(?:\\.|[^""])*?""", // string
 18              @"\d+(?:\.\d+)",        // number with optional decimal part
 19              @"\w+",                 // word
 20              @"\S",                  // other 1-char tokens (or eat up one character in case of an error)
 21          }) + @")\s*";
 22          /// <summary>get 1st char of current token (or a Space if no 1st char is obtained)</summary>
 23          private char Ch { get { return string.IsNullOrEmpty(Curr) ? ' ' : Curr[0]; } }
 24          /// <summary>move one token ahead</summary><returns>true = moved ahead, false = end of stream</returns>
 25          private bool Move() { return _tokens.MoveNext(); }
 26          /// <summary>the token stream implemented as IEnumerator&lt;string&gt;</summary>
 27          private IEnumerator<string> _tokens;
 28          /// <summary>constructs the scanner for the given input string</summary>
 29          protected PredicateParser(string s)
 30          {
 31              _tokens = Regex.Matches(s, _pattern, RegexOptions.Compiled).Cast<Match>()
 32                        .Select(m => m.Groups[1].Value).GetEnumerator();
 33              Move();
 34          }
 35          protected bool IsNumber { get { return char.IsNumber(Ch); } }
 36          protected bool IsDouble { get { return IsNumber && Curr.Contains('.'); } }
 37          protected bool IsString { get { return Ch == '"'; } }
 38          protected bool IsIdent { get { char c = Ch; return char.IsLower(c) || char.IsUpper(c) || c == '_'; } }
 39          /// <summary>throw an argument exception</summary>
 40          protected void Abort(string msg) { throw new ArgumentException("Error: " + (msg ?? "unknown error")); }
 41          /// <summary>get the current item of the stream or an empty string after the end</summary>
 42          protected string Curr { get { return _tokens.Current ?? string.Empty; }}
 43          /// <summary>get current and move to the next token (error if at end of stream)</summary>
 44          protected string CurrAndNext { get { string s = Curr; if (!Move()) Abort("data expected"); return s; } }
 45          /// <summary>get current and move to the next token if available</summary>
 46          protected string CurrOptNext { get { string s = Curr; Move(); return s; } }
 47          /// <summary>moves forward if current token matches and returns that (next token must exist)</summary>
 48          protected string CurrOpAndNext(params string[] ops)
 49          {
 50              string s = ops.Contains(Curr) ? Curr : null;
 51              if (s != null && !Move()) Abort("data expected"); return s;
 52          }
 53          #endregion
 54      }
 55      public class PredicateParser<TData>: PredicateParser
 56      {
 57          #region code generator
 58          private static readonly Type _bool = typeof(bool);
 59          private static readonly Type[] _prom = new Type[]
 60          { typeof(decimal), typeof(double), typeof(float), typeof(ulong), typeof(long), typeof(uint),
 61            typeof(int), typeof(ushort), typeof(char), typeof(short), typeof(byte), typeof(sbyte) };
 62          /// <summary>enforce the type on the expression (by a cast) if not already of that type</summary>
 63          private static Expression Coerce(Expression expr, Type type)
 64          {
 65              return expr.Type == type ? expr : Expression.Convert(expr, type);
 66          }
 67          /// <summary>casts if needed the expr to the "largest" type of both arguments</summary>
 68          private static Expression Coerce(Expression expr, Expression sibling)
 69          {
 70              if (expr.Type != sibling.Type)
 71              {
 72                  Type maxType = MaxType(expr.Type, sibling.Type);
 73                  if (maxType != expr.Type) expr = Expression.Convert(expr, maxType);
 74              }
 75              return expr;
 76          }
 77          /// <summary>returns the first if both are same, or the largest type of both (or the first)</summary>
 78          private static Type MaxType(Type a, Type b) { return a==b?a:(_prom.FirstOrDefault(t=>t==a||t==b)??a); }
 79          /// <summary>
 80          /// Code generation of binary and unary epressions, utilizing type coercion where needed
 81          /// </summary>
 82          private static readonly Dictionary<string, Func<Expression, Expression, Expression>> _binOp =
 83              new Dictionary<string,Func<Expression,Expression,Expression>>()
 84          {
 85              { "||", (a,b)=>Expression.OrElse(Coerce(a, _bool), Coerce(b, _bool)) },
 86              { "&&", (a,b)=>Expression.AndAlso(Coerce(a, _bool), Coerce(b, _bool)) },
 87              { "==", (a,b)=>Expression.Equal(Coerce(a,b), Coerce(b,a)) },
 88              { "!=", (a,b)=>Expression.NotEqual(Coerce(a,b), Coerce(b,a)) },
 89              { "<", (a,b)=>Expression.LessThan(Coerce(a,b), Coerce(b,a)) },
 90              { "<=", (a,b)=>Expression.LessThanOrEqual(Coerce(a,b), Coerce(b,a)) },
 91              { ">=", (a,b)=>Expression.GreaterThanOrEqual(Coerce(a,b), Coerce(b,a)) },
 92              { ">", (a,b)=>Expression.GreaterThan(Coerce(a,b), Coerce(b,a)) },
 93          };
 94          private static readonly Dictionary<string, Func<Expression, Expression>> _unOp =
 95              new Dictionary<string, Func<Expression, Expression>>()
 96          {
 97              { "!", a=>Expression.Not(Coerce(a, _bool)) },
 98          };
 99          /// <summary>create a constant of a value</summary>
100          private static ConstantExpression Const(object v) { return Expression.Constant(v); }
101          /// <summary>create lambda parameter field or property access</summary>
102          private MemberExpression ParameterMember(string s) { return Expression.PropertyOrField(_param, s); }
103          /// <summary>create lambda parameter field or property access</summary>
104          private Expression<Func<TData, bool>> Lambda(Expression expr)
105          { return Expression.Lambda<Func<TData, bool>>(expr, _param); }
106          /// <summary>the lambda's parameter (all names are members of this)</summary>
107          private readonly ParameterExpression _para = Expression.Parameter(typeof(TData), "_p_");
108          #endregion
109          #region parser
110          /// <summary>initialize the parser (and thus, the scanner)</summary>
111          private PredicateParser(string s): base(s) { }
112          /// <summary>main entry point</summary>
113          public static Expression<Func<TData, bool>> Parse(string s)
114          { return new PredicateParser<TData>(s).Parse(); }
115          private Expression<Func<TData, bool>> Parse() { return Lambda(ParseExpression()); }
116          private Expression ParseExpression() { return ParseOr(); }
117          private Expression ParseOr()         { return ParseBinary(ParseAnd, "||"); }
118          private Expression ParseAnd()        { return ParseBinary(ParseEquality, "&&"); }
119          private Expression ParseEquality()   { return ParseBinary(ParseRelation, "==", "!="); }
120          private Expression ParseRelation()   { return ParseBinary(ParseUnary, "<", "<=", ">=", ">"); }
121          private Expression ParseUnary()      { return CurrOpAndNext("!") != null
122                                                 ? _unOp["!"](ParseUnary()) : ParsePrimary(); }
123          private Expression ParseIdent()      { return ParameterMember(CurrOptNext); }
124          private Expression ParseString()     { return Const(Regex.Replace(CurrOptNext, "^\"(.*)\"$",
125                                                 m => m.Groups[1].Value)); }
126          private Expression ParseNumber()     { return Const(IsDouble
127                                                 ? double.Parse(CurrOptNext) : int.Parse(CurrOptNext)); }
128          private Expression ParsePrimary()
129          {
130              if (Curr == "(") return ParseNested();
131              if (IsIdent) return ParseIdent();
132              if (IsString) return ParseString();
133              if (IsNumber) return ParseNumber();
134              Abort("(...) or number or string or identifier expected");
135              return null; // dummy
136          }
137          private Expression ParseNested()
138          {
139              if (CurrAndNext != "(") Abort("(...) expected");
140              Expression expr = ParseExpression();
141              if (CurrOptNext != ")") Abort("')' expected");
142              return expr;
143          }
144          /// <summary>generic parsing of binary expressions</summary>
145          private Expression ParseBinary(Func<Expression> parse, params string[] ops)
146          {
147              Expression expr = parse();
148              string op;
149              while ((op = CurrOpAndNext(ops)) != null) expr = _binOp[op](expr, parse());
150              return expr;
151          }
152          #endregion
153      }
154  }

This program calls the entry point of the parser above and runs various queries employing the calculated expression:

C#
  1  static void Main(string[] args)
  2  {
  3      var items = new List<Element>()
  4      {
  5          new Element("a", 1000),
  6          new Element("b", 900),
  7          new Element("c", 800),
  8          new Element("d", 700),
  9          new Element("e", 600),
 10          new Element("x", 500),
 11          new Element("y", 400),
 12          new Element("z", 300),
 13      };
 14  
 15      string s = "Name == \"x\" || Number >= 800";
 16      var pred = SimpleExpression.PredicateParser<Element>.Parse(s);
 17      Console.WriteLine("User Entry: {0}", s);
 18      Console.WriteLine("Expr Tree:  {0}", pred.ToString());
 19      var f = pred.Compile();
 20      Console.WriteLine("### mark affectd items ###");
 21      foreach (var item in items)
 22      {
 23          Console.WriteLine("{2} Name = {0}, Number = {1}", item.Name, item.Number, f(item) ? "x" : " ");
 24      }
 25      Console.WriteLine("### where-select ###");
 26      var q = from e in items where f(e) select e;
 27      foreach (var item in q)
 28      {
 29          Console.WriteLine("  Name = {0}, Number = {1}", item.Name, item.Number);
 30      }
 31  }
The output is:
User Entry: Name == "x" || Number >= 800
Expr Tree:  _p_ => ((_p_.Name == "x") OrElse (Convert(_p_.Number) >= 800))
### mark affectd items ###
x Name = a, Number = 1000
x Name = b, Number = 900
x Name = c, Number = 800
  Name = d, Number = 700
  Name = e, Number = 600
x Name = x, Number = 500
  Name = y, Number = 400
  Name = z, Number = 300
### where-select ###
  Name = a, Number = 1000
  Name = b, Number = 900
  Name = c, Number = 800
  Name = x, Number = 500

Points of Interest

As mentioned, check out the available parser generators to do real work with parsers.

LINQ Expression Tree

Dynamic LINQ

To extend the parser: You may easily add more to the expressions, especially new operators is a simple thing:

  • to add + and - binary operators, add them to the _binOp dictionary (similar to ==, e.g. , ("+": Expression.Add(...), "-": Expression.Subtract(...)) create ParseSum() as a copy of ParseRelation, pass "+", "-" as ops, pass ParseSum to ParseRelation (in place of the ParseUnary), pass ParseUnary to ParseSum. That's it.
  • likewise for "*", "/", "%": make ParseMul as copy of the above mentioned ParseSum, pass the right ParseXXX actions, add the respective Expression factories to the _binOps dictionary. Done.
  • An unary "-" is to be added in the _unOps dictionary (no coercion needed). The parsing is done in the ParseUnary() function, e.g.
C#
return CurrOpAndNext("!") != null ? _unOp["!"](ParseUnary())
     : CurrOpAndNext("-") != null ? _unOp["-"](ParseUnary())
     : ParsePrimary();)

If you like this alternative to the tip, please rate it ;-)

Any feedback is very much appreciated.

Please note: this is meant as alternative to the given tip. This alternative does not require the data provider to use special kind of properties or fields. For real work on this, checkout the available Dynamic LINQ implementaiton.

Have fun!

Andi

History

V1.02012-03-28First version

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Founder eXternSoft GmbH
Switzerland Switzerland
I feel comfortable on a variety of systems (UNIX, Windows, cross-compiled embedded systems, etc.) in a variety of languages, environments, and tools.
I have a particular affinity to computer language analysis, testing, as well as quality management.

More information about what I do for a living can be found at my LinkedIn Profile and on my company's web page (German only).

Comments and Discussions

Discussions on this specific version of this article. Add your comments on how to improve this article here. These comments will not be visible on the final published version of this article.