Skip to content

Commit dbd24b3

Browse files
Corrects an issue where a direct route is matched, but the bound action binding does not correspond to the matched controller. Fixes #44 and resolves #43. (#48)
1 parent 2976fad commit dbd24b3

17 files changed

+218
-60
lines changed

src/Microsoft.AspNet.WebApi.Versioning/Controllers/AggregatedActionMapping.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ internal AggregatedActionMapping( IReadOnlyList<ILookup<string, HttpActionDescri
2121
this.actionMappings = actionMappings;
2222
}
2323

24-
public IEnumerable<HttpActionDescriptor> this[string key] => actionMappings.Where( am => am.Contains( key ) ).SelectMany( am => am[key] );
24+
public IEnumerable<HttpActionDescriptor> this[string key] =>
25+
actionMappings.Where( am => am.Contains( key ) ).SelectMany( am => am[key] );
2526

26-
public int Count => actionMappings[0].Count;
27+
public int Count => actionMappings.Aggregate( 0, ( count, mappings ) => count + mappings.Count );
2728

2829
public bool Contains( string key ) => actionMappings.Any( am => am.Contains( key ) );
2930

30-
public IEnumerator<IGrouping<string, HttpActionDescriptor>> GetEnumerator() => actionMappings[0].GetEnumerator();
31+
public IEnumerator<IGrouping<string, HttpActionDescriptor>> GetEnumerator() =>
32+
actionMappings.SelectMany( am => am ).GetEnumerator();
3133

3234
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
3335
}

src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,11 @@ public virtual ILookup<string, HttpActionDescriptor> GetActionMapping( HttpContr
154154
Arg.NotNull( controllerDescriptor, nameof( controllerDescriptor ) );
155155
Contract.Ensures( Contract.Result<ILookup<string, HttpActionDescriptor>>() != null );
156156

157-
var internalSelector = GetInternalSelector( controllerDescriptor );
158-
var actionMappings = new List<ILookup<string, HttpActionDescriptor>>();
157+
var actionMappings = ( from descriptor in controllerDescriptor.AsEnumerable()
158+
let selector = GetInternalSelector( descriptor )
159+
select selector.GetActionMapping() ).ToArray();
159160

160-
actionMappings.Add( internalSelector.GetActionMapping() );
161-
162-
foreach ( var relatedControllerDescriptor in controllerDescriptor.GetRelatedCandidates() )
163-
{
164-
if ( relatedControllerDescriptor != controllerDescriptor )
165-
{
166-
actionMappings.Add( GetInternalSelector( relatedControllerDescriptor ).GetActionMapping() );
167-
}
168-
}
169-
170-
return actionMappings.Count == 1 ? actionMappings[0] : new AggregatedActionMapping( actionMappings );
161+
return actionMappings.Length == 1 ? actionMappings[0] : new AggregatedActionMapping( actionMappings );
171162
}
172163
}
173164
}

src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerAggregator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ private static IEnumerable<HttpControllerDescriptor> EnumerateControllersInDataT
118118

119119
var value = default( object );
120120

121-
if ( dataTokens.TryGetValue( "controller", out value ) )
121+
if ( dataTokens.TryGetValue( RouteDataTokenKeys.Controller, out value ) )
122122
{
123123
var controllerDescriptor = value as HttpControllerDescriptor;
124124

@@ -130,7 +130,7 @@ private static IEnumerable<HttpControllerDescriptor> EnumerateControllersInDataT
130130
yield break;
131131
}
132132

133-
if ( dataTokens.TryGetValue( "actions", out value ) )
133+
if ( dataTokens.TryGetValue( RouteDataTokenKeys.Actions, out value ) )
134134
{
135135
var actionDescriptors = value as HttpActionDescriptor[];
136136

@@ -146,4 +146,4 @@ private static IEnumerable<HttpControllerDescriptor> EnumerateControllersInDataT
146146
}
147147
}
148148
}
149-
}
149+
}

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

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,50 @@ internal static void SetApiVersionModel( this HttpControllerDescriptor controlle
107107
internal static void SetConventionsApiVersionModel( this HttpControllerDescriptor controllerDescriptor, ApiVersionModel model ) =>
108108
controllerDescriptor.Properties.AddOrUpdate( ConventionsApiVersionInfoKey, model, ( key, currentModel ) => ( (ApiVersionModel) currentModel ).Aggregate( model ) );
109109

110-
internal static IEnumerable<HttpControllerDescriptor> GetRelatedCandidates( this HttpControllerDescriptor controllerDescriptor ) =>
111-
(IEnumerable<HttpControllerDescriptor>) controllerDescriptor.Properties.GetOrAdd( RelatedControllerCandidatesKey, key => Enumerable.Empty<HttpControllerDescriptor>() );
112-
113110
internal static void SetRelatedCandidates( this HttpControllerDescriptor controllerDescriptor, IEnumerable<HttpControllerDescriptor> value ) =>
114111
controllerDescriptor.Properties.AddOrUpdate( RelatedControllerCandidatesKey, value, ( key, oldValue ) => value );
115112

113+
internal static IEnumerable<HttpControllerDescriptor> AsEnumerable( this HttpControllerDescriptor controllerDescriptor )
114+
{
115+
IEnumerable<HttpControllerDescriptor> relatedCandidates;
116+
117+
if ( controllerDescriptor.Properties.TryGetValue( RelatedControllerCandidatesKey, out relatedCandidates ) )
118+
{
119+
using ( var relatedControllerDescriptors = relatedCandidates.GetEnumerator() )
120+
{
121+
if ( relatedControllerDescriptors.MoveNext() )
122+
{
123+
yield return controllerDescriptor;
124+
125+
do
126+
{
127+
if ( relatedControllerDescriptors.Current != controllerDescriptor )
128+
{
129+
yield return relatedControllerDescriptors.Current;
130+
}
131+
}
132+
while ( relatedControllerDescriptors.MoveNext() );
133+
134+
yield break;
135+
}
136+
}
137+
}
138+
139+
var groupedControllerDescriptors = controllerDescriptor as IEnumerable<HttpControllerDescriptor>;
140+
141+
if ( groupedControllerDescriptors == null )
142+
{
143+
yield return controllerDescriptor;
144+
}
145+
else
146+
{
147+
foreach ( var groupedControllerDescriptor in groupedControllerDescriptors )
148+
{
149+
yield return groupedControllerDescriptor;
150+
}
151+
}
152+
}
153+
116154
/// <summary>
117155
/// Gets a value indicating whether the controller is API version neutral.
118156
/// </summary>

test/Microsoft.AspNet.WebApi.Acceptance.Tests/AcceptanceTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using static System.Web.Http.IncludeErrorDetailPolicy;
1515

1616
[Trait( "Kind", "Acceptance" )]
17+
[Trait( "Framework", "Web API" )]
1718
public abstract class AcceptanceTest : IDisposable
1819
{
1920
private sealed class FilteredControllerTypeResolver : List<Type>, IHttpControllerTypeResolver

test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/ByNamespaceAcceptanceTest.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,34 @@
22
{
33
using Microsoft.Web.Http.Routing;
44
using System.Web.Http;
5+
using System.Web.Http.Routing;
56
using static System.Web.Http.RouteParameter;
67

78
public abstract class ByNamespaceAcceptanceTest : AcceptanceTest
89
{
9-
protected ByNamespaceAcceptanceTest()
10+
protected enum SetupKind
11+
{
12+
None,
13+
HelloWorld,
14+
Agreements
15+
}
16+
17+
protected ByNamespaceAcceptanceTest(SetupKind kind)
18+
{
19+
switch ( kind )
20+
{
21+
case SetupKind.HelloWorld:
22+
ConfigureHellWorld();
23+
break;
24+
case SetupKind.Agreements:
25+
ConfigureAgreements();
26+
break;
27+
}
28+
29+
Configuration.EnsureInitialized();
30+
}
31+
32+
private void ConfigureAgreements()
1033
{
1134
FilteredControllerTypes.Add( typeof( Controllers.V1.AgreementsController ) );
1235
FilteredControllerTypes.Add( typeof( Controllers.V2.AgreementsController ) );
@@ -25,7 +48,27 @@ protected ByNamespaceAcceptanceTest()
2548
new { accountId = Optional },
2649
new { apiVersion = new ApiVersionRouteConstraint() } );
2750

28-
Configuration.EnsureInitialized();
51+
}
52+
53+
private void ConfigureHellWorld()
54+
{
55+
FilteredControllerTypes.Add( typeof( Controllers.V1.HelloWorldController ) );
56+
FilteredControllerTypes.Add( typeof( Controllers.V2.HelloWorldController ) );
57+
FilteredControllerTypes.Add( typeof( Controllers.V3.HelloWorldController ) );
58+
59+
var constraintResolver = new DefaultInlineConstraintResolver()
60+
{
61+
ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) }
62+
};
63+
64+
Configuration.MapHttpAttributeRoutes( constraintResolver );
65+
Configuration.AddApiVersioning(
66+
options =>
67+
{
68+
options.ReportApiVersions = true;
69+
options.DefaultApiVersion = new ApiVersion( 2, 0 );
70+
options.AssumeDefaultVersionWhenUnspecified = true;
71+
} );
2972
}
3073
}
3174
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Microsoft.Web.Http.ByNamespace.Controllers.V1
2+
{
3+
using Microsoft.Web.Http;
4+
using Models;
5+
using System.Web.Http;
6+
7+
[ApiVersion( "1.0", Deprecated = true )]
8+
[Route( "api/HelloWorld" )]
9+
[Route( "api/{version:apiVersion}/HelloWorld" )]
10+
public class HelloWorldController : ApiController
11+
{
12+
public IHttpActionResult Get() => Ok( "V1" );
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Microsoft.Web.Http.ByNamespace.Controllers.V2
2+
{
3+
using Microsoft.Web.Http;
4+
using Models;
5+
using System.Web.Http;
6+
7+
[ApiVersion( "2.0" )]
8+
[Route( "api/HelloWorld" )]
9+
[Route( "api/{version:apiVersion}/HelloWorld" )]
10+
public class HelloWorldController : ApiController
11+
{
12+
public IHttpActionResult Get() => Ok( "V2" );
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Microsoft.Web.Http.ByNamespace.Controllers.V3
2+
{
3+
using Microsoft.Web.Http;
4+
using Models;
5+
using System.Web.Http;
6+
7+
[ApiVersion( "3.0" )]
8+
[Route( "api/HelloWorld" )]
9+
[Route( "api/{version:apiVersion}/HelloWorld" )]
10+
public class HelloWorldController : ApiController
11+
{
12+
public IHttpActionResult Get() => Ok( "V3" );
13+
}
14+
}

test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_query_string_versioned_ApiController_per_namespace.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using FluentAssertions;
44
using Microsoft.Web;
55
using Microsoft.Web.Http.ByNamespace;
6-
using System.Collections.Generic;
76
using System.Linq;
87
using System.Net.Http;
98
using System.Threading.Tasks;
@@ -12,28 +11,24 @@
1211

1312
public class _a_query_string_versioned_ApiController_per_namespace : ByNamespaceAcceptanceTest
1413
{
14+
public _a_query_string_versioned_ApiController_per_namespace() : base( SetupKind.Agreements ) { }
15+
1516
[Theory]
1617
[InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V1.AgreementsController", "1.0" )]
1718
[InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V2.AgreementsController", "2.0" )]
1819
[InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V3.AgreementsController", "3.0" )]
1920
public async Task _get_should_return_200( string controller, string apiVersion )
2021
{
2122
// arrange
22-
23+
var example = new { Controller = "", ApiVersion = "", AccountId = "" };
2324

2425
// act
2526
var response = await GetAsync( $"api/agreements/42?api-version={apiVersion}" ).EnsureSuccessStatusCode();
26-
var content = await response.Content.ReadAsAsync<IDictionary<string, string>>();
27+
var content = await response.Content.ReadAsExampleAsync( example );
2728

2829
// assert
2930
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" );
30-
content.ShouldBeEquivalentTo(
31-
new Dictionary<string, string>()
32-
{
33-
["Controller"] = controller,
34-
["ApiVersion"] = apiVersion,
35-
["AccountId"] = "42"
36-
} );
31+
content.ShouldBeEquivalentTo( new { Controller = controller, ApiVersion = apiVersion, AccountId = "42" } );
3732
}
3833

3934
[Fact]

0 commit comments

Comments
 (0)