Roslyn 编译器Api妙用:动态生成类并实现接口

在上一篇文章中有讲到使用反射手写IL代码动态生成类并实现接口。

反射的妙用:C#通过反射动态生成类型继承接口并实现

有位网友推荐使用 Roslyn 去脚本化动态生成,今天这篇文章就主要讲怎么使用 Roslyn 动态生成类。

image

什么是Roslyn

最初 C# 语言的编译器是用 C++ 编写的,后来微软推出了一个新的用 C# 自身编写的编译器:Roslyn,它属于自举编译器。

所谓自举编译器就是指,某种编程语言的编译器就是用该语言自身来编写的。自举编译器的每个版本都是用该版本之前的版本来编译的,但它的第一个版本必须由其它语言编写的编译器来编译,比如 Roslyn 的第一个版本是由 C++ 编写的编译器来编译的。很多编程语言发展成熟后都会用该语言本身来编写自己的编译器,比如 C#Go 语言。

.NET 平台,Roslyn 编译器负责将 C# VB 代码编译为程序集。

大多数现有的传统编译器都是“黑盒”模式,它们将源代码转换成可执行文件或库文件,中间发生了什么我们无法知道。与之不同的是,Roslyn 允许你通过 API 访问代码编译过程中的每个阶段。

以上内容取自:精致码农 • 王亮

Roslyn实现拦截器

拦截器用过MVC的应该都很熟悉:Filter,比如,请求拦截器:OnActionExecutingOnActionExecuted

下面就用Roslyn简单实现类似:OnActionExecuting 的拦截效果。

1、先准备一段拦截脚本
    const string before = "public static string before(string name)" +
                           "{" +
                               "Console.WriteLine($\"{name}已被拦截,形成检测成功,无感染风险。\");" +
                               "return name+\":健康\";" +
                           "}";
2、准备传参对象
public class ParameterVector
{
    public string arg1 { get; set; }
}
3、编写脚本执行代码
    /// <summary>
    /// 执行Before脚本
    /// </summary>
    /// <param name="name"></param>
    /// <returns></returns>
    public static string ExecuteBeforeScript(string name)
    {
        StringBuilder builder = new StringBuilder();
        builder.Append("public class intercept");
        builder.Append("{");
        builder.Append(before);
        builder.Append("}");
        builder.Append("return intercept.before(arg1);");
        var result = CS.CSharpScript.RunAsync<string>(builder.ToString(),
            // 引用命名空间
            ScriptOptions.Default.AddReferences("System.Linq").AddImports("System"),
            // 参数对象
            globals: new ParameterVector() { arg1 = name },
            globalsType: typeof(ParameterVector))
            .Result;
        return result.ReturnValue;
    }
4、调用拦截器
	static void Main(string[] args)
    {
        var msg = Console.ReadLine();
        travel(msg);
        Console.WriteLine("执行完毕...");
    }

    static string travel(string userName)
    {
        var result = Script.ExecuteBeforeScript(userName);
        Console.WriteLine(result);
        return result;
    }

image

咋一看上面的逻辑其实很傻,就是将方法逻辑写成静态脚本去动态调用。还不如直接就在方法内部写相关逻辑。

但是我们将思维发散一下,将静态脚本替换为从文件读取,在业务上线后,我们只需要修改文件脚本的逻辑即可,是不是觉得用处就来了,是不是有那么点 AOP 的感觉了。

Roslyn动态实现接口

下面的内容与之前反射动态生成的结果一样,只是换了一种方法去处理。

1、准备需要实现的接口,老User了
public interface IUser
{
    string getName(string name);
}
2、准备一个拦截类
public class Intercept
{
    public static void Before(string name)
    {
        Console.WriteLine($"拦截成功,参数:{name}");
    }
}
3、根据接口生成一个静态脚本
    /// <summary>
    /// 生成静态脚本
    /// </summary>
    /// <typeparam name="Tinteface"></typeparam>
    /// <returns></returns>
    public static string GeneratorScript<Tinteface>(string typeName)
    {
        var t = typeof(Tinteface);
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("using System;");
        stringBuilder.Append($"using {t.Namespace};");
        stringBuilder.Append($"namespace {typeName}_namespace");
        stringBuilder.Append("{");
        stringBuilder.Append($"public class {typeName}:{t.Name}");
        stringBuilder.Append(" {");
        MethodInfo[] targetMethods = t.GetMethods();
        foreach (MethodInfo targetMethod in targetMethods)
        {
            if (targetMethod.IsPublic)
            {
                var returnType = targetMethod.ReturnType;
                var parameters = targetMethod.GetParameters();
                string pStr = string.Empty;
                List<string> parametersName = new List<string>();
                foreach (ParameterInfo parameterInfo in parameters)
                {
                    var pType = parameterInfo.ParameterType;
                    pStr += $"{pType.Name} _{pType.Name},";
                    parametersName.Add($"_{pType.Name}");
                }

                stringBuilder.Append($"public {returnType.Name} {targetMethod.Name}({pStr.TrimEnd(',')})");
                stringBuilder.Append(" {");
                foreach (var pName in parametersName)
                {
                    stringBuilder.Append($"Intercept.Before({pName});");
                }
                stringBuilder.Append($"return \"执行成功。\";");
                stringBuilder.Append(" }");
            }
        }
        stringBuilder.Append(" }");
        stringBuilder.Append(" }");
        return stringBuilder.ToString();
    }
4、构建程序集
	/// <summary>
    /// 构建类对象
    /// </summary>
    /// <typeparam name="Tinteface"></typeparam>
    /// <returns></returns>
    public static Type BuildType<Tinteface>()
    {
        var typeName = "_" + typeof(Tinteface).Name;
        var text = GeneratorTypeCode<Tinteface>(typeName);

        // 将代码解析成语法树
        SyntaxTree tree = SyntaxFactory.ParseSyntaxTree(text);

        var objRefe = MetadataReference.CreateFromFile(typeof(Object).Assembly.Location);
        var consoleRefe = MetadataReference.CreateFromFile(typeof(IUser).Assembly.Location);

        var compilation = CSharpCompilation.Create(
            syntaxTrees: new[] { tree },
            assemblyName: $"assembly{typeName}.dll",
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
            references: AppDomain.CurrentDomain.GetAssemblies().Select(x => MetadataReference.CreateFromFile(x.Location)));

        Assembly compiledAssembly;
        using (var stream = new MemoryStream())
        {
			// 检测脚本代码是否有误
            var compileResult = compilation.Emit(stream);
            compiledAssembly = Assembly.Load(stream.GetBuffer());
        }
        return compiledAssembly.GetTypes().FirstOrDefault(c => c.Name == typeName);
    }
5、调用动态生成类的方法
	static void Main(string[] args)
	{
		Type t = codeExtension.BuildType<IUser>();
		var method = t.GetMethod("getName");
		object obj = Activator.CreateInstance(t);
		var result = method.Invoke(obj, new object[] { "张三" }).ToString();
		Console.WriteLine(result);
	}

image

两种(Roslyn/IL)动态生成方式比起来,从编码方式比起来差别还是挺大的。

手写IL无疑要比Roslyn复杂很多,手写IL无法调试,无法直观展示代码,没有错误提示,如果业务逻辑比较复杂将会是一场灾难。Roslyn将业务逻辑脚本化,代码通过脚本可直观展示,有明确的错误提示。

至于性能方面暂时还没有做比较,后续有机会再将两种方式的性能对比放出来。

Roslyn异常提示

上面的代码中,有一小段代码:

// 检测脚本代码是否有误
var compileResult = compilation.Emit(stream);

脚本无误的返回值如下:

image

当脚本出现错误的返回值如下:

image

从上面的错误中很明显可以看到,缺少了 System 命名空间,以及方法签名与接口不匹配。

以上就是Roslyn编译器Api的一些简单的使用。

无绪分享