Skip to content

Commit ef93823

Browse files
authored
Fixes capability detection for hosted Blazor apps (#49037)
1 parent 49dade0 commit ef93823

32 files changed

+339
-129
lines changed

src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private static ProjectKey GetProjectKey(ProjectGraphNode projectNode)
6363
ProcessSpec processSpec,
6464
EnvironmentVariablesBuilder environmentBuilder,
6565
ProjectOptions projectOptions,
66-
HotReloadProfile profile,
66+
HotReloadAppModel appModel,
6767
CancellationToken cancellationToken)
6868
{
6969
BrowserRefreshServer? server;
@@ -76,7 +76,7 @@ private static ProjectKey GetProjectKey(ProjectGraphNode projectNode)
7676
hasExistingServer = _servers.TryGetValue(key, out server);
7777
if (!hasExistingServer)
7878
{
79-
server = IsServerSupported(projectNode, profile) ? new BrowserRefreshServer(context.EnvironmentOptions, context.Reporter) : null;
79+
server = IsServerSupported(projectNode, appModel) ? new BrowserRefreshServer(context.EnvironmentOptions, context.Reporter) : null;
8080
_servers.Add(key, server);
8181
}
8282
}
@@ -272,24 +272,24 @@ private bool CanLaunchBrowser(DotNetWatchContext context, ProjectGraphNode proje
272272
return true;
273273
}
274274

275-
public bool IsServerSupported(ProjectGraphNode projectNode, HotReloadProfile profile)
275+
public bool IsServerSupported(ProjectGraphNode projectNode, HotReloadAppModel appModel)
276276
{
277277
if (context.EnvironmentOptions.SuppressBrowserRefresh)
278278
{
279-
context.Reporter.Report(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.ToErrorWhen(profile.RequiresBrowserRefresh), EnvironmentVariables.SuppressBrowserRefresh);
279+
context.Reporter.Report(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.ToErrorWhen(appModel.RequiresBrowserRefresh), EnvironmentVariables.SuppressBrowserRefresh);
280280
return false;
281281
}
282282

283283
if (!projectNode.IsNetCoreApp(minVersion: s_minimumSupportedVersion))
284284
{
285-
context.Reporter.Report(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.ToErrorWhen(profile.RequiresBrowserRefresh));
285+
context.Reporter.Report(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.ToErrorWhen(appModel.RequiresBrowserRefresh));
286286
return false;
287287
}
288288

289289
// We only want to enable browser refresh if this is a WebApp (ASP.NET Core / Blazor app).
290290
if (!projectNode.IsWebApp())
291291
{
292-
context.Reporter.Report(MessageDescriptor.SkippingConfiguringBrowserRefresh_NotWebApp.ToErrorWhen(profile.RequiresBrowserRefresh));
292+
context.Reporter.Report(MessageDescriptor.SkippingConfiguringBrowserRefresh_NotWebApp.ToErrorWhen(appModel.RequiresBrowserRefresh));
293293
return false;
294294
}
295295

src/BuiltInTools/dotnet-watch/DotNetWatcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
6262
};
6363

6464
var browserRefreshServer = (projectRootNode != null)
65-
? await browserConnector.GetOrCreateBrowserRefreshServerAsync(projectRootNode, processSpec, environmentBuilder, Context.RootProjectOptions, HotReloadProfile.Default, shutdownCancellationToken)
65+
? await browserConnector.GetOrCreateBrowserRefreshServerAsync(projectRootNode, processSpec, environmentBuilder, Context.RootProjectOptions, DefaultAppModel.Instance, shutdownCancellationToken)
6666
: null;
6767

6868
environmentBuilder.SetProcessEnvironmentVariables(processSpec);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Build.Graph;
5+
6+
namespace Microsoft.DotNet.Watch;
7+
8+
internal abstract partial class HotReloadAppModel
9+
{
10+
/// <summary>
11+
/// Blazor client-only WebAssembly app.
12+
/// </summary>
13+
internal sealed class BlazorWebAssemblyAppModel(ProjectGraphNode clientProject) : HotReloadAppModel
14+
{
15+
public override bool RequiresBrowserRefresh => true;
16+
17+
/// <summary>
18+
/// Blazor WASM does not need dotnet applier as all changes are applied in the browser,
19+
/// the process being launched is a dev server.
20+
/// </summary>
21+
public override bool InjectDeltaApplier => false;
22+
23+
public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
24+
{
25+
if (browserRefreshServer == null)
26+
{
27+
// error has been reported earlier
28+
return null;
29+
}
30+
31+
return new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer, clientProject);
32+
}
33+
}
34+
}

src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public override Task<ImmutableArray<string>> GetApplyUpdateCapabilitiesAsync(Can
4848
{
4949
var targetFramework = project.GetTargetFrameworkVersion();
5050

51-
Reporter.Verbose($"Using capabilities based on target framework: '{targetFramework}'.");
51+
Reporter.Verbose($"Using capabilities based on project '{project.GetDisplayName()}' target framework: '{targetFramework}'.");
5252

5353
capabilities = targetFramework?.Major switch
5454
{
@@ -61,7 +61,7 @@ public override Task<ImmutableArray<string>> GetApplyUpdateCapabilitiesAsync(Can
6161
}
6262
else
6363
{
64-
Reporter.Verbose($"Project specifies capabilities.");
64+
Reporter.Verbose($"Project '{project.GetDisplayName()}' specifies capabilities: '{string.Join(' ', capabilities)}'");
6565
}
6666

6767
return Task.FromResult(capabilities);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Build.Graph;
5+
6+
namespace Microsoft.DotNet.Watch;
7+
8+
internal abstract partial class HotReloadAppModel
9+
{
10+
/// <summary>
11+
/// Blazor WebAssembly app hosted by an ASP.NET Core app.
12+
/// App has a client and server projects and deltas are applied to both processes.
13+
/// </summary>
14+
internal sealed class BlazorWebAssemblyHostedAppModel(ProjectGraphNode clientProject) : HotReloadAppModel
15+
{
16+
public override bool RequiresBrowserRefresh => true;
17+
public override bool InjectDeltaApplier => true;
18+
19+
public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
20+
{
21+
if (browserRefreshServer == null)
22+
{
23+
// error has been reported earlier
24+
return null;
25+
}
26+
27+
return new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer, clientProject);
28+
}
29+
}
30+
}

src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyHostedDeltaApplier.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
namespace Microsoft.DotNet.Watch
1010
{
11-
internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode project) : DeltaApplier(reporter)
11+
internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode clientProject) : DeltaApplier(reporter)
1212
{
13-
private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, project);
13+
private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, clientProject);
1414
private readonly DefaultDeltaApplier _hostApplier = new(reporter);
1515

1616
public override void Dispose()

src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,26 +100,10 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
100100
_reporter.Report(MessageDescriptor.HotReloadSessionStarted);
101101
}
102102

103-
private DeltaApplier? CreateDeltaApplier(HotReloadProfile profile, ProjectGraphNode project, BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
104-
{
105-
if (browserRefreshServer == null && profile.RequiresBrowserRefresh)
106-
{
107-
// error has been reported earlier
108-
return null;
109-
}
110-
111-
return profile switch
112-
{
113-
HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer!, project),
114-
HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer!, project),
115-
_ => new DefaultDeltaApplier(processReporter),
116-
};
117-
}
118-
119103
public async Task<RunningProject?> TrackRunningProjectAsync(
120104
ProjectGraphNode projectNode,
121105
ProjectOptions projectOptions,
122-
HotReloadProfile profile,
106+
HotReloadAppModel appModel,
123107
string namedPipeName,
124108
BrowserRefreshServer? browserRefreshServer,
125109
ProcessSpec processSpec,
@@ -130,7 +114,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
130114
{
131115
var projectPath = projectNode.ProjectInstance.FullPath;
132116

133-
var deltaApplier = CreateDeltaApplier(profile, projectNode, browserRefreshServer, processReporter);
117+
var deltaApplier = appModel.CreateDeltaApplier(browserRefreshServer, processReporter);
134118
if (deltaApplier == null)
135119
{
136120
// error already reported
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.DotNet.Watch;
5+
6+
/// <summary>
7+
/// Default model.
8+
/// </summary>
9+
internal sealed class DefaultAppModel : HotReloadAppModel
10+
{
11+
public static readonly DefaultAppModel Instance = new();
12+
13+
public override bool RequiresBrowserRefresh => false;
14+
public override bool InjectDeltaApplier => true;
15+
16+
public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
17+
=> new DefaultDeltaApplier(processReporter);
18+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Build.Execution;
5+
using Microsoft.Build.Graph;
6+
7+
namespace Microsoft.DotNet.Watch;
8+
9+
internal abstract partial class HotReloadAppModel
10+
{
11+
public abstract bool RequiresBrowserRefresh { get; }
12+
13+
/// <summary>
14+
/// True to inject delta applier to the process.
15+
/// </summary>
16+
public abstract bool InjectDeltaApplier { get; }
17+
18+
public abstract DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter);
19+
20+
public static HotReloadAppModel InferFromProject(ProjectGraphNode projectNode, IReporter reporter)
21+
{
22+
if (projectNode.IsWebApp())
23+
{
24+
var queue = new Queue<ProjectGraphNode>();
25+
queue.Enqueue(projectNode);
26+
27+
ProjectInstance? aspnetCoreProject = null;
28+
29+
var visited = new HashSet<ProjectGraphNode>();
30+
31+
while (queue.Count > 0)
32+
{
33+
var currentNode = queue.Dequeue();
34+
var projectCapability = currentNode.ProjectInstance.GetItems("ProjectCapability");
35+
36+
foreach (var item in projectCapability)
37+
{
38+
if (item.EvaluatedInclude == "AspNetCore")
39+
{
40+
aspnetCoreProject = currentNode.ProjectInstance;
41+
break;
42+
}
43+
44+
if (item.EvaluatedInclude == "WebAssembly")
45+
{
46+
// We saw a previous project that was AspNetCore. This must be a blazor hosted app.
47+
if (aspnetCoreProject is not null && aspnetCoreProject != currentNode.ProjectInstance)
48+
{
49+
reporter.Verbose($"HotReloadProfile: BlazorHosted. {aspnetCoreProject.FullPath} references BlazorWebAssembly project {currentNode.ProjectInstance.FullPath}.", emoji: "🔥");
50+
return new BlazorWebAssemblyHostedAppModel(clientProject: currentNode);
51+
}
52+
53+
reporter.Verbose("HotReloadProfile: BlazorWebAssembly.", emoji: "🔥");
54+
return new BlazorWebAssemblyAppModel(clientProject: currentNode);
55+
}
56+
}
57+
58+
foreach (var project in currentNode.ProjectReferences)
59+
{
60+
if (visited.Add(project))
61+
{
62+
queue.Enqueue(project);
63+
}
64+
}
65+
}
66+
}
67+
68+
reporter.Verbose("HotReloadProfile: Default.", emoji: "🔥");
69+
return DefaultAppModel.Instance;
70+
}
71+
}

src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfile.cs

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfileReader.cs

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)