Skip to content

Commit 9c24fa9

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Correct matching of action descriptors with same name, but different API versions. Fixes #117
1 parent 665917d commit 9c24fa9

File tree

6 files changed

+78
-26
lines changed

6 files changed

+78
-26
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Microsoft.Web.Http
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics.Contracts;
6+
7+
static class CollectionExtensions
8+
{
9+
internal static int IndexOf<TItem>( this IEnumerable<TItem> sequence, TItem item, IEqualityComparer<TItem> comparer )
10+
{
11+
Contract.Requires( sequence != null );
12+
Contract.Requires( comparer != null );
13+
Contract.Ensures( Contract.Result<int>() >= -1 );
14+
15+
var index = 0;
16+
17+
foreach ( var element in sequence )
18+
{
19+
if ( comparer.Equals( element, item ) )
20+
{
21+
return index;
22+
}
23+
24+
index++;
25+
}
26+
27+
return -1;
28+
}
29+
}
30+
}

src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -268,18 +268,32 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions()
268268

269269
// Remove ApiDescription that will lead to ambiguous action matching.
270270
// E.g. a controller with Post() and PostComment(). When the route template is {controller}, it produces POST /controller and POST /controller.
271-
descriptionsFromRoute = RemoveInvalidApiDescriptions( descriptionsFromRoute );
271+
descriptionsFromRoute = RemoveInvalidApiDescriptions( descriptionsFromRoute, apiVersion );
272272

273273
foreach ( var description in descriptionsFromRoute )
274274
{
275275
// Do not add the description if the previous route has a matching description with the same HTTP method and relative path.
276276
// E.g. having two routes with the templates "api/Values/{id}" and "api/{controller}/{id}" can potentially produce the same
277277
// relative path "api/Values/{id}" but only the first one matters.
278-
if ( !apiDescriptionGroup.ApiDescriptions.Contains( description, Comparer ) )
278+
279+
var index = apiDescriptionGroup.ApiDescriptions.IndexOf( description, Comparer );
280+
281+
if ( index < 0 )
279282
{
280283
description.GroupName = apiDescriptionGroup.Name;
281284
apiDescriptionGroup.ApiDescriptions.Add( description );
282285
}
286+
else
287+
{
288+
var model = description.ActionDescriptor.GetApiVersionModel();
289+
var overrideImplicitlyMappedApiDescription = model.DeclaredApiVersions.Contains( apiVersion );
290+
291+
if ( overrideImplicitlyMappedApiDescription )
292+
{
293+
description.GroupName = apiDescriptionGroup.Name;
294+
apiDescriptionGroup.ApiDescriptions[index] = description;
295+
}
296+
}
283297
}
284298

285299
if ( apiDescriptionGroup.ApiDescriptions.Count == 0 )
@@ -969,41 +983,34 @@ ApiParameterDescription CreateParameterDescriptionFromBinding( HttpParameterBind
969983
return parameterDescription;
970984
}
971985

972-
static Collection<VersionedApiDescription> RemoveInvalidApiDescriptions( Collection<VersionedApiDescription> apiDescriptions )
986+
static Collection<VersionedApiDescription> RemoveInvalidApiDescriptions( Collection<VersionedApiDescription> apiDescriptions, ApiVersion apiVersion )
973987
{
974988
Contract.Requires( apiDescriptions != null );
989+
Contract.Requires( apiVersion != null );
975990
Contract.Ensures( Contract.Result<Collection<VersionedApiDescription>>() != null );
976991

977-
var duplicateApiDescriptionIds = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
978-
var visitedApiDescriptionIds = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
992+
var filteredDescriptions = new Dictionary<string, VersionedApiDescription>( StringComparer.OrdinalIgnoreCase );
979993

980994
foreach ( var description in apiDescriptions )
981995
{
982996
var apiDescriptionId = description.GetUniqueID();
983997

984-
if ( visitedApiDescriptionIds.Contains( apiDescriptionId ) )
998+
if ( filteredDescriptions.ContainsKey( apiDescriptionId ) )
985999
{
986-
duplicateApiDescriptionIds.Add( apiDescriptionId );
1000+
var model = description.ActionDescriptor.GetApiVersionModel();
1001+
1002+
if ( model.DeclaredApiVersions.Contains( apiVersion ) )
1003+
{
1004+
filteredDescriptions[apiDescriptionId] = description;
1005+
}
9871006
}
9881007
else
9891008
{
990-
visitedApiDescriptionIds.Add( apiDescriptionId );
991-
}
992-
}
993-
994-
var filteredApiDescriptions = new Collection<VersionedApiDescription>();
995-
996-
foreach ( var apiDescription in apiDescriptions )
997-
{
998-
var apiDescriptionId = apiDescription.GetUniqueID();
999-
1000-
if ( !duplicateApiDescriptionIds.Contains( apiDescriptionId ) )
1001-
{
1002-
filteredApiDescriptions.Add( apiDescription );
1009+
filteredDescriptions.Add( apiDescriptionId, description );
10031010
}
10041011
}
10051012

1006-
return filteredApiDescriptions;
1013+
return new Collection<VersionedApiDescription>( filteredDescriptions.Values.ToList() );
10071014
}
10081015

10091016
static bool MatchRegexConstraint( IHttpRoute route, string parameterName, string parameterValue )

test/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.Tests/Description/TestConfigurations.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ static HttpConfiguration NewConventionRouteConfiguration()
3838
.HasApiVersion( 2, 0 )
3939
.HasDeprecatedApiVersion( 3, 0, "beta" )
4040
.HasApiVersion( 3, 0 )
41+
.Action( c => c.GetV3() ).MapToApiVersion( 3, 0 )
4142
.Action( c => c.Post( default( ClassWithId ) ) ).MapToApiVersion( 3, 0 );
4243
options.Conventions.Controller<Values3Controller>()
4344
.HasApiVersion( 4, 0 )

test/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.Tests/Description/VersionedApiExplorerTest.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,21 +356,24 @@ public void api_description_group_should_explore_v3_actions( HttpConfiguration c
356356
ID = $"GET{relativePaths[0]}",
357357
HttpMethod = Get,
358358
RelativePath = relativePaths[0],
359-
Version = apiVersion
359+
Version = apiVersion,
360+
ActionDescriptor = new { ActionName = "GetV3" }
360361
},
361362
new
362363
{
363364
ID = $"GET{relativePaths[1]}",
364365
HttpMethod = Get,
365366
RelativePath = relativePaths[1],
366-
Version = apiVersion
367+
Version = apiVersion,
368+
ActionDescriptor = new { ActionName = "Get" }
367369
},
368370
new
369371
{
370372
ID = $"POST{relativePaths[2]}",
371373
HttpMethod = Post,
372374
RelativePath = relativePaths[2],
373-
Version = apiVersion
375+
Version = apiVersion,
376+
ActionDescriptor = new { ActionName = "Post" }
374377
}
375378
},
376379
options => options.ExcludingMissingMembers() );

test/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.Tests/Simulators/AttributeValues2Controller.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
namespace Microsoft.Web.Http.Description.Simulators
22
{
33
using Microsoft.Web.Http.Description.Models;
4+
using System;
5+
using System.Web.Http.Description;
46
using System.Web.Http;
57

68
[ApiVersion( "2.0" )]
@@ -10,7 +12,12 @@
1012
public class AttributeValues2Controller : ApiController
1113
{
1214
[Route]
13-
public IHttpActionResult Get() => Ok();
15+
public string Get() => "Test";
16+
17+
[Route]
18+
[MapToApiVersion( "3.0" )]
19+
[ResponseType( typeof( string ) )]
20+
public IHttpActionResult GetV3() => Ok( "Test" );
1421

1522
[Route( "{id:int}" )]
1623
public IHttpActionResult Get( int id ) => Ok();

test/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.Tests/Simulators/Values2Controller.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
{
33
using Microsoft.Web.Http.Description.Models;
44
using System.Web.Http;
5+
using System.Web.Http.Description;
56

67
[ControllerName( "Values" )]
78
public class Values2Controller : ApiController
89
{
9-
public IHttpActionResult Get() => Ok();
10+
public string Get() => "Test";
11+
12+
[ResponseType( typeof( string ) )]
13+
public IHttpActionResult GetV3() => Ok( "Test" );
1014

1115
public IHttpActionResult Get( int id ) => Ok();
1216

0 commit comments

Comments
 (0)