[英]How to programmatically create a class library DLL using reflection?
假设我的代码拥有一个不存在的 class 库“mytest.dll”的元数据知识,例如该库中的类型、类型的函数、函数的参数和返回类型等。
我的代码如何使用反射等技术制造此 DLL?
我知道我的代码可以生成“mytest.cs”文本文件,然后执行编译器生成DLL,然后删除“mytest.cs”文件。 只是想知道是否有“更高级”或“更酷”的方法来做到这一点。
谢谢。
从您的应用程序编译和执行动态 .net 脚本的过程有 4 个主要步骤,即使是非常复杂的场景也可以通过这种方式简化:
现在让我们生成一个简单的Hello Generated C# World App::
Create a method that will generate an assembly that has 1 class called
HelloWorldApp
, this class has 1 method calledGenerateMessage
it will have X input parameters that will be integers, it will return a CSV string of the arguments that were passed in to it.
此解决方案需要安装以下 package:
PM> Install-Package 'Microsoft.CodeAnalysis.CSharp.Scripting'
并且需要以下 using 语句:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
下面的方法封装了上述步骤:
private static void GenerateAndExecuteApp(int numberOfParameters)
{
string nameSpace = "Dynamic.Example";
string className = "HelloWorldApp";
string methodName = "GenerateMessage";
// 1. Generate the code
string script = BuildScript(nameSpace, className, methodName, numberOfParameters);
// 2. Compile the script
// 3. Load the Assembly
Assembly dynamicAssembly = CompileScript(script);
// 4. Execute the code
int[] arguments = Enumerable.Range(1, numberOfParameters).ToArray();
string message = ExecuteScript(dynamicAssembly, nameSpace, className, methodName, arguments);
Console.Out.WriteLine(message);
}
你说你已经把第 1 项整理好了,你可以使用StringBuilder
、T4 模板或者其他机制来生成代码文件。
如果您需要帮助,生成代码本身就是它自己的问题。
但是,对于我们的演示应用程序,以下内容将起作用:
private static string BuildScript(string nameSpace, string className, string methodName, int numberOfParameters)
{
StringBuilder code = new StringBuilder();
code.AppendLine("using System;");
code.AppendLine("using System.Linq;");
code.AppendLine();
code.AppendLine($"namespace {nameSpace}");
code.AppendLine("{");
code.AppendLine($" public class {className}");
code.AppendLine(" {");
var parameterNames = Enumerable.Range(0, numberOfParameters).Select(x => $"p{x}").ToList();
code.Append($" public string {methodName}(");
code.Append(String.Join(",", parameterNames.Select(x => $"int {x}")));
code.AppendLine(")");
code.AppendLine(" {");
code.Append(" return $\"");
code.Append(String.Join(",", parameterNames.Select(x => $"{x}={{{x}}}")));
code.AppendLine("\";");
code.AppendLine(" }");
code.AppendLine(" }");
code.AppendLine("}");
return code.ToString();
}
对于输入值 3,将生成以下代码:
using System;
using System.Linq;
namespace Dynamic.Example
{
public class HelloWorldApp
{
public string GenerateMessage(int p0,int p1,int p2)
{
return $"p0={p0},p1={p1},p2={p2}";
}
}
}
这是两个独立的步骤,但是最简单的方法是用相同的方法将它们一起编码,对于本例,我们将忽略生成的 dll 并将程序集直接加载到 memory 中,这通常是此类脚本场景的更可能用例反正。
最难的元素通常是相关 dll 的引用。 有很多方法可以实现这一点,包括加载当前执行上下文中的所有 dll,我发现一个简单Type
Assembly
中访问我们想要在动态脚本:
List<string> dlls = new List<string> {
typeof(object).Assembly.Location,
typeof(Enumerable).Assembly.Location
};
长话短说,此方法将程序集编译并加载到 memory 中。 它包括一些粗略的编译错误处理,只是为了演示如何做到这一点:
private static Assembly CompileScript(string script)
{
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(script);
// use "mytest.dll" if you want, random works well enough
string assemblyName = System.IO.Path.GetRandomFileName();
List<string> dlls = new List<string> {
typeof(object).Assembly.Location,
typeof(Enumerable).Assembly.Location
};
MetadataReference[] references = dlls.Distinct().Select(x => MetadataReference.CreateFromFile(x)).ToArray();
CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
// Now we actually compile the script, this includes some very crude error handling, just to show you can
using (var ms = new MemoryStream())
{
EmitResult result = compilation.Emit(ms);
if (!result.Success)
{
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error);
List<string> errors = new List<string>();
foreach (Diagnostic diagnostic in failures)
{
//errors.AddDistinct(String.Format("{0} : {1}", diagnostic.Id, diagnostic.Location, diagnostic.GetMessage()));
errors.Add(diagnostic.ToString());
}
throw new ApplicationException("Compilation Errors: " + String.Join(Environment.NewLine, errors));
}
else
{
ms.Seek(0, SeekOrigin.Begin);
return Assembly.Load(ms.ToArray());
}
}
}
最后,我们可以使用反射来实例化新应用程序的实例,然后我们可以获得对该方法及其方法的引用。 参数的名称无关紧要,只要我们以正确的顺序传递它们:
对于这个演示,顺序有点无关紧要,因为它们都是相同的类型;)
private static string ExecuteScript(Assembly assembly, string nameSpace, string className, string methodName, int[] arguments)
{
var appType = assembly.GetType($"{nameSpace}.{className}");
object app = Activator.CreateInstance(appType);
MethodInfo method = appType.GetMethod(methodName);
object result = method.Invoke(app, arguments.Cast<object>().ToArray());
return result as string;
}
对于我们的方法,最终的 output 传入3
是:
p0=1,p1=2,p2=3
所以这是超级粗略的,你可以通过使用接口绕过大部分间接反射方面。 如果您生成的脚本继承自调用代码也具有强引用的类型或接口,则上述示例中的ExecuteScript
可能如下所示:
private static string ExecuteScript(Assembly assembly, string nameSpace, string className)
{
var appType = assembly.GetType($"{nameSpace}.{className}");
object app = Activator.CreateInstance(appType);
if (app is KnownInterface known)
{
return known.GenerateMessage(1,2,3);
}
throw new NotSupportedException("Couldn't resolve known type");
}
使用接口或基本 class 引用的主要好处是,您可以本机设置属性或调用其他方法,而不必全部反映对它们的引用或诉诸使用dynamic
的方法,这会起作用,但调试起来有点困难。
当然,当我们有可变数量的参数时,接口解决方案很难实现,所以这不是最好的例子,通常使用动态脚本你会构建一个已知的环境,比如已知的 class 和方法,但你可能想要注入自定义代码到方法的主体。
最后还是有点好玩,不过这个简单的例子说明C#可以作为运行时脚本引擎使用,没有太多麻烦。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.