Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Reported API Version Groups #1067

Merged
merged 2 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.ApiExplorer;

using Microsoft.AspNetCore.Http;

/// <summary>
/// Represents the default <see cref="IEndpointInspector">endpoint inspector</see>.
/// </summary>
[CLSCompliant(false)]
public sealed class DefaultEndpointInspector : IEndpointInspector
{
/// <inheritdoc />
public bool IsControllerAction( Endpoint endpoint ) => false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,29 @@ namespace Asp.Versioning.ApiExplorer;
public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider
{
private readonly EndpointDataSource endpointDataSource;
private readonly IEndpointInspector endpointInspector;
private int version;

/// <summary>
/// Initializes a new instance of the <see cref="EndpointApiVersionMetadataCollationProvider"/> class.
/// </summary>
/// <param name="endpointDataSource">The underlying <see cref="endpointDataSource">endpoint data source</see>.</param>
[Obsolete( "Use the constructor that accepts IEndpointInspector. This constructor will be removed in a future version." )]
public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource )
: this( endpointDataSource, new DefaultEndpointInspector() ) { }

/// <summary>
/// Initializes a new instance of the <see cref="EndpointApiVersionMetadataCollationProvider"/> class.
/// </summary>
/// <param name="endpointDataSource">The underlying <see cref="endpointDataSource">endpoint data source</see>.</param>
/// <param name="endpointInspector">The <see cref="IEndpointInspector">endpoint inspector</see> used to inspect endpoints.</param>
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 );
}

Expand All @@ -38,7 +52,8 @@ public void Execute( ApiVersionMetadataCollationContext context )
{
var endpoint = endpoints[i];

if ( endpoint.Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata item )
if ( endpoint.Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata item ||
endpointInspector.IsControllerAction( endpoint ) )
{
continue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.ApiExplorer;

using Microsoft.AspNetCore.Http;

/// <summary>
/// Defines the behavior of an endpoint inspector.
/// </summary>
[CLSCompliant( false )]
public interface IEndpointInspector
{
/// <summary>
/// Determines whether the specified endpoint is a controller action.
/// </summary>
/// <param name="endpoint">The <see cref="Endpoint">endpoint</see> to inspect.</param>
/// <returns>True if the <paramref name="endpoint"/> is for a controller action; otherwise, false.</returns>
bool IsControllerAction( Endpoint endpoint );
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private static void AddApiVersioningServices( IServiceCollection services )
services.TryAddEnumerable( Transient<IPostConfigureOptions<RouteOptions>, ApiVersioningRouteOptionsSetup>() );
services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() );
services.TryAddEnumerable( Singleton<IApiVersionMetadataCollationProvider, EndpointApiVersionMetadataCollationProvider>() );
services.TryAddTransient<IEndpointInspector, DefaultEndpointInspector>();
services.Replace( WithLinkGeneratorDecorator( services ) );
TryAddProblemDetailsRfc7231Compliance( services );
TryAddErrorObjectJsonOptions( services );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,29 @@ internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescript
{
private readonly ISunsetPolicyManager sunsetPolicyManager;
private readonly IApiVersionMetadataCollationProvider[] providers;
private readonly IEndpointInspector endpointInspector;
private readonly IOptions<ApiExplorerOptions> options;
private readonly Activator activator;

public ApiVersionDescriptionProviderFactory(
Activator activator,
ISunsetPolicyManager sunsetPolicyManager,
IEnumerable<IApiVersionMetadataCollationProvider> providers,
IEndpointInspector endpointInspector,
IOptions<ApiExplorerOptions> options )
{
this.activator = activator;
this.sunsetPolicyManager = sunsetPolicyManager;
this.providers = providers.ToArray();
this.endpointInspector = endpointInspector;
this.options = options;
}

public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSource )
{
var collators = new List<IApiVersionMetadataCollationProvider>( capacity: providers.Length + 1 )
{
new EndpointApiVersionMetadataCollationProvider( endpointDataSource ),
new EndpointApiVersionMetadataCollationProvider( endpointDataSource, endpointInspector ),
};

collators.AddRange( providers );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@

namespace Asp.Versioning.ApiExplorer;

using Asp.Versioning.ApiExplorer.Internal;
using Microsoft.Extensions.Options;
using static Asp.Versioning.ApiVersionMapping;
using static System.Globalization.CultureInfo;

/// <summary>
/// Represents the default implementation of an object that discovers and describes the API version information within an application.
/// </summary>
[CLSCompliant( false )]
public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider
{
private readonly ApiVersionDescriptionCollection collection;
private readonly ApiVersionDescriptionCollection<GroupedApiVersionMetadata> collection;
private readonly IOptions<ApiExplorerOptions> options;

/// <summary>
Expand All @@ -28,7 +27,7 @@ public DefaultApiVersionDescriptionProvider(
ISunsetPolicyManager sunsetPolicyManager,
IOptions<ApiExplorerOptions> apiExplorerOptions )
{
collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
SunsetPolicyManager = sunsetPolicyManager;
options = apiExplorerOptions;
}
Expand Down Expand Up @@ -58,133 +57,53 @@ protected virtual IReadOnlyList<ApiVersionDescription> Describe( IReadOnlyList<A
{
ArgumentNullException.ThrowIfNull( metadata );

var descriptions = new List<ApiVersionDescription>( capacity: metadata.Count );
var supported = new HashSet<ApiVersion>();
var deprecated = new HashSet<ApiVersion>();

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<ApiVersionMetadata> metadata, HashSet<ApiVersion> supported, HashSet<ApiVersion> deprecated )
{
var declared = new HashSet<ApiVersion>();
var advertisedSupported = new HashSet<ApiVersion>();
var advertisedDeprecated = new HashSet<ApiVersion>();

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<ApiVersionDescription>();
}

private void AppendDescriptions( List<ApiVersionDescription> descriptions, IEnumerable<ApiVersion> versions, bool deprecated )
private sealed class GroupedApiVersionMetadata :
ApiVersionMetadata,
IEquatable<GroupedApiVersionMetadata>,
IGroupedApiVersionMetadata,
IGroupedApiVersionMetadataFactory<GroupedApiVersionMetadata>
{
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<IApiVersionMetadataCollationProvider> collators )
{
private readonly object syncRoot = new();
private readonly DefaultApiVersionDescriptionProvider provider = provider;
private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
private IReadOnlyList<ApiVersionDescription>? items;
private int version;

public IReadOnlyList<ApiVersionDescription> Items
{
get
{
if ( items is not null && version == ComputeVersion() )
{
return items;
}
public string? GroupName { get; }

lock ( syncRoot )
{
var currentVersion = ComputeVersion();
static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory<GroupedApiVersionMetadata>.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();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
Expand Down Expand Up @@ -70,40 +72,36 @@ private static IApiVersionDescriptionProviderFactory ResolveApiVersionDescriptio
{
var sunsetPolicyManager = serviceProvider.GetRequiredService<ISunsetPolicyManager>();
var providers = serviceProvider.GetServices<IApiVersionMetadataCollationProvider>();
var inspector = serviceProvider.GetRequiredService<IEndpointInspector>();
var options = serviceProvider.GetRequiredService<IOptions<ApiExplorerOptions>>();
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<IApiVersionMetadataCollationProvider> providers,
ISunsetPolicyManager sunsetPolicyManager,
IOptions<ApiExplorerOptions> apiExplorerOptions ) =>
new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions );

static IApiVersionDescriptionProvider NewGroupedProvider(
IEnumerable<IApiVersionMetadataCollationProvider> providers,
ISunsetPolicyManager sunsetPolicyManager,
IOptions<ApiExplorerOptions> apiExplorerOptions ) =>
new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions );
new( providers, sunsetPolicyManager, apiExplorerOptions );
}

private static IApiVersionDescriptionProvider ResolveApiVersionDescriptionProvider( IServiceProvider serviceProvider )
{
var providers = serviceProvider.GetServices<IApiVersionMetadataCollationProvider>();
var sunsetPolicyManager = serviceProvider.GetRequiredService<ISunsetPolicyManager>();
var options = serviceProvider.GetRequiredService<IOptions<ApiExplorerOptions>>();
var mightUseCustomGroups = options.Value.FormatGroupName is not null;
var factory = serviceProvider.GetRequiredService<IApiVersionDescriptionProviderFactory>();
var endpointDataSource = new EmptyEndpointDataSource();
return factory.Create( endpointDataSource );
}

private sealed class EmptyEndpointDataSource : EndpointDataSource
{
public override IReadOnlyList<Endpoint> Endpoints => Array.Empty<Endpoint>();

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<Endpoint> GetGroupedEndpoints( RouteGroupContext context ) => Array.Empty<Endpoint>();
}
}
Loading
Loading