Add helper methods on ControllerBase to return ProblemDetails (#12298)
* Add helper methods on ControllerBase to return ProblemDetails * Introduce ControllerBase.Problem and ValidationProblem overload that accepts optional parameters * Consistently use ClientErrorData when generating ProblemDetails * Clean-up InvalidModelStateResponseFactory initialization. Fixes https://github.com/aspnet/AspNetCore/issues/8537
This commit is contained in:
parent
2ff6a5c0f8
commit
709b390157
|
|
@ -271,6 +271,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinderFactory ModelBinderFactory { get { throw null; } set { } }
|
||||
public Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary ModelState { get { throw null; } }
|
||||
public Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IObjectModelValidator ObjectValidator { get { throw null; } set { } }
|
||||
public Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory ProblemDetailsFactory { get { throw null; } set { } }
|
||||
public Microsoft.AspNetCore.Http.HttpRequest Request { get { throw null; } }
|
||||
public Microsoft.AspNetCore.Http.HttpResponse Response { get { throw null; } }
|
||||
public Microsoft.AspNetCore.Routing.RouteData RouteData { get { throw null; } }
|
||||
|
|
@ -445,6 +446,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||
public virtual Microsoft.AspNetCore.Mvc.PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTag, bool enableRangeProcessing) { throw null; }
|
||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||
public virtual Microsoft.AspNetCore.Mvc.ObjectResult Problem(string detail = null, string instance = null, int? statusCode = default(int?), string title = null, string type = null) { throw null; }
|
||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||
public virtual Microsoft.AspNetCore.Mvc.RedirectResult Redirect(string url) { throw null; }
|
||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||
public virtual Microsoft.AspNetCore.Mvc.RedirectResult RedirectPermanent(string url) { throw null; }
|
||||
|
|
@ -586,6 +589,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary) { throw null; }
|
||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ValidationProblemDetails descriptor) { throw null; }
|
||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem(string detail = null, string instance = null, int? statusCode = default(int?), string title = null, string type = null, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary = null) { throw null; }
|
||||
}
|
||||
public partial class ControllerContext : Microsoft.AspNetCore.Mvc.ActionContext
|
||||
{
|
||||
|
|
@ -2439,6 +2444,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
public long Length { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
}
|
||||
}
|
||||
public abstract partial class ProblemDetailsFactory
|
||||
{
|
||||
protected ProblemDetailsFactory() { }
|
||||
public abstract Microsoft.AspNetCore.Mvc.ProblemDetails CreateProblemDetails(Microsoft.AspNetCore.Http.HttpContext httpContext, int? statusCode = default(int?), string title = null, string type = null, string detail = null, string instance = null);
|
||||
public abstract Microsoft.AspNetCore.Mvc.ValidationProblemDetails CreateValidationProblemDetails(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary, int? statusCode = default(int?), string title = null, string type = null, string detail = null, string instance = null);
|
||||
}
|
||||
public partial class RedirectResultExecutor : Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor<Microsoft.AspNetCore.Mvc.RedirectResult>
|
||||
{
|
||||
public RedirectResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory urlHelperFactory) { }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
|||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
|
|
@ -31,6 +32,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
private IModelBinderFactory _modelBinderFactory;
|
||||
private IObjectModelValidator _objectValidator;
|
||||
private IUrlHelper _url;
|
||||
private ProblemDetailsFactory _problemDetailsFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
|
||||
|
|
@ -189,6 +191,28 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
public ProblemDetailsFactory ProblemDetailsFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_problemDetailsFactory == null)
|
||||
{
|
||||
_problemDetailsFactory = HttpContext?.RequestServices?.GetRequiredService<ProblemDetailsFactory>();
|
||||
}
|
||||
|
||||
return _problemDetailsFactory;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_problemDetailsFactory = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
|
||||
/// </summary>
|
||||
|
|
@ -1821,6 +1845,34 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||
=> new ConflictObjectResult(modelState);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="ProblemDetails"/> response.
|
||||
/// </summary>
|
||||
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />..</param>
|
||||
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
|
||||
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
|
||||
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
|
||||
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
|
||||
/// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ObjectResult Problem(
|
||||
string detail = null,
|
||||
string instance = null,
|
||||
int? statusCode = null,
|
||||
string title = null,
|
||||
string type = null)
|
||||
{
|
||||
var problemDetails = ProblemDetailsFactory.CreateProblemDetails(
|
||||
HttpContext,
|
||||
statusCode: statusCode ?? 500,
|
||||
title: title,
|
||||
type: type,
|
||||
detail: detail,
|
||||
instance: instance);
|
||||
|
||||
return new ObjectResult(problemDetails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response.
|
||||
/// </summary>
|
||||
|
|
@ -1837,31 +1889,64 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response.
|
||||
/// Creates an <see cref="ActionResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
|
||||
/// with validation errors from <paramref name="modelStateDictionary"/>.
|
||||
/// </summary>
|
||||
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.</param>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
|
||||
{
|
||||
if (modelStateDictionary == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelStateDictionary));
|
||||
}
|
||||
=> ValidationProblem(detail: null, modelStateDictionary: modelStateDictionary);
|
||||
|
||||
var validationProblem = new ValidationProblemDetails(modelStateDictionary);
|
||||
return new BadRequestObjectResult(validationProblem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
|
||||
/// Creates an <see cref="ActionResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
|
||||
/// with validation errors from <see cref="ModelState"/>.
|
||||
/// </summary>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
/// <returns>The created <see cref="ActionResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ActionResult ValidationProblem()
|
||||
=> ValidationProblem(ModelState);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ActionResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
|
||||
/// with a <see cref="ValidationProblemDetails"/> value.
|
||||
/// </summary>
|
||||
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
|
||||
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
|
||||
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
|
||||
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.
|
||||
/// When <see langword="null"/> uses <see cref="ModelState"/>.</param>
|
||||
/// <returns>The created <see cref="ActionResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ActionResult ValidationProblem(
|
||||
string detail = null,
|
||||
string instance = null,
|
||||
int? statusCode = null,
|
||||
string title = null,
|
||||
string type = null,
|
||||
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
|
||||
{
|
||||
var validationProblem = new ValidationProblemDetails(ModelState);
|
||||
return new BadRequestObjectResult(validationProblem);
|
||||
modelStateDictionary ??= ModelState;
|
||||
|
||||
var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails(
|
||||
HttpContext,
|
||||
modelStateDictionary,
|
||||
statusCode: statusCode,
|
||||
title: title,
|
||||
type: type,
|
||||
detail: detail,
|
||||
instance: instance);
|
||||
|
||||
if (validationProblem.Status == 400)
|
||||
{
|
||||
// For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400.
|
||||
return new BadRequestObjectResult(validationProblem);
|
||||
}
|
||||
|
||||
return new ObjectResult(validationProblem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
|
@ -10,13 +9,9 @@ using Microsoft.Extensions.Options;
|
|||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
internal class ApiBehaviorOptionsSetup :
|
||||
IConfigureOptions<ApiBehaviorOptions>,
|
||||
IPostConfigureOptions<ApiBehaviorOptions>
|
||||
internal class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions>
|
||||
{
|
||||
internal static readonly Func<ActionContext, IActionResult> DefaultFactory = DefaultInvalidModelStateResponse;
|
||||
internal static readonly Func<ActionContext, IActionResult> ProblemDetailsFactory =
|
||||
ProblemDetailsInvalidModelStateResponse;
|
||||
private ProblemDetailsFactory _problemDetailsFactory;
|
||||
|
||||
public void Configure(ApiBehaviorOptions options)
|
||||
{
|
||||
|
|
@ -25,20 +20,34 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
options.InvalidModelStateResponseFactory = DefaultFactory;
|
||||
options.InvalidModelStateResponseFactory = context =>
|
||||
{
|
||||
// ProblemDetailsFactory depends on the ApiBehaviorOptions instance. We intentionally avoid constructor injecting
|
||||
// it in this options setup to to avoid a DI cycle.
|
||||
_problemDetailsFactory ??= context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
|
||||
return ProblemDetailsInvalidModelStateResponse(_problemDetailsFactory, context);
|
||||
};
|
||||
|
||||
ConfigureClientErrorMapping(options);
|
||||
}
|
||||
|
||||
public void PostConfigure(string name, ApiBehaviorOptions options)
|
||||
internal static IActionResult ProblemDetailsInvalidModelStateResponse(ProblemDetailsFactory problemDetailsFactory, ActionContext context)
|
||||
{
|
||||
// We want to use problem details factory only if
|
||||
// (a) it has not been opted out of (SuppressMapClientErrors = true)
|
||||
// (b) a different factory was configured
|
||||
if (!options.SuppressMapClientErrors &&
|
||||
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
|
||||
var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState);
|
||||
ObjectResult result;
|
||||
if (problemDetails.Status == 400)
|
||||
{
|
||||
options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
|
||||
// For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400.
|
||||
result = new BadRequestObjectResult(problemDetails);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = new ObjectResult(problemDetails);
|
||||
}
|
||||
result.ContentTypes.Add("application/problem+json");
|
||||
result.ContentTypes.Add("application/problem+xml");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Internal for unit testing
|
||||
|
|
@ -91,33 +100,12 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
|
||||
Title = Resources.ApiConventions_Title_422,
|
||||
};
|
||||
}
|
||||
|
||||
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
|
||||
{
|
||||
var result = new BadRequestObjectResult(context.ModelState);
|
||||
|
||||
result.ContentTypes.Add("application/json");
|
||||
result.ContentTypes.Add("application/xml");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
|
||||
{
|
||||
var problemDetails = new ValidationProblemDetails(context.ModelState)
|
||||
options.ClientErrorMapping[500] = new ClientErrorData
|
||||
{
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
|
||||
Title = Resources.ApiConventions_Title_500,
|
||||
};
|
||||
|
||||
ProblemDetailsClientErrorFactory.SetTraceId(context, problemDetails);
|
||||
|
||||
var result = new BadRequestObjectResult(problemDetails);
|
||||
|
||||
result.ContentTypes.Add("application/problem+json");
|
||||
result.ContentTypes.Add("application/problem+xml");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,8 +149,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IPostConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());
|
||||
|
||||
|
|
@ -260,6 +258,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
|
||||
services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
|
||||
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
|
||||
services.TryAddSingleton<ProblemDetailsFactory, DefaultProblemDetailsFactory>();
|
||||
|
||||
//
|
||||
// Route Handlers
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory
|
||||
{
|
||||
private readonly ApiBehaviorOptions _options;
|
||||
|
||||
public DefaultProblemDetailsFactory(IOptions<ApiBehaviorOptions> options)
|
||||
{
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public override ProblemDetails CreateProblemDetails(
|
||||
HttpContext httpContext,
|
||||
int? statusCode = null,
|
||||
string title = null,
|
||||
string type = null,
|
||||
string detail = null,
|
||||
string instance = null)
|
||||
{
|
||||
statusCode ??= 500;
|
||||
|
||||
var problemDetails = new ProblemDetails
|
||||
{
|
||||
Status = statusCode,
|
||||
Title = title,
|
||||
Type = type,
|
||||
Detail = detail,
|
||||
Instance = instance,
|
||||
};
|
||||
|
||||
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
|
||||
public override ValidationProblemDetails CreateValidationProblemDetails(
|
||||
HttpContext httpContext,
|
||||
ModelStateDictionary modelStateDictionary,
|
||||
int? statusCode = null,
|
||||
string title = null,
|
||||
string type = null,
|
||||
string detail = null,
|
||||
string instance = null)
|
||||
{
|
||||
if (modelStateDictionary == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelStateDictionary));
|
||||
}
|
||||
|
||||
statusCode ??= 400;
|
||||
|
||||
var problemDetails = new ValidationProblemDetails(modelStateDictionary)
|
||||
{
|
||||
Status = statusCode,
|
||||
Type = type,
|
||||
Detail = detail,
|
||||
Instance = instance,
|
||||
};
|
||||
|
||||
if (title != null)
|
||||
{
|
||||
// For validation problem details, don't overwrite the default title with null.
|
||||
problemDetails.Title = title;
|
||||
}
|
||||
|
||||
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
|
||||
private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode)
|
||||
{
|
||||
problemDetails.Status ??= statusCode;
|
||||
|
||||
if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
|
||||
{
|
||||
problemDetails.Title ??= clientErrorData.Title;
|
||||
problemDetails.Type ??= clientErrorData.Link;
|
||||
}
|
||||
|
||||
var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
|
||||
if (traceId != null)
|
||||
{
|
||||
problemDetails.Extensions["traceId"] = traceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,37 +2,21 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal class ProblemDetailsClientErrorFactory : IClientErrorFactory
|
||||
{
|
||||
private static readonly string TraceIdentifierKey = "traceId";
|
||||
private readonly ApiBehaviorOptions _options;
|
||||
private readonly ProblemDetailsFactory _problemDetailsFactory;
|
||||
|
||||
public ProblemDetailsClientErrorFactory(IOptions<ApiBehaviorOptions> options)
|
||||
public ProblemDetailsClientErrorFactory(ProblemDetailsFactory problemDetailsFactory)
|
||||
{
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory));
|
||||
}
|
||||
|
||||
public IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError)
|
||||
{
|
||||
var problemDetails = new ProblemDetails
|
||||
{
|
||||
Status = clientError.StatusCode,
|
||||
Type = "about:blank",
|
||||
};
|
||||
|
||||
if (clientError.StatusCode is int statusCode &&
|
||||
_options.ClientErrorMapping.TryGetValue(statusCode, out var errorData))
|
||||
{
|
||||
problemDetails.Title = errorData.Title;
|
||||
problemDetails.Type = errorData.Link;
|
||||
|
||||
SetTraceId(actionContext, problemDetails);
|
||||
}
|
||||
var problemDetails = _problemDetailsFactory.CreateProblemDetails(actionContext.HttpContext, clientError.StatusCode);
|
||||
|
||||
return new ObjectResult(problemDetails)
|
||||
{
|
||||
|
|
@ -44,11 +28,5 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
internal static void SetTraceId(ActionContext actionContext, ProblemDetails problemDetails)
|
||||
{
|
||||
var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier;
|
||||
problemDetails.Extensions[TraceIdentifierKey] = traceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory to produce <see cref="ProblemDetails" /> and <see cref="ValidationProblemDetails" />.
|
||||
/// </summary>
|
||||
public abstract class ProblemDetailsFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ProblemDetails" /> instance that configures defaults based on values specified in <see cref="ApiBehaviorOptions" />.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext" />.</param>
|
||||
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status"/>.</param>
|
||||
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
|
||||
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
|
||||
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
|
||||
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
|
||||
/// <returns>The <see cref="ProblemDetails"/> instance.</returns>
|
||||
public abstract ProblemDetails CreateProblemDetails(
|
||||
HttpContext httpContext,
|
||||
int? statusCode = null,
|
||||
string title = null,
|
||||
string type = null,
|
||||
string detail = null,
|
||||
string instance = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ValidationProblemDetails" /> instance that configures defaults based on values specified in <see cref="ApiBehaviorOptions" />.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext" />.</param>
|
||||
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary" />.</param>
|
||||
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status"/>.</param>
|
||||
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
|
||||
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
|
||||
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
|
||||
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
|
||||
/// <returns>The <see cref="ValidationProblemDetails"/> instance.</returns>
|
||||
public abstract ValidationProblemDetails CreateValidationProblemDetails(
|
||||
HttpContext httpContext,
|
||||
ModelStateDictionary modelStateDictionary,
|
||||
int? statusCode = null,
|
||||
string title = null,
|
||||
string type = null,
|
||||
string detail = null,
|
||||
string instance = null);
|
||||
}
|
||||
}
|
||||
|
|
@ -510,4 +510,7 @@
|
|||
<data name="UnexpectedJsonEnd" xml:space="preserve">
|
||||
<value>Unexcepted end when reading JSON.</value>
|
||||
</data>
|
||||
<data name="ApiConventions_Title_500" xml:space="preserve">
|
||||
<value>An error occured while processing your request.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -9,8 +9,11 @@ using System.Reflection;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
|
@ -2187,7 +2190,6 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
{
|
||||
// Arrange
|
||||
var contentController = new ContentController();
|
||||
var expected = MediaTypeHeaderValue.Parse("text/plain; charset=utf-8");
|
||||
|
||||
// Act
|
||||
var contentResult = (ContentResult)contentController.Content_WithNoEncoding();
|
||||
|
|
@ -2290,6 +2292,163 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
Assert.Equal(statusCode, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidationProblemDetails_Works()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ControllerContext(new ActionContext(
|
||||
new DefaultHttpContext { TraceIdentifier = "some-trace" },
|
||||
new RouteData(),
|
||||
new ControllerActionDescriptor()));
|
||||
|
||||
context.ModelState.AddModelError("key1", "error1");
|
||||
|
||||
var options = GetApiBehaviorOptions();
|
||||
var controller = new TestableController
|
||||
{
|
||||
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
|
||||
ControllerContext = context,
|
||||
};
|
||||
|
||||
// Act
|
||||
var actionResult = controller.ValidationProblem();
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsType<BadRequestObjectResult>(actionResult);
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequestResult.Value);
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
|
||||
Assert.Equal("some-trace", problemDetails.Extensions["traceId"]);
|
||||
Assert.Equal(new[] { "error1" }, problemDetails.Errors["key1"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidationProblemDetails_UsesSpecifiedTitle()
|
||||
{
|
||||
// Arrange
|
||||
var detail = "My detail";
|
||||
var title = "Custom title";
|
||||
var type = "http://custom-link";
|
||||
var options = GetApiBehaviorOptions();
|
||||
|
||||
var controller = new TestableController
|
||||
{
|
||||
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
|
||||
};
|
||||
|
||||
// Act
|
||||
var actionResult = controller.ValidationProblem(detail: detail, title: title, type: type);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsType<BadRequestObjectResult>(actionResult);
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequestResult.Value);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal(type, problemDetails.Type);
|
||||
Assert.Equal(detail, problemDetails.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProblemDetails_Works()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ControllerContext(new ActionContext(
|
||||
new DefaultHttpContext { TraceIdentifier = "some-trace" },
|
||||
new RouteData(),
|
||||
new ControllerActionDescriptor()));
|
||||
|
||||
var options = GetApiBehaviorOptions();
|
||||
|
||||
var controller = new TestableController
|
||||
{
|
||||
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
|
||||
ControllerContext = context,
|
||||
};
|
||||
|
||||
// Act
|
||||
var actionResult = controller.Problem();
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
|
||||
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
|
||||
Assert.Equal(500, problemDetails.Status);
|
||||
Assert.Equal("An error occured while processing your request.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
|
||||
Assert.Equal("some-trace", problemDetails.Extensions["traceId"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProblemDetails_UsesPassedInValues()
|
||||
{
|
||||
// Arrange
|
||||
var title = "The website is down.";
|
||||
var detail = "Try again in a few minutes.";
|
||||
var options = GetApiBehaviorOptions();
|
||||
|
||||
var controller = new TestableController
|
||||
{
|
||||
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
|
||||
};
|
||||
|
||||
// Act
|
||||
var actionResult = controller.Problem(detail, title: title);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
|
||||
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
|
||||
Assert.Equal(500, problemDetails.Status);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
|
||||
Assert.Equal(detail, problemDetails.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProblemDetails_UsesPassedInStatusCode()
|
||||
{
|
||||
// Arrange
|
||||
var options = GetApiBehaviorOptions();
|
||||
|
||||
var controller = new TestableController
|
||||
{
|
||||
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
|
||||
};
|
||||
|
||||
// Act
|
||||
var actionResult = controller.Problem(statusCode: 422);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
|
||||
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
|
||||
Assert.Equal(422, problemDetails.Status);
|
||||
Assert.Equal("Unprocessable entity.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type);
|
||||
}
|
||||
|
||||
private static ApiBehaviorOptions GetApiBehaviorOptions()
|
||||
{
|
||||
return new ApiBehaviorOptions
|
||||
{
|
||||
ClientErrorMapping =
|
||||
{
|
||||
[400] = new ClientErrorData
|
||||
{
|
||||
Title = "One or more validation errors occurred.",
|
||||
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
|
||||
},
|
||||
[422] = new ClientErrorData
|
||||
{
|
||||
Title = "Unprocessable entity.",
|
||||
Link = "https://tools.ietf.org/html/rfc4918#section-11.2"
|
||||
},
|
||||
[500] = new ClientErrorData
|
||||
{
|
||||
Title = "An error occured while processing your request.",
|
||||
Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> RedirectTestData
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -6,31 +6,18 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public class ApiBehaviorOptionsSetupTest
|
||||
{
|
||||
[Fact]
|
||||
public void Configure_AssignsInvalidModelStateResponseFactory()
|
||||
{
|
||||
// Arrange
|
||||
var optionsSetup = new ApiBehaviorOptionsSetup();
|
||||
var options = new ApiBehaviorOptions();
|
||||
|
||||
// Act
|
||||
optionsSetup.Configure(options);
|
||||
|
||||
// Assert
|
||||
Assert.Same(ApiBehaviorOptionsSetup.DefaultFactory, options.InvalidModelStateResponseFactory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_AddsClientErrorMappings()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, };
|
||||
var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, 500, };
|
||||
var optionsSetup = new ApiBehaviorOptionsSetup();
|
||||
var options = new ApiBehaviorOptions();
|
||||
|
||||
|
|
@ -41,50 +28,15 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
Assert.Equal(expected, options.ClientErrorMapping.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostConfigure_SetProblemDetailsModelStateResponseFactory()
|
||||
{
|
||||
// Arrange
|
||||
var optionsSetup = new ApiBehaviorOptionsSetup();
|
||||
var options = new ApiBehaviorOptions();
|
||||
|
||||
// Act
|
||||
optionsSetup.Configure(options);
|
||||
optionsSetup.PostConfigure(string.Empty, options);
|
||||
|
||||
// Assert
|
||||
Assert.Same(ApiBehaviorOptionsSetup.ProblemDetailsFactory, options.InvalidModelStateResponseFactory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostConfigure_DoesNotSetProblemDetailsFactory_IfValueWasModified()
|
||||
{
|
||||
// Arrange
|
||||
var optionsSetup = new ApiBehaviorOptionsSetup();
|
||||
var options = new ApiBehaviorOptions();
|
||||
Func<ActionContext, IActionResult> expected = _ => null;
|
||||
|
||||
// Act
|
||||
optionsSetup.Configure(options);
|
||||
// This is equivalent to user code updating the value via ConfigureOptions
|
||||
options.InvalidModelStateResponseFactory = expected;
|
||||
optionsSetup.PostConfigure(string.Empty, options);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expected, options.InvalidModelStateResponseFactory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblemDetails()
|
||||
{
|
||||
// Arrange
|
||||
var actionContext = new ActionContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
|
||||
};
|
||||
var actionContext = GetActionContext();
|
||||
var factory = GetProblemDetailsFactory();
|
||||
|
||||
// Act
|
||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
|
||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext);
|
||||
|
||||
// Assert
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||
|
|
@ -92,6 +44,30 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProblemDetailsInvalidModelStateResponse_UsesUserConfiguredLink()
|
||||
{
|
||||
// Arrange
|
||||
var link = "http://mylink";
|
||||
var actionContext = GetActionContext();
|
||||
|
||||
var factory = GetProblemDetailsFactory(options => options.ClientErrorMapping[400].Link = link);
|
||||
|
||||
// Act
|
||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext);
|
||||
|
||||
// Assert
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, badRequest.ContentTypes.OrderBy(c => c));
|
||||
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
|
||||
Assert.Equal(link, problemDetails.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -100,13 +76,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// Arrange
|
||||
using (new ActivityReplacer())
|
||||
{
|
||||
var actionContext = new ActionContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
|
||||
};
|
||||
var actionContext = GetActionContext();
|
||||
var factory = GetProblemDetailsFactory();
|
||||
|
||||
// Act
|
||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
|
||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext);
|
||||
|
||||
// Assert
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||
|
|
@ -119,18 +93,38 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public void ProblemDetailsInvalidModelStateResponse_SetsTraceIdFromRequest_IfActivityIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var actionContext = new ActionContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
|
||||
};
|
||||
var actionContext = GetActionContext();
|
||||
var factory = GetProblemDetailsFactory();
|
||||
|
||||
// Act
|
||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
|
||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext);
|
||||
|
||||
// Assert
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
|
||||
Assert.Equal("42", problemDetails.Extensions["traceId"]);
|
||||
}
|
||||
|
||||
private static ProblemDetailsFactory GetProblemDetailsFactory(Action<ApiBehaviorOptions> configure = null)
|
||||
{
|
||||
var options = new ApiBehaviorOptions();
|
||||
var setup = new ApiBehaviorOptionsSetup();
|
||||
|
||||
setup.Configure(options);
|
||||
if (configure != null)
|
||||
{
|
||||
configure(options);
|
||||
}
|
||||
|
||||
return new DefaultProblemDetailsFactory(Options.Options.Create(options));
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext()
|
||||
{
|
||||
return new ActionContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,13 +264,6 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
typeof(ApiBehaviorOptionsSetup),
|
||||
}
|
||||
},
|
||||
{
|
||||
typeof(IPostConfigureOptions<ApiBehaviorOptions>),
|
||||
new Type[]
|
||||
{
|
||||
typeof(ApiBehaviorOptionsSetup),
|
||||
}
|
||||
},
|
||||
{
|
||||
typeof(IActionConstraintProvider),
|
||||
new Type[]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class ProblemDetailsFactoryTest
|
||||
{
|
||||
private readonly ProblemDetailsFactory Factory = GetProblemDetails();
|
||||
|
||||
[Fact]
|
||||
public void CreateProblemDetails_DefaultValues()
|
||||
{
|
||||
// Act
|
||||
var problemDetails = Factory.CreateProblemDetails(GetHttpContext());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(500, problemDetails.Status);
|
||||
Assert.Equal("An error occured while processing your request.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
|
||||
Assert.Null(problemDetails.Instance);
|
||||
Assert.Null(problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal("some-trace", kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateProblemDetails_WithStatusCode()
|
||||
{
|
||||
// Act
|
||||
var problemDetails = Factory.CreateProblemDetails(GetHttpContext(), statusCode: 406);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(406, problemDetails.Status);
|
||||
Assert.Equal("Not Acceptable", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type);
|
||||
Assert.Null(problemDetails.Instance);
|
||||
Assert.Null(problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal("some-trace", kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateProblemDetails_WithDetailAndTitle()
|
||||
{
|
||||
// Act
|
||||
var title = "Some title";
|
||||
var detail = "some detail";
|
||||
var problemDetails = Factory.CreateProblemDetails(GetHttpContext(), statusCode: 406, title: title, detail: detail);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(406, problemDetails.Status);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type);
|
||||
Assert.Null(problemDetails.Instance);
|
||||
Assert.Equal(detail, problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal("some-trace", kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationProblemDetails_DefaultValues()
|
||||
{
|
||||
// Act
|
||||
var modelState = new ModelStateDictionary();
|
||||
modelState.AddModelError("some-key", "some-value");
|
||||
var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
|
||||
Assert.Null(problemDetails.Instance);
|
||||
Assert.Null(problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal("some-trace", kvp.Value);
|
||||
});
|
||||
Assert.Collection(
|
||||
problemDetails.Errors,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("some-key", kvp.Key);
|
||||
Assert.Equal(new[] { "some-value" }, kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationProblemDetails_WithStatusCode()
|
||||
{
|
||||
// Act
|
||||
var modelState = new ModelStateDictionary();
|
||||
modelState.AddModelError("some-key", "some-value");
|
||||
var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState, 422);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(422, problemDetails.Status);
|
||||
Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type);
|
||||
Assert.Null(problemDetails.Instance);
|
||||
Assert.Null(problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal("some-trace", kvp.Value);
|
||||
});
|
||||
Assert.Collection(
|
||||
problemDetails.Errors,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("some-key", kvp.Key);
|
||||
Assert.Equal(new[] { "some-value" }, kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationProblemDetails_WithTitleAndInstance()
|
||||
{
|
||||
// Act
|
||||
var title = "Some title";
|
||||
var instance = "some instance";
|
||||
var modelState = new ModelStateDictionary();
|
||||
modelState.AddModelError("some-key", "some-value");
|
||||
var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState, title: title, instance: instance);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
|
||||
Assert.Equal(instance, problemDetails.Instance);
|
||||
Assert.Null(problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal("some-trace", kvp.Value);
|
||||
});
|
||||
Assert.Collection(
|
||||
problemDetails.Errors,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("some-key", kvp.Key);
|
||||
Assert.Equal(new[] { "some-value" }, kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
private static DefaultHttpContext GetHttpContext()
|
||||
{
|
||||
return new DefaultHttpContext
|
||||
{
|
||||
TraceIdentifier = "some-trace",
|
||||
};
|
||||
}
|
||||
|
||||
private static ProblemDetailsFactory GetProblemDetails()
|
||||
{
|
||||
var options = new ApiBehaviorOptions();
|
||||
new ApiBehaviorOptionsSetup().Configure(options);
|
||||
return new DefaultProblemDetailsFactory(Options.Create(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,13 +15,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
// Arrange
|
||||
var clientError = new UnsupportedMediaTypeResult();
|
||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
||||
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||
{
|
||||
ClientErrorMapping =
|
||||
{
|
||||
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||
},
|
||||
}));
|
||||
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||
|
||||
// Act
|
||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
||||
|
|
@ -31,7 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
|
||||
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
|
||||
Assert.Equal(415, problemDetails.Status);
|
||||
Assert.Equal("about:blank", problemDetails.Type);
|
||||
Assert.Null(problemDetails.Title);
|
||||
Assert.Null(problemDetails.Detail);
|
||||
Assert.Null(problemDetails.Instance);
|
||||
|
|
@ -42,13 +42,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
// Arrange
|
||||
var clientError = new UnsupportedMediaTypeResult();
|
||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
||||
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||
{
|
||||
ClientErrorMapping =
|
||||
{
|
||||
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||
},
|
||||
}));
|
||||
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||
|
||||
// Act
|
||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
||||
|
|
@ -71,13 +72,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
using (new ActivityReplacer())
|
||||
{
|
||||
var clientError = new UnsupportedMediaTypeResult();
|
||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
||||
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||
{
|
||||
ClientErrorMapping =
|
||||
{
|
||||
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||
},
|
||||
}));
|
||||
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||
|
||||
// Act
|
||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
||||
|
|
@ -96,13 +98,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
// Arrange
|
||||
var clientError = new UnsupportedMediaTypeResult();
|
||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
||||
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||
{
|
||||
ClientErrorMapping =
|
||||
{
|
||||
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||
},
|
||||
}));
|
||||
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||
|
||||
// Act
|
||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
Converters = { new ValidationProblemDetailsConverter() }
|
||||
});
|
||||
|
||||
Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
|
||||
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
|
||||
|
||||
Assert.Collection(
|
||||
problemDetails.Errors.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
|
|
|
|||
|
|
@ -24,15 +24,11 @@ namespace BasicWebSite
|
|||
|
||||
services.Configure<ApiBehaviorOptions>(options =>
|
||||
{
|
||||
var previous = options.InvalidModelStateResponseFactory;
|
||||
options.InvalidModelStateResponseFactory = context =>
|
||||
{
|
||||
var result = (BadRequestObjectResult)previous(context);
|
||||
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute))
|
||||
{
|
||||
result.ContentTypes.Clear();
|
||||
result.ContentTypes.Add("application/vnd.error+json");
|
||||
}
|
||||
var result = new BadRequestObjectResult(context.ModelState);
|
||||
result.ContentTypes.Clear();
|
||||
result.ContentTypes.Add("application/vnd.error+json");
|
||||
|
||||
return result;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue