Skip to content

Commit a8174e7

Browse files
author
Chris Martinez
committed
Added new support for providing custom 400 and 405 responses related to API versioning
1 parent ff4b50d commit a8174e7

File tree

8 files changed

+354
-2
lines changed

8 files changed

+354
-2
lines changed

ApiVersioningWithSamples.sln

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Versioning", "Versioning",
4949
src\Common\Versioning\ConstantApiVersionSelector.cs = src\Common\Versioning\ConstantApiVersionSelector.cs
5050
src\Common\Versioning\CurrentImplementationApiVersionSelector.cs = src\Common\Versioning\CurrentImplementationApiVersionSelector.cs
5151
src\Common\Versioning\DefaultApiVersionSelector.cs = src\Common\Versioning\DefaultApiVersionSelector.cs
52+
src\Common\Versioning\ErrorResponseContext.cs = src\Common\Versioning\ErrorResponseContext.cs
5253
src\Common\Versioning\HeaderApiVersionReader.cs = src\Common\Versioning\HeaderApiVersionReader.cs
5354
src\Common\Versioning\IApiVersionNeutral.cs = src\Common\Versioning\IApiVersionNeutral.cs
5455
src\Common\Versioning\IApiVersionProvider.cs = src\Common\Versioning\IApiVersionProvider.cs
5556
src\Common\Versioning\IApiVersionReader.cs = src\Common\Versioning\IApiVersionReader.cs
5657
src\Common\Versioning\IApiVersionSelector.cs = src\Common\Versioning\IApiVersionSelector.cs
58+
src\Common\Versioning\IErrorResponseProvider.cs = src\Common\Versioning\IErrorResponseProvider.cs
5759
src\Common\Versioning\LowestImplementedApiVersionSelector.cs = src\Common\Versioning\LowestImplementedApiVersionSelector.cs
5860
src\Common\Versioning\QueryStringApiVersionReader.cs = src\Common\Versioning\QueryStringApiVersionReader.cs
59-
src\Common\Versioning\QueryStringOrHeaderApiVersionReader.cs = src\Common\Versioning\QueryStringOrHeaderApiVersionReader.cs
61+
src\Common\Versioning\UrlSegmentApiVersionReader.cs = src\Common\Versioning\UrlSegmentApiVersionReader.cs
6062
EndProjectSection
6163
EndProject
6264
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{915BB224-B1D0-4E27-A348-67FCC77AAA44}"

src/Common/Versioning/ApiVersioningOptions.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public partial class ApiVersioningOptions
2121
private ApiVersion defaultApiVersion = ApiVersion.Default;
2222
private IApiVersionReader apiVersionReader = Combine( new QueryStringApiVersionReader(), new UrlSegmentApiVersionReader() );
2323
private IApiVersionSelector apiVersionSelector;
24+
private IErrorResponseProvider errorResponseProvider = new DefaultErrorResponseProvider();
2425
private ApiVersionConventionBuilder conventions = new ApiVersionConventionBuilder();
2526

2627
/// <summary>
@@ -85,7 +86,7 @@ public ApiVersion DefaultApiVersion
8586
/// service API version specified by a client. The default value is the
8687
/// <see cref="QueryStringApiVersionReader"/>, which only reads the service API version from
8788
/// the "api-version" query string parameter. Replace the default value with an alternate
88-
/// implementation, such as the <see cref="QueryStringOrHeaderApiVersionReader"/>, which
89+
/// implementation, such as the <see cref="HeaderApiVersionReader"/>, which
8990
/// can read the service API version from additional information like HTTP headers.</remarks>
9091
#if !WEBAPI
9192
[CLSCompliant( false )]
@@ -149,5 +150,27 @@ public ApiVersionConventionBuilder Conventions
149150
conventions = value;
150151
}
151152
}
153+
154+
/// <summary>
155+
/// Gets or sets the object used to generate HTTP error responses related to API versioning.
156+
/// </summary>
157+
/// <value>An <see cref="IErrorResponseProvider">error response provider</see> object.
158+
/// The default value is an instance of the <see cref="DefaultErrorResponseProvider"/>.</value>
159+
#if !WEBAPI
160+
[CLSCompliant( false )]
161+
#endif
162+
public IErrorResponseProvider ErrorResponses
163+
{
164+
get
165+
{
166+
Contract.Ensures( errorResponseProvider != null );
167+
return errorResponseProvider;
168+
}
169+
set
170+
{
171+
Arg.NotNull( value, nameof( value ) );
172+
errorResponseProvider = value;
173+
}
174+
}
152175
}
153176
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning
5+
#endif
6+
{
7+
/// <summary>
8+
/// Represents the contextual information used when generating HTTP error responses related to API versioning.
9+
/// </summary>
10+
public partial class ErrorResponseContext
11+
{
12+
/// <summary>
13+
/// Gets the associated error code.
14+
/// </summary>
15+
/// <value>The associated error code.</value>
16+
public string Code { get; }
17+
18+
/// <summary>
19+
/// Gets the associated error message.
20+
/// </summary>
21+
/// <value>The error message.</value>
22+
public string Message { get; }
23+
24+
/// <summary>
25+
/// Gets the detailed error message.
26+
/// </summary>
27+
/// <value>The detailed error message, if any.</value>
28+
public string MessageDetail { get; }
29+
}
30+
}
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+
#if WEBAPI
8+
using IActionResult = System.Net.Http.HttpResponseMessage;
9+
#else
10+
using Http;
11+
#endif
12+
using System;
13+
14+
/// <summary>
15+
/// Defines the behavior of an object that provides HTTP error responses related to API versioning.
16+
/// </summary>
17+
#if !WEBAPI
18+
[CLSCompliant( false )]
19+
#endif
20+
public interface IErrorResponseProvider
21+
{
22+
/// <summary>
23+
/// Creates and returns a new HTTP 400 (Bad Request) given the provided context.
24+
/// </summary>
25+
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
26+
/// <returns>The generated <see cref="IActionResult">response</see>.</returns>
27+
IActionResult BadRequest( ErrorResponseContext context );
28+
29+
/// <summary>
30+
/// Creates and returns a new HTTP 405 (Method Not Allowed) given the provided context.
31+
/// </summary>
32+
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
33+
/// <returns>The generated <see cref="IActionResult">response</see>.</returns>
34+
IActionResult MethodNotAllowed( ErrorResponseContext context );
35+
}
36+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
namespace Microsoft.Web.Http.Versioning
2+
{
3+
using System.Diagnostics.Contracts;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Web.Http;
7+
using static System.String;
8+
9+
/// <summary>
10+
/// Represents the default implementation for creating HTTP error responses related to API versioning.
11+
/// </summary>
12+
public class DefaultErrorResponseProvider : IErrorResponseProvider
13+
{
14+
/// <summary>
15+
/// Creates and returns a new HTTP 400 (Bad Request) given the provided context.
16+
/// </summary>
17+
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
18+
/// <returns>The generated <see cref="HttpResponseMessage">response</see>.</returns>
19+
public virtual HttpResponseMessage BadRequest( ErrorResponseContext context )
20+
{
21+
Arg.NotNull( context, nameof( context ) );
22+
return CreateErrorResponse( context, HttpStatusCode.BadRequest );
23+
}
24+
25+
/// <summary>
26+
/// Creates and returns a new HTTP 405 (Method Not Allowed) given the provided context.
27+
/// </summary>
28+
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
29+
/// <returns>The generated <see cref="HttpResponseMessage">response</see>.</returns>
30+
public virtual HttpResponseMessage MethodNotAllowed( ErrorResponseContext context )
31+
{
32+
Arg.NotNull( context, nameof( context ) );
33+
return CreateErrorResponse( context, HttpStatusCode.MethodNotAllowed );
34+
}
35+
36+
static HttpResponseMessage CreateErrorResponse( ErrorResponseContext context, HttpStatusCode statusCode )
37+
{
38+
Contract.Requires( context != null );
39+
Contract.Ensures( Contract.Result<HttpResponseMessage>() != null );
40+
41+
var error = IsODataRequest( context ) ? CreateODataError( context ) : CreateWebApiError( context );
42+
return context.Request.CreateErrorResponse( statusCode, error );
43+
}
44+
45+
static HttpResponseMessage CreateWebApiBadRequest( ErrorResponseContext context ) =>
46+
context.Request.CreateErrorResponse( HttpStatusCode.BadRequest, CreateWebApiError( context ) );
47+
48+
static HttpResponseMessage CreateODataBadRequest( ErrorResponseContext context ) =>
49+
context.Request.CreateErrorResponse( HttpStatusCode.BadRequest, CreateODataError( context ) );
50+
51+
static bool IsODataRequest( ErrorResponseContext context )
52+
{
53+
Contract.Requires( context != null );
54+
55+
var request = context.Request;
56+
var routeValues = request.GetRouteData();
57+
58+
if ( routeValues == null )
59+
{
60+
return false;
61+
}
62+
63+
if ( !routeValues.Values.ContainsKey( "odataPath" ) )
64+
{
65+
return false;
66+
}
67+
68+
return request.GetConfiguration()?.Formatters.JsonFormatter == null;
69+
}
70+
71+
static HttpError CreateWebApiError( ErrorResponseContext context )
72+
{
73+
Contract.Requires( context != null );
74+
Contract.Ensures( Contract.Result<HttpError>() != null );
75+
76+
var error = new HttpError();
77+
var root = new HttpError() { ["Error"] = error };
78+
79+
if ( !IsNullOrEmpty( context.Code ) )
80+
{
81+
error["Code"] = context.Code;
82+
}
83+
84+
if ( !IsNullOrEmpty( context.Message ) )
85+
{
86+
error.Message = context.Message;
87+
}
88+
89+
if ( !IsNullOrEmpty( context.MessageDetail ) && context.Request.ShouldIncludeErrorDetail() == true )
90+
{
91+
error["InnerError"] = new HttpError( context.MessageDetail );
92+
}
93+
94+
return root;
95+
}
96+
97+
static HttpError CreateODataError( ErrorResponseContext context )
98+
{
99+
Contract.Requires( context != null );
100+
Contract.Ensures( Contract.Result<HttpError>() != null );
101+
102+
var error = new HttpError();
103+
104+
if ( !IsNullOrEmpty( context.Code ) )
105+
{
106+
error[HttpErrorKeys.ErrorCodeKey] = context.Code;
107+
}
108+
109+
if ( !IsNullOrEmpty( context.Message ) )
110+
{
111+
error.Message = context.Message;
112+
}
113+
114+
if ( !IsNullOrEmpty( context.MessageDetail ) && context.Request.ShouldIncludeErrorDetail() == true )
115+
{
116+
error[HttpErrorKeys.MessageDetailKey] = context.MessageDetail;
117+
}
118+
119+
return error;
120+
}
121+
}
122+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Microsoft.Web.Http.Versioning
2+
{
3+
using System;
4+
using System.Net.Http;
5+
6+
/// <content>
7+
/// Provides additional implementation specific to ASP.NET Web API.
8+
/// </content>
9+
public partial class ErrorResponseContext
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="ErrorResponseContext"/> class.
13+
/// </summary>
14+
/// <param name="request">The current <see cref="HttpRequestMessage">HTTP request</see>.</param>
15+
/// <param name="code">The associated error code.</param>
16+
/// <param name="message">The error message.</param>
17+
/// <param name="messageDetail">The detailed error message, if any.</param>
18+
public ErrorResponseContext( HttpRequestMessage request, string code, string message, string messageDetail )
19+
{
20+
Arg.NotNull( request, nameof( request ) );
21+
22+
Request = request;
23+
Code = code;
24+
Message = message;
25+
MessageDetail = messageDetail;
26+
}
27+
28+
/// <summary>
29+
/// Gets the current HTTP request.
30+
/// </summary>
31+
/// <value>The current <see cref="HttpRequestMessage">HTTP request</see>.</value>
32+
public HttpRequestMessage Request { get; }
33+
}
34+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Hosting;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.Contracts;
7+
using static Http.StatusCodes;
8+
using static System.String;
9+
10+
/// <summary>
11+
/// Represents the default implementation for creating HTTP error responses related to API versioning.
12+
/// </summary>
13+
[CLSCompliant( false )]
14+
public class DefaultErrorResponseProvider : IErrorResponseProvider
15+
{
16+
/// <summary>
17+
/// Creates and returns a new HTTP 400 (Bad Request) given the provided context.
18+
/// </summary>
19+
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
20+
/// <returns>The generated <see cref="IActionResult">response</see>.</returns>
21+
public virtual IActionResult BadRequest( ErrorResponseContext context )
22+
{
23+
Arg.NotNull( context, nameof( context ) );
24+
return new BadRequestObjectResult( CreateErrorContent( context ) );
25+
}
26+
27+
/// <summary>
28+
/// Creates and returns a new HTTP 405 (Method Not Allowed) given the provided context.
29+
/// </summary>
30+
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
31+
/// <returns>The generated <see cref="IActionResult">response</see>.</returns>
32+
public virtual IActionResult MethodNotAllowed( ErrorResponseContext context )
33+
{
34+
Arg.NotNull( context, nameof( context ) );
35+
return new ObjectResult( CreateErrorContent( context ) ) { StatusCode = Status405MethodNotAllowed };
36+
}
37+
38+
static object CreateErrorContent( ErrorResponseContext context )
39+
{
40+
Contract.Requires( context != null );
41+
Contract.Ensures( Contract.Result<object>() != null );
42+
43+
var error = new Dictionary<string, object>();
44+
var root = new Dictionary<string, object>() { ["Error"] = error };
45+
46+
if ( !IsNullOrEmpty( context.Code ) )
47+
{
48+
error["Code"] = context.Code;
49+
}
50+
51+
if ( !IsNullOrEmpty( context.Message ) )
52+
{
53+
error["Message"] = context.Message;
54+
}
55+
56+
if ( !IsNullOrEmpty( context.MessageDetail ) )
57+
{
58+
var environment = (IHostingEnvironment) context.Request.HttpContext.RequestServices.GetService( typeof( IHostingEnvironment ) );
59+
60+
if ( environment?.IsDevelopment() == true )
61+
{
62+
error["InnerError"] = new Dictionary<string, object>() { ["Message"] = context.MessageDetail };
63+
}
64+
}
65+
66+
return root;
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)