【问题标题】:Read *.csproj property values using Roslyn APIs?使用 Roslyn API 读取 *.csproj 属性值?
【发布时间】:2020-07-31 22:33:47
【问题描述】:

我目前正在构建一个支持 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>

我的问题是:如何使用 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?
    }

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

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

【问题讨论】:

    标签: c# roslyn


    【解决方案1】:

    这比您想象的要困难 :) Roslyn api 只知道编译器知道什么,编译器不会得到任何关于 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>
    
    

    还有一个如下所示的 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}");
                }
            }
        }
    }
    
    

    【讨论】:

    • 我应该注意,如果您尝试从 Visual Studio 扩展(或 Roslyn 代码修复)中执行此类操作,我会采取不同的方法,VS 已经拥有所有这些信息,但您需要以不同的方式访问它
    • @Janoth Marolf 您知道如何从 Roslyn 源生成器中实现相同的功能(从 csproj 读取属性)吗?提前致谢!
    • 我会看看食谱的this 部分。如果您需要更多信息,我会发布一个单独的问题
    猜你喜欢
    • 1970-01-01
    • 2012-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多