之前做的一個資料庫小工具可以支持根據 Model 代碼文件生成創建表的 sql 語句,原來是基於 CodeDom 實現的,最近改成使用基於 Roslyn 去做了。實現的原理在於編譯選擇的Model 文件生成一個程式集,再從這個程式集中拿到 Model (資料庫表)信息以及屬性信息(資料庫表欄位信息)... ...
基於 Roslyn 實現動態編譯
Intro
之前做的一個資料庫小工具可以支持根據 Model 代碼文件生成創建表的 sql 語句,原來是基於 CodeDom 實現的,最近改成使用基於 Roslyn 去做了。實現的原理在於編譯選擇的Model 文件生成一個程式集,再從這個程式集中拿到 Model (資料庫表)信息以及屬性信息(資料庫表欄位信息),拿到資料庫表以及表欄位信息之後就根據資料庫類型生成大致的創建表的 sql 語句。
CodeFirst 效果如下圖所示:
如果你還不知道這個資料庫小工具,歡迎訪問這個項目瞭解更多https://github.com/WeihanLi/DbTool
遷移原因
最初的 CodeDom 也是可以用的,但是有一些比較新的 C# 語法不支持,比如 C#6 中的指定屬性初始值 public int Number {get;set;} = 1;
,最初我是遷移到了 Microsoft.CodeDom.Providers.DotNetCompilerPlatform
這個是一個 CodeDom 過渡到 Roslyn 的實現,他提供了和 CodeDom 差不多的語法,支持 C#6 的語法。但是還是有個問題,我的項目使用了新的項目文件格式,在 VS 中可以編譯通過,但是 dotnet cli 編譯不通過,詳見 issue https://github.com/aspnet/RoslynCodeDomProvider/issues/51
這個問題已經過去一年了仍未解決,最終決定遷移到 Roslyn,直接使用 Roslyn 實現動態編譯。
對 CodeDom 感興趣的童鞋可以看 DbTool 之前的 commit 記錄,在此不多敘述。
使用 Roslyn 實現動態編譯
Roslyn 好像沒有直接根據幾個文件去編譯(可能有隻是我沒發現),我就使用了一個比較笨的辦法,把幾個文件的內容都讀出來,合併在一起(命名空間需要去重),然後去編譯,完整源代碼地址
,實現代碼如下:
/// <summary>
/// 從 源代碼 中獲取表信息
/// </summary>
/// <param name="sourceFilePaths">sourceCodeFiles</param>
/// <returns></returns>
public static List<TableEntity> GeTableEntityFromSourceCode(params string[] sourceFilePaths)
{
if (sourceFilePaths == null || sourceFilePaths.Length <= 0)
{
return null;
}
var usingList = new List<string>();
var sourceCodeTextBuilder = new StringBuilder();
foreach (var path in sourceFilePaths)
{
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}"; // 獲取完整的代碼
var systemReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
var annotationReference = MetadataReference.CreateFromFile(typeof(TableAttribute).Assembly.Location);
var weihanliCommonReference = MetadataReference.CreateFromFile(typeof(IDependencyResolver).Assembly.Location);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCodeText, new CSharpParseOptions(LanguageVersion.Latest)); // 獲取代碼分析得到的語法樹
var assemblyName = $"DbTool.DynamicGenerated.{ObjectIdGenerator.Instance.NewId()}";
// 創建編譯任務
var compilation = CSharpCompilation.Create(assemblyName) //指定程式集名稱
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))//輸出為 dll 程式集
.AddReferences(systemReference, annotationReference, weihanliCommonReference) //添加程式集引用
.AddSyntaxTrees(syntaxTree) // 添加上面代碼分析得到的語法樹
;
var assemblyPath = ApplicationHelper.MapPath($"{assemblyName}.dll");
var compilationResult = compilation.Emit(assemblyPath); // 執行編譯任務,並輸出編譯後的程式集
if (compilationResult.Success)
{
// 編譯成功,獲取編譯後的程式集並從中獲取資料庫表信息以及欄位信息
try
{
byte[] assemblyBytes;
using (var fs = File.OpenRead(assemblyPath))
{
assemblyBytes = fs.ToByteArray();
}
return GeTableEntityFromAssembly(Assembly.Load(assemblyBytes));
}
finally
{
File.Delete(assemblyPath); // 清理資源
}
}
var error = new StringBuilder(compilationResult.Diagnostics.Length * 1024);
foreach (var t in compilationResult.Diagnostics)
{
error.AppendLine($"{t.GetMessage()}");
}
// 獲取編譯錯誤
throw new ArgumentException($"所選文件編譯有錯誤{Environment.NewLine}{error}");
}
Reference
- https://github.com/WeihanLi/DbTool/blob/wfdev/src/DbTool/Utils.cs#L27
- https://github.com/WeihanLi/DbTool
- https://msdn.microsoft.com/en-us/magazine/mt808499.aspx