Skip to content

Commit a0ca84d

Browse files
author
Chris Martinez
committed
Add API version parameter to Content-Type. Resolves #484.
1 parent ae108a0 commit a0ca84d

File tree

10 files changed

+205
-7
lines changed

10 files changed

+205
-7
lines changed

src/Common/Common.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionParameterSource.cs" />
6363
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionProvider.cs" />
6464
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionReader.cs" />
65+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionReaderExtensions.cs" />
6566
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionSelector.cs" />
6667
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IErrorResponseProvider.cs" />
6768
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IReportApiVersions.cs" />
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning
5+
#endif
6+
{
7+
using System;
8+
using static ApiVersionParameterLocation;
9+
10+
internal static class IApiVersionReaderExtensions
11+
{
12+
internal static bool VersionsByMediaType( this IApiVersionReader reader )
13+
{
14+
var context = new DescriptionContext();
15+
reader.AddParameters( context );
16+
return context.HasMediaTypeApiVersion;
17+
}
18+
19+
internal static string GetMediaTypeVersionParameter( this IApiVersionReader reader )
20+
{
21+
var context = new DescriptionContext();
22+
reader.AddParameters( context );
23+
return context.ParameterName;
24+
}
25+
26+
sealed class DescriptionContext : IApiVersionParameterDescriptionContext
27+
{
28+
internal string ParameterName { get; private set; }
29+
30+
internal bool HasMediaTypeApiVersion { get; private set; }
31+
32+
public void AddParameter( string name, ApiVersionParameterLocation location )
33+
{
34+
if ( HasMediaTypeApiVersion |= location == MediaTypeParameter )
35+
{
36+
ParameterName = name;
37+
}
38+
}
39+
}
40+
}
41+
}

src/Common/Versioning/MediaTypeApiVersionReader.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
2727
/// </summary>
2828
public partial class MediaTypeApiVersionReader : IApiVersionReader
2929
{
30-
string parameterName = "v";
30+
string parameterName;
3131

3232
/// <summary>
3333
/// Initializes a new instance of the <see cref="MediaTypeApiVersionReader"/> class.
3434
/// </summary>
35-
public MediaTypeApiVersionReader() { }
35+
public MediaTypeApiVersionReader() => parameterName = "v";
3636

3737
/// <summary>
3838
/// Initializes a new instance of the <see cref="MediaTypeApiVersionReader"/> class.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public static void AddApiVersioning( this HttpConfiguration configuration, Actio
5757
configuration.Filters.Add( new ReportApiVersionsAttribute() );
5858
}
5959

60+
if ( options.ApiVersionReader.VersionsByMediaType() )
61+
{
62+
configuration.Filters.Add( new ApplyContentTypeVersionActionFilter( options.ApiVersionReader ) );
63+
}
64+
6065
configuration.Properties.AddOrUpdate( ApiVersioningOptionsKey, options, ( key, oldValue ) => options );
6166
configuration.ParameterBindingRules.Add( typeof( ApiVersion ), ApiVersionParameterBinding.Create );
6267
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
namespace Microsoft.Web.Http.Versioning
2+
{
3+
using System;
4+
using System.Net.Http.Headers;
5+
using System.Web.Http;
6+
using System.Web.Http.Filters;
7+
8+
sealed class ApplyContentTypeVersionActionFilter : ActionFilterAttribute
9+
{
10+
readonly string parameterName;
11+
12+
public ApplyContentTypeVersionActionFilter( IApiVersionReader reader ) =>
13+
parameterName = reader.GetMediaTypeVersionParameter();
14+
15+
public override bool AllowMultiple => false;
16+
17+
public override void OnActionExecuted( HttpActionExecutedContext actionExecutedContext )
18+
{
19+
var response = actionExecutedContext.Response;
20+
21+
if ( response == null )
22+
{
23+
return;
24+
}
25+
26+
var headers = response.Content?.Headers;
27+
var contentType = headers?.ContentType;
28+
29+
if ( contentType == null )
30+
{
31+
return;
32+
}
33+
34+
var apiVersion = actionExecutedContext.Request.GetRequestedApiVersion();
35+
36+
if ( apiVersion == null )
37+
{
38+
return;
39+
}
40+
41+
var parameters = contentType.Parameters;
42+
var versionParameter = default( NameValueHeaderValue );
43+
var comparer = StringComparer.OrdinalIgnoreCase;
44+
45+
foreach ( var parameter in parameters )
46+
{
47+
if ( comparer.Equals( parameter.Name, parameterName ) )
48+
{
49+
versionParameter = parameter;
50+
break;
51+
}
52+
}
53+
54+
if ( versionParameter == null )
55+
{
56+
versionParameter = new NameValueHeaderValue( parameterName );
57+
parameters.Add( versionParameter );
58+
}
59+
60+
versionParameter.Value = apiVersion.ToString();
61+
headers.ContentType = contentType;
62+
}
63+
}
64+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ static void AddApiVersioningServices( IServiceCollection services )
6262
services.TryAddSingleton<IApiVersionRoutePolicy, DefaultApiVersionRoutePolicy>();
6363
services.TryAddSingleton<IApiControllerFilter, DefaultApiControllerFilter>();
6464
services.TryAddSingleton<ReportApiVersionsAttribute>();
65+
services.AddSingleton<ApplyContentTypeVersionActionFilter>();
6566
services.TryAddSingleton( OnRequestIReportApiVersions );
6667
services.TryAddEnumerable( Transient<IPostConfigureOptions<MvcOptions>, ApiVersioningMvcOptionsSetup>() );
6768
services.TryAddEnumerable( Transient<IPostConfigureOptions<RouteOptions>, ApiVersioningRouteOptionsSetup>() );

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public virtual void PostConfigure( string name, MvcOptions options )
2929
options.Filters.AddService<ReportApiVersionsAttribute>();
3030
}
3131

32+
if ( value.ApiVersionReader.VersionsByMediaType() )
33+
{
34+
options.Filters.AddService<ApplyContentTypeVersionActionFilter>();
35+
}
36+
3237
var modelMetadataDetailsProviders = options.ModelMetadataDetailsProviders;
3338

3439
modelMetadataDetailsProviders.Insert( 0, new SuppressChildValidationMetadataProvider( typeof( ApiVersion ) ) );
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc.Filters;
5+
using Microsoft.Extensions.Primitives;
6+
using Microsoft.Net.Http.Headers;
7+
using System.Threading.Tasks;
8+
using static System.StringComparison;
9+
using static System.Threading.Tasks.Task;
10+
11+
sealed class ApplyContentTypeVersionActionFilter : IActionFilter
12+
{
13+
readonly string parameterName;
14+
15+
public ApplyContentTypeVersionActionFilter( IApiVersionReader reader ) =>
16+
parameterName = reader.GetMediaTypeVersionParameter();
17+
18+
public void OnActionExecuted( ActionExecutedContext context ) { }
19+
20+
public void OnActionExecuting( ActionExecutingContext context )
21+
{
22+
var httpContext = context.HttpContext;
23+
var response = httpContext.Response;
24+
25+
if ( response == null )
26+
{
27+
return;
28+
}
29+
30+
response.OnStarting( ApplyApiVersionMediaTypeParameter, httpContext );
31+
}
32+
33+
Task ApplyApiVersionMediaTypeParameter( object state )
34+
{
35+
var context = (HttpContext) state;
36+
var headers = context.Response.GetTypedHeaders();
37+
var contentType = headers.ContentType;
38+
39+
if ( contentType == null )
40+
{
41+
return CompletedTask;
42+
}
43+
44+
var apiVersion = context.GetRequestedApiVersion();
45+
46+
if ( apiVersion == null )
47+
{
48+
return CompletedTask;
49+
}
50+
51+
var parameters = contentType.Parameters;
52+
var parameter = default( NameValueHeaderValue );
53+
54+
for ( var i = 0; i < parameters.Count; i++ )
55+
{
56+
if ( parameters[i].Name.Equals( parameterName, OrdinalIgnoreCase ) )
57+
{
58+
parameter = parameters[i];
59+
break;
60+
}
61+
}
62+
63+
if ( parameter == null )
64+
{
65+
parameter = new NameValueHeaderValue( parameterName );
66+
parameters.Add( parameter );
67+
}
68+
69+
parameter.Value = new StringSegment( apiVersion.ToString() );
70+
headers.ContentType = contentType;
71+
return CompletedTask;
72+
}
73+
}
74+
}

test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/MediaTypeNegotiation/given a versioned ApiController/when using media type negotiation.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ public async Task then_get_should_return_200( string controller, string apiVersi
2828

2929
// act
3030
var response = await GetAsync( "api/values" ).EnsureSuccessStatusCode();
31-
var content = await response.Content.ReadAsExampleAsync( example );
31+
var body = response.Content;
32+
var content = await body.ReadAsExampleAsync( example );
3233

3334
// assert
3435
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
36+
body.Headers.ContentType.Parameters.Single( p => p.Name == "v" ).Value.Should().Be( apiVersion );
3537
content.Should().BeEquivalentTo( new { controller, version = apiVersion } );
3638
}
3739

@@ -61,9 +63,11 @@ public async Task then_get_should_allow_an_unspecified_version( string requestUr
6163

6264
// act
6365
var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode();
64-
var content = await response.Content.ReadAsExampleAsync( example );
66+
var body = response.Content;
67+
var content = await body.ReadAsExampleAsync( example );
6568

6669
// assert
70+
body.Headers.ContentType.Parameters.Single( p => p.Name == "v" ).Value.Should().Be( apiVersion );
6771
content.Should().BeEquivalentTo( new { controller, version = apiVersion } );
6872
}
6973

test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/MediaTypeNegotiation/given a versioned Controller/when using media type negotiation.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ public async Task then_get_should_return_200( string controller, string apiVersi
2828

2929
// act
3030
var response = await GetAsync( "api/values" ).EnsureSuccessStatusCode();
31-
var content = await response.Content.ReadAsExampleAsync( example );
31+
var body = response.Content;
32+
var content = await body.ReadAsExampleAsync( example );
3233

3334
// assert
3435
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
36+
body.Headers.ContentType.Parameters.Single( p => p.Name == "v" ).Value.Should().Be( apiVersion );
3537
content.Should().BeEquivalentTo( new { controller, version = apiVersion } );
36-
3738
}
3839

3940
[Fact]
@@ -64,9 +65,11 @@ public async Task then_get_should_return_current_version_for_an_unspecified_vers
6465

6566
// act
6667
var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode();
67-
var content = await response.Content.ReadAsExampleAsync( example );
68+
var body = response.Content;
69+
var content = await body.ReadAsExampleAsync( example );
6870

6971
// assert
72+
body.Headers.ContentType.Parameters.Single( p => p.Name == "v" ).Value.Should().Be( apiVersion );
7073
content.Should().BeEquivalentTo( new { controller, version = apiVersion } );
7174
}
7275

0 commit comments

Comments
 (0)