.NET 脚本(一)
作者: jconwell
你知道我对于前.NET时代有什么留恋吗?脚本!我喜欢创建一个小巧的脚本文件为我完成一些小任务,或者为了测试一小段代码而无需创建一项工程或是解决方案。我喜欢处理和清除的仅仅是一个小巧的文件而不是一个解决方案文件夹,工程文件夹和附带的bin 和obj 文件夹。我怀念那些时光,这正是我创建 .NET 脚本的原因。
什么是.NET 脚本呢?基本上,它就是一个简单的控制台应用程序,从 .dnml 文件( Dot Net Markup Language, .NET 标记语言, 这是我定义的,哈哈)中读取 XML 文档。这个XML文档包含如下子元素,存储程序集引用,编写的代码所属的语言以及实际的要编译和执行的代码。那个控制台应用程序,我称之为脚本引擎,读取XML 文本并分析出需要的数据。然后它利用CSharp, VisualBasic, 和 CodeDom 命名空间中的类编译代码并将作为结果的程序集装载到内存中。教本引擎利用反射机制执行生成的程序集中的入口函数。当用户关闭控制台窗口时,脚本引擎被关闭,在内存中的程序集将不复存在,它将被垃圾回收器清理掉。没有任何的库或可执行程序生成。
让我们来看看.NET 标记语言是什么模样的。它其实非常简单。下面就是一个它的例子。我会一一说明XML 文档中的每个元素。
<dnml>
<reference assembly="System.Windows.Forms.dll" />
<language name="C#" />
<scriptCode><![CDATA[
using System.Windows.Forms;
public class Test
{
public static void Main()
{
Console.WriteLine("This is a test");
MessageBox.Show("This is another test");
Test2 two = new Test2();
two.Stuff();
}
}
public class Test2
{
public void Stuff()
{
Console.WriteLine("Instance call");
}
}
]]></scriptCode>
</dnml>
该文档XML元素称为 <dnml> (你能猜出它代表什么吗?)。在这个元素内部有三个不同的子元素,你能用它们来定义脚本如何编译。
首先是 <reference> 元素,它只有一个属性,叫“Assembly”。“Assembly”属性包含你要引用的程序集名称(包含文件扩展名)。一个.dnml 文档可以包含许多<reference> 元素,它对应于你在VS.NET 中向工程中添加的引用列表。 对于每一个你的代码执行所需要的引用,都必须添加一个<reference assembly="" /> 元素。
基于程序集探测的考虑,任何你所引用的GAC 程序集都会被CLR自动找到。但是如果你引用了一个不是GAC中的程序集,情况就不同了。假设你引用了一个称为Common.dll 的非GAC 程序集。为了让您的.NET 脚本正确执行,Common.dll 必须放在两个地方。首先它必须放在你的.dnml 文件所属的文件夹中。其次,它必须放在脚本引擎所在的文件夹中。我正在试图解决这个问题,但是就目前来说非GAC 程序集必须存放在两个不同的文件夹中。
下一个元素是<language> ,它有一个属性,称为 'name'。一个.dnml 文件只能有一个语言元素。对于’name’ 属性两个可能的值是 'C#' 和 'VB', 我希望他们是自描述的。
最后一个元素是<scriptCode> , 它含有一个CDATA XML 元素。这个元素里包含了当你执行该.dnml 文件时将要执行的代码。但是为了使用它你必须遵循一些接口规则。首先,它实际上只是普通的内嵌C# 和 VB.NET, 所有的方法和字段都必须放在类里面。其次,你可以定义任意多个类,但是必须有一个类拥有一个公有静态函数,称为 “Main”, 没有任何输入参数,也不返回结果。只是脚本引擎通过反射搜索到的入口方法;如果找到,将会调用它。当然,“Main”方法放在哪个类中是无关紧要的,因为脚本引擎将会遍历定义的每个类型,直到它找到Main 方法为止。
脚本引擎的大多数代码都是很直观的,因此我会一一描述它的每个方面。当中有一个非常有趣的部分就是一个名为AssemblyGenerator
的类,它只有一个方法,称为CreateAssembly()
。这个方法将完成所有的工作,编译并生成一个新的程序集,正如下面所看到的.
//Create an instance of the C# compiler
CodeDomProvider codeProvider = null;
if (code.IsCSharp)
codeProvider = new CSharpCodeProvider();
else
codeProvider = new VBCodeProvider();
ICodeCompiler compiler = codeProvider.CreateCompiler();
首先我需要声明一个类CodeDomProvider
的实例。它是类 CSharpCodeProvider
和类VBCodeProvider
的基类。你可以使用这些语言的特定 XxxProvider 对象来创建一个 CodeGenerator
对象,它将被用来根据它包含的你创建的CodeDom 对象图生成代码。你可以创建一个CodeParser
对象,它将根据你传入的源代码字符串生成一个CodeDom
对象图(在当前1.1 .NET Framework 版本中, 它将返回空值) 。XxxProvider 对象也可以用来创建一个CodeCompiler
, 这正是我这里所使用的。 CodeCompiler 类就是我用来编译.dnml 文件中的代码,并生成新的程序集的类。
所以,基于.dnml 文件中所定义的语言类型,我创建一个合适的XxxCodeProvider 对象。从这个对象,我请求一个CodeCompiler
实例,它将是基于语言不同而不同的。
//add compiler parameters
CompilerParameters compilerParams = new CompilerParameters();
compilerParams.CompilerOptions = "/target:library /optimize";
compilerParams.GenerateExecutable = false;
compilerParams.GenerateInMemory = true;
compilerParams.IncludeDebugInformation = false;
compilerParams.ReferencedAssemblies.Add("mscorlib.dll");
compilerParams.ReferencedAssemblies.Add("System.dll");
//add any aditional references needed
foreach (string refAssembly in code.References)
compilerParams.ReferencedAssemblies.Add(refAssembly);
下一步,我创建一个CompilerParameters
对象。这个类基本上包装了当你手工通过csc.exe (C# 编译器) 和 vbc.exe (VB.NET 编译器) 编译一个程序集时所用的所有命令行参数。一个特别重要的参数就是属性GenerateInMemory
, 这里我用到了它。它能确保当代码编译时,生成的程序集只会驻存在内存中,而不会作为结果创建任何文件。
该代码的最后一部分将脚本代码所需要的所有引用添加到CompilerParameters
中。 默认情况下,我添加了对于mscorlib.dll 和 system.dll 的引用。然后我添加了对于.dnml 文件中每一个<reference> 元素所标明的程序集的引用。
//actually compile the code
CompilerResults results = compiler.CompileAssemblyFromSource(
compilerParams, code.SourceCode));
//Do we have any compiler errors
if (results.Errors.Count > 0)
{
foreach (CompilerError error in results.Errors)
DotNetScriptEngine.LogAllErrMsgs("Compine Error:"+error.ErrorText);
return null;
}
然后,我调用了CodeCompiler.CompileAssemblyFromSource
, 它传入CompilerParameters
对象和包含所要编译的实际代码的字符串变量。返回的对象属于类CompilerResults
. 当编译出现错误时,这个对象包含一个CompileError
对象的集合,我将用它显示给用户当编译时那些地方出错了。
//get a hold of the actual assembly that was generated
Assembly generatedAssembly = results.CompiledAssembly;
//return the assembly
return generatedAssembly;
}
如果脚本代码编译成功,CompilerReslts
对象将包含一个对于新编译和创建的程序集的引用。我保留该对象并返回给调用方法。
一旦程序集成功创建并返回,脚本引擎将利用反射遍历每一个生成的类型,寻找一个叫'Main' 的静态方法。如果找到了,就再次利用反射执行它。如果没有找到,它将返回给用户一个错误,用来解释发生的问题。
.NET 脚本引擎还能够添加和删除.dnml 文件的关联。这意味着一旦文件关联好,你为了执行.dnml 文件只需双击它就可以了。 当你做个时,脚本引擎将会执行,一个相关路径的命令行参数和该 .dnml 文件名将传递给它。接着教本引擎将读取该文件并处理相应的XML。
为了创建.dnml 文件和.NET 脚本引擎之间的关联,你只需双击DotNetScriptEngine.exe 就可以了。当它不带任何命令行参数执行时,它将在您的服务器上创建文件关联。如果你在控制台运行DotNetScriptEngine.exe, 并传入'remove' 参数,该引擎将会在您的服务器上删除文件关联。
本文地址:http://com.8s8s.com/it/it44995.htm