[英]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.