diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs
new file mode 100644
index 00000000..7109d87f
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer;
+
+using Microsoft.AspNetCore.Http;
+
+///
+/// Represents the default endpoint inspector.
+///
+[CLSCompliant(false)]
+public sealed class DefaultEndpointInspector : IEndpointInspector
+{
+ ///
+ public bool IsControllerAction( Endpoint endpoint ) => false;
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs
index 45c14725..e5ce20fb 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs
@@ -12,15 +12,29 @@ namespace Asp.Versioning.ApiExplorer;
public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider
{
private readonly EndpointDataSource endpointDataSource;
+ private readonly IEndpointInspector endpointInspector;
private int version;
///
/// Initializes a new instance of the class.
///
/// The underlying endpoint data source.
+ [Obsolete( "Use the constructor that accepts IEndpointInspector. This constructor will be removed in a future version." )]
public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource )
+ : this( endpointDataSource, new DefaultEndpointInspector() ) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying endpoint data source.
+ /// The endpoint inspector used to inspect endpoints.
+ public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource, IEndpointInspector endpointInspector )
{
- this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) );
+ ArgumentNullException.ThrowIfNull( endpointDataSource );
+ ArgumentNullException.ThrowIfNull( endpointInspector );
+
+ this.endpointDataSource = endpointDataSource;
+ this.endpointInspector = endpointInspector;
ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version );
}
@@ -38,7 +52,8 @@ public void Execute( ApiVersionMetadataCollationContext context )
{
var endpoint = endpoints[i];
- if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item )
+ if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item ||
+ endpointInspector.IsControllerAction( endpoint ) )
{
continue;
}
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IEndpointInspector.cs
new file mode 100644
index 00000000..900edf94
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IEndpointInspector.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer;
+
+using Microsoft.AspNetCore.Http;
+
+///
+/// Defines the behavior of an endpoint inspector.
+///
+[CLSCompliant( false )]
+public interface IEndpointInspector
+{
+ ///
+ /// Determines whether the specified endpoint is a controller action.
+ ///
+ /// The endpoint to inspect.
+ /// True if the is for a controller action; otherwise, false.
+ bool IsControllerAction( Endpoint endpoint );
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs
index 07911329..14606936 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs
@@ -89,6 +89,7 @@ private static void AddApiVersioningServices( IServiceCollection services )
services.TryAddEnumerable( Transient, ApiVersioningRouteOptionsSetup>() );
services.TryAddEnumerable( Singleton() );
services.TryAddEnumerable( Singleton() );
+ services.TryAddTransient();
services.Replace( WithLinkGeneratorDecorator( services ) );
TryAddProblemDetailsRfc7231Compliance( services );
TryAddErrorObjectJsonOptions( services );
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs
index 05b1cba1..75b99644 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs
@@ -16,6 +16,7 @@ internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescript
{
private readonly ISunsetPolicyManager sunsetPolicyManager;
private readonly IApiVersionMetadataCollationProvider[] providers;
+ private readonly IEndpointInspector endpointInspector;
private readonly IOptions options;
private readonly Activator activator;
@@ -23,11 +24,13 @@ public ApiVersionDescriptionProviderFactory(
Activator activator,
ISunsetPolicyManager sunsetPolicyManager,
IEnumerable providers,
+ IEndpointInspector endpointInspector,
IOptions options )
{
this.activator = activator;
this.sunsetPolicyManager = sunsetPolicyManager;
this.providers = providers.ToArray();
+ this.endpointInspector = endpointInspector;
this.options = options;
}
@@ -35,7 +38,7 @@ public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSou
{
var collators = new List( capacity: providers.Length + 1 )
{
- new EndpointApiVersionMetadataCollationProvider( endpointDataSource ),
+ new EndpointApiVersionMetadataCollationProvider( endpointDataSource, endpointInspector ),
};
collators.AddRange( providers );
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs
index 63408cad..22151e11 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs
@@ -2,9 +2,8 @@
namespace Asp.Versioning.ApiExplorer;
+using Asp.Versioning.ApiExplorer.Internal;
using Microsoft.Extensions.Options;
-using static Asp.Versioning.ApiVersionMapping;
-using static System.Globalization.CultureInfo;
///
/// Represents the default implementation of an object that discovers and describes the API version information within an application.
@@ -12,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer;
[CLSCompliant( false )]
public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider
{
- private readonly ApiVersionDescriptionCollection collection;
+ private readonly ApiVersionDescriptionCollection collection;
private readonly IOptions options;
///
@@ -28,7 +27,7 @@ public DefaultApiVersionDescriptionProvider(
ISunsetPolicyManager sunsetPolicyManager,
IOptions apiExplorerOptions )
{
- collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
+ collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
SunsetPolicyManager = sunsetPolicyManager;
options = apiExplorerOptions;
}
@@ -58,133 +57,53 @@ protected virtual IReadOnlyList Describe( IReadOnlyList( capacity: metadata.Count );
- var supported = new HashSet();
- var deprecated = new HashSet();
-
- BucketizeApiVersions( metadata, supported, deprecated );
- AppendDescriptions( descriptions, supported, deprecated: false );
- AppendDescriptions( descriptions, deprecated, deprecated: true );
-
- return descriptions.OrderBy( d => d.ApiVersion ).ToArray();
- }
-
- private void BucketizeApiVersions( IReadOnlyList metadata, HashSet supported, HashSet deprecated )
- {
- var declared = new HashSet();
- var advertisedSupported = new HashSet();
- var advertisedDeprecated = new HashSet();
-
- for ( var i = 0; i < metadata.Count; i++ )
+ // TODO: consider refactoring and removing GroupedApiVersionDescriptionProvider as both implementations are now
+ // effectively the same. this cast is safe as an internal implementation detail. if this method is
+ // overridden, then this code doesn't even run
+ //
+ // REF: https://github.com/dotnet/aspnet-api-versioning/issues/1066
+ if ( metadata is GroupedApiVersionMetadata[] groupedMetadata )
{
- var model = metadata[i].Map( Explicit | Implicit );
- var versions = model.DeclaredApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- declared.Add( versions[j] );
- }
-
- versions = model.SupportedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- supported.Add( version );
- advertisedSupported.Add( version );
- }
-
- versions = model.DeprecatedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- deprecated.Add( version );
- advertisedDeprecated.Add( version );
- }
+ return DescriptionProvider.Describe( groupedMetadata, SunsetPolicyManager, Options );
}
- advertisedSupported.ExceptWith( declared );
- advertisedDeprecated.ExceptWith( declared );
- supported.ExceptWith( advertisedSupported );
- deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) );
-
- if ( supported.Count == 0 && deprecated.Count == 0 )
- {
- supported.Add( Options.DefaultApiVersion );
- }
+ return Array.Empty();
}
- private void AppendDescriptions( List descriptions, IEnumerable versions, bool deprecated )
+ private sealed class GroupedApiVersionMetadata :
+ ApiVersionMetadata,
+ IEquatable,
+ IGroupedApiVersionMetadata,
+ IGroupedApiVersionMetadataFactory
{
- foreach ( var version in versions )
- {
- var groupName = version.ToString( Options.GroupNameFormat, CurrentCulture );
- var sunsetPolicy = SunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default;
- descriptions.Add( new( version, groupName, deprecated, sunsetPolicy ) );
- }
- }
+ private GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata )
+ : base( metadata ) => GroupName = groupName;
- private sealed class ApiVersionDescriptionCollection(
- DefaultApiVersionDescriptionProvider provider,
- IEnumerable collators )
- {
- private readonly object syncRoot = new();
- private readonly DefaultApiVersionDescriptionProvider provider = provider;
- private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
- private IReadOnlyList? items;
- private int version;
-
- public IReadOnlyList Items
- {
- get
- {
- if ( items is not null && version == ComputeVersion() )
- {
- return items;
- }
+ public string? GroupName { get; }
- lock ( syncRoot )
- {
- var currentVersion = ComputeVersion();
+ static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New(
+ string? groupName,
+ ApiVersionMetadata metadata ) => new( groupName, metadata );
- if ( items is not null && version == currentVersion )
- {
- return items;
- }
+ public bool Equals( GroupedApiVersionMetadata? other ) =>
+ other is not null && other.GetHashCode() == GetHashCode();
- var context = new ApiVersionMetadataCollationContext();
+ public override bool Equals( object? obj ) =>
+ obj is not null &&
+ GetType().Equals( obj.GetType() ) &&
+ GetHashCode() == obj.GetHashCode();
- for ( var i = 0; i < collators.Length; i++ )
- {
- collators[i].Execute( context );
- }
-
- items = provider.Describe( context.Results );
- version = currentVersion;
- }
-
- return items;
- }
- }
-
- private int ComputeVersion() =>
- collators.Length switch
- {
- 0 => 0,
- 1 => collators[0].Version,
- _ => ComputeVersion( collators ),
- };
-
- private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers )
+ public override int GetHashCode()
{
var hash = default( HashCode );
- for ( var i = 0; i < providers.Length; i++ )
+ if ( !string.IsNullOrEmpty( GroupName ) )
{
- hash.Add( providers[i].Version );
+ hash.Add( GroupName, StringComparer.Ordinal );
}
+ hash.Add( base.GetHashCode() );
+
return hash.ToHashCode();
}
}
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs
index 1c1d2e63..29f0fba9 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs
@@ -5,11 +5,13 @@ namespace Microsoft.Extensions.DependencyInjection;
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
using static ServiceDescriptor;
///
@@ -70,40 +72,36 @@ private static IApiVersionDescriptionProviderFactory ResolveApiVersionDescriptio
{
var sunsetPolicyManager = serviceProvider.GetRequiredService();
var providers = serviceProvider.GetServices();
+ var inspector = serviceProvider.GetRequiredService();
var options = serviceProvider.GetRequiredService>();
- var mightUseCustomGroups = options.Value.FormatGroupName is not null;
return new ApiVersionDescriptionProviderFactory(
- mightUseCustomGroups ? NewGroupedProvider : NewDefaultProvider,
+ NewDefaultProvider,
sunsetPolicyManager,
providers,
+ inspector,
options );
- static IApiVersionDescriptionProvider NewDefaultProvider(
+ static DefaultApiVersionDescriptionProvider NewDefaultProvider(
IEnumerable providers,
ISunsetPolicyManager sunsetPolicyManager,
IOptions apiExplorerOptions ) =>
- new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions );
-
- static IApiVersionDescriptionProvider NewGroupedProvider(
- IEnumerable providers,
- ISunsetPolicyManager sunsetPolicyManager,
- IOptions apiExplorerOptions ) =>
- new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions );
+ new( providers, sunsetPolicyManager, apiExplorerOptions );
}
private static IApiVersionDescriptionProvider ResolveApiVersionDescriptionProvider( IServiceProvider serviceProvider )
{
- var providers = serviceProvider.GetServices();
- var sunsetPolicyManager = serviceProvider.GetRequiredService();
- var options = serviceProvider.GetRequiredService>();
- var mightUseCustomGroups = options.Value.FormatGroupName is not null;
+ var factory = serviceProvider.GetRequiredService();
+ var endpointDataSource = new EmptyEndpointDataSource();
+ return factory.Create( endpointDataSource );
+ }
+
+ private sealed class EmptyEndpointDataSource : EndpointDataSource
+ {
+ public override IReadOnlyList Endpoints => Array.Empty();
- if ( mightUseCustomGroups )
- {
- return new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, options );
- }
+ public override IChangeToken GetChangeToken() => new CancellationChangeToken( CancellationToken.None );
- return new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, options );
+ public override IReadOnlyList GetGroupedEndpoints( RouteGroupContext context ) => Array.Empty();
}
}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs
index ca31264b..294db52c 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs
@@ -2,10 +2,8 @@
namespace Asp.Versioning.ApiExplorer;
+using Asp.Versioning.ApiExplorer.Internal;
using Microsoft.Extensions.Options;
-using System.Buffers;
-using static Asp.Versioning.ApiVersionMapping;
-using static System.Globalization.CultureInfo;
///
/// Represents the default implementation of an object that discovers and describes the API version information within an application.
@@ -13,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer;
[CLSCompliant( false )]
public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvider
{
- private readonly ApiVersionDescriptionCollection collection;
+ private readonly ApiVersionDescriptionCollection collection;
private readonly IOptions options;
///
@@ -29,7 +27,7 @@ public GroupedApiVersionDescriptionProvider(
ISunsetPolicyManager sunsetPolicyManager,
IOptions apiExplorerOptions )
{
- collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
+ collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
SunsetPolicyManager = sunsetPolicyManager;
options = apiExplorerOptions;
}
@@ -59,191 +57,17 @@ public GroupedApiVersionDescriptionProvider(
protected virtual IReadOnlyList Describe( IReadOnlyList metadata )
{
ArgumentNullException.ThrowIfNull( metadata );
-
- var descriptions = new SortedSet( new ApiVersionDescriptionComparer() );
- var supported = new HashSet();
- var deprecated = new HashSet();
-
- BucketizeApiVersions( metadata, supported, deprecated );
- AppendDescriptions( descriptions, supported, deprecated: false );
- AppendDescriptions( descriptions, deprecated, deprecated: true );
-
- return descriptions.ToArray();
- }
-
- private void BucketizeApiVersions(
- IReadOnlyList list,
- ISet supported,
- ISet deprecated )
- {
- var declared = new HashSet();
- var advertisedSupported = new HashSet();
- var advertisedDeprecated = new HashSet();
-
- for ( var i = 0; i < list.Count; i++ )
- {
- var metadata = list[i];
- var groupName = metadata.GroupName;
- var model = metadata.Map( Explicit | Implicit );
- var versions = model.DeclaredApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- declared.Add( new( groupName, versions[j] ) );
- }
-
- versions = model.SupportedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- supported.Add( new( groupName, version ) );
- advertisedSupported.Add( new( groupName, version ) );
- }
-
- versions = model.DeprecatedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- deprecated.Add( new( groupName, version ) );
- advertisedDeprecated.Add( new( groupName, version ) );
- }
- }
-
- advertisedSupported.ExceptWith( declared );
- advertisedDeprecated.ExceptWith( declared );
- supported.ExceptWith( advertisedSupported );
- deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) );
-
- if ( supported.Count == 0 && deprecated.Count == 0 )
- {
- supported.Add( new( default, Options.DefaultApiVersion ) );
- }
- }
-
- private void AppendDescriptions(
- ICollection descriptions,
- IEnumerable versions,
- bool deprecated )
- {
- var format = Options.GroupNameFormat;
- var formatGroupName = Options.FormatGroupName;
-
- foreach ( var (groupName, version) in versions )
- {
- var formattedVersion = version.ToString( format, CurrentCulture );
- var formattedGroupName =
- string.IsNullOrEmpty( groupName ) || formatGroupName is null
- ? formattedVersion
- : formatGroupName( groupName, formattedVersion );
-
- var sunsetPolicy = SunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default;
- descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy ) );
- }
- }
-
- private sealed class ApiVersionDescriptionCollection(
- GroupedApiVersionDescriptionProvider provider,
- IEnumerable collators )
- {
- private readonly object syncRoot = new();
- private readonly GroupedApiVersionDescriptionProvider provider = provider;
- private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
- private IReadOnlyList? items;
- private int version;
-
- public IReadOnlyList Items
- {
- get
- {
- if ( items is not null && version == ComputeVersion() )
- {
- return items;
- }
-
- lock ( syncRoot )
- {
- var currentVersion = ComputeVersion();
-
- if ( items is not null && version == currentVersion )
- {
- return items;
- }
-
- var context = new ApiVersionMetadataCollationContext();
-
- for ( var i = 0; i < collators.Length; i++ )
- {
- collators[i].Execute( context );
- }
-
- var results = context.Results;
- var metadata = new GroupedApiVersionMetadata[results.Count];
-
- for ( var i = 0; i < metadata.Length; i++ )
- {
- metadata[i] = new( context.Results.GroupName( i ), results[i] );
- }
-
- items = provider.Describe( metadata );
- version = currentVersion;
- }
-
- return items;
- }
- }
-
- private int ComputeVersion() =>
- collators.Length switch
- {
- 0 => 0,
- 1 => collators[0].Version,
- _ => ComputeVersion( collators ),
- };
-
- private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers )
- {
- var hash = default( HashCode );
-
- for ( var i = 0; i < providers.Length; i++ )
- {
- hash.Add( providers[i].Version );
- }
-
- return hash.ToHashCode();
- }
- }
-
- private sealed class ApiVersionDescriptionComparer : IComparer
- {
- public int Compare( ApiVersionDescription? x, ApiVersionDescription? y )
- {
- if ( x is null )
- {
- return y is null ? 0 : -1;
- }
-
- if ( y is null )
- {
- return 1;
- }
-
- var result = x.ApiVersion.CompareTo( y.ApiVersion );
-
- if ( result == 0 )
- {
- result = StringComparer.Ordinal.Compare( x.GroupName, y.GroupName );
- }
-
- return result;
- }
+ return DescriptionProvider.Describe( metadata, SunsetPolicyManager, Options );
}
///
/// Represents the API version metadata applied to an endpoint with an optional group name.
///
- protected class GroupedApiVersionMetadata : ApiVersionMetadata, IEquatable
+ protected class GroupedApiVersionMetadata :
+ ApiVersionMetadata,
+ IEquatable,
+ IGroupedApiVersionMetadata,
+ IGroupedApiVersionMetadataFactory
{
///
/// Initializes a new instance of the class.
@@ -259,6 +83,10 @@ public GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata
/// The associated group name, if any.
public string? GroupName { get; }
+ static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New(
+ string? groupName,
+ ApiVersionMetadata metadata ) => new( groupName, metadata );
+
///
public bool Equals( GroupedApiVersionMetadata? other ) =>
other is not null && other.GetHashCode() == GetHashCode();
@@ -284,6 +112,4 @@ public override int GetHashCode()
return hash.ToHashCode();
}
}
-
- private record struct GroupedApiVersion( string? GroupName, ApiVersion ApiVersion );
}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs
new file mode 100644
index 00000000..f5847dd0
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs
@@ -0,0 +1,76 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal sealed class ApiVersionDescriptionCollection(
+ Func, IReadOnlyList> describe,
+ IEnumerable collators )
+ where T : IGroupedApiVersionMetadata, IGroupedApiVersionMetadataFactory
+{
+ private readonly object syncRoot = new();
+ private readonly Func, IReadOnlyList> describe = describe;
+ private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
+ private IReadOnlyList? items;
+ private int version;
+
+ public IReadOnlyList Items
+ {
+ get
+ {
+ if ( items is not null && version == ComputeVersion() )
+ {
+ return items;
+ }
+
+ lock ( syncRoot )
+ {
+ var currentVersion = ComputeVersion();
+
+ if ( items is not null && version == currentVersion )
+ {
+ return items;
+ }
+
+ var context = new ApiVersionMetadataCollationContext();
+
+ for ( var i = 0; i < collators.Length; i++ )
+ {
+ collators[i].Execute( context );
+ }
+
+ var results = context.Results;
+ var metadata = new T[results.Count];
+
+ for ( var i = 0; i < metadata.Length; i++ )
+ {
+ metadata[i] = T.New( context.Results.GroupName( i ), results[i] );
+ }
+
+ items = describe( metadata );
+ version = currentVersion;
+ }
+
+ return items;
+ }
+ }
+
+ private int ComputeVersion() =>
+ collators.Length switch
+ {
+ 0 => 0,
+ 1 => collators[0].Version,
+ _ => ComputeVersion( collators ),
+ };
+
+ private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers )
+ {
+ var hash = default( HashCode );
+
+ for ( var i = 0; i < providers.Length; i++ )
+ {
+ hash.Add( providers[i].Version );
+ }
+
+ return hash.ToHashCode();
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs
new file mode 100644
index 00000000..3fb73385
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs
@@ -0,0 +1,28 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal sealed class ApiVersionDescriptionComparer : IComparer
+{
+ public int Compare( ApiVersionDescription? x, ApiVersionDescription? y )
+ {
+ if ( x is null )
+ {
+ return y is null ? 0 : -1;
+ }
+
+ if ( y is null )
+ {
+ return 1;
+ }
+
+ var result = x.ApiVersion.CompareTo( y.ApiVersion );
+
+ if ( result == 0 )
+ {
+ result = StringComparer.Ordinal.Compare( x.GroupName, y.GroupName );
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs
new file mode 100644
index 00000000..ce3a0dbd
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs
@@ -0,0 +1,107 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+using static Asp.Versioning.ApiVersionMapping;
+using static System.Globalization.CultureInfo;
+
+internal static class DescriptionProvider
+{
+ internal static ApiVersionDescription[] Describe(
+ IReadOnlyList metadata,
+ ISunsetPolicyManager sunsetPolicyManager,
+ ApiExplorerOptions options )
+ where T : IGroupedApiVersionMetadata, IEquatable
+ {
+ var descriptions = new SortedSet( new ApiVersionDescriptionComparer() );
+ var supported = new HashSet();
+ var deprecated = new HashSet();
+
+ BucketizeApiVersions( metadata, supported, deprecated, options );
+ AppendDescriptions( descriptions, supported, sunsetPolicyManager, options, deprecated: false );
+ AppendDescriptions( descriptions, deprecated, sunsetPolicyManager, options, deprecated: true );
+
+ return [.. descriptions];
+ }
+
+ private static void BucketizeApiVersions(
+ IReadOnlyList list,
+ HashSet supported,
+ HashSet deprecated,
+ ApiExplorerOptions options )
+ where T : IGroupedApiVersionMetadata
+ {
+ var declared = new HashSet();
+ var advertisedSupported = new HashSet();
+ var advertisedDeprecated = new HashSet();
+
+ for ( var i = 0; i < list.Count; i++ )
+ {
+ var metadata = list[i];
+ var groupName = metadata.GroupName;
+ var model = metadata.Map( Explicit | Implicit );
+ var versions = model.DeclaredApiVersions;
+
+ for ( var j = 0; j < versions.Count; j++ )
+ {
+ declared.Add( new( groupName, versions[j] ) );
+ }
+
+ versions = model.SupportedApiVersions;
+
+ for ( var j = 0; j < versions.Count; j++ )
+ {
+ var version = versions[j];
+ supported.Add( new( groupName, version ) );
+ advertisedSupported.Add( new( groupName, version ) );
+ }
+
+ versions = model.DeprecatedApiVersions;
+
+ for ( var j = 0; j < versions.Count; j++ )
+ {
+ var version = versions[j];
+ deprecated.Add( new( groupName, version ) );
+ advertisedDeprecated.Add( new( groupName, version ) );
+ }
+ }
+
+ advertisedSupported.ExceptWith( declared );
+ advertisedDeprecated.ExceptWith( declared );
+ supported.ExceptWith( advertisedSupported );
+ deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) );
+
+ if ( supported.Count == 0 && deprecated.Count == 0 )
+ {
+ supported.Add( new( default, options.DefaultApiVersion ) );
+ }
+ }
+
+ private static void AppendDescriptions(
+ SortedSet descriptions,
+ HashSet versions,
+ ISunsetPolicyManager sunsetPolicyManager,
+ ApiExplorerOptions options,
+ bool deprecated )
+ {
+ var format = options.GroupNameFormat;
+ var formatGroupName = options.FormatGroupName;
+
+ foreach ( var (groupName, version) in versions )
+ {
+ var formattedGroupName = groupName;
+
+ if ( string.IsNullOrEmpty( formattedGroupName ) )
+ {
+ formattedGroupName = version.ToString( format, CurrentCulture );
+ }
+ else if ( formatGroupName is not null )
+ {
+ formattedGroupName = formatGroupName( formattedGroupName, version.ToString( format, CurrentCulture ) );
+ }
+
+ var sunsetPolicy = sunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default;
+ descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy ) );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs
new file mode 100644
index 00000000..8d276e60
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs
@@ -0,0 +1,5 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal record struct GroupedApiVersion( string? GroupName, ApiVersion ApiVersion );
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs
new file mode 100644
index 00000000..ec0c13e3
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal interface IGroupedApiVersionMetadata
+{
+ string? GroupName { get; }
+
+ string Name { get; }
+
+ bool IsApiVersionNeutral { get; }
+
+ ApiVersionModel Map( ApiVersionMapping mapping );
+
+ ApiVersionMapping MappingTo( ApiVersion? apiVersion );
+
+ bool IsMappedTo( ApiVersion? apiVersion );
+
+ void Deconstruct( out ApiVersionModel apiModel, out ApiVersionModel endpointModel );
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs
new file mode 100644
index 00000000..ac9d885f
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs
@@ -0,0 +1,9 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal interface IGroupedApiVersionMetadataFactory
+ where T : IGroupedApiVersionMetadata
+{
+ static abstract T New( string? groupName, ApiVersionMetadata metadata );
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs
new file mode 100644
index 00000000..8729791c
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+///
+/// Represents the inspector that understands
+/// endpoints defined by MVC controllers.
+///
+[CLSCompliant(false)]
+public sealed class MvcEndpointInspector : IEndpointInspector
+{
+ ///
+ public bool IsControllerAction( Endpoint endpoint )
+ {
+ ArgumentNullException.ThrowIfNull( endpoint );
+ return endpoint.Metadata.Any( static attribute => attribute is ControllerAttribute );
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs
index 7796ca96..5f895312 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs
@@ -67,6 +67,7 @@ private static void AddServices( IServiceCollection services )
services.TryAddEnumerable( Transient() );
services.TryAddEnumerable( Singleton() );
services.Replace( WithUrlHelperFactoryDecorator( services ) );
+ services.TryReplace();
}
private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor )
@@ -84,6 +85,23 @@ private static object CreateInstance( this IServiceProvider services, ServiceDes
return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType! );
}
+ private static void TryReplace( this IServiceCollection services )
+ {
+ var serviceType = typeof( TService );
+ var implementationType = typeof( TImplementation );
+
+ for ( var i = services.Count - 1; i >= 0; i-- )
+ {
+ var service = services[i];
+
+ if ( service.ServiceType == serviceType && service.ImplementationType == implementationType )
+ {
+ services[i] = Describe( serviceType, typeof( TReplacement ), service.Lifetime );
+ break;
+ }
+ }
+ }
+
[SkipLocalsInit]
private static DecoratedServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection services )
{