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.
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.
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.
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.
var mapperGenerator = new EntityMapperGenerator();
mapperGenerator.Parse(typeof(SalesOrder), typeof(Invoice),
textBoxInput.Text, new string[] { "System", "EntityMapper.Service" });
var mapperMethod1 = (Action<SalesOrder, Invoice>)_mapperGenerator.GetDelegate(
new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });
mapperMethod1(salesOrderObj, invoiceObj);
mapperGenerator.Parse(typeof(Invoice), typeof(Invoice), cloningRules,
new string[] { "System", "EntityMapper.Service" });
var mapperMethod2 = (Action<Invoice, Invoice>)_mapperGenerator.GetDelegate(
new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });
mapperMethod2(invoiceObj1, invoiceObj2);
mapperGenerator.Parse(typeof(Invoice), typeof(Invoice), creationRules,
new string[] { "System", "EntityMapper.Service" });
var mapperMethod2 = (Action<Invoice, Invoice>)_mapperGenerator.GetDelegate(
new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });
var newInvoiceObj = new Invoice();
mapperMethod2(newInvoiceObj, newInvoiceObj);
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).
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();
CodeNamespace codeNS = new CodeNamespace(unitNamespace);
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.
public class MapCodeMemberMethod
{
public CodeMemberMethod Create(Type fromType, Type toType,
string name = "Map", string fromParamName = "source", string toParamName = "target")
{
CodeMemberMethod method = new CodeMemberMethod();
method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
method.Name = name;
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.
public class EntityMapperGenerator
{
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.
[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.
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).
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);
}
}
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.
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.
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.
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
{
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.
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.
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);
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
{
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.<o:p>
History
- 2012.04.05: Version 1.0 and document created.
- 2012.04.07: Version 1.1 and document updated.