繁体   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