在上一篇文章中有講到使用反射手寫IL程式碼動態生成類並實現介面。
有位網友推薦使用 Roslyn
去指令碼化動態生成,今天這篇文章就主要講怎麼使用 Roslyn
動態生成類。
什麼是Roslyn
最初 C#
語言的編譯器是用 C++
編寫的,後來微軟推出了一個新的用 C#
自身編寫的編譯器:Roslyn
,它屬於自舉編譯器。
所謂自舉編譯器就是指,某種程式語言的編譯器就是用該語言自身來編寫的。自舉編譯器的每個版本都是用該版本之前的版本來編譯的,但它的第一個版本必須由其它語言編寫的編譯器來編譯,比如 Roslyn
的第一個版本是由 C++
編寫的編譯器來編譯的。很多程式語言發展成熟後都會用該語言本身來編寫自己的編譯器,比如 C#
和 Go
語言。
在 .NET
平臺,Roslyn
編譯器負責將 C#
和 VB
程式碼編譯為程式集。
大多數現有的傳統編譯器都是“黑盒”模式,它們將原始碼轉換成可執行檔案或庫檔案,中間發生了什麼我們無法知道。與之不同的是,Roslyn 允許你通過 API 訪問程式碼編譯過程中的每個階段。
以上內容取自:精緻碼農 • 王亮
Roslyn實現攔截器
攔截器用過MVC
的應該都很熟悉:Filter
,比如,請求攔截器:OnActionExecuting
、OnActionExecuted
。
下面就用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;
}
咋一看上面的邏輯其實很傻,就是將方法邏輯寫成靜態指令碼去動態呼叫。還不如直接就在方法內部寫相關邏輯。
但是我們將思維發散一下,將靜態指令碼替換為從檔案讀取,在業務上線後,我們只需要修改檔案指令碼的邏輯即可,是不是覺得用處就來了,是不是有那麼點 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);
}
兩種(Roslyn/IL
)動態生成方式比起來,從編碼方式比起來差別還是挺大的。
手寫IL無疑要比Roslyn
複雜很多,手寫IL
無法除錯,無法直觀展示程式碼,沒有錯誤提示,如果業務邏輯比較複雜將會是一場災難。Roslyn
將業務邏輯指令碼化,程式碼通過指令碼可直觀展示,有明確的錯誤提示。
至於效能方面暫時還沒有做比較,後續有機會再將兩種方式的效能對比放出來。
Roslyn異常提示
上面的程式碼中,有一小段程式碼:
// 檢測指令碼程式碼是否有誤
var compileResult = compilation.Emit(stream);
指令碼無誤的返回值如下:
當指令碼出現錯誤的返回值如下:
從上面的錯誤中很明顯可以看到,缺少了 System
名稱空間,以及方法簽名與介面不匹配。
以上就是Roslyn
編譯器Api
的一些簡單的使用。