Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support hot reload for Blazor component endpoints #50031

Merged
merged 5 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = null)
{
_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();
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
if (_hotReloadService is { MetadataUpdateSupported : true })
{
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 = null)
{
_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;
captainsafia marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Reflection.Metadata;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Components.Endpoints.DependencyInjection;
Expand Down Expand Up @@ -45,6 +46,10 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
// Endpoints
services.TryAddSingleton<RazorComponentEndpointDataSourceFactory>();
services.TryAddSingleton<RazorComponentEndpointFactory>();
if (MetadataUpdater.IsSupported)
{
services.TryAddSingleton<HotReloadService>();
}
services.TryAddScoped<IRazorComponentEndpointInvoker, RazorComponentEndpointInvoker>();

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