动态代码编译探索
原著:Mark Strawmyer
翻译:小刀人
原文出处:Discover Dynamic Code Compilation
不能确定动态代码编译在什么地方是有意义的?一个普通情况就应该可以帮助阐明这个问题。假如你不得不从一个数据库中取出数据并将它放入另一个数据库。你应该只需使用一个SQL语句从源数据库中选取数据并插入目标数据库中,这只是小菜一碟,对不对?如果你正在拷贝生产数据以生成测试数据并需要改变数据以确保目标数据在以后开发中使用是安全的又将如何?你可能会构建一个数据传输系统(DTS)或某个其它传输机制,但是如果你这样做超过足够多的数据,这就会变成你每次为拷贝数据建立数据-擦除(data-scrubbing)机制而消耗时间。你可以写一个应用程序来加工并生成测试数据,但是每次你在一个不同的应用程序上用它时你都将不得不改动(应用程序)并创建新的算法。
走进动态代码编译。胜于不停地写一些一次性的代码,你可以创建一个有特定内部运作机制的应用程序来传送数据并在传送时运用代码段来改变数据。该代码段将代理每个你需要在数据上要做的动作。它们将被作为原始文本被储存在一个数据库中或某个它们可以很容易被修改的其它位置。代码段将被编译并在执行时同时应用到数据。这将允许你获得一个完全是不同的代码段的数据库,使得你可以很容易地恢复、修改并应用它而不用每次都要改变你的应用程序的根本。
这是个相当复杂的情况,但是它应该帮助你理解一些可能性。现在,让我们看看如何实现它。
CodeCompileUnit(代码编译单元)
为了动态编译一个类,从System.CodeDom命名空间的一个CodeCompileUnit开始。CodeCompileUnit包含一个程序图形。为了构建代码,你要创建一些支撑对象并将它们添加到CodeCompileUnit实例中去。这些对象:代表应该已经在你的代码中的,就像你准备在设计时要创建它的普通对象。
. CodeNamespace—代表指定的命名空间
. CodeTypeDeclaration—代表类型声明
. CodeMemberMethod—代表一个方法
一个HelloWorld例子
你可以使用下面的示例代码来生成包含一个接收单个参数并返回一个值的SayHello方法的代码。scriptBody方法的参数值成为SayHello方法的实体(body)。你将你的代码包含在接收影响结果的参数的一个static(静态)类中以创建CodeCompileUnit。
public static CodeCompileUnit CreateExecutionClass(string typeNamespace,
string typeName,
string scriptBody)
{
// 创建CodeCompileUnit以包含代码
CodeCompileUnit ccu = new CodeCompileUnit(); // 分配需要的命名空间
CodeNamespace cns = new CodeNamespace(typeNamespace);
cns.Imports.Add(new CodeNamespaceImport("System"));
ccu.Namespaces.Add(cns);
// 创建新的类声明
CodeTypeDeclaration parentClass = new CodeTypeDeclaration(typeName);
cns.Types.Add(parentClass);
// 创建获得一个参数并返回一个字符串的SayHello方法
CodeMemberMethod method = new CodeMemberMethod();
method.Name = "SayHello";
method.Attributes = MemberAttributes.Public;
CodeParameterDeclarationExpression arg = new
CodeParameterDeclarationExpression(typeof(string),
"inputMessage");
method.Parameters.Add(arg);
method.ReturnType = new CodeTypeReference(typeof(string));
// 添加方法实体需要的代码
CodeSnippetStatement methodBody =
new CodeSnippetStatement(scriptBody);
method.Statements.Add(methodBody);
parentClass.Members.Add(method);
return ccu;
}
CodeProvider(代码提供者)
现在你已经创建了一个CodeCompileUnit包含你的代码段,使用它来生成被编译到你的动态程序集中去的全部源代码。下面的静态方法首先从前面的例子中调用方法并且同时使用CSharpCodeProvider生成全部代码:
public static string GenerateCode(string typeNamespace,
string typeName,
string scriptBody)
{
// 调用我们前面的方法创建CodeCompileUnit
CodeCompileUnit ccu = CreateExecutionClass(typeNamespace,
typeName, scriptBody);
CSharpCodeProvider provider = new CSharpCodeProvider();
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.BlankLinesBetweenMembers = false;
options.IndentString = "\t";
StringWriter sw = new StringWriter();
try
{
provider.GenerateCodeFromCompileUnit(ccu, sw, options);
sw.Flush();
}
finally
{
sw.Close();
}
return sw.GetStringBuilder().ToString();
}
作为一个例子,用输入值:"CodeGuru.DynamicCode","ScriptType",和"return
inputMessage;"调用GenerateCode方法得出以下输出:
//---------------------------------------------------------------
// <auto-generated
// 该代码是由工具生成的。
// 运行时版本:2.0.50630.0
// 更改这个文件可能导致不正确的(程序)动作并且如果代码被再次生成时将会丢掉这些更改。
// </auto-generated
//---------------------------------------------------------------
namespace CodeGuru.DynamicCode {
using System;
public class ScriptType {
public virtual string SayHello(string inputMessage) {
return inputMessage;
}
}
}
在内存中编译
最后一步是获得生成的源代码并将它编译到一个当前的程序集中去。对于这个例子,你是将这个例子装入内存而不是一个物理文件。通过特定的编程语言提供者执行当前编译动作,在这个例程中就是CSharpCodeProvider。你设定任何预定的编译选项并从源代码编译这个程序集。
下面的示例代码从你已构建的代码中生成了一个程序集:
static Assembly CompileInMemory(string code)
{
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters options = new CompilerParameters();
options.IncludeDebugInformation = false;
options.GenerateExecutable = false;
options.GenerateInMemory = true;
CompilerResults results =
provider.CompileAssemblyFromSource(options, code);
provider.Dispose();
Assembly generatedAssembly = null;
if (results.Errors.Count == 0)
{
generatedAssembly = results.CompiledAssembly;
}
return generatedAssembly;
}
如Assembly a = CompileInMemory(GenerateCode(typeNamespace,
typeName, "return inputMessage;"));的调用将会生成一个新的程序集。你可能会用任何你想要的方法实体代替"return
inputMessage;"来创建预定的变量作些并发调用。
创建一个实例
你已经动态生成了一个程序集并将其编译到内存中。下一个任务就是从程序集中创建一个类的实例。这实际上比听起来更加复杂。你已经创建的程序集存在于内存中。对它的存在没有任何参考信息,因此你不能简单的创建一个新的实例,因为它们不会解决问题。创建一个类以拥有所有已编译程序集作为一个工作区。你将不顾类型决定事件,所以当一个类型需要时你可以使用你的类型中的一个。
ExecutionHost示例代码下面的代码定义了一个名为ExecutionHost的类,它追踪了你所有的动态编译程序集:
using System;
using System.Collections;
using System.Reflection;
namespace CodeGuru.CodeDomSample
{
class ExecutionHost
{
private Hashtable assemblies = null;
public ExecutionHost()
{
assemblies = new Hashtable();
// 响应类型解析事件(the type resolution event)要求以截取它并找到我们类型
AppDomain.CurrentDomain.TypeResolve += new
ResolveEventHandler(CurrentDomain_TypeResolve);
}
private Assembly CurrentDomain_TypeResolve(object sender,
ResolveEventArgs args)
{
// 为预定的类型找出我们程序集
Assembly a = null;
if (assemblies.ContainsKey(args.Name))
{
a = (Assembly)assemblies[args.Name];
}
return a;
}
public void AddAssembly(string fullTypeName, Assembly a)
{
assemblies.Add(fullTypeName, a);
}
public string Execute(string typeFullName, string msg)
{
// 尝试创建触发事件所需要的类型
Type targetType = Type.GetType(typeFullName, true, true);
object target =
targetType.Assembly.CreateInstance(typeFullName);
IExecutableModule m = (IExecutableModule)target;
return m.SayHello(msg);
}
}
}
namespace CodeGuru.CodeDomSample
{
public interface IExecutableModule
{
string SayHello(string inputMessage);
}
}
public static CodeCompileUnit CreateExecutionClass(string typeNamespace,
string typeName,
string scriptBody)
{
// 创建CodeCompileUnit以存放代码
CodeCompileUnit ccu = new CodeCompileUnit();
// 分配给预期的命名空间
CodeNamespace cns = new CodeNamespace(typeNamespace);
cns.Imports.Add(new CodeNamespaceImport("System"));
ccu.Namespaces.Add(cns);
// 创建类
CodeTypeDeclaration parentClass = new CodeTypeDeclaration(typeName);
cns.Types.Add(parentClass);
// 新行-为IExecutableModule接口添加一个实现
parentClass.BaseTypes.Add(typeof(CodeGuru.CodeDomSample.
IExecutableModule));
// 创建获得一个参数并返回一个字符串的SayHello方法
CodeMemberMethod method = new CodeMemberMethod();
method.Name = "SayHello";
method.Attributes = MemberAttributes.Public;
CodeParameterDeclarationExpression arg = new
CodeParameterDeclarationExpression(typeof(string),
"inputMessage");
method.Parameters.Add(arg);
method.ReturnType = new CodeTypeReference(typeof(string));
// 添加预期代码到方法实体
CodeSnippetStatement methodBody = new
CodeSnippetStatement(scriptBody);
method.Statements.Add(methodBody);
parentClass.Members.Add(method);
return ccu;
}
注意Execute方法。它用反射来创建预定类型的一个实例。这将触发_TypeResolve事件并允许你的程序集中的一个被返回,如果该被返回程序集通过AddAssembly方法已被添加到ExecutionHost中了。
你也要注意在你的动态生成代码中添加的接口实现。没有它,你将不知道如何调用预期的方法。为了你的生成代码,IExecutableModule接口与作为一个基类添加的附加接口的CreateExecutionClass方法的一个最新副本一同被提供。
另外,因为你增添了一个现在需要在CodeGuru.DynamicCode程序集内部使用的接口,你必须给含有IExecutableModule
声明的CodeGuru.CodeDomSample添加一个接口。请看下面最新的CompileInMemory副本:
static Assembly CompileInMemory(string code)
{
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters options = new CompilerParameters();
options.IncludeDebugInformation = false;
options.GenerateExecutable = false;
options.GenerateInMemory = true;
// 新行-添加一个接口到需要的程序集
options.ReferencedAssemblies.Add(
"CodeGuru.CodeDomSample.exe");
CompilerResults results =
provider.CompileAssemblyFromSource(options, code);
provider.Dispose();
Assembly generatedAssembly = null;
if (results.Errors.Count == 0)
{
generatedAssembly = results.CompiledAssembly;
}
return generatedAssembly;
}
现在,你可以用下面的测试代码来测试动态生成一个程序集然后对方法做一个调用的端到端(end-to-end)过程:
string typeNamespace = "CodeGuru.DynamicCode";
string typeName = "ScriptType" + Guid.NewGuid().ToString("N");
Assembly a = CompileInMemory(GenerateCode(typeNamespace, typeName,
"return inputMessage;"));
ExecutionHost host = new ExecutionHost();
string fullTypeName = typeNamespace + "." + typeName;
host.AddAssembly(fullTypeName, a);
string test = host.Execute(fullTypeName, "Hello World!");
每次在你生成代码时使用Guid生成唯一对象名称。
后记
你已看完了一个非常基本的例子,它描述了一个复杂的主题及完成这个任务所需要的代码。在类型名称上添加Guid是为了确保其唯一性,因此你可以随心所欲地编译并使用各种不同的类型而不会在名称上发生冲突。你可以自由改变“return
inputMessage”方法实体成为任何你喜欢的代码并试用之。你可以改变它,以使得所有关于方法实体的代码被存储在一个数据库中并在运行时重新获得。
作者简介Mark Strawmyer, MCSD, MCSE, MCDBA,他是大中型组织的.Net应用的高级架构师。与Crowe
Chizek一起都是印第安纳州首府印第安纳波利斯的技术领袖。他专攻基于Microsoft的解决方案架构、设计和开发。Mark被授予C#应用开发的微软的MVP。你可以通过mstrawmyer@crowechizek.com与他取得联系。
译注: