Skip to content

Commit 59ff9e6

Browse files
Refactor and unify IApiVersionDescriptionProvider implementations. Explicit group names can only be determined at runtime. Related #1066
1 parent 9d18108 commit 59ff9e6

8 files changed

+291
-301
lines changed

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

Lines changed: 33 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22

33
namespace Asp.Versioning.ApiExplorer;
44

5+
using Asp.Versioning.ApiExplorer.Internal;
56
using Microsoft.Extensions.Options;
6-
using static Asp.Versioning.ApiVersionMapping;
7-
using static System.Globalization.CultureInfo;
87

98
/// <summary>
109
/// Represents the default implementation of an object that discovers and describes the API version information within an application.
1110
/// </summary>
1211
[CLSCompliant( false )]
1312
public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider
1413
{
15-
private readonly ApiVersionDescriptionCollection collection;
14+
private readonly ApiVersionDescriptionCollection<GroupedApiVersionMetadata> collection;
1615
private readonly IOptions<ApiExplorerOptions> options;
1716

1817
/// <summary>
@@ -28,7 +27,7 @@ public DefaultApiVersionDescriptionProvider(
2827
ISunsetPolicyManager sunsetPolicyManager,
2928
IOptions<ApiExplorerOptions> apiExplorerOptions )
3029
{
31-
collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
30+
collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
3231
SunsetPolicyManager = sunsetPolicyManager;
3332
options = apiExplorerOptions;
3433
}
@@ -58,133 +57,53 @@ protected virtual IReadOnlyList<ApiVersionDescription> Describe( IReadOnlyList<A
5857
{
5958
ArgumentNullException.ThrowIfNull( metadata );
6059

61-
var descriptions = new List<ApiVersionDescription>( capacity: metadata.Count );
62-
var supported = new HashSet<ApiVersion>();
63-
var deprecated = new HashSet<ApiVersion>();
64-
65-
BucketizeApiVersions( metadata, supported, deprecated );
66-
AppendDescriptions( descriptions, supported, deprecated: false );
67-
AppendDescriptions( descriptions, deprecated, deprecated: true );
68-
69-
return descriptions.OrderBy( d => d.ApiVersion ).ToArray();
70-
}
71-
72-
private void BucketizeApiVersions( IReadOnlyList<ApiVersionMetadata> metadata, HashSet<ApiVersion> supported, HashSet<ApiVersion> deprecated )
73-
{
74-
var declared = new HashSet<ApiVersion>();
75-
var advertisedSupported = new HashSet<ApiVersion>();
76-
var advertisedDeprecated = new HashSet<ApiVersion>();
77-
78-
for ( var i = 0; i < metadata.Count; i++ )
60+
// TODO: consider refactoring and removing GroupedApiVersionDescriptionProvider as both implementations are now
61+
// effectively the same. this cast is safe as an internal implementation detail. if this method is
62+
// overridden, then this code doesn't even run
63+
//
64+
// REF: https://github.com/dotnet/aspnet-api-versioning/issues/1066
65+
if ( metadata is GroupedApiVersionMetadata[] groupedMetadata )
7966
{
80-
var model = metadata[i].Map( Explicit | Implicit );
81-
var versions = model.DeclaredApiVersions;
82-
83-
for ( var j = 0; j < versions.Count; j++ )
84-
{
85-
declared.Add( versions[j] );
86-
}
87-
88-
versions = model.SupportedApiVersions;
89-
90-
for ( var j = 0; j < versions.Count; j++ )
91-
{
92-
var version = versions[j];
93-
supported.Add( version );
94-
advertisedSupported.Add( version );
95-
}
96-
97-
versions = model.DeprecatedApiVersions;
98-
99-
for ( var j = 0; j < versions.Count; j++ )
100-
{
101-
var version = versions[j];
102-
deprecated.Add( version );
103-
advertisedDeprecated.Add( version );
104-
}
67+
return DescriptionProvider.Describe( groupedMetadata, SunsetPolicyManager, Options );
10568
}
10669

107-
advertisedSupported.ExceptWith( declared );
108-
advertisedDeprecated.ExceptWith( declared );
109-
supported.ExceptWith( advertisedSupported );
110-
deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) );
111-
112-
if ( supported.Count == 0 && deprecated.Count == 0 )
113-
{
114-
supported.Add( Options.DefaultApiVersion );
115-
}
70+
return Array.Empty<ApiVersionDescription>();
11671
}
11772

118-
private void AppendDescriptions( List<ApiVersionDescription> descriptions, IEnumerable<ApiVersion> versions, bool deprecated )
73+
private sealed class GroupedApiVersionMetadata :
74+
ApiVersionMetadata,
75+
IEquatable<GroupedApiVersionMetadata>,
76+
IGroupedApiVersionMetadata,
77+
IGroupedApiVersionMetadataFactory<GroupedApiVersionMetadata>
11978
{
120-
foreach ( var version in versions )
121-
{
122-
var groupName = version.ToString( Options.GroupNameFormat, CurrentCulture );
123-
var sunsetPolicy = SunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default;
124-
descriptions.Add( new( version, groupName, deprecated, sunsetPolicy ) );
125-
}
126-
}
79+
private GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata )
80+
: base( metadata ) => GroupName = groupName;
12781

128-
private sealed class ApiVersionDescriptionCollection(
129-
DefaultApiVersionDescriptionProvider provider,
130-
IEnumerable<IApiVersionMetadataCollationProvider> collators )
131-
{
132-
private readonly object syncRoot = new();
133-
private readonly DefaultApiVersionDescriptionProvider provider = provider;
134-
private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
135-
private IReadOnlyList<ApiVersionDescription>? items;
136-
private int version;
137-
138-
public IReadOnlyList<ApiVersionDescription> Items
139-
{
140-
get
141-
{
142-
if ( items is not null && version == ComputeVersion() )
143-
{
144-
return items;
145-
}
82+
public string? GroupName { get; }
14683

147-
lock ( syncRoot )
148-
{
149-
var currentVersion = ComputeVersion();
84+
static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory<GroupedApiVersionMetadata>.New(
85+
string? groupName,
86+
ApiVersionMetadata metadata ) => new( groupName, metadata );
15087

151-
if ( items is not null && version == currentVersion )
152-
{
153-
return items;
154-
}
88+
public bool Equals( GroupedApiVersionMetadata? other ) =>
89+
other is not null && other.GetHashCode() == GetHashCode();
15590

156-
var context = new ApiVersionMetadataCollationContext();
91+
public override bool Equals( object? obj ) =>
92+
obj is not null &&
93+
GetType().Equals( obj.GetType() ) &&
94+
GetHashCode() == obj.GetHashCode();
15795

158-
for ( var i = 0; i < collators.Length; i++ )
159-
{
160-
collators[i].Execute( context );
161-
}
162-
163-
items = provider.Describe( context.Results );
164-
version = currentVersion;
165-
}
166-
167-
return items;
168-
}
169-
}
170-
171-
private int ComputeVersion() =>
172-
collators.Length switch
173-
{
174-
0 => 0,
175-
1 => collators[0].Version,
176-
_ => ComputeVersion( collators ),
177-
};
178-
179-
private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers )
96+
public override int GetHashCode()
18097
{
18198
var hash = default( HashCode );
18299

183-
for ( var i = 0; i < providers.Length; i++ )
100+
if ( !string.IsNullOrEmpty( GroupName ) )
184101
{
185-
hash.Add( providers[i].Version );
102+
hash.Add( GroupName, StringComparer.Ordinal );
186103
}
187104

105+
hash.Add( base.GetHashCode() );
106+
188107
return hash.ToHashCode();
189108
}
190109
}

0 commit comments

Comments
 (0)