Skip to content

Commit 722657d

Browse files
author
Chris Martinez
committed
Add support for reporting API versions when route selection doesn't match the requested version. Resolved dotnet#187
1 parent 38bd06e commit 722657d

File tree

26 files changed

+368
-142
lines changed

26 files changed

+368
-142
lines changed

src/Common/Common.projitems

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IActionConventionBuilderT.cs" />
4949
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IApiVersionConventionT.cs" />
5050
<Compile Include="$(MSBuildThisFileDirectory)Versioning\CurrentImplementationApiVersionSelector.cs" />
51+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\DefaultApiVersionReporter.cs" />
5152
<Compile Include="$(MSBuildThisFileDirectory)Versioning\DefaultApiVersionSelector.cs" />
53+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\DoNotReportApiVersions.cs" />
5254
<Compile Include="$(MSBuildThisFileDirectory)Versioning\ErrorCodes.cs" />
5355
<Compile Include="$(MSBuildThisFileDirectory)Versioning\ErrorResponseContext.cs" />
5456
<Compile Include="$(MSBuildThisFileDirectory)Versioning\HeaderApiVersionReader.cs" />
@@ -59,6 +61,7 @@
5961
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionReader.cs" />
6062
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionSelector.cs" />
6163
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IErrorResponseProvider.cs" />
64+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IReportApiVersions.cs" />
6265
<Compile Include="$(MSBuildThisFileDirectory)Versioning\LowestImplementedApiVersionSelector.cs" />
6366
<Compile Include="$(MSBuildThisFileDirectory)Versioning\MediaTypeApiVersionReader.cs" />
6467
<Compile Include="$(MSBuildThisFileDirectory)Versioning\QueryStringApiVersionReader.cs" />

src/Common/ReportApiVersionsAttribute.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,5 @@ namespace Microsoft.AspNetCore.Mvc
2020
[AttributeUsage( Class | Method, Inherited = true, AllowMultiple = false )]
2121
public sealed partial class ReportApiVersionsAttribute : ActionFilterAttribute
2222
{
23-
const string ApiSupportedVersions = "api-supported-versions";
24-
const string ApiDeprecatedVersions = "api-deprecated-versions";
25-
const string ValueSeparator = ", ";
2623
}
2724
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 System.Collections.Generic;
9+
using System.Diagnostics.Contracts;
10+
using System.Linq;
11+
using static System.String;
12+
#if WEBAPI
13+
using System.Net.Http.Headers;
14+
#else
15+
using HttpResponseHeaders = Microsoft.AspNetCore.Http.IHeaderDictionary;
16+
#endif
17+
18+
sealed partial class DefaultApiVersionReporter : IReportApiVersions
19+
{
20+
const string ApiSupportedVersions = "api-supported-versions";
21+
const string ApiDeprecatedVersions = "api-deprecated-versions";
22+
const string ValueSeparator = ", ";
23+
24+
public void Report( HttpResponseHeaders headers, Lazy<ApiVersionModel> apiVersionModel ) =>
25+
Report( headers, apiVersionModel?.Value );
26+
27+
public void Report( HttpResponseHeaders headers, ApiVersionModel apiVersionModel )
28+
{
29+
Arg.NotNull( headers, nameof( headers ) );
30+
Arg.NotNull( apiVersionModel, nameof( apiVersionModel ) );
31+
32+
AddApiVersionHeader( headers, ApiSupportedVersions, apiVersionModel.SupportedApiVersions );
33+
AddApiVersionHeader( headers, ApiDeprecatedVersions, apiVersionModel.DeprecatedApiVersions );
34+
}
35+
36+
static void AddApiVersionHeader( HttpResponseHeaders headers, string headerName, IReadOnlyList<ApiVersion> versions )
37+
{
38+
Contract.Requires( headers != null );
39+
Contract.Requires( !IsNullOrEmpty( headerName ) );
40+
Contract.Requires( versions != null );
41+
42+
if ( versions.Count > 0 &&
43+
#if WEBAPI
44+
!headers.Contains( headerName ) )
45+
#else
46+
!headers.ContainsKey( headerName ) )
47+
#endif
48+
{
49+
headers.Add( headerName, Join( ValueSeparator, versions.Select( v => v.ToString() ).ToArray() ) );
50+
}
51+
}
52+
}
53+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning
5+
#endif
6+
{
7+
using System;
8+
#if WEBAPI
9+
using System.Net.Http.Headers;
10+
#else
11+
using HttpResponseHeaders = Microsoft.AspNetCore.Http.IHeaderDictionary;
12+
#endif
13+
14+
sealed partial class DoNotReportApiVersions : IReportApiVersions
15+
{
16+
public void Report( HttpResponseHeaders headers, ApiVersionModel apiVersionModel ) { }
17+
18+
public void Report( HttpResponseHeaders headers, Lazy<ApiVersionModel> apiVersionModel ) { }
19+
}
20+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning
5+
#endif
6+
{
7+
using System;
8+
#if WEBAPI
9+
using System.Net.Http.Headers;
10+
#else
11+
using HttpResponseHeaders = Microsoft.AspNetCore.Http.IHeaderDictionary;
12+
#endif
13+
14+
/// <summary>
15+
/// Defines the behavior of an object that reports API versions as HTTP headers.
16+
/// </summary>
17+
#if !WEBAPI
18+
[CLSCompliant( false )]
19+
#endif
20+
public interface IReportApiVersions
21+
{
22+
/// <summary>
23+
/// Reports the API versions defined in the specified models using the the provided collection of HTTP headers.
24+
/// </summary>
25+
/// <param name="headers">The collection of <see cref="HttpResponseHeaders">HTTP response headers</see> used to report API versions.</param>
26+
/// <param name="apiVersionModel">The <see cref="ApiVersionModel">model</see> containing the API versions to report.</param>
27+
void Report( HttpResponseHeaders headers, ApiVersionModel apiVersionModel );
28+
29+
/// <summary>
30+
/// Reports the API versions defined in the specified models using the the provided collection of HTTP headers.
31+
/// </summary>
32+
/// <param name="headers">The collection of <see cref="HttpResponseHeaders">HTTP response headers</see> used to report API versions.</param>
33+
/// <param name="apiVersionModel">The load on-demand <see cref="ApiVersionModel">model</see> containing the API versions to report.</param>
34+
void Report( HttpResponseHeaders headers, Lazy<ApiVersionModel> apiVersionModel );
35+
}
36+
}

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Microsoft.Web.Http.Controllers
22
{
33
using Dispatcher;
4+
using Microsoft.Web.Http.Versioning;
45
using Routing;
56
using System;
67
using System.Collections.Generic;
@@ -211,10 +212,10 @@ HttpResponseMessage CreateSelectionError( HttpControllerContext controllerContex
211212
}
212213

213214
var request = controllerContext.Request;
214-
var versionNeutral = controllerContext.ControllerDescriptor.GetApiVersionModel().IsApiVersionNeutral;
215-
var exceptionFactory = new HttpResponseExceptionFactory( request );
215+
var model = controllerContext.ControllerDescriptor.GetApiVersionModel();
216+
var exceptionFactory = new HttpResponseExceptionFactory( request, new Lazy<ApiVersionModel>( () => model ) );
216217

217-
return exceptionFactory.CreateMethodNotAllowedResponse( versionNeutral, allowedMethods );
218+
return exceptionFactory.CreateMethodNotAllowedResponse( model.IsApiVersionNeutral, allowedMethods );
218219
}
219220

220221
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing of response instance." )]
@@ -224,7 +225,8 @@ static HttpResponseMessage CreateBadRequestResponse( HttpControllerContext contr
224225
Contract.Ensures( Contract.Result<HttpResponseMessage>() != null );
225226

226227
var request = controllerContext.Request;
227-
var exceptionFactory = new HttpResponseExceptionFactory( request );
228+
var model = new Lazy<ApiVersionModel>( controllerContext.ControllerDescriptor.GetApiVersionModel );
229+
var exceptionFactory = new HttpResponseExceptionFactory( request, model );
228230
return exceptionFactory.CreateBadRequestResponse( request.GetRequestedApiVersion() );
229231
}
230232

@@ -302,10 +304,10 @@ CandidateAction[] GetInitialCandidateList( HttpControllerContext controllerConte
302304
if ( actionsFoundByName.Length == 0 )
303305
{
304306
var request = controllerContext.Request;
305-
var versionNeutral = controllerContext.ControllerDescriptor.GetApiVersionModel().IsApiVersionNeutral;
306-
var exceptionFactory = new HttpResponseExceptionFactory( request );
307+
var model = controllerContext.ControllerDescriptor.GetApiVersionModel();
308+
var exceptionFactory = new HttpResponseExceptionFactory( request, new Lazy<ApiVersionModel>( () => model ) );
307309

308-
throw exceptionFactory.NewMethodNotAllowedException( versionNeutral, allowedMethods );
310+
throw exceptionFactory.NewMethodNotAllowedException( model.IsApiVersionNeutral, allowedMethods );
309311
}
310312

311313
var candidatesFoundByName = new CandidateAction[actionsFoundByName.Length];

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Microsoft.Web.Http.Controllers
22
{
33
using Dispatcher;
4+
using Microsoft.Web.Http.Versioning;
45
using System;
56
using System.Collections.Generic;
67
using System.Diagnostics.CodeAnalysis;
@@ -70,7 +71,8 @@ protected virtual HttpActionDescriptor SelectActionVersion( HttpControllerContex
7071

7172
var request = controllerContext.Request;
7273
var requestedVersion = request.GetRequestedApiVersion();
73-
var exceptionFactory = new HttpResponseExceptionFactory( request );
74+
var model = new Lazy<ApiVersionModel>( controllerContext.ControllerDescriptor.GetApiVersionModel );
75+
var exceptionFactory = new HttpResponseExceptionFactory( request, model );
7476

7577
if ( candidateActions.Count == 1 )
7678
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public virtual HttpControllerDescriptor SelectController( HttpRequestMessage req
7474
var aggregator = new ApiVersionControllerAggregator( request, GetControllerName, controllerInfoCache );
7575
var conventionRouteSelector = new ConventionRouteControllerSelector( options, controllerTypeCache );
7676
var conventionRouteResult = default( ControllerSelectionResult );
77-
var exceptionFactory = new HttpResponseExceptionFactory( request );
77+
var exceptionFactory = new HttpResponseExceptionFactory( request, new Lazy<ApiVersionModel>( () => aggregator.AllVersions ) );
7878

7979
if ( aggregator.RouteData == null )
8080
{

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Microsoft.Web.Http.Dispatcher
22
{
3+
using System;
34
using System.Collections.Generic;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Diagnostics.Contracts;
@@ -19,25 +20,52 @@ sealed class HttpResponseExceptionFactory
1920
const string Allow = nameof( Allow );
2021
static readonly string ControllerSelectorCategory = typeof( IHttpControllerSelector ).FullName;
2122
readonly HttpRequestMessage request;
23+
readonly Lazy<ApiVersionModel> allApiVersions;
2224

23-
internal HttpResponseExceptionFactory( HttpRequestMessage request )
25+
internal HttpResponseExceptionFactory( HttpRequestMessage request, Lazy<ApiVersionModel> allApiVersions )
2426
{
2527
Contract.Requires( request != null );
28+
Contract.Requires( allApiVersions != null );
29+
2630
this.request = request;
31+
this.allApiVersions = allApiVersions;
2732
}
2833

34+
ApiVersioningOptions Options => request.GetApiVersioningOptions();
35+
2936
ITraceWriter TraceWriter => request.GetConfiguration().Services.GetTraceWriter() ?? NullTraceWriter.Instance;
3037

31-
ApiVersioningOptions Options => request.GetApiVersioningOptions();
38+
IReportApiVersions ApiVersionReporter
39+
{
40+
get
41+
{
42+
var dependencyResolver = request.GetConfiguration().DependencyResolver;
43+
var reporter = ( (IReportApiVersions) dependencyResolver.GetService( typeof( IReportApiVersions ) ) );
44+
45+
if ( reporter == null )
46+
{
47+
reporter = Options.ReportApiVersions ? DefaultApiVersionReporter.Instance : DoNotReportApiVersions.Instance;
48+
}
49+
50+
return reporter;
51+
}
52+
}
3253

3354
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
3455
internal HttpResponseException NewNotFoundOrBadRequestException( ControllerSelectionResult conventionRouteResult, ControllerSelectionResult directRouteResult ) =>
3556
CreateBadRequest( conventionRouteResult, directRouteResult ) ?? CreateNotFound( conventionRouteResult );
3657

3758
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
38-
internal HttpResponseMessage CreateBadRequestResponse( ApiVersion requestedVersion ) => requestedVersion == null ?
39-
CreateBadRequestForUnspecifiedApiVersionOrInvalidApiVersion( versionNeutral: false ) :
40-
CreateBadRequestForUnsupportedApiVersion( requestedVersion );
59+
internal HttpResponseMessage CreateBadRequestResponse( ApiVersion requestedVersion )
60+
{
61+
var response = requestedVersion == null ?
62+
CreateBadRequestForUnspecifiedApiVersionOrInvalidApiVersion( versionNeutral: false ) :
63+
CreateBadRequestForUnsupportedApiVersion( requestedVersion );
64+
65+
ApiVersionReporter.Report( response.Headers, allApiVersions );
66+
67+
return response;
68+
}
4169

4270
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
4371
internal HttpResponseException CreateBadRequest( ApiVersion requestedVersion ) => new HttpResponseException( CreateBadRequestResponse( requestedVersion ) );
@@ -118,6 +146,7 @@ internal HttpResponseMessage CreateMethodNotAllowedResponse( bool versionNeutral
118146

119147
if ( response != null )
120148
{
149+
ApiVersionReporter.Report( response.Headers, allApiVersions );
121150
return response;
122151
}
123152

@@ -153,6 +182,8 @@ internal HttpResponseMessage CreateMethodNotAllowedResponse( bool versionNeutral
153182
headers.Allow.AddRange( allowedMethods.Select( m => m.Method ) );
154183
}
155184

185+
ApiVersionReporter.Report( response.Headers, allApiVersions );
186+
156187
return response;
157188
}
158189

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

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
namespace Microsoft.Web.Http
22
{
3-
using System.Collections.Generic;
3+
using Microsoft.Web.Http.Versioning;
44
using System.Diagnostics.CodeAnalysis;
5-
using System.Diagnostics.Contracts;
6-
using System.Linq;
7-
using System.Net.Http.Headers;
85
using System.Web.Http;
96
using System.Web.Http.Filters;
10-
using static System.String;
117

128
/// <content>
139
/// Provides the implementation for ASP.NET Web API.
@@ -33,26 +29,12 @@ public override void OnActionExecuted( HttpActionExecutedContext actionExecutedC
3329
var controller = actionExecutedContext.ActionContext.ActionDescriptor.ControllerDescriptor;
3430
var model = controller.GetApiVersionModel();
3531

36-
if ( model.IsApiVersionNeutral )
32+
if ( model?.IsApiVersionNeutral == false )
3733
{
38-
return;
39-
}
40-
41-
var headers = response.Headers;
42-
43-
AddApiVersionHeader( headers, ApiSupportedVersions, model.SupportedApiVersions );
44-
AddApiVersionHeader( headers, ApiDeprecatedVersions, model.DeprecatedApiVersions );
45-
}
46-
47-
static void AddApiVersionHeader( HttpHeaders headers, string headerName, IReadOnlyList<ApiVersion> versions )
48-
{
49-
Contract.Requires( headers != null );
50-
Contract.Requires( !IsNullOrEmpty( headerName ) );
51-
Contract.Requires( versions != null );
34+
var dependencyResolver = actionExecutedContext.ActionContext.ControllerContext.Configuration.DependencyResolver;
35+
var reporter = ( (IReportApiVersions) dependencyResolver.GetService( typeof( IReportApiVersions ) ) ) ?? DefaultApiVersionReporter.Instance;
5236

53-
if ( versions.Count > 0 && !headers.Contains( headerName ) )
54-
{
55-
headers.Add( headerName, Join( ValueSeparator, versions.Select( v => v.ToString() ) ) );
37+
reporter.Report( response.Headers, model );
5638
}
5739
}
5840
}

0 commit comments

Comments
 (0)