Introduction
In this article, I am going to take you through a scenario where you can use dynamically created assemblies in your software solutions. I will be focusing more on System.CodeDom
namespace, but towards the end I will discuss the use of System.Reflection.Emit
namespace that helps you to create dynamic assemblies.
Scenario
Consider business scenarios were due to market trends and other effects you decide to give your clients a special offer. This can change according to the time of the year, type of the item, etc. In addition, you will decide to give discounts for a certain selected item category according to the quantity or the total price. Furthermore, you want to change the validation rules of your application. For instance, you want to restrict the quantity of some item category that can be bought remotely.
Imagine that your system architecture is where your clients log in to your server using Windows application, using a socket or Remoting facilities provided in .NET Framework.
When you look at this problem, you will see that you cannot hard code these requirements into your client application. It’s purely because parameters and equations are decided by the server itself according to the market trends and to maximize the profit margins. So your client application must have the capability to be configured by the server. In addition in this type of architecture, you have to reduce the network communication and utilization of bandwidth unnecessarily. To tackle this problem, you can use the capabilities of the System.CodeDom
namespace. The main feature provided by this technique is to generate source code at runtime in multiple languages. Then System.CodeDom.Compiler
will give you freedom to compile the source code you dynamically created with relevant language compiler into a class library or an executable.
In my demo application, I am going to dynamically create an assembly and use it for discount calculations, special offer calculations and for some validations. Here I am going to consider a situation of a book shop, where there are a set of product categories such as Books, Magazines, Gifts and Stationary. There could be sub product categories as well. Below I have mentioned some of the rules and offers I am going to implement in this application.
Book category - “B”
- 2% discount if the total value exceeds $50
- 3.5% discount if the total value exceeds $150
- 5.75% discount if the total value exceeds $400
- 6% for first 500 and $5 discount for each $100 above 500.
Gift Category – “G”
- Maximum order value that can be ordered online is 1000$
Magazine category - “M” and sub category Fashion – “F”
- Maximum quantity that can be ordered online is 2
- There is 2.5% discount if the total value exceeds $100
Magazine category - “M” and sub category Technical – “T”
- If order value is over 750, there is a choice to select magazine from a set of magazines populated in the order window.
Stationary category - “S”
- If the order value is over 500$ there is a choice to select a free gift from a set of gifts populated in the order window.
As I clearly mentioned, these values and the parameters will be changed according to the market trends and profit margins to be achieved.
Implementation
Even though I have used a Rules.xml XML document to store my dynamic rules sent from the sever, in real circumstances these rules are sent from the server as string
s and they will change according to requirements. If you inspect the XML, you can see that in the body element what you have is C# code. Similarly ReturnType
element is C# type. Imagine that Rule.xml is sent from the server or similar set of data is sent from the server. Then the next step is to include this in a C# class and create an assembly on the fly.
First let’s concentrate on how to create a class using the code segments sent from the server. CodeCompileUnit
is the main object that maintains or models the structure of the source we are going to write also known as the CodeDOM graph of a source. Using this object, we can add specific namespaces we want, to import the referenced namespaces used in the source and define the types such as classes that will be declared inside that program structure. There are a set of classes in the System.Codedom
namespace that is referred by this CodeCompileUnit
class. Please go through the following code segment.
CodeNamespace codeNamespace = new CodeNamespace("DynamicAssemblyDemo.DynamicAssembly");
codeCompileUnit.Namespaces.Add(codeNamespace);
codeNamespace.Imports.Add(new CodeNamespaceImport("System"));
bookShopRuleClass = new CodeTypeDeclaration("BookShopRuleClass");
codeNamespace.Types.Add(bookShopRuleClass);
In the above code segment, you can see that I have used CodeNamespace
class to declare the namespace of the dynamic class I am going to create (DynamicAssemblyDemo.DynamicAssembly
). Then I have imported System
namespace which will be used in this class. The other important class that I have used here is CodeTypeDeclaration
which represents a type declaration for a class, structure, etc. The name of the dynamic class is BookShopRuleClass
.
In a CodeTypeDeclaration
object, you can find a set of interesting properties to shape your type, in my case the class just like a normal class. For instance, you can add the comment to code, break the code into regions, you can add member fields and methods. I have used the Member
property of this class extensively to add local fields and methods. CodeTypeMemberCollection
is a collection of CodeTypeMember
type which is the base class of many important types that can be added as members to a class. In the following table extracted from the MSDN, you can see those classes.
System.CodeDom.CodeMemberEvent
: Represents a declaration for an event of a type
System.CodeDom.CodeMemberField
: Represents a declaration for a field of a type
System.CodeDom.CodeMemberMethod
: Represents a declaration for a method of a type
System.CodeDom.CodeMemberProperty
: Represents a declaration for a property of a type
System.CodeDom.CodeSnippetTypeMember
: Represents a member of a type using a literal code fragment
System.CodeDom.CodeTypeDeclaration
: Represents a type declaration for a class, structure, interface, or enumeration
In this implementation, I have used CodeMemberField
and CodeMemberMethod
types to setup the class structure I want. Now let’s move in to the SetMethod
and GetFieldCode
methods.
CodeMemberField GetFieldCode(string name,CodeTypeReference fieldType)
{
CodeMemberField field = new CodeMemberField();
field.Name = name;
field.Attributes = MemberAttributes.Public;
field.Type = fieldType;
return field;
}
The above code is straightforward. Here we create a field that will be added to the class we created. This CodeMemberField
object is added to the Member
collection of the bookShopRul
eClass. Here is the sample code:
CodeMemberField tempField = GetFieldCode(“category”,
new CodeTypeReference(typeof(System.String)));
bookShopRuleClass.Members.Add(tempField);
Similarly I have added the methods that will be called from ultimately for validations and special offer calculations.
CodeMemberMethod SetMethod(string name,CodeTypeReference returnType,string methodBody)
{
CodeMemberMethod method = new CodeMemberMethod();
method.Name = name;
CodeSnippetStatement statement = new CodeSnippetStatement(methodBody);
method.ReturnType = returnType;
method.Statements.Add(statement);
method.Attributes = MemberAttributes.Public;
return method;
}
Main things you have to keep in mind are that rules you have to adhere to are the same as the normal class. You cannot have the same method signatures duplicated everywhere or have incorrect syntax inside the method bodies. To avoid the duplication of the same signature within the class, I have used the category code integrated with the method name. For example to GetDiscount_B()
and GetDiscount_M()
method signatures, avoid the duplication of the GetDiscount
method inside the same class. In the above code CodeSnippetStatement
class helps us a lot to insert a literal code segment into the source code without any modifications.
Now our BookShopRuleClass
implementation is done. The whole implantation is inserted into the CodeCompileUnit
object. The next step is to compile the code. For this vital step, we have to use the System.CodeDom.Compiler
namespace. Given the code structure or CodeCompileUnit
/s we can compile it using an appropriate compiler and generate assembly file or an executable. Here we have to use some derived class of CodeDomProvider
to compile the structure. In this case, we have to use the CSharpCodeProvider
class to compile our BookShopRuleClass
. In this implementation, I used GenerateCodeFromCompileUnit
and CompileAssemblyFromDom
methods respectively to generate the source code and to get the compiled assembly. Apart from the CompileAssemblyFromDom
method, there are two other methods to compile a code, namely CompileAssemblyFromFile
and CompileAssemblyFromSource
. I think the method says where you can use these methods.
After creating this assembly, it’s all Reflection afterwards. In the RuleManager
class, using that assembly I have created a BookShopRuleClass
instance and using reflection invoking the specific method accordingly. I will not go into details of RuleManager
class since it’s not in the scope of this article. If you have some knowledge in Refection, you can easily understand the implementation there.
For the sake of completeness, I will talk about System.Reflection.Emit
namespace very little which is another way of creating dynamic assemblies. However I must say that this is a very hectic and time consuming way. Also for the above scenario, this technique is a bit difficult to use. Even a simple mistake will cause the assembly creation process to fail. Please go through the code segment below. Before that, it is somewhat important to understand the opcodes and other syntax used in MSIL (Microsoft intermediate language). Also to get familiarized with this syntax, use the MSIL Disassembler (Ildasm.exe) tool and go through some of the Assemblies you created.
public Assembly CreatAssembly()
{
AssemblyBuilder ab = null;
try
{
AssemblyName an = new AssemblyName();
an.Version = new Version(1, 0, 0, 0);
an.Name = "BookShopRuleWithEmit";
ab = Thread.GetDomain().DefineDynamicAssembly(an, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modBuilder = ab.DefineDynamicModule("BookShopRuleWithEmit",
"BookShopRuleWithEmit.dll");
TypeBuilder tb = modBuilder.DefineType("BookShopRuleWithEmit.BookShopRules",
TypeAttributes.Public);
FieldBuilder price = tb.DefineField("price", typeof(double), FieldAttributes.Public);
FieldBuilder qty = tb.DefineField("quantity", typeof(double), FieldAttributes.Public);
MethodBuilder adderBldr = tb.DefineMethod("GetDiscount",
MethodAttributes.Public,
CallingConventions.Standard,
typeof(double),
new Type[0]);
ILGenerator ilgen = adderBldr.GetILGenerator();
Label failed = ilgen.DefineLabel();
Label failed2 = ilgen.DefineLabel();
Label endOfMthd = ilgen.DefineLabel();
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, qty);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, price);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Ldc_R8, 100.00);
ilgen.Emit(OpCodes.Bgt_S, failed);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, qty);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, price);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Ldc_R8, 0.025);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Br_S, endOfMthd);
ilgen.MarkLabel(failed);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, qty);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, price);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Ldc_R8, 500.00);
ilgen.Emit(OpCodes.Bgt_S, failed2);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, qty);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, price);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Ldc_R8, 0.05);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Br_S, endOfMthd);
ilgen.MarkLabel(failed2);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, qty);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, price);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Ldc_R8, 0.075);
ilgen.Emit(OpCodes.Mul);
ilgen.Emit(OpCodes.Br_S, endOfMthd);
ilgen.MarkLabel(endOfMthd);
ilgen.Emit(OpCodes.Ret);
tb.CreateType();
ab.Save("BookShopRuleWithEmit.dll");
}
catch (Exception e)
{
}
return ab;
}
Conclusion
Now you can see the value of the System.CodeDom
namespace. Using dynamically generated assembly, we can reduce the server client conversations immensely, hence optimizing the network usage as well as reducing the sever load.
Reference
Acknowledgements
- Mr. Rohan Mapatuna
- Mr. Uditha Bandara
History
- 24th August, 2008: Initial post