Skip to content

Commit dfcfb51

Browse files
Use endpoints instead of actions. Relates to #812
1 parent 1539418 commit dfcfb51

File tree

4 files changed

+258
-33
lines changed

4 files changed

+258
-33
lines changed

src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
namespace Asp.Versioning.ApiExplorer;
44

5-
using Microsoft.AspNetCore.Mvc.Abstractions;
6-
using Microsoft.AspNetCore.Mvc.Infrastructure;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Routing;
77
using Microsoft.Extensions.Options;
8+
using Microsoft.Extensions.Primitives;
89
using static Asp.Versioning.ApiVersionMapping;
910
using static System.Globalization.CultureInfo;
1011

@@ -14,23 +15,22 @@ namespace Asp.Versioning.ApiExplorer;
1415
[CLSCompliant( false )]
1516
public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider
1617
{
17-
private readonly Lazy<IReadOnlyList<ApiVersionDescription>> apiVersionDescriptions;
18+
private readonly ApiVersionDescriptionCollection collection;
1819
private readonly IOptions<ApiExplorerOptions> options;
1920

2021
/// <summary>
2122
/// Initializes a new instance of the <see cref="DefaultApiVersionDescriptionProvider"/> class.
2223
/// </summary>
23-
/// <param name="actionDescriptorCollectionProvider">The <see cref="IActionDescriptorCollectionProvider">provider</see>
24-
/// used to enumerate the actions within an application.</param>
24+
/// <param name="endpointDataSource">The <see cref="EndpointDataSource">data source</see> for <see cref="Endpoint">endpoints</see>.</param>
2525
/// <param name="sunsetPolicyManager">The <see cref="ISunsetPolicyManager">manager</see> used to resolve sunset policies.</param>
2626
/// <param name="apiExplorerOptions">The <see cref="IOptions{TOptions}">container</see> of configured
2727
/// <see cref="ApiExplorerOptions">API explorer options</see>.</param>
2828
public DefaultApiVersionDescriptionProvider(
29-
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
29+
EndpointDataSource endpointDataSource,
3030
ISunsetPolicyManager sunsetPolicyManager,
3131
IOptions<ApiExplorerOptions> apiExplorerOptions )
3232
{
33-
apiVersionDescriptions = LazyApiVersionDescriptions.Create( this, actionDescriptorCollectionProvider );
33+
collection = new( this, endpointDataSource );
3434
SunsetPolicyManager = sunsetPolicyManager;
3535
options = apiExplorerOptions;
3636
}
@@ -48,41 +48,46 @@ public DefaultApiVersionDescriptionProvider(
4848
protected ApiExplorerOptions Options => options.Value;
4949

5050
/// <inheritdoc />
51-
public IReadOnlyList<ApiVersionDescription> ApiVersionDescriptions => apiVersionDescriptions.Value;
51+
public IReadOnlyList<ApiVersionDescription> ApiVersionDescriptions => collection.Items;
5252

5353
/// <summary>
5454
/// Enumerates all API versions within an application.
5555
/// </summary>
56-
/// <param name="actionDescriptorCollectionProvider">The <see cref="IActionDescriptorCollectionProvider">provider</see> used to enumerate the actions within an application.</param>
56+
/// <param name="endpointDataSource">The <see cref="EndpointDataSource">data source</see> used to enumerate the endpoints within an application.</param>
5757
/// <returns>A <see cref="IReadOnlyList{T}">read-only list</see> of <see cref="ApiVersionDescription">API version descriptions</see>.</returns>
58-
protected virtual IReadOnlyList<ApiVersionDescription> EnumerateApiVersions( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider )
58+
protected virtual IReadOnlyList<ApiVersionDescription> EnumerateApiVersions( EndpointDataSource endpointDataSource )
5959
{
60-
if ( actionDescriptorCollectionProvider == null )
60+
if ( endpointDataSource == null )
6161
{
62-
throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) );
62+
throw new ArgumentNullException( nameof( endpointDataSource ) );
6363
}
6464

65-
var actions = actionDescriptorCollectionProvider.ActionDescriptors.Items;
66-
var descriptions = new List<ApiVersionDescription>( capacity: actions.Count );
65+
var endpoints = endpointDataSource.Endpoints;
66+
var descriptions = new List<ApiVersionDescription>( capacity: endpoints.Count );
6767
var supported = new HashSet<ApiVersion>();
6868
var deprecated = new HashSet<ApiVersion>();
6969

70-
BucketizeApiVersions( actions, supported, deprecated );
70+
BucketizeApiVersions( endpoints, supported, deprecated );
7171
AppendDescriptions( descriptions, supported, deprecated: false );
7272
AppendDescriptions( descriptions, deprecated, deprecated: true );
7373

7474
return descriptions.OrderBy( d => d.ApiVersion ).ToArray();
7575
}
7676

77-
private void BucketizeApiVersions( IReadOnlyList<ActionDescriptor> actions, ISet<ApiVersion> supported, ISet<ApiVersion> deprecated )
77+
private void BucketizeApiVersions( IReadOnlyList<Endpoint> endpoints, ISet<ApiVersion> supported, ISet<ApiVersion> deprecated )
7878
{
7979
var declared = new HashSet<ApiVersion>();
8080
var advertisedSupported = new HashSet<ApiVersion>();
8181
var advertisedDeprecated = new HashSet<ApiVersion>();
8282

83-
for ( var i = 0; i < actions.Count; i++ )
83+
for ( var i = 0; i < endpoints.Count; i++ )
8484
{
85-
var model = actions[i].GetApiVersionMetadata().Map( Explicit | Implicit );
85+
if ( endpoints[i].Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata metadata )
86+
{
87+
continue;
88+
}
89+
90+
var model = metadata.Map( Explicit | Implicit );
8691
var versions = model.DeclaredApiVersions;
8792

8893
for ( var j = 0; j < versions.Count; j++ )
@@ -130,28 +135,51 @@ private void AppendDescriptions( ICollection<ApiVersionDescription> descriptions
130135
}
131136
}
132137

133-
private sealed class LazyApiVersionDescriptions : Lazy<IReadOnlyList<ApiVersionDescription>>
138+
private sealed class ApiVersionDescriptionCollection
134139
{
140+
private readonly object syncRoot = new();
141+
private readonly EndpointDataSource endpointDataSource;
135142
private readonly DefaultApiVersionDescriptionProvider apiVersionDescriptionProvider;
136-
private readonly IActionDescriptorCollectionProvider actionDescriptorCollectionProvider;
143+
private IReadOnlyList<ApiVersionDescription>? items;
137144

138-
private LazyApiVersionDescriptions(
145+
public ApiVersionDescriptionCollection(
139146
DefaultApiVersionDescriptionProvider apiVersionDescriptionProvider,
140-
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider )
147+
EndpointDataSource endpointDataSource )
141148
{
142149
this.apiVersionDescriptionProvider = apiVersionDescriptionProvider;
143-
this.actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
150+
this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) );
151+
ChangeToken.OnChange( endpointDataSource.GetChangeToken, UpdateItems );
144152
}
145153

146-
internal static Lazy<IReadOnlyList<ApiVersionDescription>> Create(
147-
DefaultApiVersionDescriptionProvider apiVersionDescriptionProvider,
148-
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider )
154+
public IReadOnlyList<ApiVersionDescription> Items
149155
{
150-
var descriptions = new LazyApiVersionDescriptions( apiVersionDescriptionProvider, actionDescriptorCollectionProvider );
151-
return new( descriptions.EnumerateApiVersions );
156+
get
157+
{
158+
Initialize();
159+
return items!;
160+
}
152161
}
153162

154-
private IReadOnlyList<ApiVersionDescription> EnumerateApiVersions() =>
155-
apiVersionDescriptionProvider.EnumerateApiVersions( actionDescriptorCollectionProvider );
163+
private void Initialize()
164+
{
165+
if ( items == null )
166+
{
167+
lock ( syncRoot )
168+
{
169+
if ( items == null )
170+
{
171+
UpdateItems();
172+
}
173+
}
174+
}
175+
}
176+
177+
private void UpdateItems()
178+
{
179+
lock ( syncRoot )
180+
{
181+
items = apiVersionDescriptionProvider.EnumerateApiVersions( endpointDataSource );
182+
}
183+
}
156184
}
157185
}

src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public void api_version_descriptions_should_collate_expected_versions()
1111
{
1212
// arrange
1313
var descriptionProvider = new DefaultApiVersionDescriptionProvider(
14-
new TestActionDescriptorCollectionProvider(),
14+
new TestEndpointDataSource(),
1515
Mock.Of<ISunsetPolicyManager>(),
1616
Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) );
1717

@@ -40,7 +40,7 @@ public void api_version_descriptions_should_apply_sunset_policy()
4040
policyManager.Setup( pm => pm.TryGetPolicy( default, apiVersion, out expected ) ).Returns( true );
4141

4242
var descriptionProvider = new DefaultApiVersionDescriptionProvider(
43-
new TestActionDescriptorCollectionProvider(),
43+
new TestEndpointDataSource(),
4444
policyManager.Object,
4545
Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) );
4646

src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Asp.Versioning.ApiExplorer;
55
using Microsoft.AspNetCore.Mvc.Abstractions;
66
using Microsoft.AspNetCore.Mvc.Infrastructure;
77

8-
internal class TestActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
8+
internal sealed class TestActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
99
{
1010
private readonly Lazy<ActionDescriptorCollection> collection = new( CreateActionDescriptors );
1111

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.ApiExplorer;
4+
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Routing;
7+
using Microsoft.AspNetCore.Routing.Patterns;
8+
using Microsoft.Extensions.FileProviders;
9+
using Microsoft.Extensions.Primitives;
10+
11+
internal class TestEndpointDataSource : EndpointDataSource
12+
{
13+
public override IReadOnlyList<Endpoint> Endpoints { get; } = CreateEndpoints();
14+
15+
public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
16+
17+
private static IReadOnlyList<Endpoint> CreateEndpoints()
18+
{
19+
var endpoints = new List<Endpoint>();
20+
21+
AddOrderEndpoints( endpoints );
22+
AddPeopleEndpoints( endpoints );
23+
24+
return endpoints;
25+
}
26+
27+
private static void AddOrderEndpoints( ICollection<Endpoint> endpoints )
28+
{
29+
// api version 0.9 and 1.0
30+
endpoints.Add(
31+
NewEndpoint(
32+
"GET-orders/{id}",
33+
"orders/{id}",
34+
declared: new ApiVersion[] { new( 0, 9 ), new( 1, 0 ) },
35+
supported: new ApiVersion[] { new( 1, 0 ) },
36+
deprecated: new ApiVersion[] { new( 0, 9 ) } ) );
37+
38+
endpoints.Add(
39+
NewEndpoint(
40+
"POST-orders",
41+
"orders",
42+
declared: new ApiVersion[] { new( 1, 0 ) },
43+
supported: new ApiVersion[] { new( 1, 0 ) } ) );
44+
45+
// api version 2.0
46+
endpoints.Add(
47+
NewEndpoint(
48+
"GET-orders",
49+
"orders",
50+
declared: new ApiVersion[] { new( 2, 0 ) },
51+
supported: new ApiVersion[] { new( 2, 0 ) } ) );
52+
53+
endpoints.Add(
54+
NewEndpoint(
55+
"GET-orders/{id}",
56+
"orders/{id}",
57+
declared: new ApiVersion[] { new( 2, 0 ) },
58+
supported: new ApiVersion[] { new( 2, 0 ) } ) );
59+
60+
endpoints.Add(
61+
NewEndpoint(
62+
"POST-orders",
63+
"orders",
64+
declared: new ApiVersion[] { new( 2, 0 ) },
65+
supported: new ApiVersion[] { new( 2, 0 ) } ) );
66+
67+
// api version 3.0
68+
endpoints.Add(
69+
NewEndpoint(
70+
"GET-orders",
71+
"orders",
72+
declared: new ApiVersion[] { new( 3, 0 ) },
73+
supported: new ApiVersion[] { new( 3, 0 ) },
74+
advertised: new ApiVersion[] { new( 4, 0 ) } ) );
75+
76+
endpoints.Add(
77+
NewEndpoint(
78+
"GET-orders/{id}",
79+
"orders/{id}",
80+
declared: new ApiVersion[] { new( 3, 0 ) },
81+
supported: new ApiVersion[] { new( 3, 0 ) },
82+
advertised: new ApiVersion[] { new( 4, 0 ) } ) );
83+
84+
endpoints.Add(
85+
NewEndpoint(
86+
"POST-orders",
87+
"orders",
88+
declared: new ApiVersion[] { new( 3, 0 ) },
89+
supported: new ApiVersion[] { new( 3, 0 ) },
90+
advertised: new ApiVersion[] { new( 4, 0 ) } ) );
91+
92+
endpoints.Add(
93+
NewEndpoint(
94+
"DELETE-orders/{id}",
95+
"orders/{id}",
96+
declared: new ApiVersion[] { new( 3, 0 ) },
97+
supported: new ApiVersion[] { new( 3, 0 ) },
98+
advertised: new ApiVersion[] { new( 4, 0 ) } ) );
99+
}
100+
101+
private static void AddPeopleEndpoints( ICollection<Endpoint> endpoints )
102+
{
103+
// api version 0.9 and 1.0
104+
endpoints.Add(
105+
NewEndpoint(
106+
"GET-people/{id}",
107+
"people/{id}",
108+
declared: new ApiVersion[] { new( 0, 9 ), new( 1, 0 ) },
109+
supported: new ApiVersion[] { new( 1, 0 ) },
110+
deprecated: new ApiVersion[] { new( 0, 9 ) } ) );
111+
112+
endpoints.Add(
113+
NewEndpoint(
114+
"POST-people",
115+
"people",
116+
declared: new ApiVersion[] { new( 1, 0 ) },
117+
supported: new ApiVersion[] { new( 1, 0 ) } ) );
118+
119+
// api version 2.0
120+
endpoints.Add(
121+
NewEndpoint(
122+
"GET-people",
123+
"people",
124+
declared: new ApiVersion[] { new( 2, 0 ) },
125+
supported: new ApiVersion[] { new( 2, 0 ) } ) );
126+
127+
endpoints.Add(
128+
NewEndpoint(
129+
"GET-people/{id}",
130+
"people/{id}",
131+
declared: new ApiVersion[] { new( 2, 0 ) },
132+
supported: new ApiVersion[] { new( 2, 0 ) } ) );
133+
134+
endpoints.Add(
135+
NewEndpoint(
136+
"POST-people",
137+
"people",
138+
declared: new ApiVersion[] { new( 2, 0 ) },
139+
supported: new ApiVersion[] { new( 2, 0 ) } ) );
140+
141+
// api version 3.0
142+
endpoints.Add(
143+
NewEndpoint(
144+
"GET-people",
145+
"people",
146+
declared: new ApiVersion[] { new( 3, 0 ) },
147+
supported: new ApiVersion[] { new( 3, 0 ) },
148+
advertised: new ApiVersion[] { new( 4, 0 ) } ) );
149+
150+
endpoints.Add(
151+
NewEndpoint(
152+
"GET-people/{id}",
153+
"people/{id}",
154+
declared: new ApiVersion[] { new( 3, 0 ) },
155+
supported: new ApiVersion[] { new( 3, 0 ) },
156+
advertised: new ApiVersion[] { new( 4, 0 ) } ) );
157+
158+
endpoints.Add(
159+
NewEndpoint(
160+
"POST-people",
161+
"people",
162+
declared: new ApiVersion[] { new( 3, 0 ) },
163+
supported: new ApiVersion[] { new( 3, 0 ) },
164+
advertised: new ApiVersion[] { new( 4, 0 ) } ) );
165+
}
166+
167+
private static Endpoint NewEndpoint(
168+
string displayName,
169+
string pattern,
170+
IEnumerable<ApiVersion> declared,
171+
IEnumerable<ApiVersion> supported,
172+
IEnumerable<ApiVersion> deprecated = null,
173+
IEnumerable<ApiVersion> advertised = null,
174+
IEnumerable<ApiVersion> advertisedDeprecated = null )
175+
{
176+
var metadata = new ApiVersionMetadata(
177+
ApiVersionModel.Empty,
178+
new ApiVersionModel(
179+
declared,
180+
supported,
181+
deprecated ?? Enumerable.Empty<ApiVersion>(),
182+
advertised ?? Enumerable.Empty<ApiVersion>(),
183+
advertisedDeprecated ?? Enumerable.Empty<ApiVersion>() ) );
184+
var builder = new RouteEndpointBuilder(
185+
Route404,
186+
RoutePatternFactory.Parse( pattern ),
187+
default )
188+
{
189+
DisplayName = displayName,
190+
Metadata = { metadata },
191+
};
192+
193+
return builder.Build();
194+
}
195+
196+
private static Task Route404( HttpContext context ) => Task.CompletedTask;
197+
}

0 commit comments

Comments
 (0)