Roslyn 編譯器Api妙用:動態生成類並實現介面

暢飲無緒發表於2021-11-18

在上一篇文章中有講到使用反射手寫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的一些簡單的使用。

無緒分享

相關文章