Roslyn workspace 的简单使用

Intro

Roslyn 除了提供了编译器的 API 之外,还提供了一个 workspace 的 API,什么是 workspace 呢?

Workspace 翻译过来就是一个工作区,工作区组织了项目中的项目文件、代码、资源文件等等,VS 打开一个项目之后,就会产生一个工作区,我们也可以用代码使用 Roslyn 的 workspace API 来创建一个 workspace.

Workspace 是对整个解决方案进行代码分析和重构的基础,Workspace API 帮助您将解决方案中有关项目的所有信息组织到单个对象模型中,让您可以直接访问编译器层对象模型,如源文本、语法树、语义模型和编译,而无需 解析文件、配置选项或管理项目间的依赖关系。

Concepts

文档中有一张图片很好地介绍了 Workspace 的结构

Roslyn workspace 的简单使用
the relationships between different elements of a workspace containing projects and source files

最上层的 Host Environment 可以理解为 IDE 打开解决方案的工作区

Workspace:访问和操作项目中文件的基础

Solution/Project: 解决方案和项目,每一次变更会变成一个新的对象

Document: 项目中的文件,如代码源文件

Text:项目文件对应的文本,比如说源码文本

Syntax:源代码对应的语法树

Symbols:语法树中对应的一些元数据

AdHocWorkspace

我们前面介绍的多个文件的编译也可以通过 Workspace 来组织,文件比较多的时候更加方便一些,使用 Workspace 进行编译的示例:

var projectName = $"Dynamic_{Guid.NewGuid():N}";
var assemblyName = $"{projectName}.dll";
// 创建 project
var projectInfo = ProjectInfo.Create(
    ProjectId.CreateNewId(),
    VersionStamp.Default,
    projectName,
    assemblyName,
    LanguageNames.CSharp);

// 默认创建一个 global using 的 document
var globalUsingCode = InternalHelper.GetGlobalUsingsCodeText(execOptions.IncludeWebReferences);
var globalUsingDocument = DocumentInfo.Create(
    DocumentId.CreateNewId(projectInfo.Id, "__GlobalUsings"),
    "__GlobalUsings",
    loader: new PlainTextLoader(globalUsingCode));

// 创建document
var scriptDocument = DocumentInfo.Create(DocumentId.CreateNewId(projectInfo.Id),
                                         Path.GetFileNameWithoutExtension(scriptFile));
scriptDocument = string.IsNullOrEmpty(code)
    ? scriptDocument.WithFilePath(scriptFile)
    : scriptDocument.WithTextLoader(new PlainTextLoader(code));


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();

projectInfo = projectInfo
    .WithParseOptions(new CSharpParseOptions(execOptions.LanguageVersion))
    .WithDocuments(new[] { globalUsingDocument, scriptDocument })
    .WithMetadataReferences(references)
// 创建 Workspace
using var workspace = new AdhocWorkspace();
// 将项目添加到 workspace
var project = workspace.AddProject(projectInfo);
var compilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication);

// 获取编译结果
var compilation = await project
    .WithCompilationOptions(compilationOptions)
    .GetCompilationAsync();

这里的 PlainTextLoader 是自定义的一个直接加载文本的实现,默认是基于文件的加载,roslyn 内部有一个 TextDocumentLoader,但是不对外开放,可以参考:https://github.com/dotnet/roslyn/blob/e62c649a6fa90f0a591285d9103a4fd0a02d370e/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs#L135,所以自己实现了一个,实现也很简单,实现代码如下:

public sealed class PlainTextLoader : TextLoader
{
    private readonly TextAndVersion _textAndVersion;

    public PlainTextLoader(string text)
    {
        _textAndVersion = TextAndVersion.Create(SourceText.From(text), VersionStamp.Default);
    }

    public override Task<TextAndVersion> LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken)
    {
        return Task.FromResult(_textAndVersion);
    }
}

MSBuildWorkspace

前面的 AdHocWorkspace 比较简单,document,project 都还是需要自己去组织才行

微软默认提供了一套基于 MSBuild 的实现,可以自动从项目文件或者项目解决方案文件中自动分析项目以及项目中的文件和引用进而构建一个工作区

使用示例如下:

要使用 MSBuildWorkspace,首先需要在创建工作区之前注册 MSBuild,注册代码如下:

MSBuildLocator.RegisterDefaults();

绝大多数场景下,这样就足够了,如果你有多个 MSBuild ,想要使用特定位置的 MSBuild,也是可以的

可以使用 MSBUildLocator.QueryVisualStudioInstances() 来查询当前支持的 MSBuild 版本,找到对应的版本之后再使用 MSBUildLocator.RegisterInstance 方法来注册

这个方法以及对应的返回值类型的名字都带了 VisualStudio 感觉不太好,给人感觉是 VS 的版本,其实是 MSBuild 的版本,其实没有安装 VS 也是可以获取到 .NET SDK 带的 MSBuild 的

MSBuildWorkspace 使用示例:

using var cts = new Cancell
using var workspace = MSBuildWorkspace.Create();
// 注册加载失败事件
workspace.WorkspaceFailed += (_, args) =>
{
    Console.WriteLine($"Workspace failed, {args.Diagnostic.Kind}, {args.Diagnostic.Message}");
};
// 直接打开项目文件,如果要打开解决方案文件使用 OpenSolutionAsync
var project = await workspace.OpenProjectAsync(projectPath, cancellationToken: cancellationToken);
// 获取编译结果
var compilation = await project.GetCompilationAsync(cancellationToken);

Roslyn workspace 的简单使用

从这里的截图可以看得出来,project 中含有很丰富的信息,包括项目中的基本配置,项目中的源代码,项目引用等等

More

在使用 MSBuild workspace 的时候,加载失败事件尽可能的注册,以免项目因为意外的原因加载失败,MSBuildWorkspace 中有一个 Diagnostics 也可以帮助我们了解项目加载失败时的错误信息

前面主要是使用 Workspace 来进行代码、项目编译,其实有一些大材小用了

Workspace 还可以用来操作项目中的代码、引用等等、除了获取项目的基本信息进行分析之外,我们还可以修改部分文档再次进行编译等等,更多用法可以自己探索一下哈~~