Skip to content

Commit 7e2521c

Browse files
Add Support for Versioning by Media Type (#97)
Add support for versioning by media type. Resolves #42. Resolves #70.
1 parent ef5d447 commit 7e2521c

File tree

22 files changed

+900
-18
lines changed

22 files changed

+900
-18
lines changed

ApiVersioningWithSamples.sln

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Versioning", "Versioning",
5858
src\Common\Versioning\IApiVersionSelector.cs = src\Common\Versioning\IApiVersionSelector.cs
5959
src\Common\Versioning\IErrorResponseProvider.cs = src\Common\Versioning\IErrorResponseProvider.cs
6060
src\Common\Versioning\LowestImplementedApiVersionSelector.cs = src\Common\Versioning\LowestImplementedApiVersionSelector.cs
61+
src\Common\Versioning\MediaTypeApiVersionReader.cs = src\Common\Versioning\MediaTypeApiVersionReader.cs
6162
src\Common\Versioning\QueryStringApiVersionReader.cs = src\Common\Versioning\QueryStringApiVersionReader.cs
6263
src\Common\Versioning\UrlSegmentApiVersionReader.cs = src\Common\Versioning\UrlSegmentApiVersionReader.cs
6364
EndProjectSection
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning
5+
#endif
6+
{
7+
#if WEBAPI
8+
using Routing;
9+
#else
10+
using Microsoft.AspNetCore.Routing;
11+
using Microsoft.Net.Http.Headers;
12+
using Routing;
13+
#endif
14+
using System;
15+
using System.Collections.Generic;
16+
using System.Diagnostics.Contracts;
17+
using System.Linq;
18+
#if WEBAPI
19+
using System.Net.Http.Headers;
20+
#else
21+
using MediaTypeWithQualityHeaderValue = Microsoft.Net.Http.Headers.MediaTypeHeaderValue;
22+
#endif
23+
24+
25+
/// <summary>
26+
/// Represents a service API version reader that reads the value from a media type HTTP header in the request.
27+
/// </summary>
28+
public partial class MediaTypeApiVersionReader : IApiVersionReader
29+
{
30+
string parameterName = "v";
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="MediaTypeApiVersionReader"/> class.
34+
/// </summary>
35+
public MediaTypeApiVersionReader() { }
36+
37+
/// <summary>
38+
/// Initializes a new instance of the <see cref="MediaTypeApiVersionReader"/> class.
39+
/// </summary>
40+
/// <param name="parameterName">The name of the query string parameter to read the service API version from.</param>
41+
public MediaTypeApiVersionReader( string parameterName )
42+
{
43+
Arg.NotNullOrEmpty( parameterName, nameof( parameterName ) );
44+
this.parameterName = parameterName;
45+
}
46+
47+
/// <summary>
48+
/// Gets or sets the name of the media type parameter to read the service API version from.
49+
/// </summary>
50+
/// <value>The name of the media type parameter to read the service API version from.
51+
/// The default value is "v".</value>
52+
public string ParameterName
53+
{
54+
get
55+
{
56+
Contract.Ensures( !string.IsNullOrEmpty( parameterName ) );
57+
return parameterName;
58+
}
59+
set
60+
{
61+
Arg.NotNullOrEmpty( value, nameof( value ) );
62+
parameterName = value;
63+
}
64+
}
65+
66+
/// <summary>
67+
/// Reads the requested API version from the HTTP Accept header.
68+
/// </summary>
69+
/// <param name="accept">The <see cref="IEnumerable{T}">sequence</see> of Accept
70+
/// <see cref="MediaTypeWithQualityHeaderValue">headers</see> to read from.</param>
71+
/// <returns>The API version read or <c>null</c>.</returns>
72+
/// <remarks>The default implementation will return the first defined API version ranked by the media type
73+
/// quality parameter.</remarks>
74+
protected virtual string ReadAcceptHeader( IEnumerable<MediaTypeWithQualityHeaderValue> accept )
75+
{
76+
Arg.NotNull( accept, nameof( accept ) );
77+
78+
var comparer = StringComparer.OrdinalIgnoreCase;
79+
var contentTypes = from entry in accept
80+
orderby entry.Quality descending
81+
group entry by entry.MediaType;
82+
83+
foreach ( var contentType in contentTypes )
84+
{
85+
foreach ( var entry in contentType )
86+
{
87+
foreach ( var parameter in entry.Parameters )
88+
{
89+
if ( comparer.Equals( parameter.Name, ParameterName ) )
90+
{
91+
return parameter.Value;
92+
}
93+
}
94+
}
95+
}
96+
97+
return null;
98+
}
99+
100+
/// <summary>
101+
/// Reads the requested API version from the HTTP Content-Type header.
102+
/// </summary>
103+
/// <param name="contentType">The Content-Type <see cref="MediaTypeHeaderValue">header</see> to read from.</param>
104+
/// <returns>The API version read or <c>null</c>.</returns>
105+
protected virtual string ReadContentTypeHeader( MediaTypeHeaderValue contentType )
106+
{
107+
Arg.NotNull( contentType, nameof( contentType ) );
108+
109+
var comparer = StringComparer.OrdinalIgnoreCase;
110+
111+
foreach ( var parameter in contentType.Parameters )
112+
{
113+
if ( comparer.Equals( parameter.Name, ParameterName ) )
114+
{
115+
return parameter.Value;
116+
}
117+
}
118+
119+
return null;
120+
}
121+
}
122+
}

src/Common/Versioning/QueryStringApiVersionReader.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public QueryStringApiVersionReader() { }
2727
public QueryStringApiVersionReader( string parameterName )
2828
{
2929
Arg.NotNullOrEmpty( parameterName, nameof( parameterName ) );
30-
ParameterName = parameterName;
30+
this.parameterName = parameterName;
3131
}
3232

3333
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Microsoft.Web.Http.Versioning
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Net.Http;
7+
using System.Net.Http.Headers;
8+
9+
/// <content>
10+
/// Provides the implementation for ASP.NET Web API.
11+
/// </content>
12+
public partial class MediaTypeApiVersionReader
13+
{
14+
/// <summary>
15+
/// Reads the service API version value from a request.
16+
/// </summary>
17+
/// <param name="request">The <see cref="HttpRequestMessage">HTTP request</see> to read the API version from.</param>
18+
/// <returns>The raw, unparsed service API version value read from the request or <c>null</c> if request does not contain an API version.</returns>
19+
/// <exception cref="AmbiguousApiVersionException">Multiple, different API versions were requested.</exception>
20+
public virtual string Read( HttpRequestMessage request )
21+
{
22+
Arg.NotNull( request, nameof( request ) );
23+
24+
var contentType = request.Content?.Headers.ContentType;
25+
26+
if ( contentType != null )
27+
{
28+
return ReadContentTypeHeader( contentType );
29+
}
30+
31+
var accept = request.Headers.Accept;
32+
33+
if ( accept == null || accept.Count == 0 )
34+
{
35+
return null;
36+
}
37+
38+
return ReadAcceptHeader( accept );
39+
}
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Http;
4+
using Net.Http.Headers;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
9+
/// <content>
10+
/// Provides the implementation for ASP.NET Core.
11+
/// </content>
12+
[CLSCompliant( false )]
13+
public partial class MediaTypeApiVersionReader
14+
{
15+
/// <summary>
16+
/// Reads the service API version value from a request.
17+
/// </summary>
18+
/// <param name="request">The <see cref="HttpRequest">HTTP request</see> to read the API version from.</param>
19+
/// <returns>The raw, unparsed service API version value read from the request or <c>null</c> if request does not contain an API version.</returns>
20+
/// <exception cref="AmbiguousApiVersionException">Multiple, different API versions were requested.</exception>
21+
public virtual string Read( HttpRequest request )
22+
{
23+
Arg.NotNull( request, nameof( request ) );
24+
25+
var headers = request.GetTypedHeaders();
26+
var contentType = headers.ContentType;
27+
28+
if ( contentType != null )
29+
{
30+
return ReadContentTypeHeader( contentType );
31+
}
32+
33+
var accept = headers.Accept;
34+
35+
if ( accept == null || accept.Count == 0 )
36+
{
37+
return null;
38+
}
39+
40+
return ReadAcceptHeader( accept );
41+
}
42+
}
43+
}

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

+21-6
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717
[Trait( "Framework", "Web API" )]
1818
public abstract class AcceptanceTest : IDisposable
1919
{
20-
private sealed class FilteredControllerTypeResolver : List<Type>, IHttpControllerTypeResolver
20+
sealed class FilteredControllerTypeResolver : List<Type>, IHttpControllerTypeResolver
2121
{
2222
public ICollection<Type> GetControllerTypes( IAssembliesResolver assembliesResolver ) => this;
2323
}
2424

25-
private const string JsonMediaType = "application/json";
26-
private static readonly HttpMethod Patch = new HttpMethod( "PATCH" );
27-
private readonly FilteredControllerTypeResolver filteredControllerTypes = new FilteredControllerTypeResolver();
28-
private bool disposed;
25+
const string JsonMediaType = "application/json";
26+
static readonly HttpMethod Patch = new HttpMethod( "PATCH" );
27+
readonly FilteredControllerTypeResolver filteredControllerTypes = new FilteredControllerTypeResolver();
28+
bool disposed;
2929

3030
~AcceptanceTest()
3131
{
@@ -80,7 +80,7 @@ public void Dispose()
8080
GC.SuppressFinalize( this );
8181
}
8282

83-
private HttpRequestMessage CreateRequest<TEntity>( string requestUri, TEntity entity, HttpMethod method )
83+
HttpRequestMessage CreateRequest<TEntity>( string requestUri, TEntity entity, HttpMethod method )
8484
{
8585
var request = new HttpRequestMessage( method, requestUri );
8686

@@ -95,6 +95,15 @@ private HttpRequestMessage CreateRequest<TEntity>( string requestUri, TEntity en
9595
return request;
9696
}
9797

98+
HttpRequestMessage CreateRequest( string requestUri, HttpContent content, HttpMethod method )
99+
{
100+
var request = new HttpRequestMessage( method, requestUri ) { Content = content };
101+
102+
Client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue( JsonMediaType ) );
103+
104+
return request;
105+
}
106+
98107
protected void Accept( string metadata = null )
99108
{
100109
var mediaType = new MediaTypeWithQualityHeaderValue( JsonMediaType );
@@ -130,10 +139,16 @@ protected void Accept( string metadata = null )
130139

131140
protected virtual Task<HttpResponseMessage> PostAsync<TEntity>( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Post ) );
132141

142+
protected virtual Task<HttpResponseMessage> PostAsync( string requestUri, HttpContent content ) => Client.SendAsync( CreateRequest( requestUri, content, Post ) );
143+
133144
protected virtual Task<HttpResponseMessage> PutAsync<TEntity>( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Put ) );
134145

146+
protected virtual Task<HttpResponseMessage> PutAsync( string requestUri, HttpContent content ) => Client.SendAsync( CreateRequest( requestUri, content, Put ) );
147+
135148
protected virtual Task<HttpResponseMessage> PatchAsync<TEntity>( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Patch ) );
136149

150+
protected virtual Task<HttpResponseMessage> PatchAsync( string requestUri, HttpContent content ) => Client.SendAsync( CreateRequest( requestUri, content, Patch ) );
151+
137152
protected virtual Task<HttpResponseMessage> DeleteAsync( string requestUri ) => Client.SendAsync( CreateRequest( requestUri, default( object ), Delete ) );
138153
}
139154
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Microsoft.Web.Http.MediaTypeNegotiation.Controllers
2+
{
3+
using Microsoft.Web.Http;
4+
using Models;
5+
using System.Collections.Generic;
6+
using System.Web.Http;
7+
8+
[ApiVersion( "1.0" )]
9+
[RoutePrefix( "api/helloworld" )]
10+
public class HelloWorldController : ApiController
11+
{
12+
[Route]
13+
public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } );
14+
15+
[Route( "{id:int}", Name = "GetMessageById" )]
16+
public IHttpActionResult Get( int id ) => Ok( new { controller = GetType().Name, id = id, version = Request.GetRequestedApiVersion().ToString() } );
17+
18+
[Route]
19+
public IHttpActionResult Post( Message message ) => CreatedAtRoute( "GetMessageById", new { id = 42 }, message );
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Microsoft.Web.Http.MediaTypeNegotiation.Controllers
2+
{
3+
using Microsoft.Web.Http;
4+
using System.Web.Http;
5+
6+
[ApiVersion( "2.0" )]
7+
[Route( "api/values" )]
8+
public class Values2Controller : ApiController
9+
{
10+
public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } );
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Microsoft.Web.Http.MediaTypeNegotiation.Controllers
2+
{
3+
using Microsoft.Web.Http;
4+
using System.Web.Http;
5+
6+
[ApiVersion( "1.0" )]
7+
[Route( "api/values" )]
8+
public class ValuesController : ApiController
9+
{
10+
public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } );
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace Microsoft.Web.Http.MediaTypeNegotiation
2+
{
3+
using Controllers;
4+
using System.Web.Http;
5+
using Versioning;
6+
7+
public abstract class MediaTypeNegotiationAcceptanceTest : AcceptanceTest
8+
{
9+
protected MediaTypeNegotiationAcceptanceTest()
10+
{
11+
FilteredControllerTypes.Add( typeof( ValuesController ) );
12+
FilteredControllerTypes.Add( typeof( Values2Controller ) );
13+
FilteredControllerTypes.Add( typeof( HelloWorldController ) );
14+
Configuration.AddApiVersioning(
15+
options =>
16+
{
17+
options.ApiVersionReader = new MediaTypeApiVersionReader();
18+
options.AssumeDefaultVersionWhenUnspecified = true;
19+
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector( options );
20+
options.ReportApiVersions = true;
21+
} );
22+
Configuration.MapHttpAttributeRoutes();
23+
Configuration.EnsureInitialized();
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Microsoft.Web.Http.MediaTypeNegotiation.Models
2+
{
3+
using System;
4+
5+
public class Message
6+
{
7+
public string Text { get; set; }
8+
}
9+
}

0 commit comments

Comments
 (0)