diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1a83625bdc29..5ff2c6fbfb20 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -37,6 +37,7 @@
+
diff --git a/NuGet.config b/NuGet.config
index 85e13e71ea71..9470d3bc87c3 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -37,6 +37,7 @@
+
diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index e1ef1efed51d..244c812c5688 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -21,17 +21,18 @@ This file should be imported by eng/Versions.props
18.1.0-preview-25568-105
18.1.0-preview-25568-105
7.1.0-preview.1.6905
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
10.0.0-preview.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
10.0.0-beta.25568.105
10.0.0-beta.25568.105
10.0.0-beta.25568.105
@@ -41,8 +42,8 @@ This file should be imported by eng/Versions.props
10.0.0-beta.25568.105
10.0.0-beta.25568.105
15.0.200-servicing.25568.105
- 5.3.0-1.25568.105
- 5.3.0-1.25568.105
+ 5.3.0-dev
+ 5.3.0-dev
10.0.0-preview.7.25377.103
10.0.0-preview.25568.105
18.1.0-preview-25568-105
@@ -159,18 +160,18 @@ This file should be imported by eng/Versions.props
18.1.0-preview-25523-02
18.1.0-preview-25523-02
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
- 5.3.0-2.25560.14
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
+ 5.3.0-dev
7.1.0-preview.1.42
7.1.0-preview.1.42
diff --git a/src/BuiltInTools/Watch/Build/EvaluationResult.cs b/src/BuiltInTools/Watch/Build/EvaluationResult.cs
index bb04a3683a7e..d55b4644bfdd 100644
--- a/src/BuiltInTools/Watch/Build/EvaluationResult.cs
+++ b/src/BuiltInTools/Watch/Build/EvaluationResult.cs
@@ -2,12 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
+using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watch;
-internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph projectGraph)
+internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph projectGraph, ImmutableArray restoredProjectInstances)
{
public readonly IReadOnlyDictionary Files = files;
public readonly ProjectGraph ProjectGraph = projectGraph;
@@ -26,6 +27,9 @@ private static IReadOnlySet CreateBuildFileSet(ProjectGraph projectGraph
public IReadOnlySet BuildFiles
=> _lazyBuildFiles.Value;
+ public ImmutableArray RestoredProjectInstances
+ => restoredProjectInstances;
+
public void WatchFiles(FileWatcher fileWatcher)
{
fileWatcher.WatchContainingDirectories(Files.Keys, includeSubdirectories: true);
@@ -86,14 +90,18 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera
}
}
+ // Capture the snapshot of original project instances after Restore target has been run.
+ // These instances can be used to evaluate additional targets (e.g. deployment) if needed.
+ var restoredProjectInstances = projectGraph.ProjectNodesTopologicallySorted.Select(node => node.ProjectInstance.DeepCopy()).ToImmutableArray();
+
var fileItems = new Dictionary();
+ // Update the project instances of the graph with design-time build results.
+ // The properties and items set by DTB will be used by the Workspace to create Roslyn representation of projects.
+
foreach (var project in projectGraph.ProjectNodesTopologicallySorted)
{
- // Deep copy so that we can reuse the graph for building additional targets later on.
- // If we didn't copy the instance the targets might duplicate items that were already
- // populated by design-time build.
- var projectInstance = project.ProjectInstance.DeepCopy();
+ var projectInstance = project.ProjectInstance;
// skip outer build project nodes:
if (projectInstance.GetPropertyValue(PropertyNames.TargetFramework) == "")
@@ -116,7 +124,6 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera
var projectPath = projectInstance.FullPath;
var projectDirectory = Path.GetDirectoryName(projectPath)!;
- // TODO: Compile and AdditionalItems should be provided by Roslyn
var items = projectInstance.GetItems(ItemNames.Compile)
.Concat(projectInstance.GetItems(ItemNames.AdditionalFiles))
.Concat(projectInstance.GetItems(ItemNames.Watch));
@@ -166,6 +173,6 @@ void AddFile(string include, string? staticWebAssetPath)
buildReporter.ReportWatchedFiles(fileItems);
- return new EvaluationResult(fileItems, projectGraph);
+ return new EvaluationResult(fileItems, projectGraph, restoredProjectInstances);
}
}
diff --git a/src/BuiltInTools/Watch/Build/FilePathExclusions.cs b/src/BuiltInTools/Watch/Build/FilePathExclusions.cs
index 305ef3c0ab9e..f7b54066dadb 100644
--- a/src/BuiltInTools/Watch/Build/FilePathExclusions.cs
+++ b/src/BuiltInTools/Watch/Build/FilePathExclusions.cs
@@ -39,7 +39,7 @@ public static FilePathExclusions Create(ProjectGraph projectGraph)
{
// If default items are not enabled exclude just the output directories.
- TryAddOutputDir(projectNode.GetOutputDirectory());
+ TryAddOutputDir(projectNode.ProjectInstance.GetOutputDirectory());
TryAddOutputDir(projectNode.GetIntermediateOutputDirectory());
void TryAddOutputDir(string? dir)
diff --git a/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs b/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs
index 40314297b76a..8469a0968f4e 100644
--- a/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs
+++ b/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs
@@ -48,8 +48,8 @@ public static bool IsNetCoreApp(this ProjectGraphNode projectNode, Version minVe
public static bool IsWebApp(this ProjectGraphNode projectNode)
=> projectNode.GetCapabilities().Any(static value => value is ProjectCapability.AspNetCore or ProjectCapability.WebAssembly);
- public static string? GetOutputDirectory(this ProjectGraphNode projectNode)
- => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetPath) is { Length: >0 } path ? Path.GetDirectoryName(Path.Combine(projectNode.ProjectInstance.Directory, path)) : null;
+ public static string? GetOutputDirectory(this ProjectInstance project)
+ => project.GetPropertyValue(PropertyNames.TargetPath) is { Length: >0 } path ? Path.GetDirectoryName(Path.Combine(project.Directory, path)) : null;
public static string GetAssemblyName(this ProjectGraphNode projectNode)
=> projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetName);
diff --git a/src/BuiltInTools/Watch/FileWatcher/ChangeKind.cs b/src/BuiltInTools/Watch/FileWatcher/ChangeKind.cs
index 71e339419a1c..631b13ae5d87 100644
--- a/src/BuiltInTools/Watch/FileWatcher/ChangeKind.cs
+++ b/src/BuiltInTools/Watch/FileWatcher/ChangeKind.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api;
+
namespace Microsoft.DotNet.Watch;
internal enum ChangeKind
@@ -10,6 +12,18 @@ internal enum ChangeKind
Delete
}
+internal static class ChangeKindExtensions
+{
+ public static HotReloadFileChangeKind Convert(this ChangeKind changeKind) =>
+ changeKind switch
+ {
+ ChangeKind.Update => HotReloadFileChangeKind.Update,
+ ChangeKind.Add => HotReloadFileChangeKind.Add,
+ ChangeKind.Delete => HotReloadFileChangeKind.Delete,
+ _ => throw new InvalidOperationException()
+ };
+}
+
internal readonly record struct ChangedFile(FileItem Item, ChangeKind Kind);
internal readonly record struct ChangedPath(string Path, ChangeKind Kind);
diff --git a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs
index 2929d4f1e71e..a1c1094310bd 100644
--- a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs
+++ b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
+using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -14,7 +15,7 @@ namespace Microsoft.DotNet.Watch
{
internal sealed class CompilationHandler : IDisposable
{
- public readonly IncrementalMSBuildWorkspace Workspace;
+ public readonly HotReloadMSBuildWorkspace Workspace;
private readonly ILogger _logger;
private readonly HotReloadService _hotReloadService;
private readonly ProcessRunner _processRunner;
@@ -38,12 +39,19 @@ internal sealed class CompilationHandler : IDisposable
private ImmutableList _previousUpdates = [];
private bool _isDisposed;
+ private int _solutionUpdateId;
+
+ ///
+ /// Current set of project instances indexed by .
+ /// Updated whenever the project graph changes.
+ ///
+ private ImmutableDictionary> _projectInstances = [];
public CompilationHandler(ILogger logger, ProcessRunner processRunner)
{
_logger = logger;
_processRunner = processRunner;
- Workspace = new IncrementalMSBuildWorkspace(logger);
+ Workspace = new HotReloadMSBuildWorkspace(logger, projectFile => _projectInstances.GetValueOrDefault(projectFile, []));
_hotReloadService = new HotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
}
@@ -691,5 +699,70 @@ private static Task ForEachProjectAsync(ImmutableDictionary ToManagedCodeUpdates(ImmutableArray updates)
=> [.. updates.Select(update => new HotReloadManagedCodeUpdate(update.ModuleId, update.MetadataDelta, update.ILDelta, update.PdbDelta, update.UpdatedTypes, update.RequiredCapabilities))];
+
+ private static ImmutableDictionary> CreateProjectInstanceMap(ProjectGraph graph)
+ => graph.ProjectNodes
+ .GroupBy(static node => node.ProjectInstance.FullPath)
+ .ToImmutableDictionary(
+ keySelector: static group => group.Key,
+ elementSelector: static group => group.Select(static node => node.ProjectInstance).ToImmutableArray());
+
+ public async Task UpdateProjectConeAsync(ProjectGraph projectGraph, string projectPath, string baseDirectory, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Loading projects ...");
+ var stopwatch = Stopwatch.StartNew();
+
+ _projectInstances = CreateProjectInstanceMap(projectGraph);
+
+ var solution = await Workspace.UpdateProjectConeAsync(projectPath, cancellationToken);
+ await SolutionUpdatedAsync(solution, "project update", cancellationToken);
+
+ _logger.LogInformation("Projects loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
+ }
+
+ public async Task UpdateFileContentAsync(ImmutableList changedFiles, CancellationToken cancellationToken)
+ {
+ var solution = await Workspace.UpdateFileContentAsync(changedFiles.Select(static f => (f.Item.FilePath, f.Kind.Convert())), cancellationToken);
+ await SolutionUpdatedAsync(solution, "document update", cancellationToken);
+ }
+
+ private Task SolutionUpdatedAsync(Solution newSolution, string operationDisplayName, CancellationToken cancellationToken)
+ => ReportSolutionFilesAsync(newSolution, Interlocked.Increment(ref _solutionUpdateId), operationDisplayName, cancellationToken);
+
+ private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId);
+
+ if (!_logger.IsEnabled(LogLevel.Trace))
+ {
+ return;
+ }
+
+ foreach (var project in solution.Projects)
+ {
+ _logger.LogDebug(" Project: {Path}", project.FilePath);
+
+ foreach (var document in project.Documents)
+ {
+ await InspectDocumentAsync(document, "Document").ConfigureAwait(false);
+ }
+
+ foreach (var document in project.AdditionalDocuments)
+ {
+ await InspectDocumentAsync(document, "Additional").ConfigureAwait(false);
+ }
+
+ foreach (var document in project.AnalyzerConfigDocuments)
+ {
+ await InspectDocumentAsync(document, "Config").ConfigureAwait(false);
+ }
+ }
+
+ async ValueTask InspectDocumentAsync(TextDocument document, string kind)
+ {
+ var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
+ _logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray()));
+ }
+ }
}
}
diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs
index 5466d27b32c2..17e661b9a0dd 100644
--- a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs
+++ b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
+using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.HotReload;
@@ -166,7 +167,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
return;
}
- await compilationHandler.Workspace.UpdateProjectConeAsync(rootProjectOptions.ProjectPath, iterationCancellationToken);
+ await compilationHandler.UpdateProjectConeAsync(evaluationResult.ProjectGraph, rootProjectOptions.ProjectPath, rootProjectOptions.WorkingDirectory, iterationCancellationToken);
// Solution must be initialized after we load the solution but before we start watching for file changes to avoid race condition
// when the EnC session captures content of the file after the changes has already been made.
@@ -373,7 +374,7 @@ void FileChangedCallback(ChangedPath change)
// Deploy dependencies after rebuilding and before restarting.
if (!projectsToRedeploy.IsEmpty)
{
- DeployProjectDependencies(evaluationResult.ProjectGraph, projectsToRedeploy, iterationCancellationToken);
+ DeployProjectDependencies(evaluationResult.RestoredProjectInstances, projectsToRedeploy, iterationCancellationToken);
_context.Logger.Log(MessageDescriptor.ProjectDependenciesDeployed, projectsToRedeploy.Length);
}
@@ -454,7 +455,7 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
// additional files/directories may have been added:
evaluationResult.WatchFiles(fileWatcher);
- await compilationHandler.Workspace.UpdateProjectConeAsync(rootProjectOptions.ProjectPath, iterationCancellationToken);
+ await compilationHandler.UpdateProjectConeAsync(evaluationResult.ProjectGraph, rootProjectOptions.ProjectPath, rootProjectOptions.WorkingDirectory, iterationCancellationToken);
if (shutdownCancellationToken.IsCancellationRequested)
{
@@ -501,7 +502,7 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
{
// Update the workspace to reflect changes in the file content:.
// If the project was re-evaluated the Roslyn solution is already up to date.
- await compilationHandler.Workspace.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
+ await compilationHandler.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
}
return changedFiles;
@@ -560,35 +561,38 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
}
}
- private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray projectPaths, CancellationToken cancellationToken)
+ private void DeployProjectDependencies(ImmutableArray restoredProjectInstances, ImmutableArray projectPaths, CancellationToken cancellationToken)
{
var projectPathSet = projectPaths.ToImmutableHashSet(PathUtilities.OSSpecificPathComparer);
var buildReporter = new BuildReporter(_context.Logger, _context.Options, _context.EnvironmentOptions);
var targetName = TargetNames.ReferenceCopyLocalPathsOutputGroup;
- foreach (var node in graph.ProjectNodes)
+ foreach (var restoredProjectInstance in restoredProjectInstances)
{
cancellationToken.ThrowIfCancellationRequested();
- var projectPath = node.ProjectInstance.FullPath;
+ // Avoid modification of the restored snapshot.
+ var projectInstance = restoredProjectInstance.DeepCopy();
+
+ var projectPath = projectInstance.FullPath;
if (!projectPathSet.Contains(projectPath))
{
continue;
}
- if (!node.ProjectInstance.Targets.ContainsKey(targetName))
+ if (!projectInstance.Targets.ContainsKey(targetName))
{
continue;
}
- if (node.GetOutputDirectory() is not { } relativeOutputDir)
+ if (projectInstance.GetOutputDirectory() is not { } relativeOutputDir)
{
continue;
}
using var loggers = buildReporter.GetLoggers(projectPath, targetName);
- if (!node.ProjectInstance.Build([targetName], loggers, out var targetOutputs))
+ if (!projectInstance.Build([targetName], loggers, out var targetOutputs))
{
_context.Logger.LogDebug("{TargetName} target failed", targetName);
loggers.ReportOutput();
diff --git a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs
deleted file mode 100644
index 06b79772ddf6..000000000000
--- a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs
+++ /dev/null
@@ -1,272 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Immutable;
-using System.Diagnostics;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api;
-using Microsoft.CodeAnalysis.Host.Mef;
-using Microsoft.CodeAnalysis.MSBuild;
-using Microsoft.CodeAnalysis.Text;
-using Microsoft.Extensions.Logging;
-
-namespace Microsoft.DotNet.Watch;
-
-internal sealed class IncrementalMSBuildWorkspace : Workspace
-{
- private readonly ILogger _logger;
- private int _solutionUpdateId;
-
- public IncrementalMSBuildWorkspace(ILogger logger)
- : base(MSBuildMefHostServices.DefaultServices, WorkspaceKind.MSBuild)
- {
-#pragma warning disable CS0618 // https://github.com/dotnet/sdk/issues/49725
- WorkspaceFailed += (_sender, diag) =>
- {
- // Report both Warning and Failure as warnings.
- // MSBuildProjectLoader reports Failures for cases where we can safely continue loading projects
- // (e.g. non-C#/VB project is ignored).
- // https://github.com/dotnet/roslyn/issues/75170
- logger.LogWarning($"msbuild: {diag.Diagnostic}");
- };
-#pragma warning restore CS0618
-
- _logger = logger;
- }
-
- public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationToken cancellationToken)
- {
- _logger.LogInformation("Loading projects ...");
-
- var stopwatch = Stopwatch.StartNew();
- var oldSolution = CurrentSolution;
-
- var loader = new MSBuildProjectLoader(this);
- var projectMap = ProjectMap.Create();
-
- ImmutableArray projectInfos;
- try
- {
- projectInfos = await loader.LoadProjectInfoAsync(rootProjectPath, projectMap, progress: null, msbuildLogger: null, cancellationToken).ConfigureAwait(false);
- }
- catch (InvalidOperationException)
- {
- // TODO: workaround for https://github.com/dotnet/roslyn/issues/75956
- projectInfos = [];
- }
-
- var oldProjectIdsByPath = oldSolution.Projects.ToDictionary(keySelector: static p => (p.FilePath!, p.Name), elementSelector: static p => p.Id);
-
- // Map new project id to the corresponding old one based on file path and project name (includes TFM), if it exists, and null for added projects.
- // Deleted projects won't be included in this map.
- var projectIdMap = projectInfos.ToDictionary(
- keySelector: static info => info.Id,
- elementSelector: info => oldProjectIdsByPath.TryGetValue((info.FilePath!, info.Name), out var oldProjectId) ? oldProjectId : null);
-
- var newSolution = oldSolution;
-
- foreach (var newProjectInfo in projectInfos)
- {
- Debug.Assert(newProjectInfo.FilePath != null);
-
- var oldProjectId = projectIdMap[newProjectInfo.Id];
- if (oldProjectId == null)
- {
- newSolution = newSolution.AddProject(newProjectInfo);
- continue;
- }
-
- newSolution = HotReloadService.WithProjectInfo(newSolution, ProjectInfo.Create(
- oldProjectId,
- newProjectInfo.Version,
- newProjectInfo.Name,
- newProjectInfo.AssemblyName,
- newProjectInfo.Language,
- newProjectInfo.FilePath,
- newProjectInfo.OutputFilePath,
- newProjectInfo.CompilationOptions,
- newProjectInfo.ParseOptions,
- MapDocuments(oldProjectId, newProjectInfo.Documents),
- newProjectInfo.ProjectReferences.Select(MapProjectReference),
- newProjectInfo.MetadataReferences,
- newProjectInfo.AnalyzerReferences,
- MapDocuments(oldProjectId, newProjectInfo.AdditionalDocuments),
- isSubmission: false,
- hostObjectType: null,
- outputRefFilePath: newProjectInfo.OutputRefFilePath)
- .WithAnalyzerConfigDocuments(MapDocuments(oldProjectId, newProjectInfo.AnalyzerConfigDocuments))
- .WithCompilationOutputInfo(newProjectInfo.CompilationOutputInfo));
- }
-
- await UpdateSolutionAsync(newSolution, operationDisplayName: "project update", cancellationToken);
- UpdateReferencesAfterAdd();
-
- _logger.LogInformation("Projects loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
-
- ProjectReference MapProjectReference(ProjectReference pr)
- // Only C# and VB projects are loaded by the MSBuildProjectLoader, so some references might be missing.
- // When a new project is added along with a new project reference the old project id is also null.
- => new(projectIdMap.TryGetValue(pr.ProjectId, out var oldProjectId) && oldProjectId != null ? oldProjectId : pr.ProjectId, pr.Aliases, pr.EmbedInteropTypes);
-
- ImmutableArray MapDocuments(ProjectId mappedProjectId, IReadOnlyList documents)
- => documents.Select(docInfo =>
- {
- // TODO: can there be multiple documents of the same path in the project?
-
- // Map to a document of the same path. If there isn't one (a new document is added to the project),
- // create a new document id with the mapped project id.
- var mappedDocumentId = oldSolution.GetDocumentIdsWithFilePath(docInfo.FilePath).FirstOrDefault(id => id.ProjectId == mappedProjectId)
- ?? DocumentId.CreateNewId(mappedProjectId);
-
- return docInfo.WithId(mappedDocumentId);
- }).ToImmutableArray();
- }
-
- public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles, CancellationToken cancellationToken)
- {
- var updatedSolution = CurrentSolution;
-
- var documentsToRemove = new List();
-
- foreach (var (changedFile, change) in changedFiles)
- {
- // when a file is added we reevaluate the project:
- Debug.Assert(change != ChangeKind.Add);
-
- var documentIds = updatedSolution.GetDocumentIdsWithFilePath(changedFile.FilePath);
- if (change == ChangeKind.Delete)
- {
- documentsToRemove.AddRange(documentIds);
- continue;
- }
-
- foreach (var documentId in documentIds)
- {
- var textDocument = updatedSolution.GetDocument(documentId)
- ?? updatedSolution.GetAdditionalDocument(documentId)
- ?? updatedSolution.GetAnalyzerConfigDocument(documentId);
-
- if (textDocument == null)
- {
- _logger.LogDebug("Could not find document with path '{FilePath}' in the workspace.", changedFile.FilePath);
- continue;
- }
-
- var project = updatedSolution.GetProject(documentId.ProjectId);
- Debug.Assert(project?.FilePath != null);
-
- var oldText = await textDocument.GetTextAsync(cancellationToken);
- Debug.Assert(oldText.Encoding != null);
-
- var newText = await GetSourceTextAsync(changedFile.FilePath, oldText.Encoding, oldText.ChecksumAlgorithm, cancellationToken);
-
- _logger.LogDebug("Updating document text of '{FilePath}'.", changedFile.FilePath);
-
- updatedSolution = textDocument switch
- {
- Document document => document.WithText(newText).Project.Solution,
- AdditionalDocument ad => updatedSolution.WithAdditionalDocumentText(textDocument.Id, newText, PreservationMode.PreserveValue),
- AnalyzerConfigDocument acd => updatedSolution.WithAnalyzerConfigDocumentText(textDocument.Id, newText, PreservationMode.PreserveValue),
- _ => throw new InvalidOperationException()
- };
- }
- }
-
- updatedSolution = RemoveDocuments(updatedSolution, documentsToRemove);
-
- await UpdateSolutionAsync(updatedSolution, operationDisplayName: "document update", cancellationToken);
- }
-
- private static Solution RemoveDocuments(Solution solution, IEnumerable ids)
- => solution
- .RemoveDocuments([.. ids.Where(id => solution.GetDocument(id) != null)])
- .RemoveAdditionalDocuments([.. ids.Where(id => solution.GetAdditionalDocument(id) != null)])
- .RemoveAnalyzerConfigDocuments([.. ids.Where(id => solution.GetAnalyzerConfigDocument(id) != null)]);
-
- private static async ValueTask GetSourceTextAsync(string filePath, Encoding encoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken)
- {
- var zeroLengthRetryPerformed = false;
- for (var attemptIndex = 0; attemptIndex < 6; attemptIndex++)
- {
- try
- {
- // File.OpenRead opens the file with FileShare.Read. This may prevent IDEs from saving file
- // contents to disk
- SourceText sourceText;
- using (var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- {
- sourceText = SourceText.From(stream, encoding, checksumAlgorithm);
- }
-
- if (!zeroLengthRetryPerformed && sourceText.Length == 0)
- {
- zeroLengthRetryPerformed = true;
-
- // VSCode (on Windows) will sometimes perform two separate writes when updating a file on disk.
- // In the first update, it clears the file contents, and in the second, it writes the intended
- // content.
- // It's atypical that a file being watched for hot reload would be empty. We'll use this as a
- // hueristic to identify this case and perform an additional retry reading the file after a delay.
- await Task.Delay(20, cancellationToken);
-
- using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- sourceText = SourceText.From(stream, encoding, checksumAlgorithm);
- }
-
- return sourceText;
- }
- catch (IOException) when (attemptIndex < 5)
- {
- await Task.Delay(20 * (attemptIndex + 1), cancellationToken);
- }
- }
-
- Debug.Fail("This shouldn't happen.");
- return null;
- }
-
- private Task UpdateSolutionAsync(Solution newSolution, string operationDisplayName, CancellationToken cancellationToken)
- => ReportSolutionFilesAsync(SetCurrentSolution(newSolution), Interlocked.Increment(ref _solutionUpdateId), operationDisplayName, cancellationToken);
-
- private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken)
- {
-#if DEBUG
- _logger.LogDebug("Solution: {Path}", solution.FilePath);
-
- if (!_logger.IsEnabled(LogLevel.Debug))
- {
- return;
- }
-
- _logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId);
-
- foreach (var project in solution.Projects)
- {
- _logger.LogDebug(" Project: {Path}", project.FilePath);
-
- foreach (var document in project.Documents)
- {
- await InspectDocumentAsync(document, "Document");
- }
-
- foreach (var document in project.AdditionalDocuments)
- {
- await InspectDocumentAsync(document, "Additional");
- }
-
- foreach (var document in project.AnalyzerConfigDocuments)
- {
- await InspectDocumentAsync(document, "Config");
- }
- }
-
- async ValueTask InspectDocumentAsync(TextDocument document, string kind)
- {
- var text = await document.GetTextAsync(cancellationToken);
- _logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray()));
- }
-#else
- await Task.CompletedTask;
-#endif
- }
-}
diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
index 9e28729eb807..e27bb7abaa50 100644
--- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
+++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
@@ -3,7 +3,7 @@
"dotnet-watch": {
"commandName": "Project",
"commandLineArgs": "--verbose -bl",
- "workingDirectory": "C:\\bugs\\9756\\aspire-watch-start-issue\\Aspire.AppHost",
+ "workingDirectory": "E:\\Temp\\FS\\PFApp",
"environmentVariables": {
"DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
"DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",
diff --git a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs
index 4081a1e2102c..a20bae5f2da0 100644
--- a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs
+++ b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs
@@ -25,10 +25,13 @@ public async Task ReferenceOutputAssembly_False()
var loggerFactory = new LoggerFactory(reporter);
var logger = loggerFactory.CreateLogger("Test");
var factory = new ProjectGraphFactory(globalOptions: []);
+
var projectGraph = factory.TryLoadProjectGraph(options.ProjectPath, logger, projectGraphRequired: false, CancellationToken.None);
+ Assert.NotNull(projectGraph);
+
var handler = new CompilationHandler(logger, processRunner);
- await handler.Workspace.UpdateProjectConeAsync(hostProject, CancellationToken.None);
+ await handler.UpdateProjectConeAsync(projectGraph, hostProject, workingDirectory, CancellationToken.None);
// all projects are present
AssertEx.SequenceEqual(["Host", "Lib2", "Lib", "A", "B"], handler.Workspace.CurrentSolution.Projects.Select(p => p.Name));