Skip to content

Commit 95bf9be

Browse files
Refactor to support API version metadata collation via DI. Fixes #922
1 parent 4e1e8f8 commit 95bf9be

17 files changed

+506
-420
lines changed

src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/DefaultMetadataMatcherPolicy.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Asp.Versioning.Routing;
44

55
using Asp.Versioning;
6+
using Asp.Versioning.ApiExplorer;
67
using Microsoft.AspNetCore.Http;
78
using Microsoft.AspNetCore.OData.Routing;
89
using Microsoft.AspNetCore.OData.Routing.Template;
@@ -159,6 +160,7 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList<Policy
159160
private static int ApiVersioningPolicy() =>
160161
new ApiVersionMatcherPolicy(
161162
ApiVersionParser.Default,
163+
Enumerable.Empty<IApiVersionMetadataCollationProvider>(),
162164
Options.Create( new ApiVersioningOptions() ),
163165
new NullLogger<ApiVersionMatcherPolicy>() ).Order;
164166

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.ApiExplorer;
4+
5+
using System.Collections;
6+
7+
/// <summary>
8+
/// Represents a collection of collated API version metadata.
9+
/// </summary>
10+
public class ApiVersionMetadataCollationCollection : IList<ApiVersionMetadata>, IReadOnlyList<ApiVersionMetadata>
11+
{
12+
private readonly List<ApiVersionMetadata> items;
13+
private readonly List<string?> groups;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="ApiVersionMetadataCollationCollection"/> class.
17+
/// </summary>
18+
public ApiVersionMetadataCollationCollection()
19+
{
20+
items = new();
21+
groups = new();
22+
}
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="ApiVersionMetadataCollationCollection"/> class.
26+
/// </summary>
27+
/// <param name="capacity">The initial capacity of the collection.</param>
28+
public ApiVersionMetadataCollationCollection( int capacity )
29+
{
30+
items = new( capacity );
31+
groups = new( capacity );
32+
}
33+
34+
/// <summary>
35+
/// Gets the item in the list at the specified index.
36+
/// </summary>
37+
/// <param name="index">The zero-based index of the item to retrieve.</param>
38+
/// <returns>The item at the specified index.</returns>
39+
public ApiVersionMetadata this[int index] => items[index];
40+
41+
ApiVersionMetadata IList<ApiVersionMetadata>.this[int index]
42+
{
43+
get => items[index];
44+
set => throw new NotSupportedException();
45+
}
46+
47+
/// <inheritdoc />
48+
public int Count => items.Count;
49+
50+
#pragma warning disable CA1033 // Interface methods should be callable by child types
51+
bool ICollection<ApiVersionMetadata>.IsReadOnly => ( (ICollection<ApiVersionMetadata>) items ).IsReadOnly;
52+
#pragma warning restore CA1033 // Interface methods should be callable by child types
53+
54+
/// <inheritdoc />
55+
public void Add( ApiVersionMetadata item ) => Insert( Count, item, default );
56+
57+
/// <summary>
58+
/// Adds an item to the collection.
59+
/// </summary>
60+
/// <param name="item">The item to add.</param>
61+
/// <param name="groupName">The associated group name, if any.</param>
62+
public void Add( ApiVersionMetadata item, string? groupName ) => Insert( Count, item, groupName );
63+
64+
/// <inheritdoc />
65+
public void Clear()
66+
{
67+
items.Clear();
68+
groups.Clear();
69+
}
70+
71+
/// <inheritdoc />
72+
public bool Contains( ApiVersionMetadata item ) => item != null && items.Contains( item );
73+
74+
/// <inheritdoc />
75+
public void CopyTo( ApiVersionMetadata[] array, int arrayIndex ) => items.CopyTo( array, arrayIndex );
76+
77+
/// <inheritdoc />
78+
public IEnumerator<ApiVersionMetadata> GetEnumerator() => items.GetEnumerator();
79+
80+
/// <inheritdoc />
81+
public int IndexOf( ApiVersionMetadata item ) => item == null ? -1 : items.IndexOf( item );
82+
83+
/// <inheritdoc />
84+
public void Insert( int index, ApiVersionMetadata item ) => Insert( index, item, default );
85+
86+
/// <summary>
87+
/// Inserts an item into the collection.
88+
/// </summary>
89+
/// <param name="index">The zero-based index where insertion takes place.</param>
90+
/// <param name="item">The item to insert.</param>
91+
/// <param name="groupName">The associated group name, if any.</param>
92+
public void Insert( int index, ApiVersionMetadata item, string? groupName )
93+
{
94+
items.Insert( index, item ?? throw new ArgumentNullException( nameof( item ) ) );
95+
groups.Insert( index, groupName );
96+
}
97+
98+
/// <inheritdoc />
99+
public bool Remove( ApiVersionMetadata item )
100+
{
101+
if ( item == null )
102+
{
103+
return false;
104+
}
105+
106+
var index = items.IndexOf( item );
107+
108+
if ( index < 0 )
109+
{
110+
return false;
111+
}
112+
113+
RemoveAt( index );
114+
return true;
115+
}
116+
117+
/// <inheritdoc />
118+
public void RemoveAt( int index )
119+
{
120+
items.RemoveAt( index );
121+
groups.RemoveAt( index );
122+
}
123+
124+
IEnumerator IEnumerable.GetEnumerator() => ( (IEnumerable) items ).GetEnumerator();
125+
126+
/// <summary>
127+
/// Gets the group name for the item at the specified index.
128+
/// </summary>
129+
/// <param name="index">The zero-based index of the item to get the group name for.</param>
130+
/// <returns>The associated group name or <c>null</c>.</returns>
131+
/// <remarks>If the specified <paramref name="index"/> is out of range, <c>null</c>
132+
/// is returned.</remarks>
133+
public string? GroupName( int index ) =>
134+
index < 0 || index >= groups.Count ? default : groups[index];
135+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.ApiExplorer;
4+
5+
/// <summary>
6+
/// Represents the context used during API version metadata collation.
7+
/// </summary>
8+
public class ApiVersionMetadataCollationContext
9+
{
10+
/// <summary>
11+
/// Gets the read-only list of collation results.
12+
/// </summary>
13+
/// <value>The <see cref="IReadOnlyList{T}">read-only list</see> of collation results.</value>
14+
public ApiVersionMetadataCollationCollection Results { get; } = new();
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.ApiExplorer;
4+
5+
using Microsoft.AspNetCore.Routing;
6+
using Microsoft.Extensions.Primitives;
7+
8+
/// <summary>
9+
/// Represents the API version metadata collection provider for endpoints.
10+
/// </summary>
11+
[CLSCompliant( false )]
12+
public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider
13+
{
14+
private readonly EndpointDataSource endpointDataSource;
15+
private int version;
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="EndpointApiVersionMetadataCollationProvider"/> class.
19+
/// </summary>
20+
/// <param name="endpointDataSource">The underlying <see cref="endpointDataSource">endpoint data source</see>.</param>
21+
public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource )
22+
{
23+
this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) );
24+
ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version );
25+
}
26+
27+
/// <inheritdoc />
28+
public int Version => version;
29+
30+
/// <inheritdoc />
31+
public void Execute( ApiVersionMetadataCollationContext context )
32+
{
33+
if ( context == null )
34+
{
35+
throw new ArgumentNullException( nameof( context ) );
36+
}
37+
38+
var endpoints = endpointDataSource.Endpoints;
39+
40+
for ( var i = 0; i < endpoints.Count; i++ )
41+
{
42+
var endpoint = endpoints[i];
43+
44+
if ( endpoint.Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata item )
45+
{
46+
continue;
47+
}
48+
49+
var groupName = endpoint.Metadata.OfType<IEndpointGroupNameMetadata>().LastOrDefault()?.EndpointGroupName;
50+
context.Results.Add( item, groupName );
51+
}
52+
}
53+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.ApiExplorer;
4+
5+
/// <summary>
6+
/// Defines the behavior of an API version metadata collation provider.
7+
/// </summary>
8+
public interface IApiVersionMetadataCollationProvider
9+
{
10+
/// <summary>
11+
/// Gets version of the underlying provider results.
12+
/// </summary>
13+
/// <value>The version of the provider results. This can be used to detect changes.</value>
14+
int Version { get; }
15+
16+
/// <summary>
17+
/// Executes the provider using the given context.
18+
/// </summary>
19+
/// <param name="context">The collation context.</param>
20+
void Execute( ApiVersionMetadataCollationContext context );
21+
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Microsoft.Extensions.DependencyInjection;
44

55
using Asp.Versioning;
6-
using Asp.Versioning.Builder;
6+
using Asp.Versioning.ApiExplorer;
77
using Asp.Versioning.Routing;
88
using Microsoft.AspNetCore.Http;
99
using Microsoft.AspNetCore.Routing;
@@ -92,6 +92,7 @@ private static void AddApiVersioningServices( IServiceCollection services )
9292
services.TryAddSingleton<ISunsetPolicyManager, SunsetPolicyManager>();
9393
services.TryAddEnumerable( Transient<IPostConfigureOptions<RouteOptions>, ApiVersioningRouteOptionsSetup>() );
9494
services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() );
95+
services.TryAddEnumerable( Singleton<IApiVersionMetadataCollationProvider, EndpointApiVersionMetadataCollationProvider>() );
9596
services.Replace( WithLinkGeneratorDecorator( services ) );
9697
TryAddProblemDetailsRfc7231Compliance( services );
9798
}
@@ -157,6 +158,8 @@ LinkGenerator NewFactory( IServiceProvider serviceProvider )
157158
}
158159
}
159160

161+
// TODO: Remove in .NET 8.0
162+
// REF: https://github.com/dotnet/aspnetcore/issues/45051
160163
private static void TryAddProblemDetailsRfc7231Compliance( IServiceCollection services )
161164
{
162165
var descriptor = services.FirstOrDefault( IsDefaultProblemDetailsWriter );

0 commit comments

Comments
 (0)