Skip to content

Commit 982cae2

Browse files
oroztocilCopilot
andauthored
Fix hot reload for Blazor route changes (#63972)
* Fix hot reload for Blazor route changes * Update src/Components/Endpoints/test/HotReloadServiceTests.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 3f8c0e7 commit 982cae2

7 files changed

+87
-109
lines changed

src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp
2020
private readonly object _lock = new();
2121
private readonly List<Action<EndpointBuilder>> _conventions = [];
2222
private readonly List<Action<EndpointBuilder>> _finallyConventions = [];
23+
private readonly List<Action<ComponentApplicationBuilder>> _componentApplicationBuilderActions = [];
2324
private readonly RazorComponentDataSourceOptions _options = new();
24-
private readonly ComponentApplicationBuilder _builder;
2525
private readonly IEndpointRouteBuilder _endpointRouteBuilder;
2626
private readonly ResourceCollectionResolver _resourceCollectionResolver;
2727
private readonly RenderModeEndpointProvider[] _renderModeEndpointProviders;
@@ -32,33 +32,29 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp
3232
private IChangeToken _changeToken;
3333
private IDisposable? _disposableChangeToken; // THREADING: protected by _lock
3434

35-
public Func<IDisposable, IDisposable> SetDisposableChangeTokenAction = disposableChangeToken => disposableChangeToken;
36-
3735
// Internal for testing.
38-
internal ComponentApplicationBuilder Builder => _builder;
3936
internal List<Action<EndpointBuilder>> Conventions => _conventions;
37+
internal List<Action<ComponentApplicationBuilder>> ComponentApplicationBuilderActions => _componentApplicationBuilderActions;
38+
internal CancellationTokenSource ChangeTokenSource => _cancellationTokenSource;
4039

4140
public RazorComponentEndpointDataSource(
42-
ComponentApplicationBuilder builder,
4341
IEnumerable<RenderModeEndpointProvider> renderModeEndpointProviders,
4442
IEndpointRouteBuilder endpointRouteBuilder,
4543
RazorComponentEndpointFactory factory,
4644
HotReloadService? hotReloadService = null)
4745
{
48-
_builder = builder;
4946
_endpointRouteBuilder = endpointRouteBuilder;
5047
_resourceCollectionResolver = new ResourceCollectionResolver(endpointRouteBuilder);
5148
_renderModeEndpointProviders = renderModeEndpointProviders.ToArray();
5249
_factory = factory;
5350
_hotReloadService = hotReloadService;
54-
HotReloadService.ClearCacheEvent += OnHotReloadClearCache;
5551
DefaultBuilder = new RazorComponentsEndpointConventionBuilder(
5652
_lock,
57-
builder,
5853
endpointRouteBuilder,
5954
_options,
6055
_conventions,
61-
_finallyConventions);
56+
_finallyConventions,
57+
_componentApplicationBuilderActions);
6258

6359
_cancellationTokenSource = new CancellationTokenSource();
6460
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
@@ -106,8 +102,20 @@ private void UpdateEndpoints()
106102

107103
lock (_lock)
108104
{
105+
_disposableChangeToken?.Dispose();
106+
_disposableChangeToken = null;
107+
109108
var endpoints = new List<Endpoint>();
110-
var context = _builder.Build();
109+
110+
var componentApplicationBuilder = new ComponentApplicationBuilder();
111+
112+
foreach (var action in ComponentApplicationBuilderActions)
113+
{
114+
action?.Invoke(componentApplicationBuilder);
115+
}
116+
117+
var context = componentApplicationBuilder.Build();
118+
111119
var configuredRenderModesMetadata = new ConfiguredRenderModesMetadata(
112120
[.. Options.ConfiguredRenderModes]);
113121

@@ -168,8 +176,7 @@ private void UpdateEndpoints()
168176
oldCancellationTokenSource?.Dispose();
169177
if (_hotReloadService is { MetadataUpdateSupported: true })
170178
{
171-
_disposableChangeToken?.Dispose();
172-
_disposableChangeToken = SetDisposableChangeTokenAction(ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints));
179+
_disposableChangeToken = ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints);
173180
}
174181
}
175182
}
@@ -195,15 +202,6 @@ private void AddBlazorWebEndpoints(List<Endpoint> endpoints)
195202
}
196203
}
197204

198-
public void OnHotReloadClearCache(Type[]? types)
199-
{
200-
lock (_lock)
201-
{
202-
_disposableChangeToken?.Dispose();
203-
_disposableChangeToken = null;
204-
}
205-
}
206-
207205
public override IChangeToken GetChangeToken()
208206
{
209207
Initialize();

src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ internal class RazorComponentEndpointDataSourceFactory(
1717
{
1818
public RazorComponentEndpointDataSource<TRootComponent> CreateDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent>(IEndpointRouteBuilder endpoints)
1919
{
20-
var builder = ComponentApplicationBuilder.GetBuilder<TRootComponent>() ??
21-
DefaultRazorComponentApplication<TRootComponent>.Instance.GetBuilder();
20+
var dataSource = new RazorComponentEndpointDataSource<TRootComponent>(providers, endpoints, factory, hotReloadService);
2221

23-
return new RazorComponentEndpointDataSource<TRootComponent>(builder, providers, endpoints, factory, hotReloadService);
22+
dataSource.ComponentApplicationBuilderActions.Add(builder =>
23+
{
24+
var assembly = typeof(TRootComponent).Assembly;
25+
IRazorComponentApplication.GetBuilderForAssembly(builder, assembly);
26+
});
27+
28+
return dataSource;
2429
}
2530
}

src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilder.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,25 @@ public sealed class RazorComponentsEndpointConventionBuilder : IEndpointConventi
1818
private readonly RazorComponentDataSourceOptions _options;
1919
private readonly List<Action<EndpointBuilder>> _conventions;
2020
private readonly List<Action<EndpointBuilder>> _finallyConventions;
21+
private readonly List<Action<ComponentApplicationBuilder>> _componentApplicationBuilderActions;
2122

2223
internal RazorComponentsEndpointConventionBuilder(
2324
object @lock,
24-
ComponentApplicationBuilder builder,
2525
IEndpointRouteBuilder endpointRouteBuilder,
2626
RazorComponentDataSourceOptions options,
2727
List<Action<EndpointBuilder>> conventions,
28-
List<Action<EndpointBuilder>> finallyConventions)
28+
List<Action<EndpointBuilder>> finallyConventions,
29+
List<Action<ComponentApplicationBuilder>> componentApplicationBuilderActions)
2930
{
3031
_lock = @lock;
31-
ApplicationBuilder = builder;
3232
EndpointRouteBuilder = endpointRouteBuilder;
3333
_options = options;
3434
_conventions = conventions;
3535
_finallyConventions = finallyConventions;
36+
_componentApplicationBuilderActions = componentApplicationBuilderActions;
3637
}
3738

38-
/// <summary>
39-
/// Gets the <see cref="ComponentApplicationBuilder"/> that is used to build the endpoints.
40-
/// </summary>
41-
internal ComponentApplicationBuilder ApplicationBuilder { get; }
39+
internal List<Action<ComponentApplicationBuilder>> ComponentApplicationBuilderActions => _componentApplicationBuilderActions;
4240

4341
internal string? ManifestPath { get => _options.ManifestPath; set => _options.ManifestPath = value; }
4442

src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static RazorComponentsEndpointConventionBuilder AddAdditionalAssemblies(
3030

3131
foreach (var assembly in assemblies)
3232
{
33-
builder.ApplicationBuilder.AddAssembly(assembly);
33+
builder.ComponentApplicationBuilderActions.Add(b => b.AddAssembly(assembly));
3434
}
3535
return builder;
3636
}

src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,15 @@ public void MapRazorComponents_CanAddConventions_ToBlazorWebEndpoints(string fra
245245
private RazorComponentsEndpointConventionBuilder CreateRazorComponentsAppBuilder(IEndpointRouteBuilder endpointBuilder)
246246
{
247247
var builder = endpointBuilder.MapRazorComponents<App>();
248-
builder.ApplicationBuilder.AddLibrary(new AssemblyComponentLibraryDescriptor(
248+
builder.ComponentApplicationBuilderActions.Add(b => b.AddLibrary(new AssemblyComponentLibraryDescriptor(
249249
"App",
250250
[new PageComponentBuilder {
251251
PageType = typeof(App),
252252
RouteTemplates = ["/"],
253253
AssemblyName = "App",
254254
}],
255255
[]
256-
));
256+
)));
257257
return builder;
258258
}
259259

src/Components/Endpoints/test/HotReloadServiceTests.cs

Lines changed: 39 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ public class HotReloadServiceTests
2323
public void UpdatesEndpointsWhenHotReloadChangeTokenTriggered()
2424
{
2525
// Arrange
26-
var builder = CreateBuilder(typeof(ServerComponent));
2726
var services = CreateServices(typeof(MockEndpointProvider));
28-
var endpointDataSource = CreateDataSource<App>(builder, services);
27+
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);
2928
var invoked = false;
3029

3130
// Act
@@ -41,9 +40,8 @@ public void UpdatesEndpointsWhenHotReloadChangeTokenTriggered()
4140
public void AddNewEndpointWhenDataSourceChanges()
4241
{
4342
// Arrange
44-
var builder = CreateBuilder(typeof(ServerComponent));
4543
var services = CreateServices(typeof(MockEndpointProvider));
46-
var endpointDataSource = CreateDataSource<App>(builder, services);
44+
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);
4745

4846
// Assert - 1
4947
var endpoint = Assert.IsType<RouteEndpoint>(
@@ -52,15 +50,17 @@ public void AddNewEndpointWhenDataSourceChanges()
5250
Assert.Equal("/server", endpoint.RoutePattern.RawText);
5351

5452
// Act - 2
55-
endpointDataSource.Builder.Pages.AddFromLibraryInfo("TestAssembly2", new[]
56-
{
57-
new PageComponentBuilder
53+
endpointDataSource.ComponentApplicationBuilderActions.Add(
54+
b => b.Pages.AddFromLibraryInfo("TestAssembly2", new[]
5855
{
59-
AssemblyName = "TestAssembly2",
60-
PageType = typeof(StaticComponent),
61-
RouteTemplates = new List<string> { "/app/test" }
62-
}
63-
});
56+
new PageComponentBuilder
57+
{
58+
AssemblyName = "TestAssembly2",
59+
PageType = typeof(StaticComponent),
60+
RouteTemplates = new List<string> { "/app/test" }
61+
}
62+
}));
63+
6464
HotReloadService.UpdateApplication(null);
6565

6666
// Assert - 2
@@ -76,9 +76,8 @@ public void AddNewEndpointWhenDataSourceChanges()
7676
public void RemovesEndpointWhenDataSourceChanges()
7777
{
7878
// Arrange
79-
var builder = CreateBuilder(typeof(ServerComponent));
8079
var services = CreateServices(typeof(MockEndpointProvider));
81-
var endpointDataSource = CreateDataSource<App>(builder, services);
80+
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);
8281

8382
// Assert - 1
8483
var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpointDataSource.Endpoints,
@@ -87,7 +86,7 @@ public void RemovesEndpointWhenDataSourceChanges()
8786
Assert.Equal("/server", endpoint.RoutePattern.RawText);
8887

8988
// Act - 2
90-
endpointDataSource.Builder.RemoveLibrary("TestAssembly");
89+
endpointDataSource.ComponentApplicationBuilderActions.Add(b => b.RemoveLibrary("TestAssembly"));
9190
endpointDataSource.Options.ConfiguredRenderModes.Clear();
9291
HotReloadService.UpdateApplication(null);
9392

@@ -100,9 +99,8 @@ public void RemovesEndpointWhenDataSourceChanges()
10099
public void ModifiesEndpointWhenDataSourceChanges()
101100
{
102101
// Arrange
103-
var builder = CreateBuilder(typeof(ServerComponent));
104102
var services = CreateServices(typeof(MockEndpointProvider));
105-
var endpointDataSource = CreateDataSource<App>(builder, services);
103+
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);
106104

107105
// Assert - 1
108106
var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpointDataSource.Endpoints, e => e.Metadata.GetMetadata<RootComponentMetadata>() != null));
@@ -124,9 +122,8 @@ public void ModifiesEndpointWhenDataSourceChanges()
124122
public void NotifiesCompositeEndpointDataSource()
125123
{
126124
// Arrange
127-
var builder = CreateBuilder(typeof(ServerComponent));
128125
var services = CreateServices(typeof(MockEndpointProvider));
129-
var endpointDataSource = CreateDataSource<App>(builder, services);
126+
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);
130127
var compositeEndpointDataSource = new CompositeEndpointDataSource(
131128
new[] { endpointDataSource });
132129

@@ -137,7 +134,7 @@ public void NotifiesCompositeEndpointDataSource()
137134
Assert.Equal("/server", compositeEndpoint.RoutePattern.RawText);
138135

139136
// Act - 2
140-
endpointDataSource.Builder.Pages.RemoveFromAssembly("TestAssembly");
137+
endpointDataSource.ComponentApplicationBuilderActions.Add(b => b.Pages.RemoveFromAssembly("TestAssembly"));
141138
endpointDataSource.Options.ConfiguredRenderModes.Clear();
142139
HotReloadService.UpdateApplication(null);
143140

@@ -148,37 +145,14 @@ public void NotifiesCompositeEndpointDataSource()
148145
Assert.Empty(compositePageEndpoints);
149146
}
150147

151-
private sealed class WrappedChangeTokenDisposable : IDisposable
152-
{
153-
public bool IsDisposed { get; private set; }
154-
private readonly IDisposable _innerDisposable;
155-
156-
public WrappedChangeTokenDisposable(IDisposable innerDisposable)
157-
{
158-
_innerDisposable = innerDisposable;
159-
}
160-
161-
public void Dispose()
162-
{
163-
IsDisposed = true;
164-
_innerDisposable.Dispose();
165-
}
166-
}
167-
168148
[Fact]
169149
public void ConfirmChangeTokenDisposedHotReload()
170150
{
171151
// Arrange
172-
var builder = CreateBuilder(typeof(ServerComponent));
173152
var services = CreateServices(typeof(MockEndpointProvider));
174-
var endpointDataSource = CreateDataSource<App>(builder, services);
175-
176-
WrappedChangeTokenDisposable wrappedChangeTokenDisposable = null;
177-
178-
endpointDataSource.SetDisposableChangeTokenAction = (IDisposable disposableChangeToken) => {
179-
wrappedChangeTokenDisposable = new WrappedChangeTokenDisposable(disposableChangeToken);
180-
return wrappedChangeTokenDisposable;
181-
};
153+
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder, null);
154+
var changeTokenSource = endpointDataSource.ChangeTokenSource;
155+
var changeToken = endpointDataSource.GetChangeToken();
182156

183157
var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpointDataSource.Endpoints, e => e.Metadata.GetMetadata<RootComponentMetadata>() != null));
184158
Assert.Equal("/server", endpoint.RoutePattern.RawText);
@@ -187,18 +161,21 @@ public void ConfirmChangeTokenDisposedHotReload()
187161
// Make a modification and then perform a hot reload.
188162
endpointDataSource.Conventions.Add(builder =>
189163
builder.Metadata.Add(new TestMetadata()));
164+
190165
HotReloadService.UpdateApplication(null);
191166
HotReloadService.ClearCache(null);
192167

193168
// Confirm the change token is disposed after ClearCache
194-
Assert.True(wrappedChangeTokenDisposable.IsDisposed);
169+
Assert.True(changeToken.HasChanged);
170+
Assert.Throws<ObjectDisposedException>(() => changeTokenSource.Token);
195171
}
196172

197173
private class TestMetadata { }
198174

199-
private ComponentApplicationBuilder CreateBuilder(params Type[] types)
175+
private class TestAssembly : Assembly;
176+
177+
private static void ConfigureBuilder(ComponentApplicationBuilder builder, params Type[] types)
200178
{
201-
var builder = new ComponentApplicationBuilder();
202179
builder.AddLibrary(new AssemblyComponentLibraryDescriptor(
203180
"TestAssembly",
204181
Array.Empty<PageComponentBuilder>(),
@@ -208,8 +185,11 @@ private ComponentApplicationBuilder CreateBuilder(params Type[] types)
208185
ComponentType = t,
209186
RenderMode = t.GetCustomAttribute<RenderModeAttribute>()
210187
}).ToArray()));
188+
}
211189

212-
return builder;
190+
private static void ConfigureServerComponentBuilder(ComponentApplicationBuilder builder)
191+
{
192+
ConfigureBuilder(builder, typeof(ServerComponent));
213193
}
214194

215195
private IServiceProvider CreateServices(params Type[] types)
@@ -227,16 +207,21 @@ private IServiceProvider CreateServices(params Type[] types)
227207
}
228208

229209
private static RazorComponentEndpointDataSource<TComponent> CreateDataSource<TComponent>(
230-
ComponentApplicationBuilder builder,
231210
IServiceProvider services,
232-
IComponentRenderMode[] renderModes = null)
211+
Action<ComponentApplicationBuilder> configureBuilder = null,
212+
IComponentRenderMode[] renderModes = null,
213+
HotReloadService hotReloadService = null)
233214
{
234215
var result = new RazorComponentEndpointDataSource<TComponent>(
235-
builder,
236216
new[] { new MockEndpointProvider() },
237217
new TestEndpointRouteBuilder(services),
238218
new RazorComponentEndpointFactory(),
239-
new HotReloadService() { MetadataUpdateSupported = true });
219+
hotReloadService ?? new HotReloadService() { MetadataUpdateSupported = true });
220+
221+
if (configureBuilder is not null)
222+
{
223+
result.ComponentApplicationBuilderActions.Add(configureBuilder);
224+
}
240225

241226
if (renderModes != null)
242227
{

0 commit comments

Comments
 (0)