Skip to content

Commit c3f9cfb

Browse files
Fixed the internal action selection process to consider both convention and attributing routing using two passes when needed. (#15)
1 parent 8a4a325 commit c3f9cfb

File tree

4 files changed

+153
-10
lines changed

4 files changed

+153
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace Microsoft.Web.Http.Controllers
2+
{
3+
using System;
4+
using System.Diagnostics.Contracts;
5+
using System.Web.Http.Controllers;
6+
7+
/// <content>
8+
/// Provides additional content for the <see cref="ApiVersionActionSelector"/> class.
9+
/// </content>
10+
public partial class ApiVersionActionSelector
11+
{
12+
private sealed class ActionSelectionResult
13+
{
14+
internal ActionSelectionResult( HttpActionDescriptor action )
15+
{
16+
Contract.Requires( action != null );
17+
Action = action;
18+
}
19+
20+
internal ActionSelectionResult( Exception exception )
21+
{
22+
Contract.Requires( exception != null );
23+
Exception = exception;
24+
}
25+
26+
internal bool Succeeded => Exception == null;
27+
28+
internal HttpActionDescriptor Action { get; }
29+
30+
internal Exception Exception { get; }
31+
}
32+
}
33+
}

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

+40-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Diagnostics.Contracts;
88
using System.Linq;
9-
using System.Net;
109
using System.Net.Http;
1110
using System.Reflection;
1211
using System.Text;
@@ -124,41 +123,72 @@ internal HttpActionDescriptor SelectAction( HttpControllerContext controllerCont
124123

125124
InitializeStandardActions();
126125

127-
var selectedCandidates = FindMatchingActions( controllerContext );
126+
var firstAttempt = FindAction( controllerContext, selector, ignoreSubRoutes: false );
127+
128+
if ( firstAttempt.Succeeded )
129+
{
130+
return firstAttempt.Action;
131+
}
132+
133+
if ( controllerContext.RouteData.GetSubRoutes() == null )
134+
{
135+
throw firstAttempt.Exception;
136+
}
137+
138+
var secondAttempt = FindAction( controllerContext, selector, ignoreSubRoutes: true );
139+
140+
if ( secondAttempt.Succeeded )
141+
{
142+
return secondAttempt.Action;
143+
}
144+
145+
throw firstAttempt.Exception;
146+
}
147+
148+
private ActionSelectionResult FindAction( HttpControllerContext controllerContext, Func<HttpControllerContext, IReadOnlyList<HttpActionDescriptor>, HttpActionDescriptor> selector, bool ignoreSubRoutes )
149+
{
150+
Contract.Requires( controllerContext != null );
151+
Contract.Requires( selector != null );
152+
Contract.Ensures( Contract.Result<ActionSelectionResult>() != null );
153+
154+
var selectedCandidates = FindMatchingActions( controllerContext, ignoreSubRoutes );
128155

129156
if ( selectedCandidates.Count == 0 )
130157
{
131-
throw new HttpResponseException( CreateSelectionError( controllerContext ) );
158+
return new ActionSelectionResult( new HttpResponseException( CreateSelectionError( controllerContext ) ) );
132159
}
133160

134161
var action = selector( controllerContext, selectedCandidates ) as CandidateHttpActionDescriptor;
135162

136163
if ( action != null )
137164
{
138165
ElevateRouteData( controllerContext, action.CandidateAction );
139-
return action;
166+
return new ActionSelectionResult( action );
140167
}
141168

142169
if ( selectedCandidates.Count == 1 )
143170
{
144-
throw new HttpResponseException( CreateSelectionError( controllerContext ) );
171+
return new ActionSelectionResult( new HttpResponseException( CreateSelectionError( controllerContext ) ) );
145172
}
146173

147174
var ambiguityList = CreateAmbiguousMatchList( selectedCandidates );
148-
throw new InvalidOperationException( SR.ApiControllerActionSelector_AmbiguousMatch.FormatDefault( ambiguityList ) );
175+
176+
return new ActionSelectionResult( new InvalidOperationException( SR.ApiControllerActionSelector_AmbiguousMatch.FormatDefault( ambiguityList ) ) );
149177
}
150178

151179
private static void ElevateRouteData( HttpControllerContext controllerContext, CandidateActionWithParams selectedCandidate ) => controllerContext.RouteData = selectedCandidate.RouteDataSource;
152180

153-
private IReadOnlyList<CandidateHttpActionDescriptor> FindMatchingActions( HttpControllerContext controllerContext, bool ignoreVerbs = false )
181+
private IReadOnlyList<CandidateHttpActionDescriptor> FindMatchingActions( HttpControllerContext controllerContext, bool ignoreSubRoutes = false, bool ignoreVerbs = false )
154182
{
155183
Contract.Requires( controllerContext != null );
156184
Contract.Ensures( Contract.Result<IReadOnlyList<CandidateHttpActionDescriptor>>() != null );
157185

158186
var routeData = controllerContext.RouteData;
159-
var subRoutes = routeData.GetSubRoutes();
160-
var actionsWithParameters = GetInitialCandidateWithParameterListForRegularRoutes( controllerContext, ignoreVerbs )
161-
.Union( GetInitialCandidateWithParameterListForDirectRoutes( controllerContext, subRoutes, ignoreVerbs ) );
187+
var subRoutes = ignoreSubRoutes ? default( IEnumerable<IHttpRouteData> ) : routeData.GetSubRoutes();
188+
var actionsWithParameters = subRoutes == null ?
189+
GetInitialCandidateWithParameterListForRegularRoutes( controllerContext, ignoreVerbs ) :
190+
GetInitialCandidateWithParameterListForDirectRoutes( controllerContext, subRoutes, ignoreVerbs );
191+
162192
var actionsFoundByParams = FindActionMatchRequiredRouteAndQueryParameters( actionsWithParameters );
163193
var orderCandidates = RunOrderFilter( actionsFoundByParams );
164194
var precedenceCandidates = RunPrecedenceFilter( orderCandidates );

test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs

+57
Original file line numberDiff line numberDiff line change
@@ -860,5 +860,62 @@ public void select_controller_should_return_400_when_requested_api_version_is_am
860860
// assert
861861
selectController.ShouldThrow<HttpResponseException>().And.Response.StatusCode.Should().Be( BadRequest );
862862
}
863+
864+
[Fact]
865+
public async Task select_controller_should_resolve_controller_with_api_versionX2Dneutral_action_using_convention_and_attribute_routing()
866+
{
867+
// arrange
868+
var controllerTypes = new Collection<Type>() { typeof( AdminController ) };
869+
var controllerTypeResolver = new Mock<IHttpControllerTypeResolver>();
870+
var configuration = new HttpConfiguration();
871+
var request = new HttpRequestMessage( Post, "http://localhost/admin/markAsTest" );
872+
873+
controllerTypeResolver.Setup( r => r.GetControllerTypes( It.IsAny<IAssembliesResolver>() ) ).Returns( controllerTypes );
874+
configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object );
875+
configuration.AddApiVersioning(
876+
options =>
877+
{
878+
options.AssumeDefaultVersionWhenUnspecified = true;
879+
options.DefaultApiVersion = new ApiVersion( new DateTime( 2015, 11, 15 ) );
880+
options.ApiVersionReader = new QueryStringOrHeaderApiVersionReader() { HeaderNames = { "api-version", "x-ms-version" } };
881+
} );
882+
configuration.Routes.MapHttpRoute( "Admin-1", "admin", new { controller = "admin", action = "Get" } );
883+
configuration.Routes.MapHttpRoute( "Admin-2", "admin/seedData", new { controller = "admin", action = "SeedData" } );
884+
configuration.Routes.MapHttpRoute( "Admin-3", "admin/markAsTest", new { controller = "admin", action = "MarkAsTest" } );
885+
configuration.MapHttpAttributeRoutes();
886+
configuration.EnsureInitialized();
887+
888+
var routeData = configuration.Routes.GetRouteData( request );
889+
890+
request.SetConfiguration( configuration );
891+
request.SetRouteData( routeData );
892+
893+
var controllerSelector = configuration.Services.GetHttpControllerSelector();
894+
var actionSelector = configuration.Services.GetActionSelector();
895+
var controllerDescriptor = controllerSelector.SelectController( request );
896+
var controllerContext = new HttpControllerContext( configuration, routeData, request )
897+
{
898+
ControllerDescriptor = controllerDescriptor,
899+
RequestContext = new HttpRequestContext()
900+
{
901+
Configuration = configuration,
902+
RouteData = routeData
903+
}
904+
};
905+
906+
// act
907+
var controller = controllerSelector.SelectController( request );
908+
var action = actionSelector.SelectAction( controllerContext );
909+
910+
// assert
911+
controller.ControllerType.Should().Be( typeof( AdminController ) );
912+
action.ActionName.Should().Be( "MarkAsTest" );
913+
914+
var server = new HttpServer( configuration );
915+
var client = new HttpClient( server );
916+
var response = await client.SendAsync( request );
917+
918+
response.StatusCode.Should().Be( OK );
919+
}
863920
}
864921
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Microsoft.Web.Http.Simulators
2+
{
3+
using System;
4+
using System.Threading.Tasks;
5+
using System.Web.Http;
6+
7+
[ApiVersionNeutral]
8+
public class AdminController : ApiController
9+
{
10+
[Route( "admin" )]
11+
public IHttpActionResult Get() => Ok();
12+
13+
[HttpPost]
14+
public IHttpActionResult SeedData() => Ok();
15+
16+
[HttpPost]
17+
public IHttpActionResult MarkAsTest() => Ok();
18+
19+
[HttpPost]
20+
[Route( "admin/inject" )]
21+
public IHttpActionResult Inject() => Ok();
22+
}
23+
}

0 commit comments

Comments
 (0)