基于 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, 还有很多可以配置的选项,有需要可以自己探索一下。