简体   繁体   中英

How to programmatically create a class library DLL using reflection?

Suppose my code possesses the knowledge about the metadata of a nonexistent class library "mytest.dll", such as the types in this library, the functions of the types, the parameters and return types of the functions, etc.

How does my code manufacture this DLL using techniques such as reflection?

I know my code can generate the "mytest.cs" text file, then execute the compiler to produce the DLL, then delete the "mytest.cs" file. Just want to know if there are "more advanced" or "cooler" ways to do it.

Thanks.

There are 4 main steps in the process to compile and execute dynamic .net scripts from your application, even really complex scenarios can be simplified in this way:

  1. Generate the code
  2. Compile the script
  3. Load the assembly
  4. Execute the code

Lets generate a simple Hello Generated C# World App right now::

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.

This solution requires the following package to be installed:

PM> Install-Package 'Microsoft.CodeAnalysis.CSharp.Scripting'

And will require the following using statements:

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;

Orchestration

The following method encapsulates the above steps:

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);
}

Generate the code

You say you already have item 1 sorted out, you can use StringBuilder , T4 templates or other mechanisms to generate the code files.

generating the code itself is its own question if you need help with that.

However, for our demo app, the following would work:

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();
}

For an input value of 3, the following code is generated:

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}";
        }
    }
}

Compile the script (and Load it)

These are two discrete steps, however it is easiest to code them together in the same method, for this example we will ignore the generated dll and load the assembly directly into memory, that is generally the more likely use case for this type of scripting scenario anyway.

The hardest element of this is usually the referencing of the relevant dlls. There are a number of ways to achieve this, including loading all the dlls that are in the current executing context, I find a simple way to do this is to access the Assembly reference from the Type reference for the types we want to use inside the dynamic script:

List<string> dlls = new List<string> {
    typeof(object).Assembly.Location,
    typeof(Enumerable).Assembly.Location
};

Cut a long story short, this method compiles and loads the assembly into memory. It includes some crude compilation error handling, just to demonstrate how to do it:

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());
        }
    }
}

Execute the code

Finally, we can use reflection to instantiate an instance of the new app and then we can obtain a reference to the method and it. The name of the parameters is irrelevant, as long we pass them through in the correct order:

for this demo the order is sort of irrelevant to, given they are all the same type;)

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

The final output from all this for our method with 3 passed into it is:

p0=1,p1=2,p2=3

So that was super crude, you can bypass most of the indirect reflection aspects through the use of Interfaces . If your generated script inherits from types or interfaces that the calling code also has a strong reference to, then ExecuteScript in the above example might look like this:

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");
}

The major benefit to using an interface or base class reference is that you can natively set properties or call other methods without having to reflect references to them all or to resort to using dynamic which would work, but becomes a bit harder to debug.

Of course the interface solution is hard to implement when we had a variable number of parameters, so that's not the best example, usually with dynamic scripts you would construct a known environment, say a known class and methods, but you might want to inject custom code into the body of the method.

It's a bit of fun in the end, but this simple example shows that C# can be used as a runtime scripting engine without too much trouble.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM