簡體   English   中英

如何使用反射以編程方式創建 class 庫 DLL?

[英]How to programmatically create a class library DLL using reflection?

假設我的代碼擁有一個不存在的 class 庫“mytest.dll”的元數據知識,例如該庫中的類型、類型的函數、函數的參數和返回類型等。

我的代碼如何使用反射等技術制造此 DLL?

我知道我的代碼可以生成“mytest.cs”文本文件,然后執行編譯器生成DLL,然后刪除“mytest.cs”文件。 只是想知道是否有“更高級”或“更酷”的方法來做到這一點。

謝謝。

從您的應用程序編譯和執行動態 .net 腳本的過程有 4 個主要步驟,即使是非常復雜的場景也可以通過這種方式簡化:

  1. 生成代碼
  2. 編譯腳本
  3. 加載程序集
  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 called GenerateMessage 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

對於我們的方法,最終的 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM