Click here to Skip to main content
Click here to Skip to main content
Go to top

Entity mapping language implementation using bsn-goldparser with CodeDom

, 6 Apr 2012
Rate this:
Please Sign up or sign in to vote.
Use the GOLD Parser to define a language for mapping between two business entities, create a parser using the bsn-goldparser engine, and generate an assembly using CodeDom.

Grammar testing form

Introduction

This program shows how to use the GOLD Parser to define a simple language and produce a grammar table consumed by a parser engine. The parser engine used is from bsn-goldparser which supports creating a parser using C#. The parser implementation uses CodeDom to generate the compilation unit which is finally passed in the C# code compiler to produce C# source and assembly.

This program lights the path to define a simple domain language allowing users using the syntax to edit business rules, translates them, and dynamically generates an assembly consumed by the main program. Imagining that such business rules are just string values which can be stored in a database table, retrieved and edited by the user anytime, and finally integrated to an application without any code amendment. Surely, this solves issues that software developers faced today that different clients have their unique sets of business rules even they come from the same business sector and worse at all such rules are always changing. This program demonstrates entity mapping rules implementation which is a typical usage when facing with challenge from entity creation with related entity, e.g., issue invoice from related sales order. 

For those who are new to bsn-goldparser, I strongly recommend who have a look on this article The Whole Shebang: Building Your Own General Purpose Language Interpreter which has an excellent introduction on this parser engine and language parsing topic.

Background

Before talking about the program I created, here I explain the problem I am going to solve first. In many business applications, we often need to create an entity with properties mapped or transformed from an existing entity's properties. As an example, look at the figure below. We have an invoice object needed to set its invoice date (InvoiceDate) 30 days later than the order transaction date (TxDate), order number (OrderNo) to the related contract number (ContractNo), and freight charge (Freight) from the result of applying a formula using the order's CBM (TotalCBM) property value. Such business rules can be hard coded in a program but in the long run, it risks for code amendments when such rules are changed. Of course seasoned developers address such issues by adopting design patterns to separate the implementation details in libraries which make their life easier whenever amendments are needed. But if we need to create a commercial package to meet hundreds to thousands of clients with all combinations of business rules, it is still be a nightmare to have many different implementations of assemblies created. To meet such vast requirement variations, we can create a rule editor supported by a simple domain language and allow users to edit business rules to meet their business requirements. 

Order to invoice mapping

The point is what kind of language we use to store the source code, will it be C# or VB? Certainly, you can and especially .NET framework is flexible enough to have C# / VB read from a store, dynamically compiled and loaded into memory space. Certainly this is one of the solutions to solve the issue, but it is not quite useful to users who are responsible to maintain business rules as they are unlikely to understand such computer languages. The possible solution is to define simple language constructs which can easily be understood by them and just advance enough to solve the problem in domain. 

This business rules creation uses a domain language syntax similar to C# statement and expression syntax. I use GOLD Parser to edit my domain language and use bsn-goldparser, CodeDom and related tools to generate the assembly dynamically. Although the domain language used in business rules adopted a simplified version of C# statement and expression constructs, do not be fooled by it because you are not limited to your imagination to craft another highly verbose English like domain language for users to enter their business rules. The only reason I define the syntax like C# statements and expressions is to produce the sample in shortest time and C# is the language I am most familiar with.

Using GOLD Parser and related engines to create a specific parser implementation is not new but most samples showing how to implement it as an interpreter are not quite useful for integrating it into the main application. After reviewing different options, IL Emit, .NET 4.0 Expression, and CodeDom, to generate dynamic code, I found that CodeDom can be easily adopted especially if you have a .NET background. What other nice things come with it are that it gives a language neutral program graph (code compilation unit) and the graph can be serialized for loading later to improve performance.

Using the code

Run the sample

You only need Visual Studio 2010 to open and run the sample program from downloaded source without dependency on other libraries. There is a testing form which is prefilled with rules from the Sampler class as shown below. This sample uses two model classes: SalesOrder and Invoice, which are returned from the GetOrder() and GetInvoice() methods, respectively. The sample properties mapping rules are returned from the GetMappingStatements() method and they should be easily understood without great difficulties. Actually, the rules statements show different ways that we can map a property to the target entity (Invoice object). Besides assigning a value to target from derivation of the source property, we can invoke a global class method to assign a value to the target property. For example, the invoice number (InvoiceNo) is assigned a returned value from a global service class method GetNextInvoiceNo() in this example.

// -------------------------------------------------------------------
// Listing 1.
// -------------------------------------------------------------------
public class Sampler
{
   static public SalesOrder GetOrder()
    {
        return new SalesOrder { TxDate = DateTime.Today, 
               ContractNo = "A1123", TotalCBM = 5.5m };
    }

   static public Invoice GetInvoice()
    {
        return new Invoice();
    }

    static public string GetMappnigStatements()
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("InvoiceDate = source.TxDate.AddDays(30) ;\r\n");
        sb.Append("InvoiceNo = BusinessService.Instance.GetNextInvoiceNo() ;\r\n");
        sb.Append("OrderNo = source.ContractNo ;\r\n");
        sb.Append("Freight = (source.TotalCBM - 1.5) * 2.2 ;\r\n");
        sb.Append("CreateDate = DateTime.Today ;\r\n");
        return sb.ToString();
    }
}

You can edit the business rules and click the Parse button to test parsing. Each business rule starts with a target entity property name, followed by a "=" character, and a combination of expressions consisting of a source property (source is the keyword that represents the source entity), global class method/property, or constant. Each rule must ends with a semicolon ";". Look at the first figure at the top of this article, you will find that each statement looks like a standard C# assignment statement and you only need to specify the target property at the right hand side of the statement without using object qualifier. As said before, the design the domain language is on your hands and I used a simplified C# syntax only to save time in creating this sample program. Of course, if your have any new language token or rule added, you need to change the bsn-goldparser sematic action class implementation accordingly but it is not a difficult task and you can refer to the sample program who they can be done.

After clicking the Parse button, you can find the generated C# source code at the lower pane of the main testing form as below. This is a class with only a single method with statements in the method body reflecting business rules entered.

// -------------------------------------------------------------------
// Listing 2.
// -------------------------------------------------------------------
namespace EntityMapper.CodeGen {
    using System;
    using EntityMapper.Service;
    
    public class MapperUtility {        
        public void MapEntity(EntityMapper.Model.SalesOrder source, EntityMapper.Model.Invoice target) {
            target.InvoiceDate = source.TxDate.AddDays(30);
            target.InvoiceNo = BusinessService.Instance.GetNextInvoiceNo();
            target.OrderNo = source.ContractNo;
            target.Freight = ((source.TotalCBM - 1.5m) * 2.2m);
            target.CreateDate = DateTime.Today;
        }
    }
}

After clicking the Execute button, you can get the testing result by running the compiled business rules against two sample entities, as below and see properties of invoice entity is diligently changed according to rules entered.

------ Sales Order ------
TxDate = 4/4/2012 12:00:00 AM
ContractNo = A1123
TotalCBM = 5.5

------ Invoice before calling method ------
InvoiceDate = 1/1/0001 12:00:00 AM
InvoiceNo = 
OrderNo = 
Freight = 0
CreateDate = 1/1/0001 12:00:00 AM

------ Invoice after calling method ------
InvoiceDate = 5/4/2012 12:00:00 AM
InvoiceNo = I702692
OrderNo = A1123
Freight = 8.80
CreateDate = 4/4/2012 12:00:00 AM

Other usages

The use of SalesOrder and Invoice types are only for demonstration purpose. Actually you can make use the code here to integrate them into your application for any type of entity mapping. Interestingly, two entities of same type can be mapped also to provide cloning feature according to certain rules. And even a single entity can be mapped backed to itself to support transformation which you can imagine this is a kind of object creation policy defined and retrieved from database store. The following example shows scenarios (picture followed) to use the mapping function through the EntityMapperGenerator class which provides both parsing and compilation methods.

// -------------------------------------------------------------------
// Listing 3.
// -------------------------------------------------------------------

var mapperGenerator = new EntityMapperGenerator(); 

// ----------- Scenario 1 (mapping) ----------- 
// 1.1 Parse business rules. textBoxInput.Text contains business rule
// statements entered. Last parameter is array containing imported namespaces.
mapperGenerator.Parse(typeof(SalesOrder), typeof(Invoice), 
  textBoxInput.Text, new string[] { "System", "EntityMapper.Service" }); 

// 1.2 Compile and get the delegate which represents the mapping method.
// This example uses Action<SalesOrder, Invoice>
var mapperMethod1 = (Action<SalesOrder, Invoice>)_mapperGenerator.GetDelegate(
    new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });

// 1.3 Invoke method. salesOrderObj and invoiceObj are entity instances undergoing property mapping.
mapperMethod1(salesOrderObj, invoiceObj);

// ----------- Scenario 2 (cloning) ----------- 
// 2.1 cloningRules should contain same entity type cloning business rules.
mapperGenerator.Parse(typeof(Invoice), typeof(Invoice), cloningRules, 
                new string[] { "System", "EntityMapper.Service" }); 

// 2.2 Note that this example uses Action<Invoice, Invoice>
var mapperMethod2 = (Action<Invoice, Invoice>)_mapperGenerator.GetDelegate(
    new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });

// 2.3 invoiceObj1 maps its properties to invoiceObj2 and invoiceObj2
// is the target entity going to have property values changed.
mapperMethod2(invoiceObj1, invoiceObj2);

// ----------- Scenario 3 (entity creation) ----------- 
// 3.1 creationRules should contain entity creation business rules.
mapperGenerator.Parse(typeof(Invoice), typeof(Invoice), creationRules, 
                      new string[] { "System", "EntityMapper.Service" }); 

// 3.2 Note that this example uses Action<Invoice, Invoice>
var mapperMethod2 = (Action<Invoice, Invoice>)_mapperGenerator.GetDelegate(
    new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });

// 3.3 newInvoiceObj's properties shall undergo transformation according to
// creation business rules, e.g. InvoiceDate = DateTime.Today; CustomerId = "Unknown";
var newInvoiceObj = new Invoice();
mapperMethod2(newInvoiceObj, newInvoiceObj);

Mapping scenarios

Points of interest

Define Language Syntax

When defining the language with Backus-Naur Form (BNF), I try to figure out the minimal syntax to meet the requirements on hand. So, only an assignment statement for target properties is formed and it shows in the language rule <Statement> ::= Identifier '=' <Expression> ';' and here Identifier represents the target entity property to be assigned the value from the evaluation of the expression on the right hand side of the assignment statement.

To follow what I have done, use GOLD Parser to define the grammar and enter as many test cases as possible which you expect to handle. If they are all passed, you can use the tool to generate Compiled Grammar Tables (.cgt). Note that as from version 5, GOLD Parser generates Enhanced Grammar Tables (.egt) by default but bsn-goldparser only supports old Compiled Grammar Tables (.cgt). So, you need to select the Save the Tables menu item under the Project menu to generate the required table type to file.

"Start Symbol" = <Program>

! -------------------------------------------------
! Character Sets
! -------------------------------------------------

{ID Head}      = {Letter} + [_]
{ID Tail}      = {Alphanumeric} + [_]
{String Chars} = {Printable} + {HT} - ["\]

! -------------------------------------------------
! Terminals
! -------------------------------------------------

Identifier    = {ID Head}{ID Tail}*
StringLiteral = '"' ( {String Chars} | '\' {Printable} )* '"'
DecimalValue =  {Number}+ | {Number}+ '.' {Number}+
Source = 'source'
Target = 'target'

! -------------------------------------------------
! Rules
! -------------------------------------------------

! The grammar starts below
<Program> ::= <Statements>

<Expression>  ::= <Expression> '>'  <Add Exp> 
               |  <Expression> '<'  <Add Exp> 
               |  <Expression> '<=' <Add Exp> 
               |  <Expression> '>=' <Add Exp>
               |  <Expression> '==' <Add Exp>    !Equal
               |  <Expression> '<>' <Add Exp>    !Not equal
               |  <Add Exp> 

<Add Exp>     ::= <Add Exp> '+' <Mult Exp>
               |  <Add Exp> '-' <Mult Exp>
               |  <Mult Exp> 

<Mult Exp>    ::= <Mult Exp> '*' <Negate Exp> 
               |  <Mult Exp> '/' <Negate Exp> 
               |  <Negate Exp> 

<Negate Exp>  ::= '-' <Value> 
               |  <Value> 

!Add more values to the rule below - as needed

<Value>       ::= StringLiteral
               |  DecimalValue
               |  '(' <Expression> ')'
               |  <Member Access>
               |  <Method Access>

<Member Access> ::= <SourceTarget> '.' Identifier
               |  Identifier '.' Identifier
               | <Value> '.' Identifier

<Args>       ::= <Expression> ',' <Args>
               | <Expression> 
               |
               
<Method Access> ::= <SourceTarget> '.' Identifier '(' <Args> ')'
               | Identifier '.' Identifier '(' <Args> ')'       
               | <Value> '.' Identifier '(' <Args> ')'     
                                          

<SourceTarget> ::= Source
               |  Target

<Statement> ::= Identifier '=' <Expression> ';'

<Statements> ::= <Statement> <Statements>
               | <Statement>

Prepare CodeDom code compilation unit

To use CodeDom to generate code and compile assembly, we need to define and create the CodeCompileUnit object first. I have wrapped the creation of the CodeCompileUnit object in the class ClassTypeWrapper as shown below. The class constructor uses the passed in namespace, classname, and import namespaces (using statements in C#) to define the main class we shall create later.  Please make sure you pass namespaces wanted to be used in your business rules, otherwise qualified name is needed when referrencing any type.

Note that I have separated the CodeMemberMethod creation in another class to be discussed shortly and the created CodeMemberMethod reference shall be passed in by calling the AddMainMethod() method in the same class.  Doing this way we have the flexibility for the CodeMemberMethod creation as you should know later the CodeMemberMethod signature is closely related to the usage of business rules supported.  For example, if you target to support another usage of bussines rules such as policy premium calculation in insurance business, surely the method signature is different.

To get mapper method generation,  we have another class MapCodeMemberMethod to generate the CodeMemberMethod reference used to add to the main class, MainClass, of the compilation unit wrapped in the class ClassTypeWrapper (listing 4).

// -------------------------------------------------------------------
// Listing 4.
// -------------------------------------------------------------------
public class ClassTypeWrapper
{
    public CodeCompileUnit CompileUnit { get; private set; }
    public CodeTypeDeclaration MainClass { get; private set; }
    public CodeMemberMethod MainMethod { get; private set; }

    public ClassTypeWrapper(string unitNamespace, string className, string[] importNamespaces)
    {
        CompileUnit = new CodeCompileUnit();

        // Default namespace
        CodeNamespace codeNS = new CodeNamespace(unitNamespace);

        // Import namespaces
        foreach (string ins in importNamespaces)
        {
            codeNS.Imports.Add(new CodeNamespaceImport(ins));
        }

        MainClass = new CodeTypeDeclaration(className);
        MainClass.IsClass = true;
        MainClass.TypeAttributes = System.Reflection.TypeAttributes.Public;
        codeNS.Types.Add(MainClass);
        
        CompileUnit.Namespaces.Add(codeNS);
    }

    public int AddMainMethod(CodeMemberMethod method)
    {
        this.MainMethod = method;
        return this.MainClass.Members.Add(this.MainMethod);
    }
}

If we review listing 2, the generated C# source code, the Create() method in listing 5 shall produce C# statements something like public void MapEntity(EntityMapper.Model.SalesOrder source, EntityMapper.Model.Invoice target) { }. There is no statement inside the method body and it is expected as we still need to parse the business rules entered by the user (or from other input sources) before we can determinate how to generate the method body. Nonetheless, the method body shall contain statements reflecting the business rules we are yet to process.

// -------------------------------------------------------------------
// Listing 5.
// -------------------------------------------------------------------
public class MapCodeMemberMethod
{
    public CodeMemberMethod Create(Type fromType, Type toType, 
      string name = "Map", string fromParamName = "source", string toParamName = "target")
    {
        // Declaring a method  void Map(fromType X, toType) ;
        CodeMemberMethod method = new CodeMemberMethod();
        method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
        method.Name = name;

        // Declares parameters 
        CodeParameterDeclarationExpression paramFromType = 
          new CodeParameterDeclarationExpression(new CodeTypeReference(fromType), fromParamName);
        paramFromType.Direction = FieldDirection.In;
        method.Parameters.Add(paramFromType);

        CodeParameterDeclarationExpression paramToType = 
          new CodeParameterDeclarationExpression(new CodeTypeReference(toType), toParamName);
        paramToType.Direction = FieldDirection.In;
        method.Parameters.Add(paramToType);

        method.ReturnType = new CodeTypeReference("System.Void");

        return method;
    }
}

Before I proceed to discuss about the bsn-goldparser for business rules parsing, I need to complete my discussion on CodeDom compilation unit creation. It actually sits inside another helper class EntityMapperGenerator in the method GetClassTypeWrapper() as shown below. It creates and returns a new wrapper object of type ClassTypeWrapper added with CodeMemberMethod reference created using object of type MapCodeMemberMethod described previously.  The ClassTypeWrapper has all neccessary CodeDom typed properies to be referred by bsn-goldparser sematic action implementation classes through ExcecutionContext TypeWrapper property.

// -------------------------------------------------------------------
// Listing 6.
// -------------------------------------------------------------------
public class EntityMapperGenerator
{
//
//  skipped details here  ... 
//
	private ClassTypeWrapper GetClassTypeWrapper(string fromParmName, 
	        string toParmName, string[] importedNamespaces)
	{
	    var classWrapper = new ClassTypeWrapper(
	        this.NamespaceName, this.ClassName, importedNamespaces);
	    classWrapper.AddMainMethod(new MapCodeMemberMethod().Create(
	      this.FromType, this.ToType, this.MethodName, fromParmName, toParmName));
	    return classWrapper;
	}        
}

Parse business rules with the bsn-goldparser engine

Now, we have the compiled language grammar table, CodeDom empty bodied CodeMemberMethod reference, and suppose we also have business rules entered by an user from somewhere. The next step is to generate the necessary CodeDom statements in the method body to represent business rules.

Using the bsn-goldparser engine

Before we can generate something useful using bsn-goldparser, we need to implement all terminals and rules defined in the input grammar table. Basically, all we need is to create classes derived from SemanticToken and use the TerminalAttribute attribute to mark classes that provide implementation to the Terminals and use the RuleAttribute attribute to mark methods that provide implementation to the Rules defined in the grammar table. As in the following listing, multiple Terminals can be mapped to a single class and in this particular implementation, it doesn't provide any processing at for quite obvious reasons. Also, please note that we use the SemanticToken derived class TokenBase as the base class for all other SemanticToken implementation classes.

// -------------------------------------------------------------------
// Listing 7.
// -------------------------------------------------------------------
[Terminal("(EOF)")]
[Terminal("(Error)")]
[Terminal("(Whitespace)")]
[Terminal("(")]
[Terminal(")")]
[Terminal(";")]
[Terminal("=")]
[Terminal(".")]
[Terminal(",")]
public class TokenBase : SemanticToken
{
}

Context used in bsn-goldparser parsing

In bsn-goldparser parsing, the Context object is nothing more than a user defined data structure to help manage your code generation for compiler or execution for interpreter. In the sample that comes with the bsn-goldparser download, the Context can be a bit complex structure which helps providing the executing environment in the REPL interpreter implementation. For our case of CodeCom program graph creation during rules parsing, Context is a very simple structure that just provides the facility to help CodeDom program graph generation. Look at listing 8, we know that there are two constants defining the source and target parameter names which matched the parameter names of the CodeMemberMethod reference generated by the Create() method of MapCodeMemberMethod class shown in listing 5. What may be more interesting is the TypeWrapper property of type ClassTypeWrapper. The ClassTypeWrapper type has a MainMethod property which is referred by the AssignStatement class to add rules implementation to the MainMethod body.  Here MainMethod is the empty bodied CodeMemberMethod reference described in previous section before parsing begins.

// -------------------------------------------------------------------
// Listing 8.
// -------------------------------------------------------------------
public class ExecutionContext 
{
    public ClassTypeWrapper TypeWrapper { get; private set; }

    public const string FromParamName = "source";
    public const string ToParamName = "target";

    public ExecutionContext(ClassTypeWrapper typeWrapper)
    {
        TypeWrapper = typeWrapper;
    }
}

Reviewing listing 9 for the AssignStatement class and its AssignStatement method which implements the rule [Rule(@"<Statement> ::= Identifier ~'=' <Expression> ~';'")] (Note: ~ means to skip the token followed as it does not need any mapping in this particular implementation). It is not surprising that the overridden Execute() method builds the equivalent CodeDom assignment statement to set the target entity property and adds the newly created CodeAssignStatement reference to the Statements collection in MainMethod reference passed from the ExecutionContext reference ctx. Please pay attention that the CodeDom program graph building is cascaded down to the corresponding mapping class through proper RuleAttribute that is mapped with the expression on the right hand side of the assignment statement.

Since in my language syntax the left hand side of the assignment statment only accepts a property name (Identifier Terminal used) to indicate which properly to set its value from by evaluating the right hand side expression, it builds the CodeDom expression by taking the target argument expression passed into the CodeMemberMethod reference (refer to listing 5) using CodeArgumentReferenceExpression(target parameter name) and refers its property using CodePropertyReferenceExpression. Again, the target parameter name is taken from the context passed into the Execute() method of the AssignmentStatement class. So, the context carries important information and reference to let each class object that supports the business rule parsing have enough information to take on its processing (CodeDom expression creation).

// -------------------------------------------------------------------
// Listing 9.
// -------------------------------------------------------------------
public class AssignStatement : Statement
{
    private readonly Expression _expr;
    private readonly Identifier _propertyId;

    [Rule(@"<Statement> ::= Identifier ~'=' <Expression> ~';'")]
    public AssignStatement(Identifier propertyId, Expression expr)
    {
        _propertyId = propertyId;
        _expr = expr;
    }

    public override void Execute(ExecutionContext ctx)
    {
        var target = new CodeArgumentReferenceExpression(ExecutionContext.ToParamName);         
        var assignmentStmt = new CodeAssignStatement(
            new CodePropertyReferenceExpression(target, _propertyId.GetName(ctx)), _expr.GetValue(ctx));
        ctx.TypeWrapper.MainMethod.Statements.Add(assignmentStmt);
        // Add assignment statement to main method
    }
}

Semantic action classes mapping in bsn-goldparser

I have discussed mapping of semantic action classes, SemanticToken, to terminals or rules of domain language grammar when parsing business rules to a CodeDom program graph in the previous section.  Here let us do more discussion on this topic. When I developed this sample program, I copied the original Simple 2 REPL Interpreter source and began to do modifications such that the classes emit CodeDom expressions instead of doing immediate interpretor execution. At the end, it is easier than what I thought at the beginning.

Just a little mind adjustment, things become straightforward. As an example, look at the definition of the Expression class below. The GetValue() virtual method does not return an object value as in the original interpreter source, but it returns CodeExpression now. When we think about we are going to generate source code specifying how to get a value from expression constructs, returning a certain type of structure representing the code to get the value becomes natural. After all, CodeExpression is the generic structure we want when we need it to generate source code later.

// -------------------------------------------------------------------
// Listing 10.
// -------------------------------------------------------------------
public abstract class Expression : TokenBase
{
    public abstract CodeExpression GetValue(ExecutionContext ctx);
}

If you look at the listing below for the implementation of the CodeDom binary operation expression building (only Add operation is shown as others are similar), you are certainly convinced it is not that hard to build classes to generate CodeExpression. You may be concerned with the type conversion between operands with binary operation. That is handled by the correct CodeExpression returned from DecimalValue and StringLiteral mapped semantics classes below that they basically convert the string passed in the constructor to correct the data type before using CodePrimitiveExpression to return the correct CodeDom expression. I have not added Date or Boolean literal in the grammar for this sample program. However, adding them is easy once you get the idea of how I implement the CodeExpression generation below.

// -------------------------------------------------------------------
// Listing 11.
// -------------------------------------------------------------------
public abstract class BinaryOperator : TokenBase
{
    public abstract CodeBinaryOperatorExpression Evaluate(
           CodeExpression left, CodeExpression right);
}

[Terminal("+")]
public class PlusOperator : BinaryOperator
{
    public override CodeBinaryOperatorExpression Evaluate(CodeExpression left, CodeExpression right)
    {
        return new CodeBinaryOperatorExpression(left, CodeBinaryOperatorType.Add, right);
    }
}

public class BinaryOperation : Expression{
    private readonly Expression _left;
    private readonly BinaryOperator _op;
    private readonly Expression _right;

    [Rule(@"<Expression> ::= <Expression> '>' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '<' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '<=' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '>=' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '==' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '<>' <Add Exp>")]
    [Rule(@"<Add Exp> ::= <Add Exp> '+' <Mult Exp>")]
    [Rule(@"<Add Exp> ::= <Add Exp> '-' <Mult Exp>")]
    [Rule(@"<Mult Exp> ::= <Mult Exp> '*' <Negate Exp>")]
    [Rule(@"<Mult Exp> ::= <Mult Exp> '/' <Negate Exp>")]
    public BinaryOperation(Expression left, BinaryOperator op, Expression right){
        _left = left;
        _op = op;
        _right = right;
    }

    public override CodeExpression GetValue(ExecutionContext ctx)
    {
        CodeExpression lStart = _left.GetValue(ctx);
        CodeExpression rStart = _right.GetValue(ctx);
        return _op.Evaluate(lStart, rStart);
    }
}

[Terminal("DecimalValue")]
public class DecimalValue : Expression
{
    private readonly CodeExpression _value;

    public DecimalValue(string value)
    {
        int intValue;
        if (int.TryParse(value, out intValue)) 
        {
            _value = new CodePrimitiveExpression(intValue);
        }
        else {
            _value = new CodePrimitiveExpression(Convert.ToDecimal(value));
        }
    }

    public override CodeExpression GetValue(ExecutionContext ctx)
    {
        return _value;
    }
}

[Terminal("StringLiteral")]
public class StringLiteral : Expression
{
    private readonly CodeExpression _value;
    public StringLiteral(string value) 
    {
        string trimmedValue = value.Substring(1, value.Length - 2);
        _value = new CodePrimitiveExpression(trimmedValue);
    }

    public override CodeExpression GetValue(ExecutionContext ctx) 
    {
        return _value;
    }
}

For <Member Access> rules, we need to distinguish among expressions (e.g., ("ABC" + "XYZ).Length), the source and target parameter (e.g., source.InvoiceNo), and the class type access (e.g., DateTime.Today). The MemberAccess class shown below overcomes the difficulty by mapping each distinguish rule to overloaded constructors. <Method Access> rules implementation is similar and you can go through the source download for reviewing. I believe by looking into the downloaded source for all the terminals and rules implementation classes, you will understand the CodeDom program graph generation in the shortest time.

// -------------------------------------------------------------------
// Listing 12.
// -------------------------------------------------------------------
public class MemberAccess : Expression
{
    private readonly Expression _entity;
    private readonly Identifier _member;
    private readonly Identifier _ownerId;

    [Rule(@"<Member Access> ::= <SourceTarget> ~'.' Identifier")]
    public MemberAccess(Expression entity, Identifier member)
    {
        _entity = entity;
        _member = member;
    }

    [Rule(@"<Member Access> ::= Identifier ~'.' Identifier")]
    public MemberAccess(Identifier ownerId, Identifier member)
    {
        _ownerId = ownerId;
        _member = member;
    }

    [Rule(@"<Member Access> ::= <Value> ~'.' Identifier")]
    public MemberAccess(ValueToken val, Identifier member)
    {
        _entity = val;
        _member = member;
    }

    public override CodeExpression GetValue(ExecutionContext ctx)
    {
        if (_entity != null)
        {
            return new CodePropertyReferenceExpression(_entity.GetValue(ctx), _member.GetName(ctx));
        }
        else
        {
            // Type.Property 
            return new CodePropertyReferenceExpression(
              new CodeTypeReferenceExpression(_ownerId.GetName(ctx)), _member.GetName(ctx));
        }
    }
}

Source code generation and compilation

EntityMapperGenerator - Class manages source and assembly generation

The class responsible for C# source generation and compilation is EntityMapperGenerator. We have covered one of its methods in listing 6 before when we discussed about using the GetClassTypeWrapper() method for preparing a CodeDom compilation unit wrapper object before passing it as a context property to syntactic action classes. In this section we are going to talk about other methods in it.

Compute Hash to detect business rules change

The Parse() method in EntityMapperGenerator accepts business rules as string text and passes it to SemanticProcessor for parsing. There is one issue when using CodeDom to generate an assembly loaded dynamically in memory and it is, the loaded assembly cannot be unloaded if it is running in the same main .NET application domain for the application. Of course if you load the assembly in a separate application domain, you can unload the application domain with the assembly running in it without any issues. But there will be another issue arising: to call a method in another application domain, you need to use a proxy object similar to remoting. Not just that it suffers from slow performance but also leads to additional calling code that is quite readable (not readable means source are not directly related to problem to be solved onhand).

To minimize the effect of compiling and loading multiple assemblies for the same business rules, I established an internal dictionary for tracking loaded assemblies which uses a key computed from hashing the concatenation of source and target type name with business rules. For each business rule, I compute the hash set it to the variable cuKey. The next time the same businessRules input string is passed in for parsing, no new assembly is created if nothing has changed since the last parsing and compilation. This reduces the number of dynamic assemblies loaded and improves overall performance with reduced running code size when returning an already loaded assembly reference for the same business rules.  The whole concept can be summarized in the following figure.

Hash checking before assembly creation

Delegate bound to compiled method in assembly

After succeeding calling the Parse() method, compileUnit reference of type CodeCompileUnit is created and the next step is the compilation to assembly. The GetDelegate() method calls the GetDelegateFromCompileUnit() method in CodeCompilerUtility class that will return the delegate wrapping of the compiled method. If you look at the source of GetDelegateFromCompileUnit(), it uses the hash ccuKey passed in to lookup the assembly reference in its internal dictionary first before deciding whether compilation is needed. The returned delegate can be used to process business entities according to business rules passed in for parsing.

// -------------------------------------------------------------------
// Listing 13.
// -------------------------------------------------------------------
public class EntityMapperGenerator
{
    public string GrammarTable { get; set; }
    public string NamespaceName { get; private set; }
    public string ClassName { get; private set; }
    public string MethodName { get; private set; }
    public string SourceCode { get; private set; }
    public Type FromType { get; private set; }
    public Type ToType { get; private set; }
    
    private CodeCompileUnit _compileunit;
    private string _cuKey = null;

    public EntityMapperGenerator(string className = "MapperUtility", string methodName = "MapEntity")
    {
        this.GrammarTable = "EntityTransformation.cgt";
        this.NamespaceName = this.GetType().Namespace;
        this.ClassName = className;
        this.MethodName = methodName;
    }

    public void Parse(Type fromType, Type toType, string businessRules, string[] importedNamespaces)
    {
        string fullBusinessRules = string.Format("{0}|{1}|{2}", 
               fromType.FullName, toType.FullName, businessRules);
        string cuKey = Convert.ToBase64String(
          System.Security.Cryptography.HashAlgorithm.Create().ComputeHash(
          System.Text.Encoding.UTF8.GetBytes(fullBusinessRules)));
        if (_cuKey != cuKey)
        {
            this.FromType = fromType;
            this.ToType = toType;
            CompiledGrammar grammar = CompiledGrammar.Load(typeof(TokenBase), this.GrammarTable);
            SemanticTypeActions<TokenBase> actions = new SemanticTypeActions<TokenBase>(grammar);
            actions.Initialize(true);
            SemanticProcessor<TokenBase> processor = 
              new SemanticProcessor<TokenBase>(new StringReader(businessRules), actions);
            ParseMessage parseMessage = processor.ParseAll();
            if (parseMessage == ParseMessage.Accept)
            {
                var ctx = new ExecutionContext(GetClassTypeWrapper(
                    ExecutionContext.FromParamName, ExecutionContext.ToParamName, importedNamespaces));
                var stmts = processor.CurrentToken as Sequence<Statement>;
                foreach (Statement stmt in stmts)
                {
                    stmt.Execute(ctx);
                }
                _compileunit = ctx.TypeWrapper.CompileUnit;
                SourceCode = CodeCompilerUtility.GenerateCSharpCode(_compileunit);
                _cuKey = cuKey;
            }
            else
            {
                IToken token = processor.CurrentToken;
                throw new ApplicationException(string.Format("{0} at line {1} and column {2}", 
                      parseMessage, token.Position.Line, token.Position.Column));
            }
        }
    }

    public Delegate GetDelegate(params string[] referencedAssemblies)
    {
        if (_cuKey == null || _compileunit == null)
        {
            throw new InvalidOperationException("Parse operation is not performed or succeeded!");
        }

        string typeName = this.NamespaceName + "." + this.ClassName;

        Type delType = typeof(Action<,>).MakeGenericType(this.FromType, this.ToType);

        //Get delegate from assembly produced from CodeDom compilation unit
        var mapper = CodeCompilerUtility.GetDelegateFromCompileUnit(
            _cuKey,
            _compileunit,
            referencedAssemblies,
            typeName,
            this.MethodName,
            delType,
            false);

        return mapper;
    }

    private ClassTypeWrapper GetClassTypeWrapper(string fromParmName, 
            string toParmName, string[] importedNamespaces)
    {
        var classWrapper = new ClassTypeWrapper(this.NamespaceName, this.ClassName, importedNamespaces);
        classWrapper.AddMainMethod(new MapCodeMemberMethod().Create(this.FromType, 
                     this.ToType, this.MethodName, fromParmName, toParmName));
        return classWrapper;
    }
}


public class CodeCompilerUtility
{
	// Skipped lines ...

    public static Delegate GetDelegateFromCompileUnit(string ccuKey, 
           CodeCompileUnit compileunit, string[] referencedAssemblies, 
           string typeName, string methodName, 
           Type delegateType, bool refreshCache = false) 
    {
        Assembly assembly;
        if (!Assemblies.ContainsKey(ccuKey) || refreshCache)
        {
            assembly = CompileCodeDOM(compileunit, referencedAssemblies);
            if (Assemblies.ContainsKey(ccuKey)) {
                Assemblies.Remove(ccuKey); 
            }
            Assemblies.Add(ccuKey, assembly);
        }
        else
        {
            assembly = Assemblies[ccuKey];
        }

        var type = assembly.GetType(typeName, true);
        var method = type.GetMethod(methodName);
        var obj = assembly.CreateInstance(typeName);
        return Delegate.CreateDelegate(delegateType, obj, method);
    }
}

Summary

The creation of business rules engine by defining domain language in BNF syntax and implementing it using parser tools (GOLD Parser), engine (bsn-goldparser) and assembly generating classes seemed unnecessary at first.  But at long run, your works get paid by offerring the greatest flexibilty in your application to adapting the most demanding requirement changes from business.

History

  • 2012.04.05: Version 1.0 and document created.
  • 2012.04.07: Version 1.1 and document updated.

License

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

Share

About the Author

matthew_from_hk
Founder
Hong Kong Hong Kong
I am always interested in finding innovative ways for building better applications and founded a technology company since 2003. Welcome to exchange any idea with you and if I am not too busy before deadline of projects, I will reply your emails. Also, if you willing to pay for consulting works and customized software development, you can leave me message.

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalPrasad Khandekar22-Jul-14 7:23 
GeneralMy vote of 5 PinmemberChampion·Chen11-Sep-13 23:16 
GeneralNice use of bsn-goldparser PinmemberAvonWyss2-Aug-12 9:55 
Generalnice work man Pinmemberdave.dolan8-Apr-12 4:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140926.1 | Last Updated 7 Apr 2012
Article Copyright 2012 by matthew_from_hk
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid