Skip to content

Commit

Permalink
Support hot reload for Blazor component endpoints
Browse files Browse the repository at this point in the history
Support hot reload for Blazor component endpoints
  • Loading branch information
captainsafia committed Aug 17, 2023
1 parent b86599e commit e164c8a
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,27 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp
private readonly IApplicationBuilder _applicationBuilder;
private readonly RenderModeEndpointProvider[] _renderModeEndpointProviders;
private readonly RazorComponentEndpointFactory _factory;

private readonly HotReloadService _hotReloadService;
private List<Endpoint>? _endpoints;
// TODO: Implement endpoint data source updates https://github.com/dotnet/aspnetcore/issues/47026
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly IChangeToken _changeToken;
private CancellationTokenSource _cancellationTokenSource;
private IChangeToken _changeToken;

// Internal for testing.
internal ComponentApplicationBuilder Builder => _builder;
internal List<Action<EndpointBuilder>> Conventions => _conventions;

public RazorComponentEndpointDataSource(
ComponentApplicationBuilder builder,
IEnumerable<RenderModeEndpointProvider> renderModeEndpointProviders,
IApplicationBuilder applicationBuilder,
RazorComponentEndpointFactory factory)
RazorComponentEndpointFactory factory,
HotReloadService hotReloadService)
{
_builder = builder;
_applicationBuilder = applicationBuilder;
_renderModeEndpointProviders = renderModeEndpointProviders.ToArray();
_factory = factory;
_hotReloadService = hotReloadService;
DefaultBuilder = new RazorComponentsEndpointConventionBuilder(
_lock,
builder,
Expand All @@ -62,7 +67,7 @@ public override IReadOnlyList<Endpoint> Endpoints
// The order is as follows:
// * MapRazorComponents gets called and the data source gets created.
// * The RazorComponentEndpointConventionBuilder is returned and the user gets a chance to call on it to add conventions.
// * The first request arrives and the DfaMatcherBuilder acesses the data sources to get the endpoints.
// * The first request arrives and the DfaMatcherBuilder accesses the data sources to get the endpoints.
// * The endpoints get created and the conventions get applied.
Initialize();
Debug.Assert(_changeToken != null);
Expand All @@ -89,47 +94,61 @@ private void Initialize()

private void UpdateEndpoints()
{
var endpoints = new List<Endpoint>();
var context = _builder.Build();

foreach (var definition in context.Pages)
lock (_lock)
{
_factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions);
}
var endpoints = new List<Endpoint>();
var context = _builder.Build();

ICollection<IComponentRenderMode> renderModes = Options.ConfiguredRenderModes;
foreach (var definition in context.Pages)
{
_factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions);
}

foreach (var renderMode in renderModes)
{
var found = false;
foreach (var provider in _renderModeEndpointProviders)
ICollection<IComponentRenderMode> renderModes = Options.ConfiguredRenderModes;

foreach (var renderMode in renderModes)
{
if (provider.Supports(renderMode))
var found = false;
foreach (var provider in _renderModeEndpointProviders)
{
if (provider.Supports(renderMode))
{
found = true;
RenderModeEndpointProvider.AddEndpoints(
endpoints,
typeof(TRootComponent),
provider.GetEndpointBuilders(renderMode, _applicationBuilder.New()),
renderMode,
_conventions,
_finallyConventions);
}
}

if (!found)
{
found = true;
RenderModeEndpointProvider.AddEndpoints(
endpoints,
typeof(TRootComponent),
provider.GetEndpointBuilders(renderMode, _applicationBuilder.New()),
renderMode,
_conventions,
_finallyConventions);
throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " +
$"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " +
$"Alternatively call 'AddWebAssemblyRenderMode', 'AddServerRenderMode' might be missing if you have set UseDeclaredRenderModes = false.");
}
}

if (!found)
var oldCancellationTokenSource = _cancellationTokenSource;
_endpoints = endpoints;
_cancellationTokenSource = new CancellationTokenSource();
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
oldCancellationTokenSource?.Cancel();
if (_hotReloadService.MetadataUpdateSupported)
{
throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " +
$"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " +
$"Alternatively call 'AddWebAssemblyRenderMode', 'AddServerRenderMode' might be missing if you have set UseDeclaredRenderModes = false.");
ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints);
}
}

_endpoints = endpoints;
}

public override IChangeToken GetChangeToken()
{
// TODO: Handle updates if necessary (for hot reload).
Initialize();
Debug.Assert(_changeToken != null);
Debug.Assert(_endpoints != null);
return _changeToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@ internal class RazorComponentEndpointDataSourceFactory
{
private readonly RazorComponentEndpointFactory _factory;
private readonly IEnumerable<RenderModeEndpointProvider> _providers;
private readonly HotReloadService _hotReloadService;

public RazorComponentEndpointDataSourceFactory(
RazorComponentEndpointFactory factory,
IEnumerable<RenderModeEndpointProvider> providers)
IEnumerable<RenderModeEndpointProvider> providers,
HotReloadService hotReloadService)
{
_factory = factory;
_providers = providers;
_hotReloadService = hotReloadService;
}

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

return new RazorComponentEndpointDataSource<TRootComponent>(builder, _providers, endpoints.CreateApplicationBuilder(), _factory);
return new RazorComponentEndpointDataSource<TRootComponent>(builder, _providers, endpoints.CreateApplicationBuilder(), _factory, _hotReloadService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection.Metadata;
using Microsoft.Extensions.Primitives;

[assembly: MetadataUpdateHandler(typeof(Microsoft.AspNetCore.Components.Endpoints.HotReloadService))]

namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed class HotReloadService : IDisposable
{
public HotReloadService()
{
UpdateApplicationEvent += NotifyUpdateApplication;
MetadataUpdateSupported = MetadataUpdater.IsSupported;
}

private CancellationTokenSource _tokenSource = new();
private static event Action<Type[]?>? UpdateApplicationEvent;

public bool MetadataUpdateSupported { get; internal set; }

public IChangeToken GetChangeToken() => new CancellationChangeToken(_tokenSource.Token);

public static void UpdateApplication(Type[]? changedTypes)
{
UpdateApplicationEvent?.Invoke(changedTypes);
}

private void NotifyUpdateApplication(Type[]? changedTypes)
{
var current = Interlocked.Exchange(ref _tokenSource, new CancellationTokenSource());
current.Cancel();
}

public void Dispose()
{
UpdateApplicationEvent -= NotifyUpdateApplication;
_tokenSource.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
// Endpoints
services.TryAddSingleton<RazorComponentEndpointDataSourceFactory>();
services.TryAddSingleton<RazorComponentEndpointFactory>();
services.TryAddSingleton<HotReloadService>();
services.TryAddScoped<IRazorComponentEndpointInvoker, RazorComponentEndpointInvoker>();

// Common services required for components server side rendering
Expand Down
Loading

0 comments on commit e164c8a

Please sign in to comment.