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>
@@ -63,11 +65,7 @@ protected virtual IReadOnlyList<ApiVersionDescription> Describe( IReadOnlyList<G
6365 /// <summary>
6466 /// Represents the API version metadata applied to an endpoint with an optional group name.
6567 /// </summary>
66- protected class GroupedApiVersionMetadata :
67- ApiVersionMetadata ,
68- IEquatable < GroupedApiVersionMetadata > ,
69- IGroupedApiVersionMetadata ,
70- IGroupedApiVersionMetadataFactory < GroupedApiVersionMetadata >
68+ protected class GroupedApiVersionMetadata : ApiVersionMetadata , IEquatable < GroupedApiVersionMetadata >
7169 {
7270 /// <summary>
7371 /// Initializes a new instance of the <see cref="GroupedApiVersionMetadata"/> class.
@@ -83,10 +81,6 @@ public GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata
8381 /// <value>The associated group name, if any.</value>
8482 public string ? GroupName { get ; }
8583
86- static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory < GroupedApiVersionMetadata > . New (
87- string ? groupName ,
88- ApiVersionMetadata metadata ) => new ( groupName , metadata ) ;
89-
9084 /// <inheritdoc />
9185 public bool Equals ( GroupedApiVersionMetadata ? other ) =>
9286 other is not null && other . GetHashCode ( ) == GetHashCode ( ) ;
@@ -112,4 +106,202 @@ public override int GetHashCode()
112106 return hash . ToHashCode ( ) ;
113107 }
114108 }
109+
110+ private record struct GroupedApiVersion ( string ? GroupName , ApiVersion ApiVersion ) ;
111+
112+ private sealed class ApiVersionDescriptionCollection (
113+ Func < IReadOnlyList < GroupedApiVersionMetadata > , IReadOnlyList < ApiVersionDescription > > describe ,
114+ IEnumerable < IApiVersionMetadataCollationProvider > collators )
115+ {
116+ private readonly Lock syncRoot = new ( ) ;
117+ private readonly Func < IReadOnlyList < GroupedApiVersionMetadata > , IReadOnlyList < ApiVersionDescription > > describe = describe ;
118+ private readonly IApiVersionMetadataCollationProvider [ ] collators = [ .. collators ] ;
119+ private IReadOnlyList < ApiVersionDescription > ? items ;
120+ private int version ;
121+
122+ public IReadOnlyList < ApiVersionDescription > Items
123+ {
124+ get
125+ {
126+ if ( items is not null && version == ComputeVersion ( ) )
127+ {
128+ return items ;
129+ }
130+
131+ using ( syncRoot . EnterScope ( ) )
132+ {
133+ var currentVersion = ComputeVersion ( ) ;
134+
135+ if ( items is not null && version == currentVersion )
136+ {
137+ return items ;
138+ }
139+
140+ var context = new ApiVersionMetadataCollationContext ( ) ;
141+
142+ for ( var i = 0 ; i < collators . Length ; i ++ )
143+ {
144+ collators [ i ] . Execute ( context ) ;
145+ }
146+
147+ var results = context . Results ;
148+ var metadata = new GroupedApiVersionMetadata [ results . Count ] ;
149+
150+ for ( var i = 0 ; i < metadata . Length ; i ++ )
151+ {
152+ metadata [ i ] = new GroupedApiVersionMetadata ( context . Results . GroupName ( i ) , results [ i ] ) ;
153+ }
154+
155+ items = describe ( metadata ) ;
156+ version = currentVersion ;
157+ }
158+
159+ return items ;
160+ }
161+ }
162+
163+ private int ComputeVersion ( ) =>
164+ collators . Length switch
165+ {
166+ 0 => 0 ,
167+ 1 => collators [ 0 ] . Version ,
168+ _ => ComputeVersion ( collators ) ,
169+ } ;
170+
171+ private static int ComputeVersion ( IApiVersionMetadataCollationProvider [ ] providers )
172+ {
173+ var hash = default ( HashCode ) ;
174+
175+ for ( var i = 0 ; i < providers . Length ; i ++ )
176+ {
177+ hash . Add ( providers [ i ] . Version ) ;
178+ }
179+
180+ return hash . ToHashCode ( ) ;
181+ }
182+ }
183+
184+ private sealed class ApiVersionDescriptionComparer : IComparer < ApiVersionDescription >
185+ {
186+ public int Compare ( ApiVersionDescription ? x , ApiVersionDescription ? y )
187+ {
188+ if ( x is null )
189+ {
190+ return y is null ? 0 : - 1 ;
191+ }
192+
193+ if ( y is null )
194+ {
195+ return 1 ;
196+ }
197+
198+ var result = x . ApiVersion . CompareTo ( y . ApiVersion ) ;
199+
200+ if ( result == 0 )
201+ {
202+ result = StringComparer . Ordinal . Compare ( x . GroupName , y . GroupName ) ;
203+ }
204+
205+ return result ;
206+ }
207+ }
208+
209+ private static class DescriptionProvider
210+ {
211+ internal static ApiVersionDescription [ ] Describe (
212+ IReadOnlyList < GroupedApiVersionMetadata > metadata ,
213+ ISunsetPolicyManager sunsetPolicyManager ,
214+ ApiExplorerOptions options )
215+ {
216+ var descriptions = new SortedSet < ApiVersionDescription > ( new ApiVersionDescriptionComparer ( ) ) ;
217+ var supported = new HashSet < GroupedApiVersion > ( ) ;
218+ var deprecated = new HashSet < GroupedApiVersion > ( ) ;
219+
220+ BucketizeApiVersions ( metadata , supported , deprecated , options ) ;
221+ AppendDescriptions ( descriptions , supported , sunsetPolicyManager , options , deprecated : false ) ;
222+ AppendDescriptions ( descriptions , deprecated , sunsetPolicyManager , options , deprecated : true ) ;
223+
224+ return [ .. descriptions ] ;
225+ }
226+
227+ private static void BucketizeApiVersions (
228+ IReadOnlyList < GroupedApiVersionMetadata > list ,
229+ HashSet < GroupedApiVersion > supported ,
230+ HashSet < GroupedApiVersion > deprecated ,
231+ ApiExplorerOptions options )
232+ {
233+ var declared = new HashSet < GroupedApiVersion > ( ) ;
234+ var advertisedSupported = new HashSet < GroupedApiVersion > ( ) ;
235+ var advertisedDeprecated = new HashSet < GroupedApiVersion > ( ) ;
236+
237+ for ( var i = 0 ; i < list . Count ; i ++ )
238+ {
239+ var metadata = list [ i ] ;
240+ var groupName = metadata . GroupName ;
241+ var model = metadata . Map ( Explicit | Implicit ) ;
242+ var versions = model . DeclaredApiVersions ;
243+
244+ for ( var j = 0 ; j < versions . Count ; j ++ )
245+ {
246+ declared . Add ( new ( groupName , versions [ j ] ) ) ;
247+ }
248+
249+ versions = model . SupportedApiVersions ;
250+
251+ for ( var j = 0 ; j < versions . Count ; j ++ )
252+ {
253+ var version = versions [ j ] ;
254+ supported . Add ( new ( groupName , version ) ) ;
255+ advertisedSupported . Add ( new ( groupName , version ) ) ;
256+ }
257+
258+ versions = model . DeprecatedApiVersions ;
259+
260+ for ( var j = 0 ; j < versions . Count ; j ++ )
261+ {
262+ var version = versions [ j ] ;
263+ deprecated . Add ( new ( groupName , version ) ) ;
264+ advertisedDeprecated . Add ( new ( groupName , version ) ) ;
265+ }
266+ }
267+
268+ advertisedSupported . ExceptWith ( declared ) ;
269+ advertisedDeprecated . ExceptWith ( declared ) ;
270+ supported . ExceptWith ( advertisedSupported ) ;
271+ deprecated . ExceptWith ( supported . Concat ( advertisedDeprecated ) ) ;
272+
273+ if ( supported . Count == 0 && deprecated . Count == 0 )
274+ {
275+ supported . Add ( new ( default , options . DefaultApiVersion ) ) ;
276+ }
277+ }
278+
279+ private static void AppendDescriptions (
280+ SortedSet < ApiVersionDescription > descriptions ,
281+ HashSet < GroupedApiVersion > versions ,
282+ ISunsetPolicyManager sunsetPolicyManager ,
283+ ApiExplorerOptions options ,
284+ bool deprecated )
285+ {
286+ var format = options . GroupNameFormat ;
287+ var formatGroupName = options . FormatGroupName ;
288+
289+ foreach ( var ( groupName , version ) in versions )
290+ {
291+ var formattedGroupName = groupName ;
292+
293+ if ( string . IsNullOrEmpty ( formattedGroupName ) )
294+ {
295+ formattedGroupName = version . ToString ( format , CurrentCulture ) ;
296+ }
297+ else if ( formatGroupName is not null )
298+ {
299+ formattedGroupName = formatGroupName ( formattedGroupName , version . ToString ( format , CurrentCulture ) ) ;
300+ }
301+
302+ var sunsetPolicy = sunsetPolicyManager . TryGetPolicy ( version , out var policy ) ? policy : default ;
303+ descriptions . Add ( new ( version , formattedGroupName , deprecated , sunsetPolicy ) ) ;
304+ }
305+ }
306+ }
115307}
0 commit comments