Skip to content

Commit

Permalink
Versions that are mapped only should not be discoverable. Fixes #735
Browse files Browse the repository at this point in the history
  • Loading branch information
commonsensesoftware committed Mar 21, 2022
1 parent 973f14d commit 2c0ab86
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 53 deletions.
60 changes: 53 additions & 7 deletions src/Common/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,29 @@ internal static IReadOnlyList<T> ToSortedReadOnlyList<T>( this IEnumerable<T> se

internal static void AddRange<T>( this ICollection<T> collection, IEnumerable<T> items )
{
foreach ( var item in items )
switch ( items )
{
collection.Add( item );
case IList<T> list:
for ( var i = 0; i < list.Count; i++ )
{
collection.Add( list[i] );
}

break;
case IReadOnlyList<T> list:
for ( var i = 0; i < list.Count; i++ )
{
collection.Add( list[i] );
}

break;
default:
foreach ( var item in items )
{
collection.Add( item );
}

break;
}
}

Expand Down Expand Up @@ -81,12 +101,38 @@ internal static void UnionWith<T>( this ICollection<T> collection, IEnumerable<T
}
else
{
foreach ( var item in other )
switch ( other )
{
if ( !collection.Contains( item ) )
{
collection.Add( item );
}
case IList<T> list:
for ( var i = 0; i < list.Count; i++ )
{
if ( !collection.Contains( list[i] ) )
{
collection.Add( list[i] );
}
}

break;
case IReadOnlyList<T> list:
for ( var i = 0; i < list.Count; i++ )
{
if ( !collection.Contains( list[i] ) )
{
collection.Add( list[i] );
}
}

break;
default:
foreach ( var item in other )
{
if ( !collection.Contains( item ) )
{
collection.Add( item );
}
}

break;
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/Common/Versioning/ApiVersionModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public sealed partial class ApiVersionModel
const int DefaultModel = 0;
const int NeutralModel = 1;
const int EmptyModel = 2;
static readonly Lazy<ApiVersionModel> defaultVersion = new Lazy<ApiVersionModel>( () => new ApiVersionModel( DefaultModel ) );
static readonly Lazy<ApiVersionModel> neutralVersion = new Lazy<ApiVersionModel>( () => new ApiVersionModel( NeutralModel ) );
static readonly Lazy<ApiVersionModel> emptyVersion = new Lazy<ApiVersionModel>( () => new ApiVersionModel( EmptyModel ) );
static readonly Lazy<ApiVersionModel> defaultVersion = new( () => new( DefaultModel ) );
static readonly Lazy<ApiVersionModel> neutralVersion = new( () => new( NeutralModel ) );
static readonly Lazy<ApiVersionModel> emptyVersion = new( () => new( EmptyModel ) );
#if WEBAPI
static readonly IReadOnlyList<ApiVersion> emptyVersions = new ApiVersion[0];
#else
Expand Down Expand Up @@ -55,16 +55,16 @@ public sealed partial class ApiVersionModel

internal ApiVersionModel( ApiVersionModel original, IReadOnlyList<ApiVersion> implemented, IReadOnlyList<ApiVersion> supported, IReadOnlyList<ApiVersion> deprecated )
{
DeclaredApiVersions = original.DeclaredApiVersions;

if ( IsApiVersionNeutral = implemented.Count == 0 )
{
DeclaredApiVersions = emptyVersions;
ImplementedApiVersions = emptyVersions;
SupportedApiVersions = emptyVersions;
DeprecatedApiVersions = emptyVersions;
}
else
{
DeclaredApiVersions = original.DeclaredApiVersions;
ImplementedApiVersions = implemented;
SupportedApiVersions = supported;
DeprecatedApiVersions = deprecated;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
{
using System;
using System.Collections.Generic;
using System.Linq;
#if WEBAPI
using static Microsoft.Web.Http.Versioning.ApiVersionProviderOptions;
#else
Expand All @@ -30,15 +29,18 @@ protected ActionApiVersionConventionBuilderBase() { }
protected ICollection<ApiVersion> MappedVersions { get; } = new HashSet<ApiVersion>();

/// <inheritdoc />
protected override void MergeAttributesWithConventions( IEnumerable<object> attributes )
protected override void MergeAttributesWithConventions( IReadOnlyList<object> attributes )
{
base.MergeAttributesWithConventions( attributes );
if ( attributes == null )
{
throw new ArgumentNullException( nameof( attributes ) );
}

var providers = attributes.OfType<IApiVersionProvider>();
base.MergeAttributesWithConventions( attributes );

foreach ( var provider in providers )
for ( var i = 0; i < attributes.Count; i++ )
{
if ( provider.Options == Mapped )
if ( attributes[i] is IApiVersionProvider provider && provider.Options == Mapped )
{
MappedVersions.UnionWith( provider.Versions );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ bool InternalApplyTo( Model model )
builder = new ControllerApiVersionConventionBuilder( model.ControllerType );
}

foreach ( var convention in ControllerConventions )
for ( var i = 0; i < ControllerConventions.Count; i++ )
{
applied |= convention.Apply( builder!, model );
applied |= ControllerConventions[i].Apply( builder!, model );
}

if ( applied )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,34 +57,92 @@ protected ApiVersionConventionBuilderBase() { }
/// Merges API version information from the specified attributes with the current conventions.
/// </summary>
/// <param name="attributes">The <see cref="IEnumerable{T}">sequence</see> of attributes to merge.</param>
protected virtual void MergeAttributesWithConventions( IEnumerable<object> attributes )
protected virtual void MergeAttributesWithConventions( IEnumerable<object> attributes ) =>
MergeAttributesWithConventions( ( attributes as IReadOnlyList<object> ) ?? attributes.ToArray() );

/// <summary>
/// Merges API version information from the specified attributes with the current conventions.
/// </summary>
/// <param name="attributes">The <see cref="IReadOnlyList{T}">read-only list</see> of attributes to merge.</param>
protected virtual void MergeAttributesWithConventions( IReadOnlyList<object> attributes )
{
if ( VersionNeutral |= attributes.OfType<IApiVersionNeutral>().Any() )
if ( attributes == null )
{
throw new ArgumentNullException( nameof( attributes ) );
}

if ( VersionNeutral )
{
return;
}

const ApiVersionProviderOptions DeprecatedAdvertised = Deprecated | Advertised;
var providers = attributes.OfType<IApiVersionProvider>();
var supported = default( List<ApiVersion> );
var deprecated = default( List<ApiVersion> );
var advertised = default( List<ApiVersion> );
var deprecatedAdvertised = default( List<ApiVersion> );

foreach ( var provider in providers )
for ( var i = 0; i < attributes.Count; i++ )
{
switch ( provider.Options )
switch ( attributes[i] )
{
case None:
SupportedVersions.UnionWith( provider.Versions );
break;
case Deprecated:
DeprecatedVersions.UnionWith( provider.Versions );
break;
case Advertised:
AdvertisedVersions.UnionWith( provider.Versions );
break;
case DeprecatedAdvertised:
DeprecatedAdvertisedVersions.UnionWith( provider.Versions );
case IApiVersionNeutral:
VersionNeutral = true;
return;
case IApiVersionProvider provider:
List<ApiVersion> target;
IReadOnlyList<ApiVersion> source;

switch ( provider.Options )
{
case None:
target = supported ??= new();
source = provider.Versions;
break;
case Deprecated:
target = deprecated ??= new();
source = provider.Versions;
break;
case Advertised:
target = advertised ??= new();
source = provider.Versions;
break;
case DeprecatedAdvertised:
target = deprecatedAdvertised ??= new();
source = provider.Versions;
break;
default:
continue;
}

for ( var j = 0; j < source.Count; j++ )
{
target.Add( source[j] );
}

break;
}
}

if ( supported is not null && supported.Count > 0 )
{
SupportedVersions.UnionWith( supported );
}

if ( deprecated is not null && deprecated.Count > 0 )
{
DeprecatedVersions.UnionWith( deprecated );
}

if ( advertised is not null && advertised.Count > 0 )
{
AdvertisedVersions.UnionWith( advertised );
}

if ( deprecatedAdvertised is not null && deprecatedAdvertised.Count > 0 )
{
DeprecatedAdvertisedVersions.UnionWith( deprecatedAdvertised );
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,31 @@ public static ApiVersionMapping MappingTo( this HttpActionDescriptor action, Api

var model = action.GetApiVersionModel();

if ( model.IsApiVersionNeutral || ( apiVersion != null && model.DeclaredApiVersions.Contains( apiVersion ) ) )
if ( model.IsApiVersionNeutral )
{
return Explicit;
}
else if ( model.DeclaredApiVersions.Count == 0 )

if ( apiVersion is null )
{
return None;
}

var mappedWithImplementation = model.DeclaredApiVersions.Contains( apiVersion ) &&
model.ImplementedApiVersions.Contains( apiVersion );

if ( mappedWithImplementation )
{
return Explicit;
}

var deriveFromParent = model.DeclaredApiVersions.Count == 0;

if ( deriveFromParent )
{
model = action.ControllerDescriptor.GetApiVersionModel();

if ( apiVersion != null && model.DeclaredApiVersions.Contains( apiVersion ) )
if ( model.DeclaredApiVersions.Contains( apiVersion ) )
{
return Implicit;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public virtual void ApplyTo( HttpActionDescriptor actionDescriptor )
return;
}

var versionModel = default( ApiVersionModel );
ApiVersionModel? versionModel;

if ( MappedVersions.Count == 0 )
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,31 @@ public static ApiVersionMapping MappingTo( this ActionDescriptor action, ApiVers
{
var model = action.GetApiVersionModel();

if ( model.IsApiVersionNeutral || ( apiVersion != null && model.DeclaredApiVersions.Contains( apiVersion ) ) )
if ( model.IsApiVersionNeutral )
{
return Explicit;
}
else if ( model.DeclaredApiVersions.Count == 0 )

if ( apiVersion is null )
{
return None;
}

var mappedWithImplementation = model.DeclaredApiVersions.Contains( apiVersion ) &&
model.ImplementedApiVersions.Contains( apiVersion );

if ( mappedWithImplementation )
{
return Explicit;
}

var deriveFromParent = model.DeclaredApiVersions.Count == 0;

if ( deriveFromParent )
{
var parentModel = action.GetProperty<ControllerModel>()?.GetProperty<ApiVersionModel>();
model = action.GetProperty<ControllerModel>()?.GetProperty<ApiVersionModel>();

if ( parentModel != null && ( apiVersion != null && parentModel.DeclaredApiVersions.Contains( apiVersion ) ) )
if ( model is not null && model.DeclaredApiVersions.Contains( apiVersion ) )
{
return Implicit;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
{
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
using System.Linq;
using System.Reflection;

/// <content>
Expand Down Expand Up @@ -32,18 +31,18 @@ public virtual bool ApplyTo( ControllerModel controllerModel )

static bool HasDecoratedActions( ControllerModel controllerModel )
{
foreach ( var action in controllerModel.Actions )
for ( var i = 0; i < controllerModel.Actions.Count; i++ )
{
var attributes = action.Attributes;
var action = controllerModel.Actions[i];

if ( attributes.OfType<IApiVersionNeutral>().Any() )
for ( var j = 0; j < action.Attributes.Count; j++ )
{
return true;
}
var attribute = action.Attributes[j];

if ( attributes.OfType<IApiVersionProvider>().Any() )
{
return true;
if ( attribute is IApiVersionProvider || attribute is IApiVersionNeutral )
{
return true;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ public class HelloWorldController : ApiController

[Route]
public IHttpActionResult Post() => CreatedAtRoute( "GetMessageById", new { id = 42 }, default( object ) );

[HttpGet]
[Route( nameof( Unreachable ) )]
[MapToApiVersion( "42.0" )]
public IHttpActionResult Unreachable( ApiVersion apiVersion ) => Ok( new { Controller = GetType().Name, Version = apiVersion.ToString() } );
}
}
Loading

0 comments on commit 2c0ab86

Please sign in to comment.