Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@DustinCampbell
Copy link
Member

This change refactors MSBuildProjectLoader to allow dotnet-watch to load projects without running a design-time build out-of-proc. Because dotnet-watch runs as a built-in dotent tool, the correct MSBuild is already loaded in process. So, launching build host processes is unnecessary overhead.

The main refactoring is to extract an abstraction for loading MSBuild projects as ProjectFileInfo instances that MSBuildProjectLoader turns into ProjectInfos. This consists of an abstract ProjectFileInfoLoader class and an IProjectFileInfoLoaderFactory interface that can optionally be passed into an MSBuildProjectLoader constructor. By default, the BuildHostProjectInfoLoaderFactory is used if another IProjectFileInfoLoaderFactory isn't provided. So, the default behavior of MSBuildProjectLoader should remain intact.

In addition, types have been added to expose an external access API to dotnet-watch. These external access APIs allow dotnet-watch to plug-in a custom IProjectFileInfoLoaderFactory.

I've kept the commit history consistent so that it's easy to review commit-by-commit.

cc @jasonmalinowski and @tmat

- Convert PathResolver to a readonly struct that optionally holds a DiagnosticReporter and a string representing a base directory.
- Remove _pathResolver fields from MSBuildProjectLoader and MSBuildProjectLoader.Worker
- Add GetPathResolver(...) helper in MSBuildProjectLoader.Worker.
- Refactor how PathResolver is used in SolutionFileReader.
- Update various 'path' parameter names for consistency.
Pass projectFilePaths, baseDirectory and reportingOptions to Worker.LoadAsync rather than the constructor.
Add ProjectLoadOperationRunner struct that wraps an `IProgress<ProjectLoadProgress>?` and provides a DoOperationAndReportProgressAsync method.
This change extracts the logic from MSBuildProjectLoader.Worker that depend on BuildHostProcessManager into a separate BuildHostProjectFileInfoLoader class.
Introduce an IProjectFileInfoLoaderFactory abstraction that can be optionally passed to an MSBuildProjectLoader to allow customization of ProjectFileInfo loading. If an IProjectFileInfoLoaderFactory isn't provided, the default BuildHostProjectFileInfoLoaderFactory instance is used.

This abstraction is intended to allow dotnet-watch to provide a custom IProjectFileLoaderFactory that can produce ProjectFileInfos without using an out-of-proc build host.
Add interfaces and wrappers for dotnet-watch.
@jasonmalinowski
Copy link
Member

@DustinCampbell @tmat A very quick scroll through the code makes me realize I'm missing the big picture here: once you've run much of this code are you directly consuming the resulting workspace? Or are you still doing other steps to apply them to your workspace?

The reason I'm asking is I would have expected you were either:

  1. Consuming the workspace directly, which means you need the abstraction being introduced in the loader, but you don't need the WatchProject*Info types (and what gets pulled along with them.)
  2. Consuming the data types, which you'd have a wrapper around the loader stuff but wouldn't be introducing the abstractions. i.e. you're more or less just calling code straight in the build host.

It looks like it's a bit of a mix here which means I'm not seeing the full picture.

@jasonmalinowski jasonmalinowski self-requested a review September 18, 2025 20:17
@tmat
Copy link
Member

tmat commented Sep 18, 2025

@jasonmalinowski dotnet-watch has its own Workspace impl, we only use MSBuildWorkspace to load project info.

{
Task<LoadProjectFileInfosResult> LoadProjectFileInfosAsync(
string projectFilePath,
string languageName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't language name implied by project file path?

Copy link
Member Author

@DustinCampbell DustinCampbell Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but there's a ProjectFileExtensionRegistry that gets used in MSBuild workspace. I'd prefer to leave that as an implementation detail and not expose it via the external access. So, by the time this method is called, the language name has already been retrieved from the project path.

}

internal MSBuildProjectLoader(
Workspace workspace,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the only two things that are needed from workspace are SolutionServices and OnWorkspaceFailed. Can we remove the dependency on Workspace from DiagnosticReporter and here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking it a step further, maybe we can also remove dependency on SolutionServices by defining an interface with the few methods that are used, such as GetDefaultParseOptions(language), GetDefaultCompilationOptions(language) and ParseCommandLine(...).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are good suggestions, but I think that's probably outside of the scope of this refactoring. The goal of this change is to introduce a way for dotnet-watch to provide an alternative means of creating ProjectFileInfo instances.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into this at some point doing the C# extension work and yes, there's definitely cleanup opportunities. But I agree with out of scope here...

@jasonmalinowski
Copy link
Member

@tmat: how do you get the info applied to the actual workspace? Reusing some of the existing code somewhere or just doing your own thing?

@tmat
Copy link
Member

tmat commented Sep 19, 2025

@DustinCampbell
Copy link
Member Author

@tmat: how do you get the info applied to the actual workspace? Reusing some of the existing code somewhere or just doing your own thing?

@jasonmalinowski: dotnet-watch doesn't use MSBuildWorkspace -- it uses MSBuildProjectLoader to load a custom workspace. The code is in the .NET SDK here.


internal static class WatchMSBuildProjectLoader
{
public static MSBuildProjectLoader Create(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this overload.

@tmat
Copy link
Member

tmat commented Sep 19, 2025

@DustinCampbell

Not sure I understand how would dotnet-watch consume the API. Do you have a draft PR in dotnet-watch that shows that?

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes generally all make sense to me up until the last commit where we're adding the external access API and wrappers, because it seems like we're spending a huge amount of effort passing around data in ways I wouldn't expect. Specifically dotnet watch is expected to fill out a bunch of WatchProjectFileInfos, which we then convert to ProjectFileInfos, and then we convert to ProjectInfos, only to then convert them to another ProjectInfo here, which we then hand back to a Roslyn API to do an internal call. That's...a lot of wrapping and unwrapping the same data.

But how is dotnet watch even expected to fill that out without either consuming the stuff that lives in the BuildHost today, or reimplementing that? If I recall from an earlier conversation the concern was dotnet watch already had MSBuild project instances in-proc from the builds it had already done, and the desire was to reuse those. But we could then make a much more direct replacement of that WatchHotReloadService.WithProjectInfo() that takes the MSBuild types and does everything you need internally, which would simplify the UpdateProjectConeAsync() code as it is.

I love the first commits no matter what doing some of the miscellaneous cleanups, and thank you for breaking apart the commits since that made this much easier to review that bit.

public async Task<ImmutableArray<ProjectInfo>> LoadAsync(CancellationToken cancellationToken)
{
var results = ImmutableArray.CreateBuilder<ProjectInfo>();
var processedPaths = new HashSet<string>(PathUtilities.Comparer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you wanted to rename this processedFilePaths since you're already doing the other updates.

/// Loads the <see cref="ProjectInfo"/> from the given list of project files and all referenced projects.
/// </summary>
/// <param name="projectFilePaths">An ordered list of paths to project files that should be loaded.</param>
/// /// <param name="baseDirectory">An optional base directory to use when resolving project file paths
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra ///.


namespace Microsoft.CodeAnalysis.MSBuild;

internal abstract class ProjectFileInfoLoader(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider calling this AbstractProjectFileInfoLoader, simply because right now there are so many classes with "file" "loader" and similar that it's getting very hard to keep these apart.

}

internal MSBuildProjectLoader(
Workspace workspace,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into this at some point doing the C# extension work and yes, there's definitely cleanup opportunities. But I agree with out of scope here...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants