使用 Roslyn API 读取 *.csproj 属性值?

vin*_*ras 4 c# roslyn

我目前正在构建一个支持 ASP.NET Core 项目开发的工具。此工具使用 Roslyn API 和其他方法来验证某些开发要求(例如应用到 API 控制器上的项目特定属性、强制执行命名约定以及为访问使用 ASP.NET 编写的 API 的 JavaScript SPA 生成一些源代码核心 Web API 模板)。

为此,我目前正在使用硬编码路径为 SPA 应用程序生成代码。但是在应用程序的*.csproj文件中,实际上有一个“SpaRoot”属性指定了 SPA 应用程序在项目中的位置:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
    <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
    <IsPackable>false</IsPackable>
    <SpaRoot>ClientApp\</SpaRoot>
    ...
  </PropertyGroup>
  ...
</Project>
Run Code Online (Sandbox Code Playgroud)

我的问题是:如何使用 Roslyn API 读取“SpaRoot”属性的值?

我编写了一个最小代码示例来创建Workspace、打开Solution和检索Project的引用,它类似于以下内容:

static async Task Main(string[] args)
{
    string solutionFile = @"C:\Test\my-solution.sln";
    using (var workspace = MSBuildWorkspace.Create())
    {
        var solution = await workspace.OpenSolutionAsync(solutionFile);

        string projectName = "some-project";
        var project = solution.Projects.Single(p => p.Name == projectName);

        // How to extract the value of "SpaRoot" from the Project here?
    }
Run Code Online (Sandbox Code Playgroud)

我尝试搜索如何从Project引用中提取“SpaRoot”属性,甚至进行调试以查看我是否可以自己找到方法。不幸的是,我没有想出答案,而且我仍然在原始代码中使用硬编码路径。

甚至可以使用当前的 Roslyn API检索a的.csproj属性的值Project吗?

Jon*_*olf 7

这比你想象的要困难 :) Roslyn apis 只知道编译器知道什么,编译器不会得到任何关于SpaRoot属性的信息。不过,我们可以使用 MSBuild api 来解决这个问题。特别是Microsoft.Build.Evaluation.Project类。

我所做的一些假设

  1. 您只想检查 .NET Core 项目
  2. 您将在运行此工具的系统上安装 .NET Core SDK

所以首先我们需要一个看起来像这样的项目文件:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <!--NOTE: If the project you are analyzing is .NET Core then the commandline tool must be as well.
              .NET Framework console apps cannot load .NET Core MSBuild assemblies which is required 
              for what we want to do.-->
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>Latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <!-- NOTE: We put ExcludeAssets="runtime" on all direct MSBuild references so that we pick up whatever
               version is being used by the .NET SDK instead. This is accomplished with the Microsoft.Build.Locator
               referenced further below. -->
    <PackageReference Include="Microsoft.Build" Version="16.4.0" ExcludeAssets="runtime" />
    <PackageReference Include="Microsoft.Build.Locator" Version="1.2.6" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="2.9.8" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
    <PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="3.4.0" />
    <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="3.4.0" />
    <!-- NOTE: A lot of MSBuild tasks that we are going to load in order to analyze a project file will implicitly
               load build tasks that will require Newtonsoft.Json version 9. Since there is no way for us to ambiently 
               pick these dependencies up like with MSBuild assemblies we explicitly reference it here. -->
    <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
  </ItemGroup>

</Project>

Run Code Online (Sandbox Code Playgroud)

和一个如下所示的 Program.cs 文件:

using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;

// I use this so I don't get confused with the Roslyn Project type
using MSBuildProject = Microsoft.Build.Evaluation.Project;

namespace loadProject {
    class Program {
        static async Task Main(string[] args) {
            MSBuildWorkspaceSetup();
            // NOTE: we need to make sure we call MSBuildLocator.RegisterInstance
            // before we ask the CLR to load any MSBuild types. Therefore we moved
            // the code that uses MSBuild types to its own method (instead of being in
            // Main) so the CLR is not forced to load them on startup.
            await DoAnalysisAsync(args[0]);
        }

        private static async Task DoAnalysisAsync(string solutionPath) {
            using var workspace = MSBuildWorkspace.Create();

            // Print message for WorkspaceFailed event to help diagnosing project load failures.
            workspace.WorkspaceFailed += (o, e) => Console.WriteLine(e.Diagnostic.Message);

            Console.WriteLine($"Loading solution '{solutionPath}'");

            // Attach progress reporter so we print projects as they are loaded.
            var solution = await workspace.OpenSolutionAsync(solutionPath, new ConsoleProgressReporter());
            Console.WriteLine($"Finished loading solution '{solutionPath}'");

            // We just select the first project as a demo
            // you will want to use your own logic here
            var project = solution.Projects.First();

            // Now we use the MSBuild apis to load and evaluate our project file
            using var xmlReader = XmlReader.Create(File.OpenRead(project.FilePath));
            ProjectRootElement root = ProjectRootElement.Create(xmlReader, new ProjectCollection(), preserveFormatting: true);
            MSBuildProject msbuildProject = new MSBuildProject(root);

            // We can now ask any question about the properties or items in our project file
            // and get the correct answer
            string spaRootValue = msbuildProject.GetPropertyValue("SpaRoot");
        }

        private static void MSBuildWorkspaceSetup() {
            // Attempt to set the version of MSBuild.
            var visualStudioInstances = MSBuildLocator.QueryVisualStudioInstances().ToArray();
            var instance = visualStudioInstances.Length == 1
                // If there is only one instance of MSBuild on this machine, set that as the one to use.
                ? visualStudioInstances[0]
                // Handle selecting the version of MSBuild you want to use.
                : SelectVisualStudioInstance(visualStudioInstances);

            Console.WriteLine($"Using MSBuild at '{instance.MSBuildPath}' to load projects.");

            // NOTE: Be sure to register an instance with the MSBuildLocator 
            //       before calling MSBuildWorkspace.Create()
            //       otherwise, MSBuildWorkspace won't MEF compose.
            MSBuildLocator.RegisterInstance(instance);
        }

        private static VisualStudioInstance SelectVisualStudioInstance(VisualStudioInstance[] visualStudioInstances) {
            Console.WriteLine("Multiple installs of MSBuild detected please select one:");
            for (int i = 0; i < visualStudioInstances.Length; i++) {
                Console.WriteLine($"Instance {i + 1}");
                Console.WriteLine($"    Name: {visualStudioInstances[i].Name}");
                Console.WriteLine($"    Version: {visualStudioInstances[i].Version}");
                Console.WriteLine($"    MSBuild Path: {visualStudioInstances[i].MSBuildPath}");
            }

            while (true) {
                var userResponse = Console.ReadLine();
                if (int.TryParse(userResponse, out int instanceNumber) &&
                    instanceNumber > 0 &&
                    instanceNumber <= visualStudioInstances.Length) {
                    return visualStudioInstances[instanceNumber - 1];
                }
                Console.WriteLine("Input not accepted, try again.");
            }
        }

        private class ConsoleProgressReporter : IProgress<ProjectLoadProgress> {
            public void Report(ProjectLoadProgress loadProgress) {
                var projectDisplay = Path.GetFileName(loadProgress.FilePath);
                if (loadProgress.TargetFramework != null) {
                    projectDisplay += $" ({loadProgress.TargetFramework})";
                }

                Console.WriteLine($"{loadProgress.Operation,-15} {loadProgress.ElapsedTime,-15:m\\:ss\\.fffffff} {projectDisplay}");
            }
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

  • 我应该注意,如果您尝试在 Visual Studio 扩展(或 Roslyn 代码修复)中执行类似的操作,我会采取不同的方法,VS 已经拥有所有这些信息,但您需要以不同的方式访问它 (2认同)