Skip to content

Commit

Permalink
[Blazor] Update discovery APIs and automatically configure the endpoi…
Browse files Browse the repository at this point in the history
…nts based on the discovered RenderModes (#48283)

* Removes IRazorComponentApplication<TRootComponent>
* Adds APIs for component discovery across assemblies.
* Adds support for wiring up server endpoints.
* Adds support for automatic render mode detection.
  • Loading branch information
javiercn authored May 23, 2023
1 parent d9b636c commit d622921
Show file tree
Hide file tree
Showing 36 changed files with 1,878 additions and 177 deletions.
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.");
}
}

_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
58 changes: 58 additions & 0 deletions src/Components/Endpoints/src/Builder/RenderModeEndpointProvider.cs
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

0 comments on commit d622921

Please sign in to comment.