22
33namespace Asp . Versioning . ApiExplorer ;
44
5- using Asp . Versioning . ApiExplorer . Internal ;
65using Microsoft . Extensions . Options ;
6+ using static Asp . Versioning . ApiVersionMapping ;
7+ using static System . Globalization . CultureInfo ;
78
89/// <summary>
9- /// Represents the default implementation of an object that discovers and describes the API version information within an application.
10+ /// Represents the default implementation of an object that discovers and describes the API version information within
11+ /// an application.
1012/// </summary>
1113[ CLSCompliant ( false ) ]
1214public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvider
1315{
14- private readonly ApiVersionDescriptionCollection < GroupedApiVersionMetadata > collection ;
16+ private readonly ApiVersionDescriptionCollection collection ;
1517 private readonly IOptions < ApiExplorerOptions > options ;
1618
1719 /// <summary>
@@ -72,11 +74,7 @@ protected virtual IReadOnlyList<ApiVersionDescription> Describe( IReadOnlyList<G
7274 /// <summary>
7375 /// Represents the API version metadata applied to an endpoint with an optional group name.
7476 /// </summary>
75- protected class GroupedApiVersionMetadata :
76- ApiVersionMetadata ,
77- IEquatable < GroupedApiVersionMetadata > ,
78- IGroupedApiVersionMetadata ,
79- IGroupedApiVersionMetadataFactory < GroupedApiVersionMetadata >
77+ protected class GroupedApiVersionMetadata : ApiVersionMetadata , IEquatable < GroupedApiVersionMetadata >
8078 {
8179 /// <summary>
8280 /// Initializes a new instance of the <see cref="GroupedApiVersionMetadata"/> class.
@@ -92,10 +90,6 @@ public GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata
9290 /// <value>The associated group name, if any.</value>
9391 public string ? GroupName { get ; }
9492
95- static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory < GroupedApiVersionMetadata > . New (
96- string ? groupName ,
97- ApiVersionMetadata metadata ) => new ( groupName , metadata ) ;
98-
9993 /// <inheritdoc />
10094 public bool Equals ( GroupedApiVersionMetadata ? other ) =>
10195 other is not null && other . GetHashCode ( ) == GetHashCode ( ) ;
@@ -121,4 +115,202 @@ public override int GetHashCode()
121115 return hash . ToHashCode ( ) ;
122116 }
123117 }
118+
119+ private record struct GroupedApiVersion ( string ? GroupName , ApiVersion ApiVersion ) ;
120+
121+ private sealed class ApiVersionDescriptionCollection (
122+ Func < IReadOnlyList < GroupedApiVersionMetadata > , IReadOnlyList < ApiVersionDescription > > describe ,
123+ IEnumerable < IApiVersionMetadataCollationProvider > collators )
124+ {
125+ private readonly Lock syncRoot = new ( ) ;
126+ private readonly Func < IReadOnlyList < GroupedApiVersionMetadata > , IReadOnlyList < ApiVersionDescription > > describe = describe ;
127+ private readonly IApiVersionMetadataCollationProvider [ ] collators = [ .. collators ] ;
128+ private IReadOnlyList < ApiVersionDescription > ? items ;
129+ private int version ;
130+
131+ public IReadOnlyList < ApiVersionDescription > Items
132+ {
133+ get
134+ {
135+ if ( items is not null && version == ComputeVersion ( ) )
136+ {
137+ return items ;
138+ }
139+
140+ using ( syncRoot . EnterScope ( ) )
141+ {
142+ var currentVersion = ComputeVersion ( ) ;
143+
144+ if ( items is not null && version == currentVersion )
145+ {
146+ return items ;
147+ }
148+
149+ var context = new ApiVersionMetadataCollationContext ( ) ;
150+
151+ for ( var i = 0 ; i < collators . Length ; i ++ )
152+ {
153+ collators [ i ] . Execute ( context ) ;
154+ }
155+
156+ var results = context . Results ;
157+ var metadata = new GroupedApiVersionMetadata [ results . Count ] ;
158+
159+ for ( var i = 0 ; i < metadata . Length ; i ++ )
160+ {
161+ metadata [ i ] = new GroupedApiVersionMetadata ( context . Results . GroupName ( i ) , results [ i ] ) ;
162+ }
163+
164+ items = describe ( metadata ) ;
165+ version = currentVersion ;
166+ }
167+
168+ return items ;
169+ }
170+ }
171+
172+ private int ComputeVersion ( ) =>
173+ collators . Length switch
174+ {
175+ 0 => 0 ,
176+ 1 => collators [ 0 ] . Version ,
177+ _ => ComputeVersion ( collators ) ,
178+ } ;
179+
180+ private static int ComputeVersion ( IApiVersionMetadataCollationProvider [ ] providers )
181+ {
182+ var hash = default ( HashCode ) ;
183+
184+ for ( var i = 0 ; i < providers . Length ; i ++ )
185+ {
186+ hash . Add ( providers [ i ] . Version ) ;
187+ }
188+
189+ return hash . ToHashCode ( ) ;
190+ }
191+ }
192+
193+ private sealed class ApiVersionDescriptionComparer : IComparer < ApiVersionDescription >
194+ {
195+ public int Compare ( ApiVersionDescription ? x , ApiVersionDescription ? y )
196+ {
197+ if ( x is null )
198+ {
199+ return y is null ? 0 : - 1 ;
200+ }
201+
202+ if ( y is null )
203+ {
204+ return 1 ;
205+ }
206+
207+ var result = x . ApiVersion . CompareTo ( y . ApiVersion ) ;
208+
209+ if ( result == 0 )
210+ {
211+ result = StringComparer . Ordinal . Compare ( x . GroupName , y . GroupName ) ;
212+ }
213+
214+ return result ;
215+ }
216+ }
217+
218+ private static class DescriptionProvider
219+ {
220+ internal static ApiVersionDescription [ ] Describe (
221+ IReadOnlyList < GroupedApiVersionMetadata > metadata ,
222+ ISunsetPolicyManager sunsetPolicyManager ,
223+ ApiExplorerOptions options )
224+ {
225+ var descriptions = new SortedSet < ApiVersionDescription > ( new ApiVersionDescriptionComparer ( ) ) ;
226+ var supported = new HashSet < GroupedApiVersion > ( ) ;
227+ var deprecated = new HashSet < GroupedApiVersion > ( ) ;
228+
229+ BucketizeApiVersions ( metadata , supported , deprecated , options ) ;
230+ AppendDescriptions ( descriptions , supported , sunsetPolicyManager , options , deprecated : false ) ;
231+ AppendDescriptions ( descriptions , deprecated , sunsetPolicyManager , options , deprecated : true ) ;
232+
233+ return [ .. descriptions ] ;
234+ }
235+
236+ private static void BucketizeApiVersions (
237+ IReadOnlyList < GroupedApiVersionMetadata > list ,
238+ HashSet < GroupedApiVersion > supported ,
239+ HashSet < GroupedApiVersion > deprecated ,
240+ ApiExplorerOptions options )
241+ {
242+ var declared = new HashSet < GroupedApiVersion > ( ) ;
243+ var advertisedSupported = new HashSet < GroupedApiVersion > ( ) ;
244+ var advertisedDeprecated = new HashSet < GroupedApiVersion > ( ) ;
245+
246+ for ( var i = 0 ; i < list . Count ; i ++ )
247+ {
248+ var metadata = list [ i ] ;
249+ var groupName = metadata . GroupName ;
250+ var model = metadata . Map ( Explicit | Implicit ) ;
251+ var versions = model . DeclaredApiVersions ;
252+
253+ for ( var j = 0 ; j < versions . Count ; j ++ )
254+ {
255+ declared . Add ( new ( groupName , versions [ j ] ) ) ;
256+ }
257+
258+ versions = model . SupportedApiVersions ;
259+
260+ for ( var j = 0 ; j < versions . Count ; j ++ )
261+ {
262+ var version = versions [ j ] ;
263+ supported . Add ( new ( groupName , version ) ) ;
264+ advertisedSupported . Add ( new ( groupName , version ) ) ;
265+ }
266+
267+ versions = model . DeprecatedApiVersions ;
268+
269+ for ( var j = 0 ; j < versions . Count ; j ++ )
270+ {
271+ var version = versions [ j ] ;
272+ deprecated . Add ( new ( groupName , version ) ) ;
273+ advertisedDeprecated . Add ( new ( groupName , version ) ) ;
274+ }
275+ }
276+
277+ advertisedSupported . ExceptWith ( declared ) ;
278+ advertisedDeprecated . ExceptWith ( declared ) ;
279+ supported . ExceptWith ( advertisedSupported ) ;
280+ deprecated . ExceptWith ( supported . Concat ( advertisedDeprecated ) ) ;
281+
282+ if ( supported . Count == 0 && deprecated . Count == 0 )
283+ {
284+ supported . Add ( new ( default , options . DefaultApiVersion ) ) ;
285+ }
286+ }
287+
288+ private static void AppendDescriptions (
289+ SortedSet < ApiVersionDescription > descriptions ,
290+ HashSet < GroupedApiVersion > versions ,
291+ ISunsetPolicyManager sunsetPolicyManager ,
292+ ApiExplorerOptions options ,
293+ bool deprecated )
294+ {
295+ var format = options . GroupNameFormat ;
296+ var formatGroupName = options . FormatGroupName ;
297+
298+ foreach ( var ( groupName , version ) in versions )
299+ {
300+ var formattedGroupName = groupName ;
301+
302+ if ( string . IsNullOrEmpty ( formattedGroupName ) )
303+ {
304+ formattedGroupName = version . ToString ( format , CurrentCulture ) ;
305+ }
306+ else if ( formatGroupName is not null )
307+ {
308+ formattedGroupName = formatGroupName ( formattedGroupName , version . ToString ( format , CurrentCulture ) ) ;
309+ }
310+
311+ var sunsetPolicy = sunsetPolicyManager . TryGetPolicy ( version , out var policy ) ? policy : default ;
312+ descriptions . Add ( new ( version , formattedGroupName , deprecated , sunsetPolicy ) ) ;
313+ }
314+ }
315+ }
124316}
0 commit comments