Skip to content

Commit e7603bc

Browse files
authored
[Blazor] Endpoint discovery and configuration (#47126)
* Adds support for discovering razor pages and registering them as endpoints. * Makes EndpointDataSource generic to support multiple instances in the future. * Removes reflection from discovery and pushes it to the default implementation. * Adds the appropriate metadata to the endpoints. * Renders the component using the ComponentResult.
1 parent 6a7b392 commit e7603bc

17 files changed

+642
-43
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Builder;
5+
6+
/// <summary>
7+
/// Metadata that represents the component associated with an endpoint.
8+
/// </summary>
9+
public class ComponentTypeMetadata
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of <see cref="ComponentTypeMetadata"/>.
13+
/// </summary>
14+
/// <param name="componentType">The component type.</param>
15+
public ComponentTypeMetadata(Type componentType)
16+
{
17+
Type = componentType;
18+
}
19+
20+
/// <summary>
21+
/// Gets the component type.
22+
/// </summary>
23+
public Type Type { get; }
24+
}

src/Components/Endpoints/src/Builder/RazorComponentEndpointConventionBuilder.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Components;
5+
46
namespace Microsoft.AspNetCore.Builder;
57

6-
internal class RazorComponentEndpointConventionBuilder : IEndpointConventionBuilder
8+
/// <summary>
9+
/// Builds conventions that will be used for customization of <see cref="EndpointBuilder"/> instances.
10+
/// </summary>
11+
12+
// TODO: This will have APIs to add and remove entire assemblies from the list of considered endpoints
13+
// as well as adding/removing individual pages as endpoints.
14+
public class RazorComponentEndpointConventionBuilder : IEndpointConventionBuilder
715
{
816
private readonly object _lock;
17+
private readonly ComponentApplicationBuilder _builder;
918
private readonly List<Action<EndpointBuilder>> _conventions;
19+
private readonly List<Action<EndpointBuilder>> _finallyConventions;
1020

11-
internal RazorComponentEndpointConventionBuilder(object @lock, List<Action<EndpointBuilder>> conventions)
21+
internal RazorComponentEndpointConventionBuilder(
22+
object @lock,
23+
ComponentApplicationBuilder builder,
24+
List<Action<EndpointBuilder>> conventions,
25+
List<Action<EndpointBuilder>> finallyConventions)
1226
{
1327
_lock = @lock;
28+
_builder = builder;
1429
_conventions = conventions;
30+
_finallyConventions = finallyConventions;
1531
}
1632

33+
/// <inheritdoc/>
1734
public void Add(Action<EndpointBuilder> convention)
1835
{
1936
ArgumentNullException.ThrowIfNull(convention);
@@ -25,4 +42,15 @@ public void Add(Action<EndpointBuilder> convention)
2542
_conventions.Add(convention);
2643
}
2744
}
45+
46+
/// <inheritdoc/>
47+
public void Finally(Action<EndpointBuilder> finallyConvention)
48+
{
49+
// The lock is shared with the data source. We want to lock here
50+
// to avoid mutating this list while its read in the data source.
51+
lock (_lock)
52+
{
53+
_finallyConventions.Add(finallyConvention);
54+
}
55+
}
2856
}

src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,43 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5-
using System.Linq;
6-
using System.Reflection;
7-
using Microsoft.AspNetCore.Components;
8-
using Microsoft.AspNetCore.Components.Endpoints;
5+
using Microsoft.AspNetCore.Builder;
96
using Microsoft.AspNetCore.Http;
107
using Microsoft.AspNetCore.Routing;
11-
using Microsoft.AspNetCore.Routing.Patterns;
128
using Microsoft.Extensions.Primitives;
139

14-
namespace Microsoft.AspNetCore.Builder;
10+
namespace Microsoft.AspNetCore.Components.Endpoints;
1511

16-
internal class RazorComponentEndpointDataSource : EndpointDataSource
12+
internal class RazorComponentEndpointDataSource<TRootComponent> : EndpointDataSource
1713
{
18-
private readonly object _lock = new object();
14+
private readonly object _lock = new();
1915
private readonly List<Action<EndpointBuilder>> _conventions = new();
16+
private readonly List<Action<EndpointBuilder>> _finallyConventions = new();
2017

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

26-
public RazorComponentEndpointDataSource()
23+
public RazorComponentEndpointDataSource(
24+
ComponentApplicationBuilder builder,
25+
RazorComponentEndpointFactory factory)
2726
{
28-
DefaultBuilder = new RazorComponentEndpointConventionBuilder(_lock, _conventions);
27+
_builder = builder;
28+
_factory = factory;
29+
DefaultBuilder = new RazorComponentEndpointConventionBuilder(
30+
_lock,
31+
builder,
32+
_conventions,
33+
_finallyConventions);
2934

3035
_cancellationTokenSource = new CancellationTokenSource();
3136
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
3237
}
3338

39+
private readonly ComponentApplicationBuilder _builder;
40+
private readonly RazorComponentEndpointFactory _factory;
41+
3442
internal RazorComponentEndpointConventionBuilder DefaultBuilder { get; }
3543

3644
public override IReadOnlyList<Endpoint> Endpoints
@@ -67,26 +75,11 @@ private void Initialize()
6775

6876
private void UpdateEndpoints()
6977
{
70-
// TODO: https://github.com/dotnet/aspnetcore/issues/46980
71-
72-
var entryPoint = Assembly.GetEntryAssembly() ??
73-
throw new InvalidOperationException("Can't find entry assembly.");
74-
75-
var pages = entryPoint.GetExportedTypes()
76-
.Select(t => (type: t, route: t.GetCustomAttribute<RouteAttribute>()))
77-
.Where(p => p.route != null);
78-
7978
var endpoints = new List<Endpoint>();
80-
foreach (var (type, route) in pages)
79+
var context = _builder.Build();
80+
foreach (var definitions in context.Pages)
8181
{
82-
// TODO: Proper endpoint definition https://github.com/dotnet/aspnetcore/issues/46985
83-
var endpoint = new RouteEndpoint(
84-
RazorComponentEndpoint.CreateRouteDelegate(type),
85-
RoutePatternFactory.Parse(route!.Template),
86-
order: 0,
87-
new EndpointMetadataCollection(type.GetCustomAttributes(inherit: true)),
88-
route.Template);
89-
endpoints.Add(endpoint);
82+
_factory.AddEndpoints(endpoints, definitions, _conventions, _finallyConventions);
9083
}
9184

9285
_endpoints = endpoints;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components.Endpoints;
5+
6+
namespace Microsoft.AspNetCore.Components.Infrastructure;
7+
8+
internal class RazorComponentEndpointDataSourceFactory
9+
{
10+
private readonly RazorComponentEndpointFactory _factory;
11+
12+
public RazorComponentEndpointDataSourceFactory(RazorComponentEndpointFactory factory)
13+
{
14+
_factory = factory;
15+
}
16+
17+
public RazorComponentEndpointDataSource<TRootComponent> CreateDataSource<TRootComponent>()
18+
where TRootComponent : IRazorComponentApplication<TRootComponent>
19+
{
20+
var builder = TRootComponent.GetBuilder();
21+
return new RazorComponentEndpointDataSource<TRootComponent>(builder, _factory);
22+
}
23+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Routing;
7+
using Microsoft.AspNetCore.Routing.Patterns;
8+
9+
namespace Microsoft.AspNetCore.Components.Endpoints;
10+
11+
internal class RazorComponentEndpointFactory
12+
{
13+
private static readonly HttpMethodMetadata HttpGet = new(new[] { HttpMethods.Get });
14+
15+
#pragma warning disable CA1822 // It's a singleton
16+
internal void AddEndpoints(
17+
#pragma warning restore CA1822 // It's a singleton
18+
List<Endpoint> endpoints,
19+
PageDefinition pageDefinition,
20+
IReadOnlyList<Action<EndpointBuilder>> conventions,
21+
IReadOnlyList<Action<EndpointBuilder>> finallyConventions)
22+
{
23+
// We do not provide a way to establish the order or the name for the page routes.
24+
// Order is not supported in our client router.
25+
// Name is only relevant for Link generation, which we don't support either.
26+
var builder = new RouteEndpointBuilder(
27+
null,
28+
RoutePatternFactory.Parse(pageDefinition.Route),
29+
order: 0);
30+
31+
// All attributes defined for the type are included as metadata.
32+
foreach (var attribute in pageDefinition.Metadata)
33+
{
34+
builder.Metadata.Add(attribute);
35+
}
36+
37+
// We do not support link generation, so explicitly opt-out.
38+
builder.Metadata.Add(new SuppressLinkGenerationMetadata());
39+
builder.Metadata.Add(HttpGet);
40+
builder.Metadata.Add(new ComponentTypeMetadata(pageDefinition.Type));
41+
42+
foreach (var convention in conventions)
43+
{
44+
convention(builder);
45+
}
46+
47+
foreach (var finallyConvention in finallyConventions)
48+
{
49+
finallyConvention(builder);
50+
}
51+
52+
// Always override the order, since our client router does not support it.
53+
builder.Order = 0;
54+
55+
// The display name is for debug purposes by endpoint routing.
56+
builder.DisplayName = $"{builder.RoutePattern.RawText} ({pageDefinition.DisplayName})";
57+
58+
builder.RequestDelegate = RazorComponentEndpoint.CreateRouteDelegate(pageDefinition.Type);
59+
60+
endpoints.Add(builder.Build());
61+
}
62+
}

src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Linq;
5+
using Microsoft.AspNetCore.Components;
56
using Microsoft.AspNetCore.Components.Endpoints;
7+
using Microsoft.AspNetCore.Components.Infrastructure;
68
using Microsoft.AspNetCore.Routing;
79
using Microsoft.Extensions.DependencyInjection;
810

@@ -18,24 +20,27 @@ public static class RazorComponentsEndpointRouteBuilderExtensions
1820
/// </summary>
1921
/// <param name="endpoints"></param>
2022
/// <returns></returns>
21-
public static IEndpointConventionBuilder MapRazorComponents(this IEndpointRouteBuilder endpoints)
23+
public static RazorComponentEndpointConventionBuilder MapRazorComponents<TRootComponent>(this IEndpointRouteBuilder endpoints)
24+
where TRootComponent : IRazorComponentApplication<TRootComponent>
2225
{
2326
ArgumentNullException.ThrowIfNull(endpoints);
2427

2528
EnsureRazorComponentServices(endpoints);
2629

27-
return GetOrCreateDataSource(endpoints).DefaultBuilder;
30+
return GetOrCreateDataSource<TRootComponent>(endpoints).DefaultBuilder;
2831
}
2932

30-
private static RazorComponentEndpointDataSource GetOrCreateDataSource(IEndpointRouteBuilder endpoints)
33+
private static RazorComponentEndpointDataSource<TRootComponent> GetOrCreateDataSource<TRootComponent>(IEndpointRouteBuilder endpoints)
34+
where TRootComponent : IRazorComponentApplication<TRootComponent>
3135
{
32-
var dataSource = endpoints.DataSources.OfType<RazorComponentEndpointDataSource>().FirstOrDefault();
36+
var dataSource = endpoints.DataSources.OfType<RazorComponentEndpointDataSource<TRootComponent>>().FirstOrDefault();
3337
if (dataSource == null)
3438
{
3539
// Very likely this needs to become a factory and we might need to have multiple endpoint data
3640
// sources, once we figure out the exact scenarios for
3741
// https://github.com/dotnet/aspnetcore/issues/46992
38-
dataSource = endpoints.ServiceProvider.GetRequiredService<RazorComponentEndpointDataSource>();
42+
var factory = endpoints.ServiceProvider.GetRequiredService<RazorComponentEndpointDataSourceFactory>();
43+
dataSource = factory.CreateDataSource<TRootComponent>();
3944
endpoints.DataSources.Add(dataSource);
4045
}
4146

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.AspNetCore.Builder;
54
using Microsoft.AspNetCore.Components;
65
using Microsoft.AspNetCore.Components.Endpoints;
76
using Microsoft.AspNetCore.Components.Infrastructure;
@@ -30,10 +29,9 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
3029
// Results
3130
services.TryAddSingleton<RazorComponentResultExecutor>();
3231

33-
// Routing
34-
// This can't be a singleton
35-
// https://github.com/dotnet/aspnetcore/issues/46980
36-
services.TryAddSingleton<RazorComponentEndpointDataSource>();
32+
// Endpoints
33+
services.TryAddSingleton<RazorComponentEndpointDataSourceFactory>();
34+
services.TryAddSingleton<RazorComponentEndpointFactory>();
3735

3836
// Common services required for components server side rendering
3937
services.TryAddSingleton<ServerComponentSerializer>(services => new ServerComponentSerializer(services.GetRequiredService<IDataProtectionProvider>()));
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
/// <summary>
7+
/// Builder used to configure a <see cref="RazorComponentApplication"/> instance.
8+
/// </summary>
9+
public class ComponentApplicationBuilder
10+
{
11+
private readonly HashSet<string> _assemblies = new();
12+
private PageCollection? _pages;
13+
14+
// TODO: When we support proper discovery this will be public
15+
// (and probably have a different shape).
16+
internal void AddAssembly(string name)
17+
{
18+
_assemblies.Add(name);
19+
}
20+
21+
/// <summary>
22+
/// Builds the component application definition.
23+
/// </summary>
24+
/// <returns>The <see cref="RazorComponentApplication"/>.</returns>
25+
public RazorComponentApplication Build()
26+
{
27+
return new RazorComponentApplication(_pages ?? PageCollection.Empty);
28+
}
29+
30+
internal void RegisterPages(PageCollection pages)
31+
{
32+
_pages = pages;
33+
}
34+
}

0 commit comments

Comments
 (0)