Skip to content

Commit 9f5a605

Browse files
When using URL segment API versioning with the ApiVersionRouteConstraint, URL generation and 400 responses are now correctly returned. Fixes #18, #19, and #20 (#22)
1 parent 154c489 commit 9f5a605

File tree

5 files changed

+111
-56
lines changed

5 files changed

+111
-56
lines changed

src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionRouteConstraint.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
using AspNetCore.Routing;
44
using Http;
55
using System;
6-
using System.Diagnostics.Contracts;
76
using static ApiVersion;
87
using static AspNetCore.Routing.RouteDirection;
8+
using static System.String;
99

1010
/// <summary>
1111
/// Represents a route constraint for service <see cref="ApiVersion">API versions</see>.
@@ -24,12 +24,13 @@ public sealed class ApiVersionRouteConstraint : IRouteConstraint
2424
/// <returns>True if the route constraint is matched; otherwise, false.</returns>
2525
public bool Match( HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection )
2626
{
27-
if ( routeDirection != IncomingRequest )
27+
var value = default( string );
28+
29+
if ( routeDirection == UrlGeneration )
2830
{
29-
return false;
31+
return !IsNullOrEmpty( routeKey ) && values.TryGetValue( routeKey, out value ) && !IsNullOrEmpty( value );
3032
}
3133

32-
var value = default( string );
3334
var requestedVersion = default( ApiVersion );
3435

3536
if ( !values.TryGetValue( routeKey, out value ) || !TryParse( value, out requestedVersion ) )
@@ -41,4 +42,4 @@ public bool Match( HttpContext httpContext, IRouter route, string routeKey, Rout
4142
return true;
4243
}
4344
}
44-
}
45+
}

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs

+17-15
Original file line numberDiff line numberDiff line change
@@ -218,35 +218,37 @@ private BadRequestHandler IsValidRequest( ActionSelectionContext context )
218218
return null;
219219
}
220220

221-
var requestedVersion = context.HttpContext.GetRawRequestedApiVersion();
221+
var code = default( string );
222+
var requestedVersion = default( string );
222223
var parsedVersion = context.RequestedVersion;
223224
var actionNames = new Lazy<string>( () => Join( NewLine, context.MatchingActions.Select( a => a.DisplayName ) ) );
224225

225-
if ( IsNullOrEmpty( requestedVersion ) )
226+
if ( parsedVersion == null )
226227
{
227-
if ( parsedVersion == null )
228+
requestedVersion = context.HttpContext.GetRawRequestedApiVersion();
229+
230+
if ( IsNullOrEmpty( requestedVersion ) )
228231
{
229232
logger.ApiVersionUnspecified( actionNames.Value );
233+
return null;
234+
}
235+
else if ( TryParse( requestedVersion, out parsedVersion ) )
236+
{
237+
code = "UnsupportedApiVersion";
238+
logger.ApiVersionUnmatched( parsedVersion, actionNames.Value );
230239
}
231240
else
232241
{
233-
logger.ApiVersionUnspecified( parsedVersion, actionNames.Value );
242+
code = "InvalidApiVersion";
243+
logger.ApiVersionInvalid( requestedVersion );
234244
}
235-
return null;
236245
}
237-
238-
var code = default( string );
239-
240-
if ( TryParse( requestedVersion, out parsedVersion ) )
246+
else
241247
{
248+
requestedVersion = parsedVersion.ToString();
242249
code = "UnsupportedApiVersion";
243250
logger.ApiVersionUnmatched( parsedVersion, actionNames.Value );
244251
}
245-
else
246-
{
247-
code = "InvalidApiVersion";
248-
logger.ApiVersionInvalid( requestedVersion );
249-
}
250252

251253
var message = SR.VersionedResourceNotSupported.FormatDefault( context.HttpContext.Request.GetDisplayUrl(), requestedVersion );
252254
return new BadRequestHandler( code, message );
@@ -410,4 +412,4 @@ private IReadOnlyList<ActionSelectorCandidate> EvaluateActionConstraintsCore( Ro
410412
}
411413
}
412414
}
413-
}
415+
}

test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs

+74-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,59 @@
11
namespace Microsoft.AspNetCore.Mvc.Routing
22
{
33
using AspNetCore.Routing;
4+
using Builder;
5+
using Extensions.DependencyInjection;
6+
using Extensions.ObjectPool;
47
using FluentAssertions;
58
using Http;
69
using Moq;
10+
using System;
711
using System.Collections.Generic;
12+
using System.Text.Encodings.Web;
13+
using System.Threading.Tasks;
814
using Xunit;
915
using static AspNetCore.Routing.RouteDirection;
16+
using static System.String;
1017

1118
public class ApiVersionRouteConstraintTest
1219
{
13-
[Fact]
14-
public void match_should_return_false_for_url_generation()
20+
private class PassThroughRouter : IRouter
21+
{
22+
public VirtualPathData GetVirtualPath( VirtualPathContext context ) => null;
23+
24+
public Task RouteAsync( RouteContext context )
25+
{
26+
context.Handler = c => Task.CompletedTask;
27+
return Task.CompletedTask;
28+
}
29+
}
30+
31+
private static ServiceCollection CreateServices()
32+
{
33+
var services = new ServiceCollection();
34+
35+
services.AddOptions();
36+
services.AddLogging();
37+
services.AddRouting();
38+
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
39+
.AddSingleton( UrlEncoder.Default );
40+
41+
return services;
42+
}
43+
44+
private static IRouteBuilder CreateRouteBuilder( IServiceProvider services )
45+
{
46+
var app = new Mock<IApplicationBuilder>();
47+
app.SetupGet( a => a.ApplicationServices ).Returns( services );
48+
return new RouteBuilder( app.Object ) { DefaultHandler = new PassThroughRouter() };
49+
}
50+
51+
[Theory]
52+
[InlineData( "apiVersion", "1", true )]
53+
[InlineData( "apiVersion", null, false )]
54+
[InlineData( "apiVersion", "", false )]
55+
[InlineData( null, "", false )]
56+
public void match_should_return_expected_result_for_url_generation( string key, string value, bool expected )
1557
{
1658
// arrange
1759
var httpContext = new Mock<HttpContext>().Object;
@@ -20,11 +62,16 @@ public void match_should_return_false_for_url_generation()
2062
var routeDirection = UrlGeneration;
2163
var constraint = new ApiVersionRouteConstraint();
2264

65+
if ( !IsNullOrEmpty( key ) )
66+
{
67+
values[key] = value;
68+
}
69+
2370
// act
24-
var matched = constraint.Match( httpContext, route, null, values, routeDirection );
71+
var matched = constraint.Match( httpContext, route, key, values, routeDirection );
2572

2673
// assert
27-
matched.Should().BeFalse();
74+
matched.Should().Be( expected );
2875
}
2976

3077
[Fact]
@@ -87,5 +134,27 @@ public void match_should_return_true_when_matched()
87134
// assert
88135
matched.Should().BeTrue();
89136
}
137+
138+
[Fact]
139+
public void url_helper_should_create_route_link_with_api_version_constriant()
140+
{
141+
// arrange
142+
var services = CreateServices().AddApiVersioning();
143+
var provider = services.BuildServiceProvider();
144+
var routeBuilder = CreateRouteBuilder( provider );
145+
var actionContext = new ActionContext() { HttpContext = new DefaultHttpContext() { RequestServices = provider } };
146+
147+
routeBuilder.MapRoute( "default", "v{version:apiVersion}/{controller}/{action}" );
148+
actionContext.RouteData = new RouteData();
149+
actionContext.RouteData.Routers.Add( routeBuilder.Build() );
150+
151+
var urlHelper = new UrlHelper( actionContext );
152+
153+
// act
154+
var url = urlHelper.Link( "default", new { version = "1", controller = "Store", action = "Buy" } );
155+
156+
// assert
157+
url.Should().Be( "/v1/Store/Buy" );
158+
}
90159
}
91-
}
160+
}

test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Simulators/OrdersController.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
namespace Microsoft.AspNetCore.Mvc.Simulators
22
{
3+
using Routing;
34
using System;
45
using System.Threading.Tasks;
56

67
[ApiVersion( "2015-11-15" )]
78
[ApiVersion( "2016-06-06" )]
89
public class OrdersController : Controller
910
{
10-
[MapToApiVersion( "2015-11-15" )]
11-
public Task<IActionResult> Get_2015_11_15() => Task.FromResult<IActionResult>( Ok( "Version 2015-11-15" ) );
11+
[HttpGet]
12+
public Task<IActionResult> Get() => Task.FromResult<IActionResult>( Ok( "Version 2015-11-15" ) );
1213

13-
[Route( "orders" )]
14+
[HttpGet]
1415
[MapToApiVersion( "2016-06-06" )]
1516
public Task<IActionResult> Get_2016_06_06() => Task.FromResult<IActionResult>( Ok( "Version 2016-06-06" ) );
1617
}

test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionActionSelectorTest.cs

+10-28
Original file line numberDiff line numberDiff line change
@@ -251,50 +251,32 @@ public async Task select_best_candidate_should_assume_configured_default_api_ver
251251

252252
[Fact]
253253
public async Task select_best_candidate_should_use_api_version_selector_for_conventionX2Dbased_controller_when_allowed()
254-
{
255-
// arrange
256-
var controllerType = typeof( OrdersController ).GetTypeInfo();
257-
Action<ApiVersioningOptions> versioningSetup = o =>
258-
{
259-
o.AssumeDefaultVersionWhenUnspecified = true;
260-
o.ApiVersionSelector = new ConstantApiVersionSelector( new ApiVersion( new DateTime( 2015, 11, 15 ) ) );
261-
};
262-
Action<IRouteBuilder> routesSetup = r => r.MapRoute( "default", "api/{controller}/{action=Get_2015_11_15}/{id?}" );
263-
264-
using ( var server = new WebServer( versioningSetup, routesSetup ) )
265-
{
266-
await server.Client.GetAsync( "api/orders" );
267-
268-
// act
269-
var action = ( (TestApiVersionActionSelector) server.Services.GetRequiredService<IActionSelector>() ).SelectedCandidate;
270-
271-
// assert
272-
action.As<ControllerActionDescriptor>().ControllerTypeInfo.Should().Be( controllerType );
273-
}
274-
}
275-
276-
[Fact]
277-
public async Task select_best_candidate_should_use_api_version_selector_for_attributeX2Dbased_controller_when_allowed()
278254
{
279255
// arrange
280256
var controllerType = typeof( OrdersController ).GetTypeInfo();
281257
Action<ApiVersioningOptions> versioningSetup = o =>
282258
{
283259
o.AssumeDefaultVersionWhenUnspecified = true;
284260
o.ApiVersionSelector = new LowestImplementedApiVersionSelector( o );
261+
285262
};
286-
Action<IRouteBuilder> routesSetup = r => r.MapRoute( "default", "{controller}/{action=Get_2015_11_15}/{id?}" );
263+
Action<IRouteBuilder> routesSetup = r => r.MapRoute( "default", "api/{controller}/{action=Get}/{id?}" );
287264

288265
using ( var server = new WebServer( versioningSetup, routesSetup ) )
289266
{
290-
await server.Client.GetAsync( "orders" );
267+
await server.Client.GetAsync( "api/orders" );
291268

292269
// act
293270
var action = ( (TestApiVersionActionSelector) server.Services.GetRequiredService<IActionSelector>() ).SelectedCandidate;
294271

295272
// assert
296-
action.As<ControllerActionDescriptor>().ControllerTypeInfo.Should().Be( controllerType );
297-
action.As<ControllerActionDescriptor>().ActionName.Should().Be( nameof( OrdersController.Get_2015_11_15 ) );
273+
action.As<ControllerActionDescriptor>().ShouldBeEquivalentTo(
274+
new
275+
{
276+
ControllerTypeInfo = controllerType,
277+
ActionName = nameof( OrdersController.Get )
278+
},
279+
options => options.ExcludingMissingMembers() );
298280
}
299281
}
300282

0 commit comments

Comments
 (0)