Skip to content

Commit f2a1a45

Browse files
authored
Introduce dynamic endpoints and fix #7011 (#7445)
* Add IDynamicEndpointMetadata for dynamic endpoints * Use a dynamic endpoint policy for pages
1 parent 27e54a1 commit f2a1a45

31 files changed

+589
-221
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Routing.Matching;
6+
7+
namespace Microsoft.AspNetCore.Routing
8+
{
9+
/// <summary>
10+
/// A metadata interface that can be used to specify that the associated <see cref="Endpoint" />
11+
/// will be dynamically replaced during matching.
12+
/// </summary>
13+
/// <remarks>
14+
/// <para>
15+
/// <see cref="IDynamicEndpointMetadata"/> and related derived interfaces signal to
16+
/// <see cref="MatcherPolicy"/> implementations that an <see cref="Endpoint"/> has dynamic behavior
17+
/// and thus cannot have its characteristics cached.
18+
/// </para>
19+
/// <para>
20+
/// Using dynamic endpoints can be useful because the default matcher implementation does not
21+
/// supply extensibility for how URLs are processed. Routing implementations that have dynamic
22+
/// behavior can apply their dynamic logic after URL processing, by replacing a endpoints as
23+
/// part of a <see cref="CandidateSet"/>.
24+
/// </para>
25+
/// </remarks>
26+
public interface IDynamicEndpointMetadata
27+
{
28+
/// <summary>
29+
/// Returns a value that indicates whether the associated endpoint has dynamic matching
30+
/// behavior.
31+
/// </summary>
32+
bool IsDynamic { get; }
33+
}
34+
}

src/Http/Routing/src/Matching/CandidateSet.cs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -281,6 +281,57 @@ public void SetValidity(int index, bool value)
281281
}
282282
}
283283

284+
/// <summary>
285+
/// Replaces the <see cref="Endpoint"/> at the provided <paramref name="index"/> with the
286+
/// provided <paramref name="endpoint"/>.
287+
/// </summary>
288+
/// <param name="index">The candidate index.</param>
289+
/// <param name="endpoint">
290+
/// The <see cref="Endpoint"/> to replace the original <see cref="Endpoint"/> at
291+
/// the <paramref name="index"/>. If <paramref name="endpoint"/> the candidate will be marked
292+
/// as invalid.
293+
/// </param>
294+
/// <param name="values">
295+
/// The <see cref="RouteValueDictionary"/> to replace the original <see cref="RouteValueDictionary"/> at
296+
/// the <paramref name="index"/>.
297+
/// </param>
298+
public void ReplaceEndpoint(int index, Endpoint endpoint, RouteValueDictionary values)
299+
{
300+
// Friendliness for inlining
301+
if ((uint)index >= Count)
302+
{
303+
ThrowIndexArgumentOutOfRangeException();
304+
}
305+
306+
switch (index)
307+
{
308+
case 0:
309+
_state0 = new CandidateState(endpoint, values, _state0.Score);
310+
break;
311+
312+
case 1:
313+
_state1 = new CandidateState(endpoint, values, _state1.Score);
314+
break;
315+
316+
case 2:
317+
_state2 = new CandidateState(endpoint, values, _state2.Score);
318+
break;
319+
320+
case 3:
321+
_state3 = new CandidateState(endpoint, values, _state3.Score);
322+
break;
323+
324+
default:
325+
_additionalCandidates[index - 4] = new CandidateState(endpoint, values, _additionalCandidates[index - 4].Score);
326+
break;
327+
}
328+
329+
if (endpoint == null)
330+
{
331+
SetValidity(index, false);
332+
}
333+
}
334+
284335
private static void ThrowIndexArgumentOutOfRangeException()
285336
{
286337
throw new ArgumentOutOfRangeException("index");

src/Http/Routing/src/Matching/MatcherPolicy.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.AspNetCore.Http;
47
using Microsoft.AspNetCore.Routing.Matching;
58

69
namespace Microsoft.AspNetCore.Routing
@@ -24,5 +27,30 @@ public abstract class MatcherPolicy
2427
/// property.
2528
/// </summary>
2629
public abstract int Order { get; }
30+
31+
/// <summary>
32+
/// Returns a value that indicates whether the provided <paramref name="endpoints"/> contains
33+
/// one or more dynamic endpoints.
34+
/// </summary>
35+
/// <param name="endpoints">The set of endpoints.</param>
36+
/// <returns><c>true</c> if a dynamic endpoint is found; otherwise returns <c>false</c>.</returns>
37+
protected static bool ContainsDynamicEndpoints(IReadOnlyList<Endpoint> endpoints)
38+
{
39+
if (endpoints == null)
40+
{
41+
throw new ArgumentNullException(nameof(endpoints));
42+
}
43+
44+
for (var i = 0; i < endpoints.Count; i++)
45+
{
46+
var metadata = endpoints[i].Metadata.GetMetadata<IDynamicEndpointMetadata>();
47+
if (metadata?.IsDynamic == true)
48+
{
49+
return true;
50+
}
51+
}
52+
53+
return false;
54+
}
2755
}
2856
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Routing.Patterns;
7+
using Xunit;
8+
9+
namespace Microsoft.AspNetCore.Routing
10+
{
11+
public class MatcherPolicyTest
12+
{
13+
[Fact]
14+
public void ContainsDynamicEndpoint_FindsDynamicEndpoint()
15+
{
16+
// Arrange
17+
var endpoints = new Endpoint[]
18+
{
19+
CreateEndpoint("1"),
20+
CreateEndpoint("2"),
21+
CreateEndpoint("3", new DynamicEndpointMetadata(isDynamic: true)),
22+
};
23+
24+
// Act
25+
var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints);
26+
27+
// Assert
28+
Assert.True(result);
29+
}
30+
31+
[Fact]
32+
public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint()
33+
{
34+
// Arrange
35+
var endpoints = new Endpoint[]
36+
{
37+
CreateEndpoint("1"),
38+
CreateEndpoint("2"),
39+
CreateEndpoint("3", new DynamicEndpointMetadata(isDynamic: false)),
40+
};
41+
42+
// Act
43+
var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints);
44+
45+
// Assert
46+
Assert.False(result);
47+
}
48+
49+
[Fact]
50+
public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint_Empty()
51+
{
52+
// Arrange
53+
var endpoints = new Endpoint[]{ };
54+
55+
// Act
56+
var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints);
57+
58+
// Assert
59+
Assert.False(result);
60+
}
61+
62+
private RouteEndpoint CreateEndpoint(string template, params object[] metadata)
63+
{
64+
return new RouteEndpoint(
65+
TestConstants.EmptyRequestDelegate,
66+
RoutePatternFactory.Parse(template),
67+
0,
68+
new EndpointMetadataCollection(metadata),
69+
"test");
70+
}
71+
72+
private class DynamicEndpointMetadata : IDynamicEndpointMetadata
73+
{
74+
public DynamicEndpointMetadata(bool isDynamic)
75+
{
76+
IsDynamic = isDynamic;
77+
}
78+
79+
public bool IsDynamic { get; }
80+
}
81+
82+
private class TestMatcherPolicy : MatcherPolicy
83+
{
84+
public override int Order => throw new System.NotImplementedException();
85+
86+
public new static bool ContainsDynamicEndpoints(IReadOnlyList<Endpoint> endpoints)
87+
{
88+
return MatcherPolicy.ContainsDynamicEndpoints(endpoints);
89+
}
90+
}
91+
}
92+
}

src/Http/Routing/test/UnitTests/Matching/CandidateSetTest.cs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -55,6 +55,91 @@ public void Create_CreatesCandidateSet(int count)
5555
}
5656
}
5757

58+
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
59+
// of input sizes.
60+
[Theory]
61+
[InlineData(0)]
62+
[InlineData(1)]
63+
[InlineData(2)]
64+
[InlineData(3)]
65+
[InlineData(4)]
66+
[InlineData(5)] // this is the break-point where we start to use a list.
67+
[InlineData(6)]
68+
[InlineData(31)]
69+
[InlineData(32)] // this is the break point where we use a BitArray
70+
[InlineData(33)]
71+
public void ReplaceEndpoint_WithEndpoint(int count)
72+
{
73+
// Arrange
74+
var endpoints = new RouteEndpoint[count];
75+
for (var i = 0; i < endpoints.Length; i++)
76+
{
77+
endpoints[i] = CreateEndpoint($"/{i}");
78+
}
79+
80+
var builder = CreateDfaMatcherBuilder();
81+
var candidates = builder.CreateCandidates(endpoints);
82+
83+
var candidateSet = new CandidateSet(candidates);
84+
85+
for (var i = 0; i < candidateSet.Count; i++)
86+
{
87+
ref var state = ref candidateSet[i];
88+
89+
var endpoint = CreateEndpoint($"/test{i}");
90+
var values = new RouteValueDictionary();
91+
92+
// Act
93+
candidateSet.ReplaceEndpoint(i, endpoint, values);
94+
95+
// Assert
96+
Assert.Same(endpoint, state.Endpoint);
97+
Assert.Same(values, state.Values);
98+
Assert.True(candidateSet.IsValidCandidate(i));
99+
}
100+
}
101+
102+
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
103+
// of input sizes.
104+
[Theory]
105+
[InlineData(0)]
106+
[InlineData(1)]
107+
[InlineData(2)]
108+
[InlineData(3)]
109+
[InlineData(4)]
110+
[InlineData(5)] // this is the break-point where we start to use a list.
111+
[InlineData(6)]
112+
[InlineData(31)]
113+
[InlineData(32)] // this is the break point where we use a BitArray
114+
[InlineData(33)]
115+
public void ReplaceEndpoint_WithEndpoint_Null(int count)
116+
{
117+
// Arrange
118+
var endpoints = new RouteEndpoint[count];
119+
for (var i = 0; i < endpoints.Length; i++)
120+
{
121+
endpoints[i] = CreateEndpoint($"/{i}");
122+
}
123+
124+
var builder = CreateDfaMatcherBuilder();
125+
var candidates = builder.CreateCandidates(endpoints);
126+
127+
var candidateSet = new CandidateSet(candidates);
128+
129+
for (var i = 0; i < candidateSet.Count; i++)
130+
{
131+
ref var state = ref candidateSet[i];
132+
133+
// Act
134+
candidateSet.ReplaceEndpoint(i, null, null);
135+
136+
// Assert
137+
Assert.Null(state.Endpoint);
138+
Assert.Null(state.Values);
139+
Assert.False(candidateSet.IsValidCandidate(i));
140+
}
141+
}
142+
58143
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
59144
// of input sizes.
60145
[Theory]

src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionEndpointDatasourceBenchmark.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,7 @@ private ActionEndpointDataSource CreateDataSource(IActionDescriptorCollectionPro
114114
{
115115
var dataSource = new ActionEndpointDataSource(
116116
actionDescriptorCollectionProvider,
117-
new ActionEndpointFactory(
118-
new MockRoutePatternTransformer(),
119-
new MvcEndpointInvokerFactory(
120-
new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>()))));
117+
new ActionEndpointFactory(new MockRoutePatternTransformer()));
121118

122119
return dataSource;
123120
}

src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,6 @@ internal static void AddMvcCoreServices(IServiceCollection services)
272272
services.TryAddSingleton<ActionEndpointDataSource>();
273273
services.TryAddSingleton<ControllerActionEndpointDataSource>();
274274
services.TryAddSingleton<ActionEndpointFactory>();
275-
services.TryAddSingleton<MvcEndpointInvokerFactory>();
276275

277276
//
278277
// Middleware pipeline filter related

src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ActionContextAccessor.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
77
{
88
public class ActionContextAccessor : IActionContextAccessor
99
{
10+
internal static readonly IActionContextAccessor Null = new NullActionContextAccessor();
11+
1012
private static readonly AsyncLocal<ActionContext> _storage = new AsyncLocal<ActionContext>();
1113

1214
public ActionContext ActionContext
1315
{
1416
get { return _storage.Value; }
1517
set { _storage.Value = value; }
1618
}
19+
20+
private class NullActionContextAccessor : IActionContextAccessor
21+
{
22+
public ActionContext ActionContext
23+
{
24+
get => null;
25+
set { }
26+
}
27+
}
1728
}
1829
}

src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ControllerActionInvoker.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ internal class ControllerActionInvoker : ResourceInvoker, IActionInvoker
2727
internal ControllerActionInvoker(
2828
ILogger logger,
2929
DiagnosticListener diagnosticListener,
30+
IActionContextAccessor actionContextAccessor,
3031
IActionResultTypeMapper mapper,
3132
ControllerContext controllerContext,
3233
ControllerActionInvokerCacheEntry cacheEntry,
3334
IFilterMetadata[] filters)
34-
: base(diagnosticListener, logger, mapper, controllerContext, filters, controllerContext.ValueProviderFactories)
35+
: base(diagnosticListener, logger, actionContextAccessor, mapper, controllerContext, filters, controllerContext.ValueProviderFactories)
3536
{
3637
if (cacheEntry == null)
3738
{

0 commit comments

Comments
 (0)