Skip to content

Commit

Permalink
Refactor and unify IApiVersionDescriptionProvider implementations. Ex…
Browse files Browse the repository at this point in the history
…plicit group names can only be determined at runtime. Related #1066
  • Loading branch information
commonsensesoftware committed Mar 20, 2024
1 parent 9d18108 commit 59ff9e6
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 301 deletions.
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
Loading

0 comments on commit 59ff9e6

Please sign in to comment.