Skip to content

Commit 006d96f

Browse files
authored
[release/7.0-rc1] Add support for running final conventions during build (#43225)
* Add support for running final conventions during build * Fix typo and use stack for backing store * Update application order and address feedback - Fix ordering of group and entry-specific conventions in parameter list - Update to use FIFO ordering within finally conventions of the same level - Add tests for WithOpenApi on group and endpoint - Revert to using List instead of Stack for convention - Fix ordering of outer and inner group in RouteGroups * Add new test * Update tests
1 parent 2e9a5c0 commit 006d96f

File tree

38 files changed

+1012
-197
lines changed

38 files changed

+1012
-197
lines changed

src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,12 @@ public void Add(Action<EndpointBuilder> convention)
2929
_disconnectEndpoint.Add(convention);
3030
_jsInitializersEndpoint.Add(convention);
3131
}
32+
33+
/// <inheritdoc/>
34+
public void Finally(Action<EndpointBuilder> finalConvention)
35+
{
36+
_hubEndpoint.Finally(finalConvention);
37+
_disconnectEndpoint.Finally(finalConvention);
38+
_jsInitializersEndpoint.Finally(finalConvention);
39+
}
3240
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder.Finally(System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>! finalConvention) -> void

src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics;
55
using Microsoft.AspNetCore.Builder;
66
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.Http;
78
using Microsoft.Extensions.Configuration;
89
using Microsoft.Extensions.DependencyInjection;
910
using Microsoft.Extensions.FileProviders;
@@ -52,6 +53,75 @@ public void MapBlazorHub_MostGeneralOverload_MapsUnderlyingHub()
5253
Assert.True(called);
5354
}
5455

56+
[Fact]
57+
public void MapBlazorHub_AppliesFinalConventionToEachBuilder()
58+
{
59+
// Arrange
60+
var applicationBuilder = CreateAppBuilder();
61+
var buildersAffected = new List<string>();
62+
var called = false;
63+
64+
// Act
65+
var app = applicationBuilder
66+
.UseRouting()
67+
.UseEndpoints(endpoints =>
68+
{
69+
endpoints
70+
.MapBlazorHub(dispatchOptions => called = true)
71+
.WithMetadata("initial-md")
72+
.Finally(builder =>
73+
{
74+
if (builder.Metadata.Any(md => md is string smd && smd == "initial-md"))
75+
{
76+
buildersAffected.Add(builder.DisplayName);
77+
}
78+
});
79+
}).Build();
80+
81+
// Trigger endpoint construction
82+
app.Invoke(new DefaultHttpContext());
83+
84+
// Assert
85+
Assert.True(called);
86+
// Final conventions are applied to each of the builders
87+
// in the Blazor component hub
88+
Assert.Equal(4, buildersAffected.Count);
89+
Assert.Contains("/_blazor/negotiate", buildersAffected);
90+
Assert.Contains("/_blazor", buildersAffected);
91+
Assert.Contains("Blazor disconnect", buildersAffected);
92+
Assert.Contains("Blazor initializers", buildersAffected);
93+
}
94+
95+
[Fact]
96+
public void MapBlazorHub_AppliesFinalConventionsInFIFOOrder()
97+
{
98+
// Arrange
99+
var applicationBuilder = CreateAppBuilder();
100+
var called = false;
101+
var populatedMetadata = Array.Empty<string>();
102+
103+
// Act
104+
var app = applicationBuilder
105+
.UseRouting()
106+
.UseEndpoints(endpoints =>
107+
{
108+
var builder = endpoints.MapBlazorHub(dispatchOptions => called = true);
109+
builder.Finally(b => b.Metadata.Add("first-in"));
110+
builder.Finally(b => b.Metadata.Add("last-in"));
111+
builder.Finally(b =>
112+
{
113+
populatedMetadata = b.Metadata.OfType<string>().ToArray();
114+
});
115+
}).Build();
116+
117+
// Trigger endpoint construction
118+
app.Invoke(new DefaultHttpContext());
119+
120+
// Assert
121+
Assert.True(called);
122+
Assert.Equal(new[] { "first-in", "last-in" }, populatedMetadata);
123+
}
124+
55125
private IApplicationBuilder CreateAppBuilder()
56126
{
57127
var environment = new Mock<IWebHostEnvironment>();

src/Http/Http.Abstractions/src/Extensions/IEndpointConventionBuilder.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,11 @@ public interface IEndpointConventionBuilder
1616
/// </summary>
1717
/// <param name="convention">The convention to add to the builder.</param>
1818
void Add(Action<EndpointBuilder> convention);
19+
20+
/// <summary>
21+
/// Registers the specified convention for execution after conventions registered
22+
/// via <see cref="Add(Action{EndpointBuilder})"/>
23+
/// </summary>
24+
/// <param name="finallyConvention">The convention to add to the builder.</param>
25+
void Finally(Action<EndpointBuilder> finallyConvention) => throw new NotImplementedException();
1926
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ abstract Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.HttpContext.g
77
Microsoft.AspNetCore.Builder.EndpointBuilder.ApplicationServices.get -> System.IServiceProvider!
88
Microsoft.AspNetCore.Builder.EndpointBuilder.ApplicationServices.set -> void
99
Microsoft.AspNetCore.Builder.EndpointBuilder.FilterFactories.get -> System.Collections.Generic.IList<System.Func<Microsoft.AspNetCore.Http.EndpointFilterFactoryContext!, Microsoft.AspNetCore.Http.EndpointFilterDelegate!, Microsoft.AspNetCore.Http.EndpointFilterDelegate!>!>!
10+
Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.Finally(System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>! finallyConvention) -> void
1011
Microsoft.AspNetCore.Http.AsParametersAttribute
1112
Microsoft.AspNetCore.Http.AsParametersAttribute.AsParametersAttribute() -> void
1213
Microsoft.AspNetCore.Http.CookieBuilder.Extensions.get -> System.Collections.Generic.IList<string!>!

src/Http/Routing/src/Builder/RouteHandlerBuilder.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ public sealed class RouteHandlerBuilder : IEndpointConventionBuilder
1212
{
1313
private readonly IEnumerable<IEndpointConventionBuilder>? _endpointConventionBuilders;
1414
private readonly ICollection<Action<EndpointBuilder>>? _conventions;
15+
private readonly ICollection<Action<EndpointBuilder>>? _finallyConventions;
1516

1617
/// <summary>
1718
/// Instantiates a new <see cref="RouteHandlerBuilder" /> given a ThrowOnAddAfterEndpointBuiltConventionCollection from
1819
/// <see cref="RouteEndpointDataSource.AddRouteHandler(Routing.Patterns.RoutePattern, Delegate, IEnumerable{string}?, bool)"/>.
1920
/// </summary>
2021
/// <param name="conventions">The convention list returned from <see cref="RouteEndpointDataSource"/>.</param>
21-
internal RouteHandlerBuilder(ICollection<Action<EndpointBuilder>> conventions)
22+
/// <param name="finallyConventions">The final convention list returned from <see cref="RouteEndpointDataSource"/>.</param>
23+
internal RouteHandlerBuilder(ICollection<Action<EndpointBuilder>> conventions, ICollection<Action<EndpointBuilder>> finallyConventions)
2224
{
2325
_conventions = conventions;
26+
_finallyConventions = finallyConventions;
2427
}
2528

2629
/// <summary>
@@ -51,4 +54,20 @@ public void Add(Action<EndpointBuilder> convention)
5154
}
5255
}
5356
}
57+
58+
/// <inheritdoc />
59+
public void Finally(Action<EndpointBuilder> finalConvention)
60+
{
61+
if (_finallyConventions is not null)
62+
{
63+
_finallyConventions.Add(finalConvention);
64+
}
65+
else
66+
{
67+
foreach (var endpointConventionBuilder in _endpointConventionBuilders!)
68+
{
69+
endpointConventionBuilder.Finally(finalConvention);
70+
}
71+
}
72+
}
5473
}

src/Http/Routing/src/DefaultEndpointConventionBuilder.cs

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/Http/Routing/src/EndpointDataSource.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public virtual IReadOnlyList<Endpoint> GetGroupedEndpoints(RouteGroupContext con
7373
routeEndpointBuilder.Metadata.Add(metadata);
7474
}
7575

76+
foreach (var finallyConvention in context.FinallyConventions)
77+
{
78+
finallyConvention(routeEndpointBuilder);
79+
}
80+
7681
// The RoutePattern, RequestDelegate, Order and DisplayName can all be overridden by non-group-aware conventions.
7782
// Unlike with metadata, if a convention is applied to a group that changes any of these, I would expect these
7883
// to be overridden as there's no reasonable way to merge these properties.

src/Http/Routing/src/PublicAPI.Unshipped.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Builder.RouteHandlerBuilder.Finally(System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>! finalConvention) -> void
23
Microsoft.AspNetCore.Http.EndpointFilterExtensions
34
Microsoft.AspNetCore.Routing.CompositeEndpointDataSource.Dispose() -> void
45
Microsoft.AspNetCore.Routing.HttpMethodMetadata.AcceptCorsPreflight.set -> void
56
Microsoft.AspNetCore.Routing.IHttpMethodMetadata.AcceptCorsPreflight.set -> void
67
Microsoft.AspNetCore.Routing.RouteGroupBuilder
78
Microsoft.AspNetCore.Routing.RouteGroupContext
89
Microsoft.AspNetCore.Routing.RouteGroupContext.ApplicationServices.get -> System.IServiceProvider!
10+
Microsoft.AspNetCore.Routing.RouteGroupContext.ApplicationServices.init -> void
911
Microsoft.AspNetCore.Routing.RouteGroupContext.Conventions.get -> System.Collections.Generic.IReadOnlyList<System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>!>!
12+
Microsoft.AspNetCore.Routing.RouteGroupContext.Conventions.init -> void
13+
Microsoft.AspNetCore.Routing.RouteGroupContext.FinallyConventions.get -> System.Collections.Generic.IReadOnlyList<System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>!>!
14+
Microsoft.AspNetCore.Routing.RouteGroupContext.FinallyConventions.init -> void
1015
Microsoft.AspNetCore.Routing.RouteGroupContext.Prefix.get -> Microsoft.AspNetCore.Routing.Patterns.RoutePattern!
11-
Microsoft.AspNetCore.Routing.RouteGroupContext.RouteGroupContext(Microsoft.AspNetCore.Routing.Patterns.RoutePattern! prefix, System.Collections.Generic.IReadOnlyList<System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>!>! conventions, System.IServiceProvider! applicationServices) -> void
16+
Microsoft.AspNetCore.Routing.RouteGroupContext.Prefix.init -> void
17+
Microsoft.AspNetCore.Routing.RouteGroupContext.RouteGroupContext() -> void
1218
Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy(string! token, System.Type! type) -> void
1319
Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy<T>(string! token) -> void
1420
override Microsoft.AspNetCore.Routing.CompositeEndpointDataSource.GetGroupedEndpoints(Microsoft.AspNetCore.Routing.RouteGroupContext! context) -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint!>!

src/Http/Routing/src/RouteEndpointDataSource.cs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public RouteHandlerBuilder AddRequestDelegate(
3131
{
3232

3333
var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection();
34+
var finallyConventions = new ThrowOnAddAfterEndpointBuiltConventionCollection();
3435

3536
_routeEntries.Add(new()
3637
{
@@ -39,9 +40,10 @@ public RouteHandlerBuilder AddRequestDelegate(
3940
HttpMethods = httpMethods,
4041
RouteAttributes = RouteAttributes.None,
4142
Conventions = conventions,
43+
FinallyConventions = finallyConventions
4244
});
4345

44-
return new RouteHandlerBuilder(conventions);
46+
return new RouteHandlerBuilder(conventions, finallyConventions);
4547
}
4648

4749
public RouteHandlerBuilder AddRouteHandler(
@@ -51,6 +53,7 @@ public RouteHandlerBuilder AddRouteHandler(
5153
bool isFallback)
5254
{
5355
var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection();
56+
var finallyConventions = new ThrowOnAddAfterEndpointBuiltConventionCollection();
5457

5558
var routeAttributes = RouteAttributes.RouteHandler;
5659
if (isFallback)
@@ -65,9 +68,10 @@ public RouteHandlerBuilder AddRouteHandler(
6568
HttpMethods = httpMethods,
6669
RouteAttributes = routeAttributes,
6770
Conventions = conventions,
71+
FinallyConventions = finallyConventions
6872
});
6973

70-
return new RouteHandlerBuilder(conventions);
74+
return new RouteHandlerBuilder(conventions, finallyConventions);
7175
}
7276

7377
public override IReadOnlyList<RouteEndpoint> Endpoints
@@ -88,7 +92,7 @@ public override IReadOnlyList<RouteEndpoint> GetGroupedEndpoints(RouteGroupConte
8892
var endpoints = new RouteEndpoint[_routeEntries.Count];
8993
for (int i = 0; i < _routeEntries.Count; i++)
9094
{
91-
endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i], context.Prefix, context.Conventions).Build();
95+
endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i], context.Prefix, context.Conventions, context.FinallyConventions).Build();
9296
}
9397
return endpoints;
9498
}
@@ -109,7 +113,7 @@ internal RouteEndpointBuilder GetSingleRouteEndpointBuilder()
109113
[UnconditionalSuppressMessage("Trimmer", "IL2026",
110114
Justification = "We surface a RequireUnreferencedCode in the call to the Map method adding this EndpointDataSource. The trimmer is unable to infer this.")]
111115
private RouteEndpointBuilder CreateRouteEndpointBuilder(
112-
RouteEntry entry, RoutePattern? groupPrefix = null, IReadOnlyList<Action<EndpointBuilder>>? groupConventions = null)
116+
RouteEntry entry, RoutePattern? groupPrefix = null, IReadOnlyList<Action<EndpointBuilder>>? groupConventions = null, IReadOnlyList<Action<EndpointBuilder>>? groupFinallyConventions = null)
113117
{
114118
var pattern = RoutePatternFactory.Combine(groupPrefix, entry.RoutePattern);
115119
var handler = entry.RouteHandler;
@@ -190,7 +194,7 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder(
190194
}
191195
}
192196

193-
entry.Conventions.IsReadonly = true;
197+
entry.Conventions.IsReadOnly = true;
194198
foreach (var entrySpecificConvention in entry.Conventions)
195199
{
196200
entrySpecificConvention(builder);
@@ -225,6 +229,22 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder(
225229
builder.RequestDelegate = factoryCreatedRequestDelegate;
226230
}
227231

232+
entry.FinallyConventions.IsReadOnly = true;
233+
foreach (var entryFinallyConvention in entry.FinallyConventions)
234+
{
235+
entryFinallyConvention(builder);
236+
}
237+
238+
if (groupFinallyConventions is not null)
239+
{
240+
// Group conventions are ordered by the RouteGroupBuilder before
241+
// being provided here.
242+
foreach (var groupFinallyConvention in groupFinallyConventions)
243+
{
244+
groupFinallyConvention(builder);
245+
}
246+
}
247+
228248
return builder;
229249
}
230250

@@ -265,6 +285,7 @@ private struct RouteEntry
265285
public IEnumerable<string>? HttpMethods { get; init; }
266286
public RouteAttributes RouteAttributes { get; init; }
267287
public ThrowOnAddAfterEndpointBuiltConventionCollection Conventions { get; init; }
288+
public ThrowOnAddAfterEndpointBuiltConventionCollection FinallyConventions { get; init; }
268289
}
269290

270291
[Flags]
@@ -283,11 +304,11 @@ private sealed class ThrowOnAddAfterEndpointBuiltConventionCollection : List<Act
283304
{
284305
// We throw if someone tries to add conventions to the RouteEntry after endpoints have already been resolved meaning the conventions
285306
// will not be observed given RouteEndpointDataSource is not meant to be dynamic and uses NullChangeToken.Singleton.
286-
public bool IsReadonly { get; set; }
307+
public bool IsReadOnly { get; set; }
287308

288309
void ICollection<Action<EndpointBuilder>>.Add(Action<EndpointBuilder> convention)
289310
{
290-
if (IsReadonly)
311+
if (IsReadOnly)
291312
{
292313
throw new InvalidOperationException(Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild);
293314
}

0 commit comments

Comments
 (0)