65.9K
CodeProject 正在变化。 阅读更多。
Home

动态程序集

2008 年 8 月 24 日

CPOL

8分钟阅读

viewsIcon

38518

downloadIcon

678

在 C# 中创建动态程序集

引言

在本文中,我将向您介绍一个可以在软件解决方案中使用动态创建程序集的场景。我将主要关注 System.CodeDom 命名空间,但在文章的最后,我将讨论使用 System.Reflection.Emit 命名空间来帮助您创建动态程序集。

场景

考虑一些业务场景,由于市场趋势和其他因素,您决定为您的客户提供特别优惠。这可以根据一年中的时间、商品类型等进行更改。此外,您将决定为某些选定的商品类别根据数量或总价提供折扣。此外,您希望更改应用程序的验证规则。例如,您希望限制可以在远程购买的某些商品类别的数量。

想象一下您的系统架构是您的客户使用 Windows 应用程序,通过 .NET Framework 提供的套接字或 Remoting 功能登录到您的服务器。

当您审视这个问题时,您会发现无法将这些要求硬编码到您的客户端应用程序中。这完全是因为参数和方程式由服务器本身根据市场趋势和最大化利润率来决定。因此,您的客户端应用程序必须能够由服务器进行配置。此外,在这种类型的架构中,您必须减少网络通信和不必要的带宽利用率。要解决这个问题,您可以使用 System.CodeDom 命名空间的功能。此技术提供的​​主要功能是在运行时生成多种语言的源代码。然后,System.CodeDom.Compiler 将使您能够自由地将您动态创建的源代码与相关的语言编译器编译成类库或可执行文件。

在我的演示应用程序中,我将动态创建一个程序集,并将其用于折扣计算、特别优惠计算和一些验证。在这里,我将考虑一个书店的场景,其中有一组产品类别,如书籍、杂志、礼品和文具。可能还有子产品类别。下面我列出了一些我将在本应用程序中实现的规则和优惠。

书籍类别 - “B”

  • 如果总价值超过 50 美元,则打 2% 折扣
  • 如果总价值超过 150 美元,则打 3.5% 折扣
  • 如果总价值超过 400 美元,则打 5.75% 折扣
  • 前 500 件打 6% 折扣,超过 500 件的每 100 美元折扣 5 美元。

礼品类别 - “G”

  • 在线订购的最大订单价值为 1000 美元

杂志类别 - “M”和子类别时尚 - “F”

  • 在线订购的最大数量为 2
  • 如果总价值超过 100 美元,则打 2.5% 折扣

杂志类别 - “M”和子类别技术 - “T”

  • 如果订单价值超过 750 美元,则可以选择从订单窗口中显示的杂志集合中选择一本杂志。

文具类别 - “S”

  • 如果订单价值超过 500 美元,则可以选择从订单窗口中显示的礼品集合中选择一个免费礼品。

正如我清楚提到的,这些值和参数将根据市场趋势和要实现的利润率而变化。

实现

虽然我使用了 Rules.xml XML 文档来存储从服务器发送的动态规则,但在实际情况下,这些规则将作为 strings 从服务器发送,并且它们会根据需求而变化。如果您检查 XML,您会发现在 body 元素中您拥有的是 C# 代码。同样,ReturnType 元素是 C# 类型。想象一下 Rule.xml 是从服务器发送的,或者一组类似的数据是从服务器发送的。然后下一步就是将其包含在一个 C# 类中,并即时创建一个程序集。

首先,我们来关注如何使用从服务器发送的代码段创建类。CodeCompileUnit 是维护或建模我们要编写的源代码结构的主要对象,也称为源代码的 CodeDOM 图。使用此对象,我们可以添加我们想要的特定命名空间,导入源代码中引用的命名空间,并定义将在该程序结构中声明的类型(如类)。System.Codedom 命名空间中有一组类被此 CodeCompileUnit 类引用。请参考以下代码片段。

// Declare a new namespace 
CodeNamespace codeNamespace = new CodeNamespace("DynamicAssemblyDemo.DynamicAssembly"); 
// Add the new namespace to the compile unit. 
codeCompileUnit.Namespaces.Add(codeNamespace); 
// Add the new namespace import for the System namespace. 
codeNamespace.Imports.Add(new CodeNamespaceImport("System")); 
// Declare a new type called BookShopRuleAssembly. 
bookShopRuleClass = new CodeTypeDeclaration("BookShopRuleClass"); 
// Add the new type to the namespace type collection. 
codeNamespace.Types.Add(bookShopRuleClass);

在上面的代码片段中,您可以看到我使用了 CodeNamespace 类来声明我要创建的动态类的命名空间(DynamicAssemblyDemo.DynamicAssembly)。然后我导入了 System 命名空间,该命名空间将在该类中使用。我使用的另一个重要类是 CodeTypeDeclaration ,它表示类、结构等类型的声明。动态类的名称是 BookShopRuleClass

CodeTypeDeclaration 对象中,您可以找到一组有趣的属性来塑造您的类型,在我的情况下,类就像一个普通类。例如,您可以为代码添加注释,将代码分解为区域,可以添加成员字段和方法。我广泛使用了该类的 Member 属性来添加局部字段和方法。CodeTypeMemberCollection CodeTypeMember 类型的集合,它是可以作为成员添加到类中的许多重要类型的基类。在从 MSDN 提取的下表中,您可以看到这些类。

  • System.CodeDom.CodeMemberEvent: 表示类型的事件声明
  • System.CodeDom.CodeMemberField: 表示类型的字段声明
  • System.CodeDom.CodeMemberMethod: 表示类型的成员方法声明
  • System.CodeDom.CodeMemberProperty: 表示类型的属性声明
  • System.CodeDom.CodeSnippetTypeMember: 使用文字代码片段表示类型的成员
  • System.CodeDom.CodeTypeDeclaration: 表示类、结构、接口或枚举的类型声明

在此实现中,我使用了 CodeMemberField CodeMemberMethod 类型来设置我想要的类结构。现在让我们进入 SetMethod GetFieldCode 方法。

CodeMemberField GetFieldCode(string name,CodeTypeReference fieldType) 
{ 
CodeMemberField field = new CodeMemberField(); 
field.Name = name; 
field.Attributes = MemberAttributes.Public; 
field.Type = fieldType; 
return field; 
}

上面的代码很简单。在这里,我们创建一个将被添加到我们创建的类中的字段。此 CodeMemberField 对象被添加到 bookShopRuleClass 的 Member 集合中。以下是示例代码

CodeMemberField tempField = GetFieldCode(“category”, 
	new CodeTypeReference(typeof(System.String))); 
bookShopRuleClass.Members.Add(tempField);//adding to the class 

同样,我也添加了将最终用于验证和特别优惠计算的方法。

CodeMemberMethod SetMethod(string name,CodeTypeReference returnType,string methodBody)
{ 
CodeMemberMethod method = new CodeMemberMethod(); 
method.Name = name; 
// adding method body 
CodeSnippetStatement statement = new CodeSnippetStatement(methodBody); 
method.ReturnType = returnType; 
method.Statements.Add(statement); // adding the statement into the method 
method.Attributes = MemberAttributes.Public; 
return method; 
}

您需要记住的主要事情是,您必须遵守的规则与普通类相同。您不能在所有地方拥有相同的重复方法签名,也不能在方法体内包含错误的语法。为了避免在类中重复相同的签名,我使用了与方法名集成的类别代码。例如,对于 GetDiscount_B()GetDiscount_M() 方法签名,可以避免在同一个类中重复 GetDiscount 方法。在上面的代码中,CodeSnippetStatement 类在插入文字代码片段而无需任何修改方面为我们提供了很大的帮助。

现在我们的 BookShopRuleClass 实现已经完成。整个实现被插入到 CodeCompileUnit 对象中。下一步是编译代码。对于这个关键步骤,我们必须使用 System.CodeDom.Compiler 命名空间。给定代码结构或 CodeCompileUnit/s,我们可以使用合适的编译器来编译它并生成程序集文件或可执行文件。在这里,我们必须使用 CodeDomProvider 的派生类来编译结构。在这种情况下,我们必须使用 CSharpCodeProvider 类来编译我们的 BookShopRuleClass。在此实现中,我分别使用了 GenerateCodeFromCompileUnit CompileAssemblyFromDom 方法来生成源代码并获取编译后的程序集。除了 CompileAssemblyFromDom 方法外,还有两个其他方法可以编译代码,即 CompileAssemblyFromFile CompileAssemblyFromSource。我认为方法名称说明了您可以使用这些方法的地点。

创建此程序集后,一切都将是 **反射**。在 RuleManager 类中,我使用该程序集创建了一个 BookShopRuleClass 实例,并使用反射相应地调用了特定方法。我将不详细介绍 RuleManager 类,因为它不属于本文的范围。如果您对反射有一定的了解,您可以轻松理解那里的实现。

为了完整起见,我将简要介绍 System.Reflection.Emit 命名空间,这是另一种创建动态程序集的方法。不过我必须说,这是一种非常繁琐且耗时的方法。而且对于上述场景,这种技术有点难以使用。即使是一个简单的错误也会导致程序集创建过程失败。请参考下面的代码片段。在此之前,了解 MSIL(Microsoft 之间语言)中使用的操作码和其他语法是有些重要的。另外,为了熟悉这种语法,请使用 MSIL 反汇编器(Ildasm.exe)工具并查看一些您创建的程序集。

public Assembly CreatAssembly() 
{ 
AssemblyBuilder ab = null; 
try 
{ 
AssemblyName an = new AssemblyName(); // we can define assemblies unique identity 
// Version is 1.0.0.0 
an.Version = new Version(1, 0, 0, 0); 
// Set the assembly name 
an.Name = "BookShopRuleWithEmit"; 
// Define a dynamic assembly 
// AssemblyBuilderAccess : Defines the access mode of the assembly to Run 
ab = Thread.GetDomain().DefineDynamicAssembly(an, AssemblyBuilderAccess.RunAndSave); 
// Define a dynamic module and the filename of the assembly 
ModuleBuilder modBuilder = ab.DefineDynamicModule("BookShopRuleWithEmit",
"BookShopRuleWithEmit.dll"); 
// Create the public BookShopRules class (can specify the namespace if needed) 
TypeBuilder tb = modBuilder.DefineType("BookShopRuleWithEmit.BookShopRules",
TypeAttributes.Public); 
// Define two fields (both Public) 
FieldBuilder price = tb.DefineField("price", typeof(double), FieldAttributes.Public); 
FieldBuilder qty = tb.DefineField("quantity", typeof(double), FieldAttributes.Public); 
// Define GetDiscount public method, with return type double 
MethodBuilder adderBldr = tb.DefineMethod("GetDiscount", 
MethodAttributes.Public, 
CallingConventions.Standard, 
typeof(double), 
new Type[0]); 
//now the implementation of the method 
ILGenerator ilgen = adderBldr.GetILGenerator(); // ILGenerator emit the instructions. 
Label failed = ilgen.DefineLabel(); 
Label failed2 = ilgen.DefineLabel(); // to label the instruction. 
Label endOfMthd = ilgen.DefineLabel(); 
ilgen.Emit(OpCodes.Ldarg_0); 
ilgen.Emit(OpCodes.Ldfld, qty); 
ilgen.Emit(OpCodes.Ldarg_0); //condition one if((price*quantity) < 100) 
ilgen.Emit(OpCodes.Ldfld, price); //return ((price*quantity)*.025) 
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); // Body of the statement 
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); // condition two 
ilgen.Emit(OpCodes.Ldfld, qty); // else if ((price*quantity) < 500) 
ilgen.Emit(OpCodes.Ldarg_0); //return ((price*quantity)*.05) 
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); // body of else if 
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); // body of else return ((price*quantity)*.075) 
ilgen.Emit(OpCodes.Mul); 
ilgen.Emit(OpCodes.Ldc_R8, 0.075); 
ilgen.Emit(OpCodes.Mul); 
ilgen.Emit(OpCodes.Br_S, endOfMthd); 
ilgen.MarkLabel(endOfMthd); // returning from the method 
ilgen.Emit(OpCodes.Ret); 
tb.CreateType(); 
ab.Save("BookShopRuleWithEmit.dll"); // saving the DLL in the disk 
} 
catch (Exception e) 
{ 
} 
return ab; 
} 

结论

现在您可以看到 System.CodeDom 命名空间的价值。通过使用动态生成的程序集,我们可以大大减少服务器与客户端之间的通信,从而优化网络使用并减轻服务器负载。

参考

致谢

  • Rohan Mapatuna 先生
  • Uditha Bandara 先生

历史

  • 2008 年 8 月 24 日:初次发布
© . All rights reserved.