动态创建程序集/应用程序






4.71/5 (35投票s)
如何使用 CodeDOM 和 CompilerServices 动态创建程序集/应用程序。
引言
这是一篇有些奇怪的文章,可能不是非常有用,但我认为它是一个非常有趣的主题,有些人可能甚至不知道它的存在。本文将介绍 .NET 中一些不太为人所知的命名空间,例如 System.CodeDom
和 System.CodeDom.Compiler
。我将在本文中演示这些命名空间有多么巧妙以及它们可以用来做什么。具体来说,我将演示我们能够使用 System.CodeDom
命名空间在运行时构建全新的源代码文件,并使用 System.CodeDom.Compiler
命名空间中的类来编译这些新创建的源代码,使其成为一个可运行的应用程序。所有这些都将在运行时完成。我写这篇文章的目的是提供对这两个命名空间的一个简单介绍;读完后您不会成为专家,但已经接触到足够的内容,能够意识到这两个命名空间允许您实现什么。
为什么我需要在运行时创建新的源代码和应用程序?
嗯,我开始研究这个问题的原因是基于初始架构动态生成数据访问层 (DAL) / 业务访问层 (BAL) 代码。因此,这可能是创建运行时代码的一个可能领域;基本上,让一个应用程序创建一堆自动生成的源代码文件,然后可以在另一个项目中使用。这就是我们在(工作)中发现这种东西用武之地的地方。
这促使我调查 System.CodeDom.Compiler
命名空间中的类,因此,我认为我会花一些时间讨论使用这些类所能实现的各种可能性。
本文的其余部分将大致分为对 System.CodeDom
命名空间和 System.CodeDom.Compiler
命名空间的讨论。最后,简要讨论附带的演示应用程序。
System.CodeDom
System.CodeDom
命名空间包含可用于在运行时创建源代码的类、接口和枚举。总体的想法是创建一个新构建的文档对象模型 (Document Object Model),其中方法、使用指令的构造函数、变量都可以使用 System.CodeDom
命名空间创建。CodeDOM 真正发挥作用的地方在于它是语言无关的,它只是提供了一个 CodeCompileUnit
,然后可以被 System.CodeDom.Compiler
命名空间使用,后者利用这个 CodeCompileUnit
以任何 .NET 语言创建源代码。
我将演示一些使用 CodeDOM 的示例。
创建命名空间
CodeNamespace ns = new CodeNamespace("TestApp");
ns.Imports.Add(new CodeNamespaceImport("System"));
ns.Imports.Add(new CodeNamespaceImport("System.Text"));
compileUnit.Namespaces.Add(ns);
创建类
CodeTypeDeclaration ctd = new CodeTypeDeclaration(Name);
ctd.IsClass = true;
ctd.Attributes = MemberAttributes.Public;
compileUnit.Namespaces[0].Types.Add(ctd);
创建 Main 方法
CodeEntryPointMethod method = new CodeEntryPointMethod();
method.Attributes = MemberAttributes.Public |
MemberAttributes.Static;
ctd.Members.Add(method);
显然,这只能让您对 CodeDOM 的工作方式有一个非常小的了解。我不得不说它的语法非常冗长,但它能够从 CodeDOM 创建任何 .NET 源,这使其非常有吸引力。也有一些优秀的 CodeDOM 包装器可以缩短语法。我在此文章底部包含了一个相关文章列表,如果您想进一步阅读这个主题。
System.CodeDom.Compiler
如果您想使用 System.CodeDom.Compiler
命名空间来编译构建 CodeDOM 对象的结果,您需要确保无论我们在哪里构建 CodeDOM 对象(使用上面所示的语法),都返回一个 CodeCompileUnit
。正是通过使用这个 CodeCompileUnit
,我们才能够使用 System.CodeDom.Compiler
命名空间,以任何 .NET 语言创建源代码。
例如,演示应用程序代码的这一部分显示了我们如何使用 System.CodeDom
和 System.CodeDom.Compiler
命名空间构建了一个新的源代码文件。
/// <summary>
/// Generates the source code by calling the
/// <see cref="CodeDomExample">GenerateCode</see> method
/// </summary>
private void picGenerate_Click(object sender, EventArgs e)
{
CodeDomProvider domProvider = (CodeDomProvider)getCodeEditorSyntaxOrProvdiderForLanguage
(cmbPickLanguage.SelectedItem.ToString(),false);
//create the code
cde.GenerateCode(domProvider, cde.BuildSimpleAdder());
// Display the generated source code file
using (StreamReader sr = new StreamReader(getSourceFileName(domProvider)))
{
txtCode.Document.Text = sr.ReadToEnd();
sr.Close();
}
}
....
....
/// <summary>
/// Returns either a SyntaxLanguage or a CodeDomProvider based on the selectedLanguage
/// </summary>
/// <param name="selectedLanguage">A string
/// respresenting the selected language</param>
/// <param name="syntaxObjectRequired">True if this method
/// should return a SyntaxLanguage object</param>
/// <returns></returns>
private object getCodeEditorSyntaxOrProvdiderForLanguage(string selectedLanguage,
bool syntaxObjectRequired)
{
switch (selectedLanguage)
{
case "CSharp":
if (syntaxObjectRequired)
return SyntaxLanguage.CSharp;
else
return CodeDomProvider.CreateProvider("CSharp");
case "Visual Basic":
if (syntaxObjectRequired)
return SyntaxLanguage.VBNET;
else
return CodeDomProvider.CreateProvider("VisualBasic");
default:
if (syntaxObjectRequired)
return SyntaxLanguage.CSharp;
else
return CodeDomProvider.CreateProvider("CSharp");
}
}
....
....
/// <summary>
/// Returns a string respresenting the filename for the generated app, which is based
/// on the extension of the current
/// <see cref="CodeDomProvider">CodeDomProvider</see>
/// </summary>
/// <param name="domProvider">The current
/// <see cref="CodeDomProvider">CodeDomProvider</see></param>
/// <returns>A string respresenting the filename for the generated app</returns>
private string getSourceFileName(CodeDomProvider domProvider)
{
if (domProvider.FileExtension[0] == '.')
{
return "app" + domProvider.FileExtension;
}
else
{
return "app." + domProvider.FileExtension;
}
}
....
....
/// <summary>
/// Uses the CodeDom classes to create a simply
/// built in run time source code file (graph). And returns
/// a <see cref="CodeCompileUnit">CodeCompileUnit</see>
/// that can be used by the <see cref="CompilerParameters">
/// CompilerParameters</see> to build an actual
/// <see cref="Assembly">Assembly</see>
/// </summary>
/// <returns></returns>
public CodeCompileUnit BuildSimpleAdder()
{
CodeCompileUnit compileUnit = new CodeCompileUnit();
compileUnit.Namespaces.Add(InitializeNameSpace("TestApp"));
CodeTypeDeclaration ctd = CreateClass("SimplePrint");
// Add the class to the namespace
compileUnit.Namespaces[0].Types.Add(ctd);
CodeEntryPointMethod mtd = CreateMainMethod();
ctd.Members.Add(mtd);
CodeVariableDeclarationStatement VariableDeclaration =
DeclareVariables(typeof(StringBuilder), "sbMessage");
// Add the variable declaration to the method
mtd.Statements.Add(VariableDeclaration);
//and print the results
mtd.Statements.Add(new CodeSnippetExpression(
"Console.WriteLine(sbMessage.Append(\"the result of adding (1+2) is \" + " +
"(1+2).ToString()))"));
// Build a call to System.Console.ReadLine, to keep console window alive to see results
CodeTypeReferenceExpression csSystemConsoleType =
new CodeTypeReferenceExpression("System.Console");
CodeMethodInvokeExpression csReadLine =
new CodeMethodInvokeExpression(csSystemConsoleType, "ReadLine");
//// Add the ReadLine statement.
mtd.Statements.Add(csReadLine);
//lastly return it all
return compileUnit;
}
....
....
/// <summary>
/// Generates the code using either a CSharp or Visual Basic
/// CodeDomProvider. This generated code uses the
/// <see cref="CodeCompileUnit">CodeCompileUnit</see> that was created
/// by the BuildSimpleAdder method
/// </summary>
/// <param name="domProvider">The CodeDomProvider to use,
/// CSharp or Visual Basic CodeDomProviders in our case</param>
/// <param name="compileunit">A
/// <see cref="CodeCompileUnit">CodeCompileUnit</see> that can be used by
/// the System.CodeDom.Compiler classes</param>
public void GenerateCode(CodeDomProvider domProvider, CodeCompileUnit compileunit)
{
//builds the source based on the selected language
String srcFile;
if (domProvider.FileExtension[0] == '.')
{
srcFile = "app" + domProvider.FileExtension;
}
else
{
srcFile = "app." + domProvider.FileExtension;
}
//we want a nice indemtable output file
using (IndentedTextWriter tw =
new IndentedTextWriter(new StreamWriter(srcFile, false), " "))
{
// Create the source code
domProvider.GenerateCodeFromCompileUnit(compileunit, tw,
new CodeGeneratorOptions());
tw.Close();
}
}
这样做足以创建任何 .NET 语言的新源代码文件。这都归功于 CodeDomProvider
类,它可以为所有 .NET 语言创建提供程序。因此,只要我们使用与 CodeDOM 生成的 CodeCompileUnit
相关的 CodeDomProvider
,我们就可以生成任何 .NET 语言的源代码。很酷,对吧?
那么我们目前有什么?嗯,我们创建了一个表示新类的 DOM,最终得到了一个 CodeCompileUnit
,然后我们使用它通过使用特定语言的 CodeDomProvider
类来创建相关的源代码,以生成我们想要的语言。
这很酷,我们有了一个源代码文件。但在本文的开头,我也说过我将向您展示如何将源代码编译成程序集或可执行文件。所以,让我们继续看看。
同样,我认为最好的展示方式是相关的源代码。
/// <summary>
/// Compile the source code by calling the
/// <see cref="CodeDomExample">Compile</see> method
/// </summary>
private void picCompile_Click(object sender, EventArgs e)
{
CodeDomProvider domProvider =
(CodeDomProvider)getCodeEditorSyntaxOrProvdiderForLanguage(
cmbPickLanguage.SelectedItem.ToString(), false);
// Compile the source file into an executable output file.
string srcFile = getSourceFileName(domProvider);
CompilerResults cr = cde.Compile(domProvider,srcFile,"app.exe");
string message = string.Empty;
if (cr.Errors.Count > 0)
{
// Display compilation errors.
message+= "Errors encountered while building " +
srcFile + " into " + cr.PathToAssembly + ": \r\n\n";
foreach (CompilerError ce in cr.Errors)
message +=ce.ToString() + "\r\n";
picRun.Enabled = false;
MessageBox.Show(message);
}
else
{
message += "Source " + srcFile + " built into " +
cr.PathToAssembly + " with no errors.";
picRun.Enabled = true;
MessageBox.Show(message);
}
}
....
....
/// <summary>
/// Returns either a SyntaxLanguage or a CodeDomProvider
/// based on the selectedLanguage
/// </summary>
/// <param name="selectedLanguage">A string
/// respresenting the selected language</param>
/// <param name="syntaxObjectRequired">True if this
/// method should return a SyntaxLanguage object</param>
/// <returns></returns>
private object getCodeEditorSyntaxOrProvdiderForLanguage(string selectedLanguage,
bool syntaxObjectRequired)
{
switch (selectedLanguage)
{
case "CSharp":
if (syntaxObjectRequired)
return SyntaxLanguage.CSharp;
else
return CodeDomProvider.CreateProvider("CSharp");
case "Visual Basic":
if (syntaxObjectRequired)
return SyntaxLanguage.VBNET;
else
return CodeDomProvider.CreateProvider("VisualBasic");
default:
if (syntaxObjectRequired)
return SyntaxLanguage.CSharp;
else
return CodeDomProvider.CreateProvider("CSharp");
}
}
....
....
/// <summary>
/// Returns a string respresenting the filename for the generated app, which is based
/// on the extension of the current <see cref="CodeDomProvider">CodeDomProvider</see>
/// </summary>
/// <param name="domProvider">The current
/// <see cref="CodeDomProvider">CodeDomProvider</see></param>
/// <returns>A string respresenting the filename for the generated app</returns>
private string getSourceFileName(CodeDomProvider domProvider)
{
if (domProvider.FileExtension[0] == '.')
{
return "app" + domProvider.FileExtension;
}
else
{
return "app." + domProvider.FileExtension;
}
}
....
....
/// <summary>
/// Uses the <see cref="CompilerParameters">CompilerParameters</see>
/// to compile the the source file, creating the output file
/// </summary>
/// <param name="domProvider">The CodeDomProvider to use,
/// CSharp or Visual Basic CodeDomProviders in our case</param>
/// <param name="srcFile">The source file</param>
/// <param name="exeFile">The name for the output file</param>
/// <returns>The actual <see cref="CompilerResults">CompilerResults</see>
/// which will holds the compiled <see cref="Assembly">Assembly</see></returns>
public CompilerResults Compile(CodeDomProvider domProvider, String srcFile,String exeFile)
{
// Configure a CompilerParameters that links System.dll
// and produces the specified executable file.
String[] referenceAssemblies = { "System.dll" };
CompilerParameters cp = new CompilerParameters(referenceAssemblies,exeFile, false);
//make sure we create an EXE not a Dll, and then compile
cp.GenerateExecutable = true;
CompilerResults compiledResults = domProvider.CompileAssemblyFromFile(cp, srcFile);
return compiledResults;
}
这就足够让我们为之前创建的源代码创建一个 EXE。记住,所有这些都是在运行时完成的。我认为这实际上相当酷。在运行时创建新的源代码并编译运行它。这很酷。
演示应用程序
总之,让我们快速聊聊演示应用程序。我使用了出色的 Fireball 代码高亮器,可以在 CodeProject 的 这里 找到。我试图在运行时创建的基本程序如下:
Namespace TestApp {
using System;
using System.Text;
Public Class SimplePrint {
Public Static void Main() {
System.Text.StringBuilder sbMessage = new System.Text.StringBuilder();
Console.WriteLine(sbMessage.Append(
"the result of adding (1+2) is " + (1+2).ToString()));
System.Console.ReadLine();
}
}
}
在 Visual Basic .NET 中生成后,看起来如下:
Option Strict Off
Option Explicit On
Imports System
Imports System.Text
Namespace TestApp
Public Class SimplePrint
Public Shared Sub Main()
Dim sbMessage As System.Text.StringBuilder = _
New System.Text.StringBuilder
Console.WriteLine(sbMessage.Append("the result of adding (1+2) is " + _
(1+2).ToString()))
System.Console.ReadLine
End Sub
End Class
End Namespace
演示应用程序看起来是这样的,有三个按钮:
- 生成按钮:生成源代码
- 编译按钮:编译源代码
- 运行:运行新编译的应用程序
这是演示应用程序的屏幕截图,就在执行了生成和编译之后,所选的 .NET 语言是 Visual Basic:
这是实际新创建的应用程序正在运行:
就这样。如我所说,您绝非因此成为专家,但我希望它能让您对这两个非常酷的命名空间能够做什么有一个大致的了解。
其他 CodeDOM 资源
- msdn2.microsoft.com/en-us/library/system.codedom.aspx
- msdn2.microsoft.com/en-us/library/system.codedom.compiler(VS.71).aspx
- http://msdn2.microsoft.com/en-us/library/system.codedom.compiler.codedomprovider.aspx
- codedom_assistant.aspx
- refly.aspx
- https://codeproject.org.cn/KB/recipes/codedomparser.aspx
- https://codeproject.org.cn/KB/architecture/codedompatterns.aspx#CodePatternCompileUnithttps://codeproject.org.cn/KB/database/ObjectMapping2.aspx
未来工作
我可能会在未来进一步阐述这篇文章,讨论如何使用一些 Visual Studio SDK 工具更有效地处理代码生成过程。
历史
- 2008年1月27日:初始版本。