Skip to content

Commit 2c0ab86

Browse files
Versions that are mapped only should not be discoverable. Fixes #735
1 parent 973f14d commit 2c0ab86

File tree

13 files changed

+264
-53
lines changed

13 files changed

+264
-53
lines changed

src/Common/CollectionExtensions.cs

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,29 @@ internal static IReadOnlyList<T> ToSortedReadOnlyList<T>( this IEnumerable<T> se
5151

5252
internal static void AddRange<T>( this ICollection<T> collection, IEnumerable<T> items )
5353
{
54-
foreach ( var item in items )
54+
switch ( items )
5555
{
56-
collection.Add( item );
56+
case IList<T> list:
57+
for ( var i = 0; i < list.Count; i++ )
58+
{
59+
collection.Add( list[i] );
60+
}
61+
62+
break;
63+
case IReadOnlyList<T> list:
64+
for ( var i = 0; i < list.Count; i++ )
65+
{
66+
collection.Add( list[i] );
67+
}
68+
69+
break;
70+
default:
71+
foreach ( var item in items )
72+
{
73+
collection.Add( item );
74+
}
75+
76+
break;
5777
}
5878
}
5979

@@ -81,12 +101,38 @@ internal static void UnionWith<T>( this ICollection<T> collection, IEnumerable<T
81101
}
82102
else
83103
{
84-
foreach ( var item in other )
104+
switch ( other )
85105
{
86-
if ( !collection.Contains( item ) )
87-
{
88-
collection.Add( item );
89-
}
106+
case IList<T> list:
107+
for ( var i = 0; i < list.Count; i++ )
108+
{
109+
if ( !collection.Contains( list[i] ) )
110+
{
111+
collection.Add( list[i] );
112+
}
113+
}
114+
115+
break;
116+
case IReadOnlyList<T> list:
117+
for ( var i = 0; i < list.Count; i++ )
118+
{
119+
if ( !collection.Contains( list[i] ) )
120+
{
121+
collection.Add( list[i] );
122+
}
123+
}
124+
125+
break;
126+
default:
127+
foreach ( var item in other )
128+
{
129+
if ( !collection.Contains( item ) )
130+
{
131+
collection.Add( item );
132+
}
133+
}
134+
135+
break;
90136
}
91137
}
92138
}

src/Common/Versioning/ApiVersionModel.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ public sealed partial class ApiVersionModel
1919
const int DefaultModel = 0;
2020
const int NeutralModel = 1;
2121
const int EmptyModel = 2;
22-
static readonly Lazy<ApiVersionModel> defaultVersion = new Lazy<ApiVersionModel>( () => new ApiVersionModel( DefaultModel ) );
23-
static readonly Lazy<ApiVersionModel> neutralVersion = new Lazy<ApiVersionModel>( () => new ApiVersionModel( NeutralModel ) );
24-
static readonly Lazy<ApiVersionModel> emptyVersion = new Lazy<ApiVersionModel>( () => new ApiVersionModel( EmptyModel ) );
22+
static readonly Lazy<ApiVersionModel> defaultVersion = new( () => new( DefaultModel ) );
23+
static readonly Lazy<ApiVersionModel> neutralVersion = new( () => new( NeutralModel ) );
24+
static readonly Lazy<ApiVersionModel> emptyVersion = new( () => new( EmptyModel ) );
2525
#if WEBAPI
2626
static readonly IReadOnlyList<ApiVersion> emptyVersions = new ApiVersion[0];
2727
#else
@@ -55,16 +55,16 @@ public sealed partial class ApiVersionModel
5555

5656
internal ApiVersionModel( ApiVersionModel original, IReadOnlyList<ApiVersion> implemented, IReadOnlyList<ApiVersion> supported, IReadOnlyList<ApiVersion> deprecated )
5757
{
58+
DeclaredApiVersions = original.DeclaredApiVersions;
59+
5860
if ( IsApiVersionNeutral = implemented.Count == 0 )
5961
{
60-
DeclaredApiVersions = emptyVersions;
6162
ImplementedApiVersions = emptyVersions;
6263
SupportedApiVersions = emptyVersions;
6364
DeprecatedApiVersions = emptyVersions;
6465
}
6566
else
6667
{
67-
DeclaredApiVersions = original.DeclaredApiVersions;
6868
ImplementedApiVersions = implemented;
6969
SupportedApiVersions = supported;
7070
DeprecatedApiVersions = deprecated;

src/Common/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
66
{
77
using System;
88
using System.Collections.Generic;
9-
using System.Linq;
109
#if WEBAPI
1110
using static Microsoft.Web.Http.Versioning.ApiVersionProviderOptions;
1211
#else
@@ -30,15 +29,18 @@ protected ActionApiVersionConventionBuilderBase() { }
3029
protected ICollection<ApiVersion> MappedVersions { get; } = new HashSet<ApiVersion>();
3130

3231
/// <inheritdoc />
33-
protected override void MergeAttributesWithConventions( IEnumerable<object> attributes )
32+
protected override void MergeAttributesWithConventions( IReadOnlyList<object> attributes )
3433
{
35-
base.MergeAttributesWithConventions( attributes );
34+
if ( attributes == null )
35+
{
36+
throw new ArgumentNullException( nameof( attributes ) );
37+
}
3638

37-
var providers = attributes.OfType<IApiVersionProvider>();
39+
base.MergeAttributesWithConventions( attributes );
3840

39-
foreach ( var provider in providers )
41+
for ( var i = 0; i < attributes.Count; i++ )
4042
{
41-
if ( provider.Options == Mapped )
43+
if ( attributes[i] is IApiVersionProvider provider && provider.Options == Mapped )
4244
{
4345
MappedVersions.UnionWith( provider.Versions );
4446
}

src/Common/Versioning/Conventions/ApiVersionConventionBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ bool InternalApplyTo( Model model )
113113
builder = new ControllerApiVersionConventionBuilder( model.ControllerType );
114114
}
115115

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

121121
if ( applied )

src/Common/Versioning/Conventions/ApiVersionConventionBuilderBase.cs

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,92 @@ protected ApiVersionConventionBuilderBase() { }
5757
/// Merges API version information from the specified attributes with the current conventions.
5858
/// </summary>
5959
/// <param name="attributes">The <see cref="IEnumerable{T}">sequence</see> of attributes to merge.</param>
60-
protected virtual void MergeAttributesWithConventions( IEnumerable<object> attributes )
60+
protected virtual void MergeAttributesWithConventions( IEnumerable<object> attributes ) =>
61+
MergeAttributesWithConventions( ( attributes as IReadOnlyList<object> ) ?? attributes.ToArray() );
62+
63+
/// <summary>
64+
/// Merges API version information from the specified attributes with the current conventions.
65+
/// </summary>
66+
/// <param name="attributes">The <see cref="IReadOnlyList{T}">read-only list</see> of attributes to merge.</param>
67+
protected virtual void MergeAttributesWithConventions( IReadOnlyList<object> attributes )
6168
{
62-
if ( VersionNeutral |= attributes.OfType<IApiVersionNeutral>().Any() )
69+
if ( attributes == null )
70+
{
71+
throw new ArgumentNullException( nameof( attributes ) );
72+
}
73+
74+
if ( VersionNeutral )
6375
{
6476
return;
6577
}
6678

6779
const ApiVersionProviderOptions DeprecatedAdvertised = Deprecated | Advertised;
68-
var providers = attributes.OfType<IApiVersionProvider>();
80+
var supported = default( List<ApiVersion> );
81+
var deprecated = default( List<ApiVersion> );
82+
var advertised = default( List<ApiVersion> );
83+
var deprecatedAdvertised = default( List<ApiVersion> );
6984

70-
foreach ( var provider in providers )
85+
for ( var i = 0; i < attributes.Count; i++ )
7186
{
72-
switch ( provider.Options )
87+
switch ( attributes[i] )
7388
{
74-
case None:
75-
SupportedVersions.UnionWith( provider.Versions );
76-
break;
77-
case Deprecated:
78-
DeprecatedVersions.UnionWith( provider.Versions );
79-
break;
80-
case Advertised:
81-
AdvertisedVersions.UnionWith( provider.Versions );
82-
break;
83-
case DeprecatedAdvertised:
84-
DeprecatedAdvertisedVersions.UnionWith( provider.Versions );
89+
case IApiVersionNeutral:
90+
VersionNeutral = true;
91+
return;
92+
case IApiVersionProvider provider:
93+
List<ApiVersion> target;
94+
IReadOnlyList<ApiVersion> source;
95+
96+
switch ( provider.Options )
97+
{
98+
case None:
99+
target = supported ??= new();
100+
source = provider.Versions;
101+
break;
102+
case Deprecated:
103+
target = deprecated ??= new();
104+
source = provider.Versions;
105+
break;
106+
case Advertised:
107+
target = advertised ??= new();
108+
source = provider.Versions;
109+
break;
110+
case DeprecatedAdvertised:
111+
target = deprecatedAdvertised ??= new();
112+
source = provider.Versions;
113+
break;
114+
default:
115+
continue;
116+
}
117+
118+
for ( var j = 0; j < source.Count; j++ )
119+
{
120+
target.Add( source[j] );
121+
}
122+
85123
break;
86124
}
87125
}
126+
127+
if ( supported is not null && supported.Count > 0 )
128+
{
129+
SupportedVersions.UnionWith( supported );
130+
}
131+
132+
if ( deprecated is not null && deprecated.Count > 0 )
133+
{
134+
DeprecatedVersions.UnionWith( deprecated );
135+
}
136+
137+
if ( advertised is not null && advertised.Count > 0 )
138+
{
139+
AdvertisedVersions.UnionWith( advertised );
140+
}
141+
142+
if ( deprecatedAdvertised is not null && deprecatedAdvertised.Count > 0 )
143+
{
144+
DeprecatedAdvertisedVersions.UnionWith( deprecatedAdvertised );
145+
}
88146
}
89147
}
90148
}

src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,31 @@ public static ApiVersionMapping MappingTo( this HttpActionDescriptor action, Api
7979

8080
var model = action.GetApiVersionModel();
8181

82-
if ( model.IsApiVersionNeutral || ( apiVersion != null && model.DeclaredApiVersions.Contains( apiVersion ) ) )
82+
if ( model.IsApiVersionNeutral )
8383
{
8484
return Explicit;
8585
}
86-
else if ( model.DeclaredApiVersions.Count == 0 )
86+
87+
if ( apiVersion is null )
88+
{
89+
return None;
90+
}
91+
92+
var mappedWithImplementation = model.DeclaredApiVersions.Contains( apiVersion ) &&
93+
model.ImplementedApiVersions.Contains( apiVersion );
94+
95+
if ( mappedWithImplementation )
96+
{
97+
return Explicit;
98+
}
99+
100+
var deriveFromParent = model.DeclaredApiVersions.Count == 0;
101+
102+
if ( deriveFromParent )
87103
{
88104
model = action.ControllerDescriptor.GetApiVersionModel();
89105

90-
if ( apiVersion != null && model.DeclaredApiVersions.Contains( apiVersion ) )
106+
if ( model.DeclaredApiVersions.Contains( apiVersion ) )
91107
{
92108
return Implicit;
93109
}

src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public virtual void ApplyTo( HttpActionDescriptor actionDescriptor )
3737
return;
3838
}
3939

40-
var versionModel = default( ApiVersionModel );
40+
ApiVersionModel? versionModel;
4141

4242
if ( MappedVersions.Count == 0 )
4343
{

src/Microsoft.AspNetCore.Mvc.Versioning/Abstractions/ActionDescriptorExtensions.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,31 @@ public static ApiVersionMapping MappingTo( this ActionDescriptor action, ApiVers
6868
{
6969
var model = action.GetApiVersionModel();
7070

71-
if ( model.IsApiVersionNeutral || ( apiVersion != null && model.DeclaredApiVersions.Contains( apiVersion ) ) )
71+
if ( model.IsApiVersionNeutral )
7272
{
7373
return Explicit;
7474
}
75-
else if ( model.DeclaredApiVersions.Count == 0 )
75+
76+
if ( apiVersion is null )
77+
{
78+
return None;
79+
}
80+
81+
var mappedWithImplementation = model.DeclaredApiVersions.Contains( apiVersion ) &&
82+
model.ImplementedApiVersions.Contains( apiVersion );
83+
84+
if ( mappedWithImplementation )
85+
{
86+
return Explicit;
87+
}
88+
89+
var deriveFromParent = model.DeclaredApiVersions.Count == 0;
90+
91+
if ( deriveFromParent )
7692
{
77-
var parentModel = action.GetProperty<ControllerModel>()?.GetProperty<ApiVersionModel>();
93+
model = action.GetProperty<ControllerModel>()?.GetProperty<ApiVersionModel>();
7894

79-
if ( parentModel != null && ( apiVersion != null && parentModel.DeclaredApiVersions.Contains( apiVersion ) ) )
95+
if ( model is not null && model.DeclaredApiVersions.Contains( apiVersion ) )
8096
{
8197
return Implicit;
8298
}

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
{
33
using Microsoft.AspNetCore.Mvc.ApplicationModels;
44
using System;
5-
using System.Linq;
65
using System.Reflection;
76

87
/// <content>
@@ -32,18 +31,18 @@ public virtual bool ApplyTo( ControllerModel controllerModel )
3231

3332
static bool HasDecoratedActions( ControllerModel controllerModel )
3433
{
35-
foreach ( var action in controllerModel.Actions )
34+
for ( var i = 0; i < controllerModel.Actions.Count; i++ )
3635
{
37-
var attributes = action.Attributes;
36+
var action = controllerModel.Actions[i];
3837

39-
if ( attributes.OfType<IApiVersionNeutral>().Any() )
38+
for ( var j = 0; j < action.Attributes.Count; j++ )
4039
{
41-
return true;
42-
}
40+
var attribute = action.Attributes[j];
4341

44-
if ( attributes.OfType<IApiVersionProvider>().Any() )
45-
{
46-
return true;
42+
if ( attribute is IApiVersionProvider || attribute is IApiVersionNeutral )
43+
{
44+
return true;
45+
}
4746
}
4847
}
4948

test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/HelloWorldController.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@ public class HelloWorldController : ApiController
1515

1616
[Route]
1717
public IHttpActionResult Post() => CreatedAtRoute( "GetMessageById", new { id = 42 }, default( object ) );
18+
19+
[HttpGet]
20+
[Route( nameof( Unreachable ) )]
21+
[MapToApiVersion( "42.0" )]
22+
public IHttpActionResult Unreachable( ApiVersion apiVersion ) => Ok( new { Controller = GetType().Name, Version = apiVersion.ToString() } );
1823
}
1924
}

0 commit comments

Comments
 (0)