Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -20,8 +20,8 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp
private readonly object _lock = new();
private readonly List<Action<EndpointBuilder>> _conventions = [];
private readonly List<Action<EndpointBuilder>> _finallyConventions = [];
private readonly List<Action<ComponentApplicationBuilder>> _componentApplicationBuilderActions = [];
private readonly RazorComponentDataSourceOptions _options = new();
private readonly ComponentApplicationBuilder _builder;
private readonly IEndpointRouteBuilder _endpointRouteBuilder;
private readonly ResourceCollectionResolver _resourceCollectionResolver;
private readonly RenderModeEndpointProvider[] _renderModeEndpointProviders;
Expand All @@ -32,33 +32,29 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp
private IChangeToken _changeToken;
private IDisposable? _disposableChangeToken; // THREADING: protected by _lock

public Func<IDisposable, IDisposable> SetDisposableChangeTokenAction = disposableChangeToken => disposableChangeToken;

// Internal for testing.
internal ComponentApplicationBuilder Builder => _builder;
internal List<Action<EndpointBuilder>> Conventions => _conventions;
internal List<Action<ComponentApplicationBuilder>> ComponentApplicationBuilderActions => _componentApplicationBuilderActions;
internal CancellationTokenSource ChangeTokenSource => _cancellationTokenSource;

public RazorComponentEndpointDataSource(
ComponentApplicationBuilder builder,
IEnumerable<RenderModeEndpointProvider> renderModeEndpointProviders,
IEndpointRouteBuilder endpointRouteBuilder,
RazorComponentEndpointFactory factory,
HotReloadService? hotReloadService = null)
{
_builder = builder;
_endpointRouteBuilder = endpointRouteBuilder;
_resourceCollectionResolver = new ResourceCollectionResolver(endpointRouteBuilder);
_renderModeEndpointProviders = renderModeEndpointProviders.ToArray();
_factory = factory;
_hotReloadService = hotReloadService;
HotReloadService.ClearCacheEvent += OnHotReloadClearCache;
DefaultBuilder = new RazorComponentsEndpointConventionBuilder(
_lock,
builder,
endpointRouteBuilder,
_options,
_conventions,
_finallyConventions);
_finallyConventions,
_componentApplicationBuilderActions);

_cancellationTokenSource = new CancellationTokenSource();
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
Expand Down Expand Up @@ -106,8 +102,20 @@ private void UpdateEndpoints()

lock (_lock)
{
_disposableChangeToken?.Dispose();
_disposableChangeToken = null;

var endpoints = new List<Endpoint>();
var context = _builder.Build();

var componentApplicationBuilder = new ComponentApplicationBuilder();

foreach (var action in ComponentApplicationBuilderActions)
{
action?.Invoke(componentApplicationBuilder);
}

var context = componentApplicationBuilder.Build();

var configuredRenderModesMetadata = new ConfiguredRenderModesMetadata(
[.. Options.ConfiguredRenderModes]);

Expand Down Expand Up @@ -168,8 +176,7 @@ private void UpdateEndpoints()
oldCancellationTokenSource?.Dispose();
if (_hotReloadService is { MetadataUpdateSupported: true })
{
_disposableChangeToken?.Dispose();
_disposableChangeToken = SetDisposableChangeTokenAction(ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints));
_disposableChangeToken = ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints);
}
}
}
Expand All @@ -195,15 +202,6 @@ private void AddBlazorWebEndpoints(List<Endpoint> endpoints)
}
}

public void OnHotReloadClearCache(Type[]? types)
{
lock (_lock)
{
_disposableChangeToken?.Dispose();
_disposableChangeToken = null;
}
}

public override IChangeToken GetChangeToken()
{
Initialize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ internal class RazorComponentEndpointDataSourceFactory(
{
public RazorComponentEndpointDataSource<TRootComponent> CreateDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent>(IEndpointRouteBuilder endpoints)
{
var builder = ComponentApplicationBuilder.GetBuilder<TRootComponent>() ??
DefaultRazorComponentApplication<TRootComponent>.Instance.GetBuilder();
var dataSource = new RazorComponentEndpointDataSource<TRootComponent>(providers, endpoints, factory, hotReloadService);

return new RazorComponentEndpointDataSource<TRootComponent>(builder, providers, endpoints, factory, hotReloadService);
dataSource.ComponentApplicationBuilderActions.Add(builder =>
{
var assembly = typeof(TRootComponent).Assembly;
IRazorComponentApplication.GetBuilderForAssembly(builder, assembly);
});

return dataSource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,25 @@ public sealed class RazorComponentsEndpointConventionBuilder : IEndpointConventi
private readonly RazorComponentDataSourceOptions _options;
private readonly List<Action<EndpointBuilder>> _conventions;
private readonly List<Action<EndpointBuilder>> _finallyConventions;
private readonly List<Action<ComponentApplicationBuilder>> _componentApplicationBuilderActions;

internal RazorComponentsEndpointConventionBuilder(
object @lock,
ComponentApplicationBuilder builder,
IEndpointRouteBuilder endpointRouteBuilder,
RazorComponentDataSourceOptions options,
List<Action<EndpointBuilder>> conventions,
List<Action<EndpointBuilder>> finallyConventions)
List<Action<EndpointBuilder>> finallyConventions,
List<Action<ComponentApplicationBuilder>> componentApplicationBuilderActions)
{
_lock = @lock;
ApplicationBuilder = builder;
EndpointRouteBuilder = endpointRouteBuilder;
_options = options;
_conventions = conventions;
_finallyConventions = finallyConventions;
_componentApplicationBuilderActions = componentApplicationBuilderActions;
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static RazorComponentsEndpointConventionBuilder AddAdditionalAssemblies(

foreach (var assembly in assemblies)
{
builder.ApplicationBuilder.AddAssembly(assembly);
builder.ComponentApplicationBuilderActions.Add(b => b.AddAssembly(assembly));
}
return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,15 @@ public void MapRazorComponents_CanAddConventions_ToBlazorWebEndpoints(string fra
private RazorComponentsEndpointConventionBuilder CreateRazorComponentsAppBuilder(IEndpointRouteBuilder endpointBuilder)
{
var builder = endpointBuilder.MapRazorComponents<App>();
builder.ApplicationBuilder.AddLibrary(new AssemblyComponentLibraryDescriptor(
builder.ComponentApplicationBuilderActions.Add(b => b.AddLibrary(new AssemblyComponentLibraryDescriptor(
"App",
[new PageComponentBuilder {
PageType = typeof(App),
RouteTemplates = ["/"],
AssemblyName = "App",
}],
[]
));
)));
return builder;
}

Expand Down
93 changes: 39 additions & 54 deletions src/Components/Endpoints/test/HotReloadServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ public class HotReloadServiceTests
public void UpdatesEndpointsWhenHotReloadChangeTokenTriggered()
{
// Arrange
var builder = CreateBuilder(typeof(ServerComponent));
var services = CreateServices(typeof(MockEndpointProvider));
var endpointDataSource = CreateDataSource<App>(builder, services);
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);
var invoked = false;

// Act
Expand All @@ -41,9 +40,8 @@ public void UpdatesEndpointsWhenHotReloadChangeTokenTriggered()
public void AddNewEndpointWhenDataSourceChanges()
{
// Arrange
var builder = CreateBuilder(typeof(ServerComponent));
var services = CreateServices(typeof(MockEndpointProvider));
var endpointDataSource = CreateDataSource<App>(builder, services);
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);

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

// Act - 2
endpointDataSource.Builder.Pages.AddFromLibraryInfo("TestAssembly2", new[]
{
new PageComponentBuilder
endpointDataSource.ComponentApplicationBuilderActions.Add(
b => b.Pages.AddFromLibraryInfo("TestAssembly2", new[]
{
AssemblyName = "TestAssembly2",
PageType = typeof(StaticComponent),
RouteTemplates = new List<string> { "/app/test" }
}
});
new PageComponentBuilder
{
AssemblyName = "TestAssembly2",
PageType = typeof(StaticComponent),
RouteTemplates = new List<string> { "/app/test" }
}
}));

HotReloadService.UpdateApplication(null);

// Assert - 2
Expand All @@ -76,9 +76,8 @@ public void AddNewEndpointWhenDataSourceChanges()
public void RemovesEndpointWhenDataSourceChanges()
{
// Arrange
var builder = CreateBuilder(typeof(ServerComponent));
var services = CreateServices(typeof(MockEndpointProvider));
var endpointDataSource = CreateDataSource<App>(builder, services);
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);

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

// Act - 2
endpointDataSource.Builder.RemoveLibrary("TestAssembly");
endpointDataSource.ComponentApplicationBuilderActions.Add(b => b.RemoveLibrary("TestAssembly"));
endpointDataSource.Options.ConfiguredRenderModes.Clear();
HotReloadService.UpdateApplication(null);

Expand All @@ -100,9 +99,8 @@ public void RemovesEndpointWhenDataSourceChanges()
public void ModifiesEndpointWhenDataSourceChanges()
{
// Arrange
var builder = CreateBuilder(typeof(ServerComponent));
var services = CreateServices(typeof(MockEndpointProvider));
var endpointDataSource = CreateDataSource<App>(builder, services);
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);

// Assert - 1
var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpointDataSource.Endpoints, e => e.Metadata.GetMetadata<RootComponentMetadata>() != null));
Expand All @@ -124,9 +122,8 @@ public void ModifiesEndpointWhenDataSourceChanges()
public void NotifiesCompositeEndpointDataSource()
{
// Arrange
var builder = CreateBuilder(typeof(ServerComponent));
var services = CreateServices(typeof(MockEndpointProvider));
var endpointDataSource = CreateDataSource<App>(builder, services);
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder);
var compositeEndpointDataSource = new CompositeEndpointDataSource(
new[] { endpointDataSource });

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

// Act - 2
endpointDataSource.Builder.Pages.RemoveFromAssembly("TestAssembly");
endpointDataSource.ComponentApplicationBuilderActions.Add(b => b.Pages.RemoveFromAssembly("TestAssembly"));
endpointDataSource.Options.ConfiguredRenderModes.Clear();
HotReloadService.UpdateApplication(null);

Expand All @@ -148,37 +145,14 @@ public void NotifiesCompositeEndpointDataSource()
Assert.Empty(compositePageEndpoints);
}

private sealed class WrappedChangeTokenDisposable : IDisposable
{
public bool IsDisposed { get; private set; }
private readonly IDisposable _innerDisposable;

public WrappedChangeTokenDisposable(IDisposable innerDisposable)
{
_innerDisposable = innerDisposable;
}

public void Dispose()
{
IsDisposed = true;
_innerDisposable.Dispose();
}
}

[Fact]
public void ConfirmChangeTokenDisposedHotReload()
{
// Arrange
var builder = CreateBuilder(typeof(ServerComponent));
var services = CreateServices(typeof(MockEndpointProvider));
var endpointDataSource = CreateDataSource<App>(builder, services);

WrappedChangeTokenDisposable wrappedChangeTokenDisposable = null;

endpointDataSource.SetDisposableChangeTokenAction = (IDisposable disposableChangeToken) => {
wrappedChangeTokenDisposable = new WrappedChangeTokenDisposable(disposableChangeToken);
return wrappedChangeTokenDisposable;
};
var endpointDataSource = CreateDataSource<App>(services, ConfigureServerComponentBuilder, null);
var changeTokenSource = endpointDataSource.ChangeTokenSource;
var changeToken = endpointDataSource.GetChangeToken();

var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpointDataSource.Endpoints, e => e.Metadata.GetMetadata<RootComponentMetadata>() != null));
Assert.Equal("/server", endpoint.RoutePattern.RawText);
Expand All @@ -187,18 +161,21 @@ public void ConfirmChangeTokenDisposedHotReload()
// Make a modification and then perform a hot reload.
endpointDataSource.Conventions.Add(builder =>
builder.Metadata.Add(new TestMetadata()));

HotReloadService.UpdateApplication(null);
HotReloadService.ClearCache(null);

// Confirm the change token is disposed after ClearCache
Assert.True(wrappedChangeTokenDisposable.IsDisposed);
Assert.True(changeToken.HasChanged);
Assert.Throws<ObjectDisposedException>(() => changeTokenSource.Token);
}

private class TestMetadata { }

private ComponentApplicationBuilder CreateBuilder(params Type[] types)
private class TestAssembly : Assembly;

private static void ConfigureBuilder(ComponentApplicationBuilder builder, params Type[] types)
{
var builder = new ComponentApplicationBuilder();
builder.AddLibrary(new AssemblyComponentLibraryDescriptor(
"TestAssembly",
Array.Empty<PageComponentBuilder>(),
Expand All @@ -208,8 +185,11 @@ private ComponentApplicationBuilder CreateBuilder(params Type[] types)
ComponentType = t,
RenderMode = t.GetCustomAttribute<RenderModeAttribute>()
}).ToArray()));
}

return builder;
private static void ConfigureServerComponentBuilder(ComponentApplicationBuilder builder)
{
ConfigureBuilder(builder, typeof(StaticComponent));
}

private IServiceProvider CreateServices(params Type[] types)
Expand All @@ -227,16 +207,21 @@ private IServiceProvider CreateServices(params Type[] types)
}

private static RazorComponentEndpointDataSource<TComponent> CreateDataSource<TComponent>(
ComponentApplicationBuilder builder,
IServiceProvider services,
IComponentRenderMode[] renderModes = null)
Action<ComponentApplicationBuilder> configureBuilder = null,
IComponentRenderMode[] renderModes = null,
HotReloadService hotReloadService = null)
{
var result = new RazorComponentEndpointDataSource<TComponent>(
builder,
new[] { new MockEndpointProvider() },
new TestEndpointRouteBuilder(services),
new RazorComponentEndpointFactory(),
new HotReloadService() { MetadataUpdateSupported = true });
hotReloadService ?? new HotReloadService() { MetadataUpdateSupported = true });

if (configureBuilder is not null)
{
result.ComponentApplicationBuilderActions.Add(configureBuilder);
}

if (renderModes != null)
{
Expand Down
Loading
Loading