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

[Blazor] Update discovery APIs and automatically configure the endpoints based on the discovered RenderModes #48283

Merged
merged 14 commits into from
May 23, 2023
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Discovery;

namespace Microsoft.AspNetCore.Components.Infrastructure;

/// <summary>
/// Indicates how to collect the components that are part of a razor components
/// application.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public abstract class RazorComponentApplicationAttribute : Attribute, IRazorComponentApplication
{
/// <summary>
/// Creates a builder that can be used to customize the definition of the application.
/// For example, to add or remove pages, change routes, etc.
/// </summary>
/// <returns>
/// The <see cref="ComponentApplicationBuilder"/> associated with the application definition.
/// </returns>
public abstract ComponentApplicationBuilder GetBuilder();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;

namespace Microsoft.AspNetCore.Components.Endpoints;

/// <summary>
/// Options associated with the endpoints defined by the components in the
/// given <see cref="RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents{TRootComponent}(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder)"/>
/// invocation.
/// </summary>
public class RazorComponentDataSourceOptions
{
/// <summary>
/// Gets or sets whether to automatically wire up the necessary endpoints
/// based on the declared render modes of the components that are
/// part of this set of endpoints.
/// </summary>
/// <remarks>
/// The default value is <c>true</c>.
/// </remarks>
public bool UseDeclaredRenderModes { get; set; } = true;

internal IList<IComponentRenderMode> ConfiguredRenderModes { get; } = new List<IComponentRenderMode>();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Discovery;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Components.Web;

namespace Microsoft.AspNetCore.Builder;

Expand All @@ -15,21 +17,69 @@ public class RazorComponentEndpointConventionBuilder : IEndpointConventionBuilde
{
private readonly object _lock;
private readonly ComponentApplicationBuilder _builder;
private readonly RazorComponentDataSourceOptions _options;
private readonly List<Action<EndpointBuilder>> _conventions;
private readonly List<Action<EndpointBuilder>> _finallyConventions;

internal RazorComponentEndpointConventionBuilder(
object @lock,
ComponentApplicationBuilder builder,
RazorComponentDataSourceOptions options,
List<Action<EndpointBuilder>> conventions,
List<Action<EndpointBuilder>> finallyConventions)
{
_lock = @lock;
_builder = builder;
_options = options;
_conventions = conventions;
_finallyConventions = finallyConventions;
}

/// <summary>
/// Gets the <see cref="ComponentApplicationBuilder"/> that is used to build the endpoints.
/// </summary>
public ComponentApplicationBuilder ApplicationBuilder => _builder;

/// <summary>
/// Configures the <see cref="RenderMode.WebAssembly"/> for this application.
/// </summary>
/// <returns>The <see cref="RazorComponentEndpointConventionBuilder"/>.</returns>
public RazorComponentEndpointConventionBuilder AddWebAssemblyRenderMode()
{
for (var i = 0; i < _options.ConfiguredRenderModes.Count; i++)
{
var mode = _options.ConfiguredRenderModes[i];
if (mode is WebAssemblyRenderMode)
{
return this;
}
}

_options.ConfiguredRenderModes.Add(RenderMode.WebAssembly);

return this;
}

/// <summary>
/// Configures the <see cref="RenderMode.Server"/> for this application.
/// </summary>
/// <returns>The <see cref="RazorComponentEndpointConventionBuilder"/>.</returns>
public RazorComponentEndpointConventionBuilder AddServerRenderMode()
{
for (var i = 0; i < _options.ConfiguredRenderModes.Count; i++)
{
var mode = _options.ConfiguredRenderModes[i];
if (mode is ServerRenderMode)
{
return this;
}
}

_options.ConfiguredRenderModes.Add(RenderMode.Server);

return this;
}

/// <inheritdoc/>
public void Add(Action<EndpointBuilder> convention)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Discovery;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
Expand All @@ -14,6 +16,11 @@ internal class RazorComponentEndpointDataSource<TRootComponent> : EndpointDataSo
private readonly object _lock = new();
private readonly List<Action<EndpointBuilder>> _conventions = new();
private readonly List<Action<EndpointBuilder>> _finallyConventions = new();
private readonly RazorComponentDataSourceOptions _options = new();
private readonly ComponentApplicationBuilder _builder;
private readonly IApplicationBuilder _applicationBuilder;
private readonly RenderModeEndpointProvider[] _renderModeEndpointProviders;
private readonly RazorComponentEndpointFactory _factory;

private List<Endpoint>? _endpoints;
// TODO: Implement endpoint data source updates https://github.com/dotnet/aspnetcore/issues/47026
Expand All @@ -22,23 +29,25 @@ internal class RazorComponentEndpointDataSource<TRootComponent> : EndpointDataSo

public RazorComponentEndpointDataSource(
ComponentApplicationBuilder builder,
IEnumerable<RenderModeEndpointProvider> renderModeEndpointProviders,
IApplicationBuilder applicationBuilder,
RazorComponentEndpointFactory factory)
{
_builder = builder;
_applicationBuilder = applicationBuilder;
_renderModeEndpointProviders = renderModeEndpointProviders.ToArray();
_factory = factory;
DefaultBuilder = new RazorComponentEndpointConventionBuilder(
_lock,
builder,
_options,
_conventions,
_finallyConventions);

_cancellationTokenSource = new CancellationTokenSource();
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
}

private readonly ComponentApplicationBuilder _builder;
private readonly RazorComponentEndpointFactory _factory;

internal RazorComponentEndpointConventionBuilder DefaultBuilder { get; }

public override IReadOnlyList<Endpoint> Endpoints
Expand All @@ -59,6 +68,8 @@ public override IReadOnlyList<Endpoint> Endpoints
}
}

internal RazorComponentDataSourceOptions Options => _options;

private void Initialize()
{
if (_endpoints == null)
Expand All @@ -77,14 +88,49 @@ private void UpdateEndpoints()
{
var endpoints = new List<Endpoint>();
var context = _builder.Build();

foreach (var definition in context.Pages)
{
_factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions);
}

ICollection<IComponentRenderMode> renderModes = Options.ConfiguredRenderModes;

if (Options.UseDeclaredRenderModes)
{
var componentRenderModes = context.GetDeclaredRenderModesByDiscoveredComponents();
componentRenderModes.UnionWith(Options.ConfiguredRenderModes);
renderModes = componentRenderModes;
}

foreach (var renderMode in renderModes)
{
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)
{
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.");
}
}
javiercn marked this conversation as resolved.
Show resolved Hide resolved

_endpoints = endpoints;
}

public override IChangeToken GetChangeToken()
{
// TODO: Handle updates if necessary (for hot reload).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Discovery;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Routing;

namespace Microsoft.AspNetCore.Components.Infrastructure;

internal class RazorComponentEndpointDataSourceFactory
{
private readonly RazorComponentEndpointFactory _factory;
private readonly IEnumerable<RenderModeEndpointProvider> _providers;

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

public RazorComponentEndpointDataSource<TRootComponent> CreateDataSource<TRootComponent>()
where TRootComponent : IRazorComponentApplication<TRootComponent>
public RazorComponentEndpointDataSource<TRootComponent> CreateDataSource<TRootComponent>(IEndpointRouteBuilder endpoints)
{
var builder = TRootComponent.GetBuilder();
return new RazorComponentEndpointDataSource<TRootComponent>(builder, _factory);
var builder = ComponentApplicationBuilder.GetBuilder<TRootComponent>() ??
DefaultRazorComponentApplication<TRootComponent>.Instance.GetBuilder();

return new RazorComponentEndpointDataSource<TRootComponent>(builder, _providers, endpoints.CreateApplicationBuilder(), _factory);
}
}
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 Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Discovery;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
Expand All @@ -17,7 +18,7 @@ internal void AddEndpoints(
#pragma warning restore CA1822 // It's a singleton
List<Endpoint> endpoints,
Type rootComponent,
PageDefinition pageDefinition,
PageComponentInfo pageDefinition,
IReadOnlyList<Action<EndpointBuilder>> conventions,
IReadOnlyList<Action<EndpointBuilder>> finallyConventions)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Http;
Expand All @@ -24,7 +23,6 @@ public static class RazorComponentsEndpointRouteBuilderExtensions
/// <param name="endpoints"></param>
/// <returns></returns>
public static RazorComponentEndpointConventionBuilder MapRazorComponents<TRootComponent>(this IEndpointRouteBuilder endpoints)
where TRootComponent : IRazorComponentApplication<TRootComponent>
{
ArgumentNullException.ThrowIfNull(endpoints);

Expand Down Expand Up @@ -66,7 +64,6 @@ private static void AddBlazorWebJsEndpoint(IEndpointRouteBuilder endpoints)
}

private static RazorComponentEndpointDataSource<TRootComponent> GetOrCreateDataSource<TRootComponent>(IEndpointRouteBuilder endpoints)
where TRootComponent : IRazorComponentApplication<TRootComponent>
{
var dataSource = endpoints.DataSources.OfType<RazorComponentEndpointDataSource<TRootComponent>>().FirstOrDefault();
if (dataSource == null)
Expand All @@ -75,7 +72,7 @@ private static RazorComponentEndpointDataSource<TRootComponent> GetOrCreateDataS
// sources, once we figure out the exact scenarios for
// https://github.com/dotnet/aspnetcore/issues/46992
var factory = endpoints.ServiceProvider.GetRequiredService<RazorComponentEndpointDataSourceFactory>();
dataSource = factory.CreateDataSource<TRootComponent>();
dataSource = factory.CreateDataSource<TRootComponent>(endpoints);
endpoints.DataSources.Add(dataSource);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace Microsoft.AspNetCore.Components.Endpoints;

/// <summary>
/// A provider that can register endpoints to support a specific <see cref="IComponentRenderMode"/>.
/// </summary>
public abstract class RenderModeEndpointProvider
{
/// <summary>
/// Determines whether this <see cref="RenderModeEndpointProvider"/> supports the specified <paramref name="renderMode"/>.
/// </summary>
/// <param name="renderMode">The <see cref="IComponentRenderMode"/>.</param>
/// <returns><c>true</c> if the <see cref="IComponentRenderMode"/> is supported; <c>false</c> otherwise.</returns>
public abstract bool Supports(IComponentRenderMode renderMode);

/// <summary>
/// Gets the endpoints for the specified <paramref name="renderMode"/>.
/// </summary>
/// <param name="renderMode">The <see cref="IComponentRenderMode"/>.</param>
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> used to configure non endpoint aware endpoints.</param>
/// <returns>The list of endpoints this provider is registering.</returns>
public abstract IEnumerable<RouteEndpointBuilder> GetEndpointBuilders(
IComponentRenderMode renderMode,
IApplicationBuilder applicationBuilder);

internal static void AddEndpoints(
List<Endpoint> endpoints,
Type rootComponent,
IEnumerable<RouteEndpointBuilder> renderModeEndpoints,
IComponentRenderMode renderMode,
List<Action<EndpointBuilder>> conventions,
List<Action<EndpointBuilder>> finallyConventions)
{
foreach (var builder in renderModeEndpoints)
{
builder.Metadata.Add(new RootComponentMetadata(rootComponent));
builder.Metadata.Add(renderMode);

foreach (var convention in conventions)
{
convention(builder);
}

foreach (var finallyConvention in finallyConventions)
{
finallyConvention(builder);
}

endpoints.Add(builder.Build());
}
}
}
Loading