Introduction
If you do a search on CodeProject for "expression evaluation", you'll find
several good articles and different techniques for trying to accomplish this in
.NET. Like the solution I'm presenting here, they all have their advantages and
disadvantages. Perhaps in the near future, we'll have a secure, fast, and
easy-to-use solution that stands above the token parsers, CodeDOM manipulators,
and DynamicMethod examples of today. Expression evaluation may even be
a by-product of the dynamic language craze (read Ruby on Rails) that Microsoft is
catching on to. Have you heard about the Dynamic
Language Runtime and Lightweight
Code Generation?
But I digress. I was looking to provide expression evaluation capabilities in
my apps without having to write a lot of code or work around security concerns. I
wanted the speed, security, and control of the token parsers but I wanted the
power and language richness of the CodeDOM. In this article, I'll explain how I
use LocalReport
as the basis for a general-purpose expression evaluation class which I call
LR-Evaluator. Here's a rundown of LR-Evaluator features:
- It can evaluate up to 1,584 separate expressions in a single call to
Eval()
.
- While LR-Evaluator is written in C#, expressions are written in VB.NET.
- Expressions can reference custom code. Custom code can be public methods,
fields, or properties.
- Custom code and expressions run under Code Access
Security (CAS) in a restricted sandbox by default. This means that file
I/O or network access won't work unless you explicitly set
ExecuteInCurrentAppDomain
to True in an Evaluator instance.
- Parameters can be passed to the evaluator and they can be referenced by
expressions.
- Multiple data sources can be passed to the evaluator. Expressions can
reference fields in these data sources. A data source can be a data table or a
custom business object collection.
Background
Microsoft's
Reporting Technology consists of several components that allow us to make
powerful reporting solutions in our Windows Forms and ASP.NET applications. What
might not be so clear about this technology is that it consists of both a client
side as well as a server-side reporting engine. The server-side technology,
better known as Microsoft
SQL Server Reporting Services, requires SQL Server and uses a web service to
publish, manage, and serve reports. The client-side components come with Visual
Studio or can
be downloaded so that you can make stand-alone reports that do not require
SQL Server or any other database. The client-side features are a little more
scaled down than its server-based brother but it still has all the
expression-evaluation goodness which I'll be focusing on in this article.
The client-side components primarily consist of the LocalReport class, a
WinForm ReportViewer control, and an ASP.NET ReportViewer control. The controls
use the LocalReport class under-the-scenes.
After doing a couple of projects with Microsoft's Reporting Services (which
is pretty awesome after you get a few reports under your belt), I started
getting the idea that this has all the features I wanted in an expression
evaluation engine - I just didn't need the "reporting" part. You'll see that
LocalReport not only evaluates expressions but it covers the security concern
pretty well through Code Access Security. Other useful features for an
expression evaluation engine include attaching custom code, passing in
parameters, and passing in data sources. It can also be loaded, executed, and
unloaded pretty reliably. So after some dabbling and testing, I decided that
performance was good enough for my purposes and that it might be worth putting
together a project and sharing it with the community.
Expression Evaluation
If you have used Reporting Services, then you likely have a good idea of what
expressions are and how powerful they can be. For example, the Report Designer,
like all report designers, gives you the ability to create a report template
containing textboxes and other data-bound containers that will display
underlying data source values when the report is run. You can populate
properties of a textbox at design time to dictate not only the contents of the
textbox but visual aspects too such as the font name, font size, forecolor,
backcolor, etc. An interesting thing about Microsoft's reporting technology is
that nearly every property of every report control can be a code expression
rather than just a literal value. So, a textbox's forecolor property can be a
literal value like "Black", or it can be a runtime expression like
=IIF(Fields!Status.Value= "OUT OF STOCK", "Red","Black")
.
When is expression evaluation with LR-Evaluator useful? While it is pretty
tough to generalize, the Report Designer itself makes a good example. You may
notice that it follows a pattern like this:
- Your app might have a "designer" or "editor" module that allows a user to
create some sort of "template". Ex. A report template, a data-entry template,
a mail-merge template, a graphic template, a code generation template, etc.
- Templates have place-holders embedded within them. These place-holders may
contain expressions.
- The template is rendered at runtime to produce a result. That is, the
expressions are pulled out of the template, evaluated, and the results are
merged back into the template to produce something useful such as a populated
form, something to be printed,something to be executed, etc.
Using the code
Here is a listing from Evaluator_Tests.cs that shows how to use
Eval()
from all angles...
[Test]
public void Case_StaticSingleEval()
{
Debug.WriteLine(Evaluator.Eval("=today()"));
}
[Test]
public void Case_StaticMultiEval()
{
string[] sResults = Evaluator.Eval(new string[] { "=today()",
"literal val", "=1+1+2" });
foreach (string s in sResults)
Debug.WriteLine("StaticMultiEval result = " + s);
}
[Test]
public void Case_InstanceSingleEval()
{
string sResult = "";
Evaluator ev = new Evaluator();
ev.Expressions.Add("MyExpressionKey", new EvaluatorExpression("=today()"));
ev.Eval();
sResult = ev.Expressions["MyExpressionKey"].LastResult;
Debug.WriteLine("InstanceSingleEval result = " + sResult);
}
[Test]
public void Case_InstanceMultiEval()
{
Evaluator ev = new Evaluator();
ev.Expressions.Add("Expression1", new EvaluatorExpression("=today()"));
ev.Expressions.Add("Expression2",
new EvaluatorExpression("=today().AddDays(1)"));
ev.Expressions.Add("Expression3",
new EvaluatorExpression("=today().AddDays(2)"));
ev.Eval();
foreach (KeyValuePair<string, EvaluatorExpression>
kvp in ev.Expressions)
{
Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " +
kvp.Value.LastResult);
}
}
[Test]
public void Case_SimpleParmEval()
{
Evaluator ev = new Evaluator();
ev.Parameters.Add("MyParm", new EvaluatorParameter("12345"));
ev.AddExpression("=\"Your parameter value is \" +
Parameters!MyParm.Value");
ev.Eval();
foreach (KeyValuePair<string, EvaluatorExpression>
kvp in ev.Expressions)
{
Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " +
kvp.Value.LastResult);
}
}
[Test]
public void Case_ArrayParmEval()
{
Evaluator ev = new Evaluator();
ev.Parameters.Add("MyMultiParm", new EvaluatorParameter(new string[] {
"parmValA", "parmValB", "parmValC" }));
ev.AddExpression("=\"The first item of array parm is \" +
Parameters!MyMultiParm.Value(0)");
ev.AddExpression("=\"The joined array parm is \" +
Join(Parameters!MyMultiParm.Value, \",\")");
ev.Eval();
foreach (KeyValuePair<string, EvaluatorExpression>
kvp in ev.Expressions)
{
Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " +
kvp.Value.LastResult);
}
}
[Test]
public void Case_DataTableEval()
{
Evaluator ev = new Evaluator();
DataTable dt = new DataTable();
dt.Columns.Add("FirstName");
dt.Columns.Add("LastName");
dt.Rows.Add(new Object[] { "Ronald", "McDonald" });
dt.Rows.Add(new Object[] { "Barney", "Rubble" });
dt.Rows.Add(new Object[] { "Scooby", "Doo" });
ev.AddDataSource("Customer", dt);
ev.AddExpression("=\"Field ref with no scope: \" +
Fields!FirstName.value");
ev.AddExpression("=\"Field ref with First() scope: \" +
First(Fields!FirstName.value, \"Customer\")");
ev.Eval();
foreach (KeyValuePair<string, EvaluatorExpression>
kvp in ev.Expressions)
{
Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " +
kvp.Value.LastResult);
}
}
[Test]
public void Case_BizObjectEval()
{
Evaluator ev = new Evaluator();
Customer customer = new Customer();
customer.FirstName = "Fred";
customer.LastName = "Flintstone";
customer.Address.Addr = "301 Cobblestone Way";
customer.Address.City = "Bedrock";
customer.Address.State = "BC";
customer.Address.Zip = "00001";
List<Object> custs = new List<Object>();
custs.Add(customer);
ev.AddDataSource("Customer", custs);
ev.AddExpression("=Fields!FirstName.value");
ev.AddExpression("=Fields!Address.value.Zip");
ev.Eval();
foreach (KeyValuePair<string, EvaluatorExpression>
kvp in ev.Expressions)
{
Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " +
kvp.Value.LastResult);
}
}
[Test]
public void Case_EmbeddedCodeEval()
{
string sResult = "";
Evaluator ev = new Evaluator();
ev.Code = "Public Function DoSomethingWith(
ByVal s As String) as string \r\n" +
"dim x as string = s & \"bar\" \r\n" +
"return x \r\n" +
"End Function \r\n";
ev.Expressions.Add("MyExpression",
new EvaluatorExpression("=Code.DoSomethingWith(\"foo\")"));
ev.Eval();
sResult = ev.Expressions["MyExpression"].LastResult;
Debug.WriteLine("EmbeddedCodeEval result = " + sResult);
}
[Test]
public void Case_ComplexEval()
{
Evaluator ev = new Evaluator();
ev.Parameters.Add("Parm1", new EvaluatorParameter("***Parm1 value***"));
ev.Parameters.Add("Parm2", new EvaluatorParameter(new string[] {
"parm2.a", "parm2.b", "parm2.c" }));
List<Object> TestCustomers = new List<Object>();
TestCustomers.Add(new Customer("John", "Doe"));
TestCustomers.Add(new Customer("Jane", "Smith"));
ev.AddDataSource("BizObjectCollection", TestCustomers);
DataTable dt = new DataTable("MyTable");
dt.Columns.Add("FirstName");
dt.Columns.Add("LastName");
dt.Rows.Add(new Object[] { "Ronald", "McDonald" });
dt.Rows.Add(new Object[] { "Fred", "Flintstone" });
ev.AddDataSource("SomeDataTable", dt);
ev.Expressions.Add("Expression1", new EvaluatorExpression(
"=\"Today is: \" + Today().ToLongDateString()"));
ev.Expressions.Add("Expression2", new EvaluatorExpression(
"=\"Tomorrow is: \" + Today().AddDays(1).ToLongDateString()"));
ev.Expressions.Add("Expression3", new EvaluatorExpression("=(1+1)*(2+2)"));
ev.Expressions.Add("Expression4", new EvaluatorExpression(
"=\"Value of first passed-in parameter is: \" +
Parameters!Parm1.Value"));
ev.Expressions.Add("Expression5", new EvaluatorExpression(
"=\"Value of second passed-in parameter (second element) is: \" +
Parameters!Parm2.Value(1)"));
ev.Expressions.Add("Expression6", new EvaluatorExpression(
"=\"FirstName field value from biz object is: \" +
First(Fields!FirstName.value, \"BizObjectCollection\")"));
ev.Expressions.Add("Expression7", new EvaluatorExpression("=\"FirstName
field value from datatable row is: \" + First(Fields!FirstName.value,
\"SomeDataTable\")"));
ev.Eval();
foreach (KeyValuePair<string, EvaluatorExpression>
ee in ev.Expressions)
{
System.Diagnostics.Debug.WriteLine("-----------------------------");
System.Diagnostics.Debug.WriteLine("Expression key: " + ee.Key);
System.Diagnostics.Debug.WriteLine("Expression string: " +
ee.Value.Expression);
System.Diagnostics.Debug.WriteLine("Expression last result: " +
ee.Value.LastResult);
}
}
LR-Evaluator Mail Merge Demo
The Mail Merge sample application allows a user to enter free-form text with
embedded VB.NET expressions that can be evaluated and merged with records from a
data source. It slightly resembles mail merge functionality found in word
processors such as Microsoft Word. The top textbox is where CSV formatted data
can be keyed-in or pasted. This data is loaded into a DataTable which we'll use
to create a row-specific data source for our instance of LR-Evaluator. The
bottom textbox is used for editing a form letter template. Expressions may be
inserted as desired and may contain VB.NET expressions that reference fields in
the data source. When the "Preview" button is clicked, the app will do the
following:
- Save the current template to a string variable
- Create a data source (a new DataTable) for the currently active row in the
Data textbox. This row is passed as a data source to the instance of
LR-Evaluator.
- Parse out our expressions. For this demo, expressions are anything between
"«" and "»".
- Add each expression to the LR-Evaluator Expressions collection.
- Call the
Eval()
method on the LR-Evaluator instance.
- Step through the Expressions collection and replace each expression in our
template with the LastResult value.
- Go into "Preview" mode and display the merged result.
Once the user is in "preview" mode, they can skip to the previous or next
record to change the data source and re-evaluate/re-merge the expressions.
Clicking on the "Edit" button restores the saved template and allows the user
to modify the template or data.
LR-Evaluator ASP.NET Demo
The web sample application allows a user to enter input parameters, a data
source,custom code, and expressions which will all be passed to an instance of
LR-Evaluator when the user clicks on the "Evaluate" button. Expressions can
reference the parameters, data source fields, and public
functions/fields/properties in the embedded code.
Points of Interest
LR-Evaluator Under The Hood
LR-Evaluator is pretty much a wrapper around LocalReport's existing
expression evaluation functionality. Most of the action takes place in
LR-Evaluator's Eval()
method call. Eval()
takes the
following steps to manipulate LocalReport to get results:
- An RDLC
file is generated in memory and loaded into LocalReport as an input stream.
The RDLC contains all necessary report object definitions such as textboxes,
parameter names, data source definitions, and embedded code. For our purposes,
we generate a textbox element for each expression that we have in the
Expressions collection in the LR-Evaluator object. Our 1,584 expression limit
it due to an apparent limit in LocalReport (Note that the limit is for
expressions only - not textboxes that may contain literal values).
- We add items from the LR-Evaluator collections to corresponding
LocalReport collections such the parameter values, and data source instances.
- If
ExecuteInCurrentAppDomain
is True then we set the LocalReport's ExecuteReportInCurrentAppDomain
to the evidence of the current app domain. Beware: if you set this to true and
security is any concern, you may want to read up on System.Security.Policy.
- We call LocalReport's Render()
method which will return a rendered report output stream based on our
dynamically generated RDLC data and other input. In our case, we're telling
LocalReport to return the output in Adobe PDF format. LocalReport currently
only supports outputting a stream to Excel, PDF, or Image format. Fortunately,
in our case, the PDF output isn't compressed so it seemed to be the lesser of
the evils to parse.
- We take the rendered output stream, convert it into a string, and parse
out the results. It sure would have been nice if XML output was supported on
LocalReport as it is on the server-based technology. Fortunately, PDF is an open
specification so it wasn't too hard to figure out how to parse out our
results.
- Finally, we update the
EvaluatorExpression.LastResult
values
in the Expressions collection.
Custom Code and External Assemblies
That LocalReport Class supports two types of custom code...
- VB.NET public functions/properties/fields embedded as a string in the RDLC
- References to custom external assemblies
LR-Evaluator supports the first option no problem. While I tried to support
referencing external assemblies in LR-Evaluator I just couldn't get it working
properly. There are a number of posts
out there from people whom have also fought with this. I think the problem
has to do with Code Access Security and the dynamic in-memory RDLC creation vs.
using a physical file for the RDLC. I wanted to stay away from generating
physical files for this utility because I intended to use it on a web server; I
don't want the extra I/O. And since security was one of my primary goals with
this library, getting external assemblies to work seemed to involve straying
away from the restricted sandbox feature which wasn't in my interest.
I
appreciate any feedback or suggestions for code improvements. Thanks for
looking!
History
- May 7, 2007 Submitted to Code Project.