Skip to content

Commit b376328

Browse files
author
Chris Martinez
committed
Support API version in URL generation. Resolves #663
1 parent c96147c commit b376328

File tree

17 files changed

+411
-120
lines changed

17 files changed

+411
-120
lines changed

samples/aspnetcore/BasicSample/Controllers/HelloWorldController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ public class HelloWorldController : ControllerBase
1414

1515
// GET api/v{version}/helloworld/{id}
1616
[HttpGet( "{id:int}" )]
17-
public IActionResult Get( int id, ApiVersion apiVersion ) => Ok( new { Controller = GetType().Name, Id = id, Version = apiVersion.ToString() } );
17+
public IActionResult Get( int id, ApiVersion apiVersion ) => Ok( new { Controller = GetType().Name, Id = id } );
1818

1919
// POST api/v{version}/helloworld
2020
[HttpPost]
21-
public IActionResult Post( ApiVersion apiVersion ) => CreatedAtAction( nameof( Get ), new { id = 42, version = apiVersion.ToString() }, null );
21+
public IActionResult Post( ApiVersion apiVersion ) => CreatedAtAction( nameof( Get ), new { id = 42 }, null );
2222
}
2323
}

samples/aspnetcore/SwaggerSample/V3/Controllers/PeopleController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public IActionResult Get( int id ) =>
9191
public IActionResult Post( [FromBody] Person person, ApiVersion apiVersion )
9292
{
9393
person.Id = 42;
94-
return CreatedAtAction( nameof( Get ), new { id = person.Id, version = apiVersion.ToString() }, person );
94+
return CreatedAtAction( nameof( Get ), new { id = person.Id }, person );
9595
}
9696
}
9797
}

src/Common/Versioning/IApiVersionReaderExtensions.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,38 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
1111

1212
internal static class IApiVersionReaderExtensions
1313
{
14+
internal static bool VersionsByUrlSegment( this IApiVersionReader reader )
15+
{
16+
var context = new UrlSegmentDescriptionContext();
17+
reader.AddParameters( context );
18+
return context.HasPathApiVersion;
19+
}
20+
1421
internal static bool VersionsByMediaType( this IApiVersionReader reader )
1522
{
16-
var context = new DescriptionContext();
23+
var context = new MediaTypeDescriptionContext();
1724
reader.AddParameters( context );
1825
return context.HasMediaTypeApiVersion;
1926
}
2027

2128
internal static string GetMediaTypeVersionParameter( this IApiVersionReader reader )
2229
{
23-
var context = new DescriptionContext();
30+
var context = new MediaTypeDescriptionContext();
2431
reader.AddParameters( context );
2532
return context.ParameterName;
2633
}
2734

28-
sealed class DescriptionContext : IApiVersionParameterDescriptionContext
35+
sealed class UrlSegmentDescriptionContext : IApiVersionParameterDescriptionContext
36+
{
37+
internal bool HasPathApiVersion { get; private set; }
38+
39+
public void AddParameter( string name, ApiVersionParameterLocation location )
40+
{
41+
HasPathApiVersion |= location == Path;
42+
}
43+
}
44+
45+
sealed class MediaTypeDescriptionContext : IApiVersionParameterDescriptionContext
2946
{
3047
readonly StringComparer comparer = StringComparer.OrdinalIgnoreCase;
3148
readonly List<string> parameterNames = new List<string>();

src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,7 @@ public override bool Match( HttpRequestMessage request, IHttpRoute route, string
8282
return false;
8383
}
8484

85-
if ( ApiVersion == requestedVersion )
86-
{
87-
DecorateUrlHelperWithApiVersionRouteValueIfNecessary( request, values );
88-
return true;
89-
}
90-
91-
return false;
85+
return ApiVersion == requestedVersion;
9286
}
9387

9488
static ApiVersion? GetRequestedApiVersionOrReturnBadRequest( HttpRequestMessage request )
@@ -105,33 +99,5 @@ public override bool Match( HttpRequestMessage request, IHttpRoute route, string
10599
throw new HttpResponseException( request.CreateResponse( BadRequest, error ) );
106100
}
107101
}
108-
109-
static void DecorateUrlHelperWithApiVersionRouteValueIfNecessary( HttpRequestMessage request, IDictionary<string, object> values )
110-
{
111-
object apiVersion;
112-
string routeConstraintName;
113-
var configuration = request.GetConfiguration();
114-
115-
if ( configuration == null )
116-
{
117-
routeConstraintName = nameof( apiVersion );
118-
}
119-
else
120-
{
121-
routeConstraintName = configuration.GetApiVersioningOptions().RouteConstraintName;
122-
}
123-
124-
if ( !values.TryGetValue( routeConstraintName, out apiVersion ) )
125-
{
126-
return;
127-
}
128-
129-
var requestContext = request.GetRequestContext();
130-
131-
if ( !( requestContext.Url is VersionedUrlHelperDecorator ) )
132-
{
133-
requestContext.Url = new VersionedUrlHelperDecorator( requestContext.Url, routeConstraintName, apiVersion );
134-
}
135-
}
136102
}
137103
}

src/Microsoft.AspNet.OData.Versioning/Routing/VersionedUrlHelperDecorator.cs

Lines changed: 0 additions & 44 deletions
This file was deleted.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public virtual IDictionary<string, HttpControllerDescriptor> GetControllerMappin
7979

8080
if ( conventionRouteResult.Succeeded )
8181
{
82+
EnsureUrlHelper( request );
8283
return request.ApiVersionProperties().SelectedController = conventionRouteResult.Controller;
8384
}
8485

@@ -90,13 +91,15 @@ public virtual IDictionary<string, HttpControllerDescriptor> GetControllerMappin
9091

9192
if ( directRouteResult.Succeeded )
9293
{
94+
EnsureUrlHelper( request );
9395
return request.ApiVersionProperties().SelectedController = directRouteResult.Controller;
9496
}
9597

9698
conventionRouteResult = conventionRouteSelector.SelectController( context );
9799

98100
if ( conventionRouteResult.Succeeded )
99101
{
102+
EnsureUrlHelper( request );
100103
return request.ApiVersionProperties().SelectedController = conventionRouteResult.Controller;
101104
}
102105

@@ -293,5 +296,22 @@ static void EnsureRequestHasValidApiVersion( HttpRequestMessage request )
293296
throw new HttpResponseException( response.BadRequest( request, AmbiguousApiVersion, ex.Message ) );
294297
}
295298
}
299+
300+
static void EnsureUrlHelper( HttpRequestMessage request )
301+
{
302+
var context = request.GetRequestContext();
303+
304+
if ( context == null || context.Url is ApiVersionUrlHelper )
305+
{
306+
return;
307+
}
308+
309+
var options = request.GetApiVersioningOptions();
310+
311+
if ( options.ApiVersionReader.VersionsByUrlSegment() )
312+
{
313+
context.Url = new ApiVersionUrlHelper( context.Url );
314+
}
315+
}
296316
}
297317
}

src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public bool Match( HttpRequestMessage request, IHttpRoute route, string paramete
4747

4848
var properties = request.ApiVersionProperties();
4949

50+
properties.RouteParameter = parameterName;
5051
properties.RawRequestedApiVersion = value;
5152

5253
if ( TryParse( value, out var requestedVersion ) )

src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionRequestProperties.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Microsoft.Web.Http.Versioning
22
{
3+
using Microsoft.Web.Http.Routing;
34
using System.ComponentModel;
45
using System.Net.Http;
56
using System.Web.Http;
@@ -21,6 +22,14 @@ public class ApiVersionRequestProperties
2122
/// <param name="request">The current <see cref="HttpRequestMessage">HTTP request</see>.</param>
2223
public ApiVersionRequestProperties( HttpRequestMessage request ) => this.request = request;
2324

25+
/// <summary>
26+
/// Gets or sets the name of the route parameter containing the API Version value.
27+
/// </summary>
28+
/// <value>The name of the API version route parameter or <c>null</c>.</value>
29+
/// <remarks>This property will be <c>null</c> unless versioning by URL segment and the incoming request
30+
/// matches the <see cref="ApiVersionRouteConstraint">API version route constraint</see>.</remarks>
31+
public string? RouteParameter { get; set; }
32+
2433
/// <summary>
2534
/// Gets or sets the raw, unparsed API version for the current request.
2635
/// </summary>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
namespace Microsoft.Web.Http.Versioning
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Web.Http;
6+
using System.Web.Http.Routing;
7+
8+
/// <summary>
9+
/// Represents an API version aware <see cref="UrlHelper">URL helper</see>.
10+
/// </summary>
11+
public class ApiVersionUrlHelper : UrlHelper
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="ApiVersionUrlHelper"/> class.
15+
/// </summary>
16+
/// <param name="url">The inner <see cref="UrlHelper">URL helper</see>.</param>
17+
public ApiVersionUrlHelper( UrlHelper url )
18+
{
19+
Url = url ?? throw new ArgumentNullException( nameof( url ) );
20+
21+
if ( url.Request != null )
22+
{
23+
Request = url.Request;
24+
}
25+
}
26+
27+
/// <summary>
28+
/// Gets the inner URL helper.
29+
/// </summary>
30+
/// <value>The inner <see cref="UrlHelper">URL helper</see>.</value>
31+
protected UrlHelper Url { get; }
32+
33+
/// <inheritdoc />
34+
public override string Content( string path ) => Url.Content( path );
35+
36+
/// <inheritdoc />
37+
public override string Link( string routeName, IDictionary<string, object> routeValues ) =>
38+
Url.Link( routeName, AddApiVersionRouteValueIfNecessary( routeValues ) );
39+
40+
/// <inheritdoc />
41+
public override string Route( string routeName, IDictionary<string, object> routeValues ) =>
42+
Url.Route( routeName, AddApiVersionRouteValueIfNecessary( routeValues ) );
43+
44+
IDictionary<string, object>? AddApiVersionRouteValueIfNecessary( IDictionary<string, object>? routeValues )
45+
{
46+
if ( Request == null )
47+
{
48+
return routeValues;
49+
}
50+
51+
var properties = Request.ApiVersionProperties();
52+
var key = properties.RouteParameter;
53+
54+
if ( string.IsNullOrEmpty( key ) )
55+
{
56+
return routeValues;
57+
}
58+
59+
var value = properties.RawRequestedApiVersion;
60+
61+
if ( string.IsNullOrEmpty( value ) )
62+
{
63+
return routeValues;
64+
}
65+
66+
if ( routeValues == null )
67+
{
68+
return new HttpRouteValueDictionary() { [key!] = value! };
69+
}
70+
71+
if ( !routeValues.ContainsKey( key! ) )
72+
{
73+
routeValues.Add( key!, value! );
74+
}
75+
76+
return routeValues;
77+
}
78+
}
79+
}

src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.Extensions.DependencyInjection.Extensions;
1212
using Microsoft.Extensions.Options;
1313
using System;
14+
using System.Linq;
1415
using static ServiceDescriptor;
1516

1617
/// <summary>
@@ -66,6 +67,7 @@ static void AddApiVersioningServices( IServiceCollection services )
6667
services.TryAddEnumerable( Transient<IApiControllerSpecification, ApiBehaviorSpecification>() );
6768
services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() );
6869
services.AddTransient<IStartupFilter, AutoRegisterMiddleware>();
70+
services.Replace( WithUrlHelperFactoryDecorator( services ) );
6971
}
7072

7173
static IReportApiVersions OnRequestIReportApiVersions( IServiceProvider serviceProvider )
@@ -79,5 +81,39 @@ static IReportApiVersions OnRequestIReportApiVersions( IServiceProvider serviceP
7981

8082
return new DoNotReportApiVersions();
8183
}
84+
85+
static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor )
86+
{
87+
if ( descriptor.ImplementationInstance != null )
88+
{
89+
return descriptor.ImplementationInstance;
90+
}
91+
92+
if ( descriptor.ImplementationFactory != null )
93+
{
94+
return descriptor.ImplementationFactory( services );
95+
}
96+
97+
return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType );
98+
}
99+
100+
static ServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection services )
101+
{
102+
var descriptor = services.First( sd => sd.ServiceType == typeof( IUrlHelperFactory ) );
103+
var factory = ActivatorUtilities.CreateFactory( typeof( ApiVersionUrlHelperFactory ), new[] { typeof( IUrlHelperFactory ) } );
104+
105+
IUrlHelperFactory NewFactory( IServiceProvider serviceProvider )
106+
{
107+
var decorated = serviceProvider.CreateInstance( descriptor! );
108+
var options = serviceProvider.GetRequiredService<IOptions<ApiVersioningOptions>>().Value;
109+
var instance = options.ApiVersionReader.VersionsByUrlSegment() ?
110+
factory( serviceProvider, new[] { decorated } ) :
111+
decorated;
112+
113+
return (IUrlHelperFactory) instance;
114+
}
115+
116+
return Describe( typeof( IUrlHelperFactory ), NewFactory, descriptor.Lifetime );
117+
}
82118
}
83119
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public bool Match( HttpContext httpContext, IRouter route, string routeKey, Rout
5252

5353
var feature = httpContext.Features.Get<IApiVersioningFeature>();
5454

55+
feature.RouteParameter = routeKey;
5556
feature.RawRequestedApiVersion = value;
5657

5758
if ( TryParse( value, out var requestedVersion ) )

0 commit comments

Comments
 (0)