Skip to content

Commit 0e41ace

Browse files
Simplify Error Response Provider (#111)
Simplifies API versioning error responses
1 parent 97f8071 commit 0e41ace

15 files changed

+138
-89
lines changed

src/Common/Common.projitems

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IApiVersionConventionT.cs" />
4040
<Compile Include="$(MSBuildThisFileDirectory)Versioning\CurrentImplementationApiVersionSelector.cs" />
4141
<Compile Include="$(MSBuildThisFileDirectory)Versioning\DefaultApiVersionSelector.cs" />
42+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\ErrorCodes.cs" />
4243
<Compile Include="$(MSBuildThisFileDirectory)Versioning\ErrorResponseContext.cs" />
4344
<Compile Include="$(MSBuildThisFileDirectory)Versioning\HeaderApiVersionReader.cs" />
4445
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionNeutral.cs" />

src/Common/Versioning/ErrorCodes.cs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning
5+
#endif
6+
{
7+
using System;
8+
9+
/// <summary>
10+
/// Defines the standard error codes returned in responses related to API versioning.
11+
/// </summary>
12+
public static class ErrorCodes
13+
{
14+
/// <summary>
15+
/// Indicates that the API version requested by the client is not supported.
16+
/// </summary>
17+
public const string UnsupportedApiVersion = nameof( UnsupportedApiVersion );
18+
19+
/// <summary>
20+
/// Indicates that an API version is required, but was not specified by the client.
21+
/// </summary>
22+
public const string ApiVersionUnspecified = nameof( ApiVersionUnspecified );
23+
24+
/// <summary>
25+
/// Indicates that API version requested by the client is invalid or malformed.
26+
/// </summary>
27+
public const string InvalidApiVersion = nameof( InvalidApiVersion );
28+
29+
/// <summary>
30+
/// Indicates that the client specified an API version multiple times and with different values.
31+
/// </summary>
32+
public const string AmbiguousApiVersion = nameof( AmbiguousApiVersion );
33+
}
34+
}

src/Common/Versioning/ErrorResponseContext.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,29 @@ namespace Microsoft.Web.Http.Versioning
44
namespace Microsoft.AspNetCore.Mvc.Versioning
55
#endif
66
{
7+
#if WEBAPI
8+
using System.Net;
9+
#endif
10+
711
/// <summary>
812
/// Represents the contextual information used when generating HTTP error responses related to API versioning.
913
/// </summary>
1014
public partial class ErrorResponseContext
1115
{
16+
/// <summary>
17+
/// Gets the associated HTTP status code.
18+
/// </summary>
19+
/// <value>The associated HTTP status code.</value>
20+
#if WEBAPI
21+
public HttpStatusCode StatusCode { get; }
22+
#else
23+
public int StatusCode { get; }
24+
#endif
1225
/// <summary>
1326
/// Gets the associated error code.
1427
/// </summary>
1528
/// <value>The associated error code.</value>
16-
public string Code { get; }
29+
public string ErrorCode { get; }
1730

1831
/// <summary>
1932
/// Gets the associated error message.

src/Common/Versioning/IErrorResponseProvider.cs

+2-9
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,10 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
2020
public interface IErrorResponseProvider
2121
{
2222
/// <summary>
23-
/// Creates and returns a new HTTP 400 (Bad Request) given the provided context.
23+
/// Creates and returns a new error response given the provided context.
2424
/// </summary>
2525
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
2626
/// <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 );
27+
IActionResult CreateResponse( ErrorResponseContext context );
3528
}
3629
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Versioning;
1515
using static Controllers.HttpControllerDescriptorComparer;
1616
using static System.StringComparer;
17+
using static Versioning.ErrorCodes;
1718

1819
/// <summary>
1920
/// Represents the logic for selecting a versioned controller.
@@ -164,8 +165,7 @@ static void EnsureRequestHasValidApiVersion( HttpRequestMessage request )
164165
catch ( AmbiguousApiVersionException ex )
165166
{
166167
var options = request.GetApiVersioningOptions();
167-
var context = new ErrorResponseContext( request, "AmbiguousApiVersion", ex.Message, messageDetail: null );
168-
throw new HttpResponseException( options.ErrorResponses.BadRequest( context ) );
168+
throw new HttpResponseException( options.ErrorResponses.BadRequest( request, AmbiguousApiVersion, ex.Message ) );
169169
}
170170
}
171171
}

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

+5-11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using static ApiVersion;
1313
using static System.Net.HttpStatusCode;
1414
using static System.String;
15+
using static Versioning.ErrorCodes;
1516

1617
sealed class HttpResponseExceptionFactory
1718
{
@@ -69,7 +70,6 @@ HttpResponseMessage CreateBadRequestForUnspecifiedApiVersionOrInvalidApiVersion(
6970
{
7071
var requestedVersion = request.ApiVersionProperties().RawApiVersion;
7172
var message = default( string );
72-
var context = default( ErrorResponseContext );
7373

7474
if ( IsNullOrEmpty( requestedVersion ) )
7575
{
@@ -80,8 +80,7 @@ HttpResponseMessage CreateBadRequestForUnspecifiedApiVersionOrInvalidApiVersion(
8080

8181
message = SR.ApiVersionUnspecified;
8282
TraceWriter.Info( request, ControllerSelectorCategory, message );
83-
context = new ErrorResponseContext( request, "ApiVersionUnspecified", message, messageDetail: null );
84-
return Options.ErrorResponses.BadRequest( context );
83+
return Options.ErrorResponses.BadRequest( request, ApiVersionUnspecified, message );
8584
}
8685
else if ( TryParse( requestedVersion, out var parsedVersion ) )
8786
{
@@ -90,11 +89,10 @@ HttpResponseMessage CreateBadRequestForUnspecifiedApiVersionOrInvalidApiVersion(
9089

9190
message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
9291
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );
93-
context = new ErrorResponseContext( request, "InvalidApiVersion", message, messageDetail );
9492

9593
TraceWriter.Info( request, ControllerSelectorCategory, message );
9694

97-
return Options.ErrorResponses.BadRequest( context );
95+
return Options.ErrorResponses.BadRequest( request, InvalidApiVersion, message, messageDetail );
9896
}
9997

10098
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
@@ -105,11 +103,10 @@ HttpResponseMessage CreateBadRequestForUnsupportedApiVersion( ApiVersion request
105103

106104
var message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
107105
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );
108-
var context = new ErrorResponseContext( request, "UnsupportedApiVersion", message, messageDetail );
109106

110107
TraceWriter.Info( request, ControllerSelectorCategory, message );
111108

112-
return Options.ErrorResponses.BadRequest( context );
109+
return Options.ErrorResponses.BadRequest( request, UnsupportedApiVersion, message, messageDetail );
113110
}
114111

115112
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
@@ -141,10 +138,7 @@ internal HttpResponseMessage CreateMethodNotAllowedResponse( bool versionNeutral
141138
}
142139

143140
TraceWriter.Info( request, ControllerSelectorCategory, message );
144-
145-
var context = new ErrorResponseContext( request, "UnsupportedApiVersion", message, messageDetail );
146-
147-
response = Options.ErrorResponses.MethodNotAllowed( context );
141+
response = Options.ErrorResponses.MethodNotAllowed( request, UnsupportedApiVersion, message, messageDetail );
148142

149143
if ( response.Content == null )
150144
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Microsoft.Web.Http
2+
{
3+
using Microsoft.Web.Http.Versioning;
4+
using System.Net;
5+
using System.Net.Http;
6+
7+
static class IErrorResponseProviderExtensions
8+
{
9+
internal static HttpResponseMessage BadRequest( this IErrorResponseProvider responseProvider, HttpRequestMessage request, string code, string message, string messageDetail = null ) =>
10+
responseProvider.CreateResponse( new ErrorResponseContext( request, HttpStatusCode.BadRequest, code, message, messageDetail ) );
11+
12+
internal static HttpResponseMessage MethodNotAllowed( this IErrorResponseProvider responseProvider, HttpRequestMessage request, string code, string message, string messageDetail = null ) =>
13+
responseProvider.CreateResponse( new ErrorResponseContext( request, HttpStatusCode.MethodNotAllowed, code, message, messageDetail ) );
14+
}
15+
}

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

+13-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
namespace Microsoft.Web.Http.Versioning
22
{
33
using System.Diagnostics.Contracts;
4-
using System.Net;
54
using System.Net.Http;
65
using System.Web.Http;
76
using static System.String;
@@ -12,42 +11,29 @@
1211
public class DefaultErrorResponseProvider : IErrorResponseProvider
1312
{
1413
/// <summary>
15-
/// Creates and returns a new HTTP 400 (Bad Request) given the provided context.
14+
/// Creates and returns a new error response given the provided context.
1615
/// </summary>
1716
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to generate response.</param>
1817
/// <returns>The generated <see cref="HttpResponseMessage">response</see>.</returns>
19-
public virtual HttpResponseMessage BadRequest( ErrorResponseContext context )
18+
public virtual HttpResponseMessage CreateResponse( ErrorResponseContext context )
2019
{
2120
Arg.NotNull( context, nameof( context ) );
22-
return CreateErrorResponse( context, HttpStatusCode.BadRequest );
21+
return context.Request.CreateErrorResponse( context.StatusCode, CreateErrorContent( context ) );
2322
}
2423

2524
/// <summary>
26-
/// Creates and returns a new HTTP 405 (Method Not Allowed) given the provided context.
25+
/// Creates the default error content using the given context.
2726
/// </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 )
27+
/// <param name="context">The <see cref="ErrorResponseContext">error context</see> used to create the error content.</param>
28+
/// <returns>A <see cref="HttpError">HTTP error</see> representing the error content.</returns>
29+
protected virtual HttpError CreateErrorContent( ErrorResponseContext context )
3130
{
3231
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 );
32+
Contract.Ensures( Contract.Result<HttpError>() != null );
4033

41-
var error = IsODataRequest( context ) ? CreateODataError( context ) : CreateWebApiError( context );
42-
return context.Request.CreateErrorResponse( statusCode, error );
34+
return IsODataRequest( context ) ? CreateODataError( context ) : CreateWebApiError( context );
4335
}
4436

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-
5137
static bool IsODataRequest( ErrorResponseContext context )
5238
{
5339
Contract.Requires( context != null );
@@ -76,9 +62,9 @@ static HttpError CreateWebApiError( ErrorResponseContext context )
7662
var error = new HttpError();
7763
var root = new HttpError() { ["Error"] = error };
7864

79-
if ( !IsNullOrEmpty( context.Code ) )
65+
if ( !IsNullOrEmpty( context.ErrorCode ) )
8066
{
81-
error["Code"] = context.Code;
67+
error["Code"] = context.ErrorCode;
8268
}
8369

8470
if ( !IsNullOrEmpty( context.Message ) )
@@ -101,9 +87,9 @@ static HttpError CreateODataError( ErrorResponseContext context )
10187

10288
var error = new HttpError();
10389

104-
if ( !IsNullOrEmpty( context.Code ) )
90+
if ( !IsNullOrEmpty( context.ErrorCode ) )
10591
{
106-
error[HttpErrorKeys.ErrorCodeKey] = context.Code;
92+
error[HttpErrorKeys.ErrorCodeKey] = context.ErrorCode;
10793
}
10894

10995
if ( !IsNullOrEmpty( context.Message ) )

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Microsoft.Web.Http.Versioning
22
{
33
using System;
4+
using System.Net;
45
using System.Net.Http;
56

67
/// <content>
@@ -12,15 +13,17 @@ public partial class ErrorResponseContext
1213
/// Initializes a new instance of the <see cref="ErrorResponseContext"/> class.
1314
/// </summary>
1415
/// <param name="request">The current <see cref="HttpRequestMessage">HTTP request</see>.</param>
15-
/// <param name="code">The associated error code.</param>
16+
/// <param name="statusCode">The associated <see cref="HttpStatusCode">HTTP status code</see>.</param>
17+
/// <param name="errorCode">The associated error code.</param>
1618
/// <param name="message">The error message.</param>
1719
/// <param name="messageDetail">The detailed error message, if any.</param>
18-
public ErrorResponseContext( HttpRequestMessage request, string code, string message, string messageDetail )
20+
public ErrorResponseContext( HttpRequestMessage request, HttpStatusCode statusCode, string errorCode, string message, string messageDetail )
1921
{
2022
Arg.NotNull( request, nameof( request ) );
2123

2224
Request = request;
23-
Code = code;
25+
StatusCode = statusCode;
26+
ErrorCode = errorCode;
2427
Message = message;
2528
MessageDetail = messageDetail;
2629
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Microsoft.AspNetCore.Mvc
2+
{
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc.Versioning;
5+
using System;
6+
using static Microsoft.AspNetCore.Http.StatusCodes;
7+
8+
static class IErrorResponseProviderExtensions
9+
{
10+
internal static IActionResult BadRequest( this IErrorResponseProvider responseProvider, HttpContext context, string code, string message, string messageDetail = null ) =>
11+
responseProvider.CreateResponse( new ErrorResponseContext( context.Request, Status400BadRequest, code, message, messageDetail ) );
12+
13+
internal static IActionResult MethodNotAllowed( this IErrorResponseProvider responseProvider, HttpContext context, string code, string message, string messageDetail = null ) =>
14+
responseProvider.CreateResponse( new ErrorResponseContext( context.Request, Status405MethodNotAllowed, code, message, messageDetail ) );
15+
}
16+
}

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using static ApiVersion;
1717
using static System.Environment;
1818
using static System.String;
19+
using static ErrorCodes;
1920

2021
/// <summary>
2122
/// Represents the logic for selecting an API-versioned, action method.
@@ -192,7 +193,7 @@ RequestHandler VerifyRequestedApiVersionIsNotAmbiguous( HttpContext httpContext,
192193
{
193194
logger.LogInformation( ex.Message );
194195
apiVersion = default( ApiVersion );
195-
return new BadRequestHandler( Options, "AmbiguousApiVersion", ex.Message );
196+
return new BadRequestHandler( Options, AmbiguousApiVersion, ex.Message );
196197
}
197198

198199
return null;
@@ -224,13 +225,13 @@ RequestHandler IsValidRequest( ActionSelectionContext context, IReadOnlyList<Act
224225

225226
if ( IsNullOrEmpty( requestedVersion ) )
226227
{
227-
code = "ApiVersionUnspecified";
228+
code = ApiVersionUnspecified;
228229
logger.ApiVersionUnspecified( actionNames.Value );
229230
return new BadRequestHandler( Options, code, SR.ApiVersionUnspecified );
230231
}
231232
else if ( TryParse( requestedVersion, out parsedVersion ) )
232233
{
233-
code = "UnsupportedApiVersion";
234+
code = UnsupportedApiVersion;
234235
logger.ApiVersionUnmatched( parsedVersion, actionNames.Value );
235236

236237
if ( allowedMethods.Value.Contains( context.HttpContext.Request.Method ) )
@@ -244,15 +245,15 @@ RequestHandler IsValidRequest( ActionSelectionContext context, IReadOnlyList<Act
244245
}
245246
else
246247
{
247-
code = "InvalidApiVersion";
248+
code = InvalidApiVersion;
248249
logger.ApiVersionInvalid( requestedVersion );
249250
newRequestHandler = ( o, c, m ) => new BadRequestHandler( o, c, m );
250251
}
251252
}
252253
else
253254
{
254255
requestedVersion = parsedVersion.ToString();
255-
code = "UnsupportedApiVersion";
256+
code = UnsupportedApiVersion;
256257
logger.ApiVersionUnmatched( parsedVersion, actionNames.Value );
257258

258259
if ( allowedMethods.Value.Contains( context.HttpContext.Request.Method ) )

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ sealed class BadRequestHandler : RequestHandler
77
internal BadRequestHandler( ApiVersioningOptions options, string code, string message )
88
: base( options, code, message ) { }
99

10-
protected override IActionResult CreateResult( HttpContext context )
11-
{
12-
var errorContext = new ErrorResponseContext( context.Request, Code, Message, messageDetail: null );
13-
return Options.ErrorResponses.BadRequest( errorContext );
14-
}
10+
protected override IActionResult CreateResult( HttpContext context ) =>
11+
Options.ErrorResponses.BadRequest( context, Code, Message );
1512
}
1613
}

0 commit comments

Comments
 (0)