Skip to content

Commit e164c8a

Browse files
committed
Support hot reload for Blazor component endpoints
Support hot reload for Blazor component endpoints
1 parent b86599e commit e164c8a

11 files changed

+322
-41
lines changed

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

+52-33
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,27 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp
2424
private readonly IApplicationBuilder _applicationBuilder;
2525
private readonly RenderModeEndpointProvider[] _renderModeEndpointProviders;
2626
private readonly RazorComponentEndpointFactory _factory;
27-
27+
private readonly HotReloadService _hotReloadService;
2828
private List<Endpoint>? _endpoints;
29-
// TODO: Implement endpoint data source updates https://github.com/dotnet/aspnetcore/issues/47026
30-
private readonly CancellationTokenSource _cancellationTokenSource;
31-
private readonly IChangeToken _changeToken;
29+
private CancellationTokenSource _cancellationTokenSource;
30+
private IChangeToken _changeToken;
31+
32+
// Internal for testing.
33+
internal ComponentApplicationBuilder Builder => _builder;
34+
internal List<Action<EndpointBuilder>> Conventions => _conventions;
3235

3336
public RazorComponentEndpointDataSource(
3437
ComponentApplicationBuilder builder,
3538
IEnumerable<RenderModeEndpointProvider> renderModeEndpointProviders,
3639
IApplicationBuilder applicationBuilder,
37-
RazorComponentEndpointFactory factory)
40+
RazorComponentEndpointFactory factory,
41+
HotReloadService hotReloadService)
3842
{
3943
_builder = builder;
4044
_applicationBuilder = applicationBuilder;
4145
_renderModeEndpointProviders = renderModeEndpointProviders.ToArray();
4246
_factory = factory;
47+
_hotReloadService = hotReloadService;
4348
DefaultBuilder = new RazorComponentsEndpointConventionBuilder(
4449
_lock,
4550
builder,
@@ -62,7 +67,7 @@ public override IReadOnlyList<Endpoint> Endpoints
6267
// The order is as follows:
6368
// * MapRazorComponents gets called and the data source gets created.
6469
// * The RazorComponentEndpointConventionBuilder is returned and the user gets a chance to call on it to add conventions.
65-
// * The first request arrives and the DfaMatcherBuilder acesses the data sources to get the endpoints.
70+
// * The first request arrives and the DfaMatcherBuilder accesses the data sources to get the endpoints.
6671
// * The endpoints get created and the conventions get applied.
6772
Initialize();
6873
Debug.Assert(_changeToken != null);
@@ -89,47 +94,61 @@ private void Initialize()
8994

9095
private void UpdateEndpoints()
9196
{
92-
var endpoints = new List<Endpoint>();
93-
var context = _builder.Build();
94-
95-
foreach (var definition in context.Pages)
97+
lock (_lock)
9698
{
97-
_factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions);
98-
}
99+
var endpoints = new List<Endpoint>();
100+
var context = _builder.Build();
99101

100-
ICollection<IComponentRenderMode> renderModes = Options.ConfiguredRenderModes;
102+
foreach (var definition in context.Pages)
103+
{
104+
_factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions);
105+
}
101106

102-
foreach (var renderMode in renderModes)
103-
{
104-
var found = false;
105-
foreach (var provider in _renderModeEndpointProviders)
107+
ICollection<IComponentRenderMode> renderModes = Options.ConfiguredRenderModes;
108+
109+
foreach (var renderMode in renderModes)
106110
{
107-
if (provider.Supports(renderMode))
111+
var found = false;
112+
foreach (var provider in _renderModeEndpointProviders)
113+
{
114+
if (provider.Supports(renderMode))
115+
{
116+
found = true;
117+
RenderModeEndpointProvider.AddEndpoints(
118+
endpoints,
119+
typeof(TRootComponent),
120+
provider.GetEndpointBuilders(renderMode, _applicationBuilder.New()),
121+
renderMode,
122+
_conventions,
123+
_finallyConventions);
124+
}
125+
}
126+
127+
if (!found)
108128
{
109-
found = true;
110-
RenderModeEndpointProvider.AddEndpoints(
111-
endpoints,
112-
typeof(TRootComponent),
113-
provider.GetEndpointBuilders(renderMode, _applicationBuilder.New()),
114-
renderMode,
115-
_conventions,
116-
_finallyConventions);
129+
throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " +
130+
$"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " +
131+
$"Alternatively call 'AddWebAssemblyRenderMode', 'AddServerRenderMode' might be missing if you have set UseDeclaredRenderModes = false.");
117132
}
118133
}
119134

120-
if (!found)
135+
var oldCancellationTokenSource = _cancellationTokenSource;
136+
_endpoints = endpoints;
137+
_cancellationTokenSource = new CancellationTokenSource();
138+
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
139+
oldCancellationTokenSource?.Cancel();
140+
if (_hotReloadService.MetadataUpdateSupported)
121141
{
122-
throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " +
123-
$"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " +
124-
$"Alternatively call 'AddWebAssemblyRenderMode', 'AddServerRenderMode' might be missing if you have set UseDeclaredRenderModes = false.");
142+
ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints);
125143
}
126144
}
127-
128-
_endpoints = endpoints;
129145
}
146+
130147
public override IChangeToken GetChangeToken()
131148
{
132-
// TODO: Handle updates if necessary (for hot reload).
149+
Initialize();
150+
Debug.Assert(_changeToken != null);
151+
Debug.Assert(_endpoints != null);
133152
return _changeToken;
134153
}
135154
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,23 @@ internal class RazorComponentEndpointDataSourceFactory
1414
{
1515
private readonly RazorComponentEndpointFactory _factory;
1616
private readonly IEnumerable<RenderModeEndpointProvider> _providers;
17+
private readonly HotReloadService _hotReloadService;
1718

1819
public RazorComponentEndpointDataSourceFactory(
1920
RazorComponentEndpointFactory factory,
20-
IEnumerable<RenderModeEndpointProvider> providers)
21+
IEnumerable<RenderModeEndpointProvider> providers,
22+
HotReloadService hotReloadService)
2123
{
2224
_factory = factory;
2325
_providers = providers;
26+
_hotReloadService = hotReloadService;
2427
}
2528

2629
public RazorComponentEndpointDataSource<TRootComponent> CreateDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent>(IEndpointRouteBuilder endpoints)
2730
{
2831
var builder = ComponentApplicationBuilder.GetBuilder<TRootComponent>() ??
2932
DefaultRazorComponentApplication<TRootComponent>.Instance.GetBuilder();
3033

31-
return new RazorComponentEndpointDataSource<TRootComponent>(builder, _providers, endpoints.CreateApplicationBuilder(), _factory);
34+
return new RazorComponentEndpointDataSource<TRootComponent>(builder, _providers, endpoints.CreateApplicationBuilder(), _factory, _hotReloadService);
3235
}
3336
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 System.Reflection.Metadata;
5+
using Microsoft.Extensions.Primitives;
6+
7+
[assembly: MetadataUpdateHandler(typeof(Microsoft.AspNetCore.Components.Endpoints.HotReloadService))]
8+
9+
namespace Microsoft.AspNetCore.Components.Endpoints;
10+
11+
internal sealed class HotReloadService : IDisposable
12+
{
13+
public HotReloadService()
14+
{
15+
UpdateApplicationEvent += NotifyUpdateApplication;
16+
MetadataUpdateSupported = MetadataUpdater.IsSupported;
17+
}
18+
19+
private CancellationTokenSource _tokenSource = new();
20+
private static event Action<Type[]?>? UpdateApplicationEvent;
21+
22+
public bool MetadataUpdateSupported { get; internal set; }
23+
24+
public IChangeToken GetChangeToken() => new CancellationChangeToken(_tokenSource.Token);
25+
26+
public static void UpdateApplication(Type[]? changedTypes)
27+
{
28+
UpdateApplicationEvent?.Invoke(changedTypes);
29+
}
30+
31+
private void NotifyUpdateApplication(Type[]? changedTypes)
32+
{
33+
var current = Interlocked.Exchange(ref _tokenSource, new CancellationTokenSource());
34+
current.Cancel();
35+
}
36+
37+
public void Dispose()
38+
{
39+
UpdateApplicationEvent -= NotifyUpdateApplication;
40+
_tokenSource.Dispose();
41+
}
42+
}

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
4545
// Endpoints
4646
services.TryAddSingleton<RazorComponentEndpointDataSourceFactory>();
4747
services.TryAddSingleton<RazorComponentEndpointFactory>();
48+
services.TryAddSingleton<HotReloadService>();
4849
services.TryAddScoped<IRazorComponentEndpointInvoker, RazorComponentEndpointInvoker>();
4950

5051
// Common services required for components server side rendering

0 commit comments

Comments
 (0)