Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

LR-Evaluator: An Expression Evaluation Engine Based on LocalReport

4.77/5 (12 votes)
8 May 2007CPOL9 min read 1   2K  
Check out this unconventional use of Microsoft's Client-side Reporting Technology that can make your Winform or ASP.NET apps "expression aware" and more powerful for your users.

Screenshot - LREvaluator1.png

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:

  1. It can evaluate up to 1,584 separate expressions in a single call to Eval().
  2. While LR-Evaluator is written in C#, expressions are written in VB.NET.
  3. Expressions can reference custom code. Custom code can be public methods, fields, or properties.
  4. 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.
  5. Parameters can be passed to the evaluator and they can be referenced by expressions.
  6. 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:

  1. 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.
  2. Templates have place-holders embedded within them. These place-holders may contain expressions.
  3. 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...

C#
[Test]
public void Case_StaticSingleEval()
{
    // This sample demonstrates a simple static call to evaluate a single 
    // expression.
    // Returned value is always a string representation of result.
    // If the expression does not begin with an equals sign then it is treated
    // as a literal value which will just get returned back.
    // If the expression is invalid, no exception is thrown but the returned
    // result is "#Error".

    Debug.WriteLine(Evaluator.Eval("=today()"));
}

[Test]
public void Case_StaticMultiEval()
{
    // Multiple expressions can be passed to a static call of Eval in the form 
    // of a string array. A corresponding string array is returned which 
    // contains evaluated results.
    // The return array is always the same number of elements is the input 
    // array.
    // If any of the expressions are invalid, all results will be "#Error".

    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()
{
    // This sample demonstrates how to evaluate a single expression
    // via an instance of the Evaluator class.
    // Using an instance of the evaluator provides more control than static 
    // calls.
    // You can assign each expression a key which can be used after the Eval() 
    // to retrieve the corresponding evaluated result.
    // Note that for instance calls to Eval(), bad expressions will throw
    // trappable exceptions.
    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()
{
    // This sample demonstrates how to evaluate multiple expressions
    // via an instance of the Evaluator class.

    Evaluator ev = new Evaluator();


    // Note: If you want to work off boolean return value from Eval() call 
    // rather than having it throw an exception for bad expressions, uncomment 
    // the next line...
    //ev.ThrowExceptions = false; // default is true for instance calls, false 
    //for static calls

    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();

    // Note: If ThrowExceptions is turned off, you can do something like 
    // this...
    //if (ev.Eval())
    // Debug.WriteLine("All expressions evaluated successfully.");
    //else
    // Debug.WriteLine("Bad expression somewhere. Inspect ev.LastException for 
    // details.");

    foreach (KeyValuePair<string, EvaluatorExpression> 
        kvp in ev.Expressions)
    {
        Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " + 
            kvp.Value.LastResult);
    }
}

[Test]
public void Case_SimpleParmEval()
{
    // Parameters can be passed to an instance of the Evaluator class.
    // An input parameter can be a string or an array of strings.
    // Parameters can be referenced in expressions like 
    // "=Parameters!MyParm.Value"

    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()
{
    // Parameters can be passed to an instance of the Evaluator class.
    // An input parameter can be a string or an array of strings.
    // Expressions can access individual elements of a parameter that is a 
    // string array by using an index number. Arrays are zero-based.

    Evaluator ev = new Evaluator();
    ev.Parameters.Add("MyMultiParm", new EvaluatorParameter(new string[] { 
        "parmValA", "parmValB", "parmValC" }));

    // note: the following is an example of using an array index to return 
    // first item in VB array
    ev.AddExpression("=\"The first item of array parm is \" + 
        Parameters!MyMultiParm.Value(0)");

    // note: the following is an example to get all array items joined 
    // together with comma delimiter. Use Split() function to split.
    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()
{
    // Multiple data sources can be passed to an instance of the Evaluator 
    // class.
    // A data source can be either a DataTable or a business object collection.
    // Note that if you work with DataSets, you must pass an individual table 
    // of the dataset (and not the dataset) to the Evaluator instance.

    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);

    // not referencing a field with a scope function will return values from 
    // the last row
    ev.AddExpression("=\"Field ref with no scope: \" + 
        Fields!FirstName.value");

    // if more than one datasource is added to localreport then a scope 
    // function required
    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()
{
    // Multiple data sources can be passed to an instance of the Evaluator 
    // class.
    // A data source can be either a DataTable or a business object collection.
    // A business object is simply a class definition that contains public 
    // properties.
    // The properties are treated like column values. Each instance of a 
    // business object in a
    // collection is treated like a row in a table.

    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");

    // note how we can reference nested object properties in a biz object
    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()
{
    // Embedded code is a string of public fields, properties, and/or 
    // functions in VB.NET syntax which can be referenced by expressions. The 
    // report engine compiles these members into a "Code" class. So your 
    // expressions call embedded code like "=Code.MyMethod()" or 
    // "=Code.MyField".

    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()
{
    // This example demonstrates how to use an instance of the Evaluator class 
    // to evaluated multiple expressions referencing parameters, data sources
    // (data table and biz object).

    Evaluator ev = new Evaluator();

    // add a couple parameters...

    // a parameter can be a single string value
    ev.Parameters.Add("Parm1", new EvaluatorParameter("***Parm1 value***"));
    // or a parameter can be an array of strings
    ev.Parameters.Add("Parm2", new EvaluatorParameter(new string[] { 
        "parm2.a", "parm2.b", "parm2.c" }));

    // now create a couple of datasources for our evaluator...

    // first make and add a list of customer objects...
    List<Object> TestCustomers = new List<Object>();
    TestCustomers.Add(new Customer("John", "Doe"));
    TestCustomers.Add(new Customer("Jane", "Smith"));
    ev.AddDataSource("BizObjectCollection", TestCustomers);

    // now make and add a standard datatable
    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);

    // now add some expressions to be evaluated...
    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\")"));

    // do the evaluating...
    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:

  1. Save the current template to a string variable
  2. 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.
  3. Parse out our expressions. For this demo, expressions are anything between "«" and "»".
  4. Add each expression to the LR-Evaluator Expressions collection.
  5. Call the Eval() method on the LR-Evaluator instance.
  6. Step through the Expressions collection and replace each expression in our template with the LastResult value.
  7. Go into "Preview" mode and display the merged result.

Screenshot - LREvaluator2.png

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.

Screenshot - LREvaluator3.png

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.

Screenshot - LREvaluator4.png

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:

  1. 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).
  2. We add items from the LR-Evaluator collections to corresponding LocalReport collections such the parameter values, and data source instances.
  3. 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.
  4. 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.
  5. 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.
  6. 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...

  1. VB.NET public functions/properties/fields embedded as a string in the RDLC
  2. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)