基于 Roslyn 實現代碼動態編譯
Intro
之前做的一個數據庫小工具可以支持根據 Model 代碼文件生成創建表的 sql 語句,原來是基于 CodeDom 實現的,后來改成使用基于 Roslyn 去做了。
實現的原理在于編譯選擇的Model 文件生成一個程序集,再從這個程序集中拿到 Model (數據庫表)信息以及屬性信息(數據庫表字段信息),拿到數據庫表以及表字段信息之后就根據數據庫類型生成大致的創建表的 sql 語句。
最近做的一個小工具 dotnet-exec
也是類似的,將代碼編譯成一個程序集并通過反射的方式執行代碼邏輯,
分享一下用到的一些代碼
Sample
來看一個最簡單的編譯一段文本為程序集示例:
//?分析語法樹
var?syntaxTree?=?CSharpSyntaxTree.ParseText(sourceText,?new?CSharpParseOptions(LanguageVersion.Latest));//?配置引用
var?references?=?new[]
{typeof(object).Assembly,Assembly.Load("netstandard"),Assembly.Load("System.Runtime"),
}
.Select(assembly?=>?assembly.Location).Distinct().Select(l?=>?MetadataReference.CreateFromFile(l)).Cast<MetadataReference>().ToArray();var?assemblyName?=?$"DbTool.DynamicGenerated.{GuidIdGenerator.Instance.NewId()}";
//?獲取編譯
var?compilation?=?CSharpCompilation.Create(assemblyName).WithOptions(new?CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)).AddReferences(references).AddSyntaxTrees(syntaxTree);using?var?ms?=?new?MemoryStream();
//?生成編譯結果并導出程序集信息到?stream?中
var?compilationResult?=?compilation.Emit(ms);
if?(compilationResult.Success)
{var?assemblyBytes?=?ms.ToArray();//?加載程序集return?Assembly.Load(assemblyBytes);
}var?error?=?new?StringBuilder();
foreach?(var?t?in?compilationResult.Diagnostics)
{error.AppendLine($"{t.GetMessage()}");
}
throw?new?ArgumentException($"Compile?error:{Environment.NewLine}{error}");
多段文本的編譯示例:
var?parseOptions?=?new?CSharpParseOptions(LanguageVersion.Latest);
var?syntaxTrees?=?sourceText.Select(text?=>?CSharpSyntaxTree.ParseText(text)).ToArray();
var?references?=?new[]
{typeof(object).Assembly,Assembly.Load("netstandard"),Assembly.Load("System.Runtime"),
}
.Select(assembly?=>?assembly.Location).Distinct().Select(l?=>?MetadataReference.CreateFromFile(l)).Cast<MetadataReference>().ToArray();
var?assemblyName?=?$"DbTool.DynamicGenerated.{GuidIdGenerator.Instance.NewId()}";
var?compilationOptions?=?new?CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var?compilation?=?CSharpCompilation.Create(assemblyName,?syntaxTrees,?references,?compilationOptions);await?using?var?ms?=?new?MemoryStream();
var?compilationResult?=?compilation.Emit(ms);
if?(compilationResult.Success)
{var?assemblyBytes?=?ms.ToArray();return?Assembly.Load(assemblyBytes);
}var?error?=?new?StringBuilder();
foreach?(var?t?in?compilationResult.Diagnostics)
{var?msg?=?CSharpDiagnosticFormatter.Instance.Format(t);error.AppendLine($"{msg}");
}
throw?new?ArgumentException($"Compile?error:{error}");
之前的做法是合并成一段文本,并將多段代碼的 using 引用合并,可以參考下面的將多個文件代碼合并成一段文本,后來發現自己傻了,改成了上面的用法,直接生成多個語法樹再生成編譯,推薦使用上面的方式,會更加的友好和
var?usingList?=?new?List<string>();var?sourceCodeTextBuilder?=?new?StringBuilder();
foreach?(var?path?in?sourceFilePaths.Distinct())
{foreach?(var?line?in?File.ReadAllLines(path)){if?(line.StartsWith("using?")?&&?line.EndsWith(";")){usingList.AddIfNotContains(line);}else{sourceCodeTextBuilder.AppendLine(line);}}
}
var?sourceCodeText?=$"{usingList.StringJoin(Environment.NewLine)}{Environment.NewLine}{sourceCodeTextBuilder}";
More
如果需要指定 C# 代碼版本可以通過CSharpParseOptions
來指定,比如要使用 preview
特性可以使用 new CSharpParseOptions(LanguageVersion.Preview)
默認地編譯會編譯成一個 dll 程序集,如果包含 Main 方法要生成一個可執行程序可以通過指定 CSharpCompilationOptions
的 OutputKind
為 OutputKind.ConsoleApplication
, 還有很多可以配置的選項,有需要可以自己探索一下
References
https://github.com/WeihanLi/DbTool/blob/packages/src/DbTool.Core/DefaultModelCodeExactor.cs
https://github.com/WeihanLi/DbTool.Packages/blob/main/src/DbTool.Core/DefaultCSharpModelCodeExtractor.cs
https://mp.weixin.qq.com/s?__biz=Mzg5MDEzNjA3Nw==&mid=2247483821&idx=1&sn=c2f4a672bc9bb1f939cdaaebb26eb8ac&chksm=cfe072cff897fbd9a0de68eec9a45a21d63b943d497723853b8944c5a5c48daede13d16db048&scene=21#wechat_redirect
https://github.com/WeihanLi/dotnet-exec/blob/main/src/dotnet-exec/CodeCompiler.cs