0

I typically use the Project(string projectFile) constructor. However, in an attempt to better manage my IO workload vs CPU workload (as needed for improved performance processing thousands of C# files across hundreds of C# projects) I wanted to call the Project(XmlReader xmlReader) constructor. More details on what I am trying to achieve can be found here - How to optimize performance in a simple TPL DataFlow pipeline?

However, the Project(XmlReader xmlReader) constructor does not work and I think I know why. The Project(string projectFile) constructor initializes the Project's FullPath and DirectoryPath properties right away from the given projectFile argument. The xmlReader constructor does not do it and this yields wrong Project object.

I have also found this comment in the source code:

public class Project : ILinkableObject
{
...
    /// <summary>
    /// The root directory for this project.
    /// Is never null: in-memory projects use the current directory from the time of load.
    /// </summary>
    public string DirectoryPath => Xml.DirectoryPath;
...
}

So one would think that setting the current directory to the project folder and then calling the Project(XmlReader xmlReader) constructor should work. No, it does not, the problem is that MSBuildProjectFullPath is null at a point where it is expected not to be. And setting it explicitly in the global properties is not allowed.

An important note - my code is .NET 5, but the projects I am trying to evaluate are .NET Framework. The Microsoft.Build NuGet package does not work here, at least I failed to make it work. So instead I am using the Microsoft.Build dll distributed with the VS like this:

  <PropertyGroup>
    <VSINSTALLDIR Condition="'$(VSINSTALLDIR)' == '' AND '$(VSAPPIDDIR)' != ''">$(VSAPPIDDIR)\..\..\</VSINSTALLDIR>
  </PropertyGroup>

  <ItemGroup>
    <Reference Include="Microsoft.Build">
      <HintPath>$(VSINSTALLDIR)MSBuild\Current\Bin\Microsoft.Build.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Build.Framework">
      <HintPath>$(VSINSTALLDIR)MSBuild\Current\Bin\Microsoft.Build.Framework.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Build.Utilities.Core">
      <HintPath>$(VSINSTALLDIR)MSBuild\Current\Bin\Microsoft.Build.Utilities.Core.dll</HintPath>
    </Reference>
  </ItemGroup>

So, how can we evaluate a Project object created from memory?

Use Cases

In all the use cases I am going to parse legacy non SDK projects from .NET 5 code.

The repo https://github.com/MarkKharitonov/InMemoryMSBuild has 3 versions:

  1. msbuild_from_nuget branch - using Microsoft.Build from NuGet
  2. msbuild_from_vs branch: - using Microsoft.Build from VS
    1. from_disk tag - reading from disk
    2. from_memory tag - reading from memory

Use case 1: using Microsoft.Build from nuget

Source code - https://github.com/MarkKharitonov/InMemoryMSBuild/tree/msbuild_from_nuget

InMemoryMSBuild.csproj

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build" Version="16.11.0" />
  </ItemGroup>

</Project>

Program.cs

using Microsoft.Build.Evaluation;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;

namespace InMemoryMSBuild
{
    class Program
    {
        static void Main(string[] args)
        {
            var sut = Path.GetFullPath(Path.Combine(Assembly.GetExecutingAssembly().Location, @"..\..\..\..\..\LegacyDotNetSUT\LegacyDotNetSUT.csproj"));
            Debug.Assert(File.Exists(sut));
            var project = new Project(sut);
            foreach (var filePath in project.GetItems("Compile").Select(o => o.GetMetadataValue("FullPath")))
            {
                Console.WriteLine(filePath);
            }
        }
    }
}

Output

C:\work\InMemoryMSBuild [msbuild_from_nuget ≡]> dotnet build /restore
Microsoft (R) Build Engine version 16.11.0+0538acc04 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored C:\work\InMemoryMSBuild\InMemoryMSBuild\InMemoryMSBuild.csproj (in 328 ms).
  LegacyDotNetSUT -> C:\work\InMemoryMSBuild\LegacyDotNetSUT\Bin\Debug\LegacyDotNetSUT.dll
  InMemoryMSBuild -> C:\work\InMemoryMSBuild\InMemoryMSBuild\bin\Debug\net5.0\InMemoryMSBuild.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.91
C:\work\InMemoryMSBuild [msbuild_from_nuget ≡]> .\InMemoryMSBuild\bin\Debug\net5.0\InMemoryMSBuild.exe
Unhandled exception. Microsoft.Build.Exceptions.InvalidProjectFileException: The imported project "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\amd64\Current\Microsoft.Common.props" was not found. Confirm that the expression in the Import declaration "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\amd64\Current\Microsoft.Common.props" is correct, and that the file exists on disk.  C:\work\InMemoryMSBuild\LegacyDotNetSUT\LegacyDotNetSUT.csproj
   at Microsoft.Build.Shared.ProjectErrorUtilities.ThrowInvalidProject(String errorSubCategoryResourceName, IElementLocation elementLocation, String resourceName, Object[] args)
   at Microsoft.Build.Shared.ProjectErrorUtilities.VerifyThrowInvalidProject[T1,T2](Boolean condition, String errorSubCategoryResourceName, IElementLocation elementLocation, String resourceName, T1 arg0, T2 arg1)
   at Microsoft.Build.Shared.ProjectErrorUtilities.ThrowInvalidProject[T1,T2](IElementLocation elementLocation, String resourceName, T1 arg0, T2 arg1)
   at Microsoft.Build.Evaluation.Evaluator`4.ExpandAndLoadImportsFromUnescapedImportExpression(String directoryOfImportingFile, ProjectImportElement importElement, String unescapedExpression, Boolean throwOnFileNotExistsError, List`1& imports)
   at Microsoft.Build.Evaluation.Evaluator`4.ExpandAndLoadImportsFromUnescapedImportExpressionConditioned(String directoryOfImportingFile, ProjectImportElement importElement, List`1& projects, SdkResult& sdkResult, Boolean throwOnFileNotExistsError)
   at Microsoft.Build.Evaluation.Evaluator`4.ExpandAndLoadImports(String directoryOfImportingFile, ProjectImportElement importElement, SdkResult& sdkResult)
   at Microsoft.Build.Evaluation.Evaluator`4.EvaluateImportElement(String directoryOfImportingFile, ProjectImportElement importElement)
   at Microsoft.Build.Evaluation.Evaluator`4.PerformDepthFirstPass(ProjectRootElement currentProjectOrImport)
   at Microsoft.Build.Evaluation.Evaluator`4.Evaluate()
   at Microsoft.Build.Evaluation.Evaluator`4.Evaluate(IEvaluatorData`4 data, ProjectRootElement root, ProjectLoadSettings loadSettings, Int32 maxNodeCount, PropertyDictionary`1 environmentProperties, ILoggingService loggingService, IItemFactory`2 itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCacheBase projectRootElementCache, BuildEventContext buildEventContext, ISdkResolverService sdkResolverService, Int32 submissionId, EvaluationContext evaluationContext, Boolean interactive)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.Reevaluate(ILoggingService loggingServiceForEvaluation, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.ReevaluateIfNecessary(ILoggingService loggingServiceForEvaluation, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.ReevaluateIfNecessary(ILoggingService loggingServiceForEvaluation, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.ReevaluateIfNecessary(EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.Initialize(IDictionary`2 globalProperties, String toolsVersion, String subToolsetVersion, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion, String subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion, String subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings)
   at Microsoft.Build.Evaluation.Project..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings)
   at Microsoft.Build.Evaluation.Project..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion, ProjectCollection projectCollection)
   at Microsoft.Build.Evaluation.Project..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion)
   at Microsoft.Build.Evaluation.Project..ctor(String projectFile)
   at InMemoryMSBuild.Program.Main(String[] args) in C:\work\InMemoryMSBuild\InMemoryMSBuild\Program.cs:line 16
C:\work\InMemoryMSBuild [msbuild_from_nuget ≡]>

Since parsing from disk does not work, no sense to proceed to in memory.

Use case 2: using Microsoft.Build from VS

Source code - https://github.com/MarkKharitonov/InMemoryMSBuild/tree/from_disk

InMemoryMSBuild.csproj

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <PropertyGroup>
    <VSINSTALLDIR Condition="'$(VSINSTALLDIR)' == '' AND '$(VSAPPIDDIR)' != ''">$(VSAPPIDDIR)\..\..\</VSINSTALLDIR>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
  </ItemGroup>

  <ItemGroup>
    <Reference Include="Microsoft.Build">
      <HintPath>$(VSINSTALLDIR)MSBuild\Current\Bin\Microsoft.Build.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Build.Framework">
      <HintPath>$(VSINSTALLDIR)MSBuild\Current\Bin\Microsoft.Build.Framework.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Build.Utilities.Core">
      <HintPath>$(VSINSTALLDIR)MSBuild\Current\Bin\Microsoft.Build.Utilities.Core.dll</HintPath>
    </Reference>
  </ItemGroup>
</Project>

Program.cs

Same implementation

Output

C:\work\InMemoryMSBuild [(from_disk)]> dotnet build /restore
Microsoft (R) Build Engine version 16.11.0+0538acc04 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored C:\work\InMemoryMSBuild\InMemoryMSBuild\InMemoryMSBuild.csproj (in 177 ms).
  LegacyDotNetSUT -> C:\work\InMemoryMSBuild\LegacyDotNetSUT\Bin\Debug\LegacyDotNetSUT.dll
  InMemoryMSBuild -> C:\work\InMemoryMSBuild\InMemoryMSBuild\bin\Debug\net5.0\InMemoryMSBuild.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.31
C:\work\InMemoryMSBuild [(from_disk)]> .\InMemoryMSBuild\bin\Debug\net5.0\InMemoryMSBuild.exe
C:\work\InMemoryMSBuild\LegacyDotNetSUT\Class1.cs
C:\work\InMemoryMSBuild [(from_disk)]>

Works fine

Use case 3: using Microsoft.Build from VS to parse byte array

Source code - https://github.com/MarkKharitonov/InMemoryMSBuild/tree/from_memory

InMemoryMSBuild.csproj

Same implementation

Program.cs

using Microsoft.Build.Evaluation;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;

namespace InMemoryMSBuild
{
    class Program
    {
        static void Main(string[] args)
        {
            var sut = Path.GetFullPath(Path.Combine(Assembly.GetExecutingAssembly().Location, @"..\..\..\..\..\LegacyDotNetSUT\LegacyDotNetSUT.csproj"));
            Debug.Assert(File.Exists(sut));

            Console.WriteLine("Reading from disk");
            var project = new Project(sut);
            foreach (var filePath in project.GetItems("Compile").Select(o => o.GetMetadataValue("FullPath")))
            {
                Console.WriteLine($"File.Exists(\"{filePath}\") = {File.Exists(filePath)}");
            }
            ProjectCollection.GlobalProjectCollection.UnloadAllProjects();

            Console.WriteLine("Reading from memory");
            project = new Project(XmlReader.Create(new MemoryStream(File.ReadAllBytes(sut))));
            foreach (var filePath in project.GetItems("Compile").Select(o => o.GetMetadataValue("FullPath")))
            {
                Console.WriteLine($"File.Exists(\"{filePath}\") = {File.Exists(filePath)}");
            }
        }
    }
}

Output

C:\work\InMemoryMSBuild [(from_memory)]> dotnet build /restore
Microsoft (R) Build Engine version 16.11.0+0538acc04 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  LegacyDotNetSUT -> C:\work\InMemoryMSBuild\LegacyDotNetSUT\Bin\Debug\LegacyDotNetSUT.dll
  InMemoryMSBuild -> C:\work\InMemoryMSBuild\InMemoryMSBuild\bin\Debug\net5.0\InMemoryMSBuild.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.92
C:\work\InMemoryMSBuild [(from_memory)]> .\InMemoryMSBuild\bin\Debug\net5.0\InMemoryMSBuild.exe
Reading from disk
File.Exists("C:\work\InMemoryMSBuild\LegacyDotNetSUT\Class1.cs") = True
Reading from memory
File.Exists("C:\work\InMemoryMSBuild\Class1.cs") = False
C:\work\InMemoryMSBuild [(from_memory)]>

In memory project is wrong, the file paths it produces are off.

mark
  • 53,726
  • 70
  • 260
  • 529

0 Answers0