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.IModelBinderFactory ModelBinderFactory { get { throw null; } set { } }
|
||||||
public Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary ModelState { get { throw null; } }
|
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.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.HttpRequest Request { get { throw null; } }
|
||||||
public Microsoft.AspNetCore.Http.HttpResponse Response { get { throw null; } }
|
public Microsoft.AspNetCore.Http.HttpResponse Response { get { throw null; } }
|
||||||
public Microsoft.AspNetCore.Routing.RouteData RouteData { get { throw null; } }
|
public Microsoft.AspNetCore.Routing.RouteData RouteData { get { throw null; } }
|
||||||
|
|
@ -445,6 +446,8 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
[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; }
|
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]
|
[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; }
|
public virtual Microsoft.AspNetCore.Mvc.RedirectResult Redirect(string url) { throw null; }
|
||||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||||
public virtual Microsoft.AspNetCore.Mvc.RedirectResult RedirectPermanent(string url) { throw null; }
|
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; }
|
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary) { throw null; }
|
||||||
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
|
||||||
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ValidationProblemDetails descriptor) { throw null; }
|
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
|
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 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 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) { }
|
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.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
@ -31,6 +32,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
private IModelBinderFactory _modelBinderFactory;
|
private IModelBinderFactory _modelBinderFactory;
|
||||||
private IObjectModelValidator _objectValidator;
|
private IObjectModelValidator _objectValidator;
|
||||||
private IUrlHelper _url;
|
private IUrlHelper _url;
|
||||||
|
private ProblemDetailsFactory _problemDetailsFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
|
/// 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>
|
/// <summary>
|
||||||
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
|
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1821,6 +1845,34 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStateDictionary modelState)
|
public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||||
=> new ConflictObjectResult(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>
|
/// <summary>
|
||||||
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response.
|
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1837,31 +1889,64 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
|
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.</param>
|
||||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||||
[NonAction]
|
[NonAction]
|
||||||
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
|
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
|
||||||
{
|
=> ValidationProblem(detail: null, modelStateDictionary: modelStateDictionary);
|
||||||
if (modelStateDictionary == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(modelStateDictionary));
|
|
||||||
}
|
|
||||||
|
|
||||||
var validationProblem = new ValidationProblemDetails(modelStateDictionary);
|
|
||||||
return new BadRequestObjectResult(validationProblem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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"/>.
|
/// with validation errors from <see cref="ModelState"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
/// <returns>The created <see cref="ActionResult"/> for the response.</returns>
|
||||||
[NonAction]
|
[NonAction]
|
||||||
public virtual ActionResult ValidationProblem()
|
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);
|
modelStateDictionary ??= ModelState;
|
||||||
return new BadRequestObjectResult(validationProblem);
|
|
||||||
|
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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
|
@ -10,13 +9,9 @@ using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
{
|
{
|
||||||
internal class ApiBehaviorOptionsSetup :
|
internal class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions>
|
||||||
IConfigureOptions<ApiBehaviorOptions>,
|
|
||||||
IPostConfigureOptions<ApiBehaviorOptions>
|
|
||||||
{
|
{
|
||||||
internal static readonly Func<ActionContext, IActionResult> DefaultFactory = DefaultInvalidModelStateResponse;
|
private ProblemDetailsFactory _problemDetailsFactory;
|
||||||
internal static readonly Func<ActionContext, IActionResult> ProblemDetailsFactory =
|
|
||||||
ProblemDetailsInvalidModelStateResponse;
|
|
||||||
|
|
||||||
public void Configure(ApiBehaviorOptions options)
|
public void Configure(ApiBehaviorOptions options)
|
||||||
{
|
{
|
||||||
|
|
@ -25,20 +20,34 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
throw new ArgumentNullException(nameof(options));
|
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);
|
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
|
var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState);
|
||||||
// (a) it has not been opted out of (SuppressMapClientErrors = true)
|
ObjectResult result;
|
||||||
// (b) a different factory was configured
|
if (problemDetails.Status == 400)
|
||||||
if (!options.SuppressMapClientErrors &&
|
|
||||||
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
|
|
||||||
{
|
{
|
||||||
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
|
// Internal for unit testing
|
||||||
|
|
@ -91,33 +100,12 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
|
Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
|
||||||
Title = Resources.ApiConventions_Title_422,
|
Title = Resources.ApiConventions_Title_422,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
|
options.ClientErrorMapping[500] = new ClientErrorData
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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>());
|
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
|
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
|
||||||
services.TryAddEnumerable(
|
|
||||||
ServiceDescriptor.Transient<IPostConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
|
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());
|
ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());
|
||||||
|
|
||||||
|
|
@ -260,6 +258,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
|
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
|
||||||
services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
|
services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
|
||||||
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
|
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
|
||||||
|
services.TryAddSingleton<ProblemDetailsFactory, DefaultProblemDetailsFactory>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Route Handlers
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
internal class ProblemDetailsClientErrorFactory : IClientErrorFactory
|
internal class ProblemDetailsClientErrorFactory : IClientErrorFactory
|
||||||
{
|
{
|
||||||
private static readonly string TraceIdentifierKey = "traceId";
|
private readonly ProblemDetailsFactory _problemDetailsFactory;
|
||||||
private readonly ApiBehaviorOptions _options;
|
|
||||||
|
|
||||||
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)
|
public IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError)
|
||||||
{
|
{
|
||||||
var problemDetails = new ProblemDetails
|
var problemDetails = _problemDetailsFactory.CreateProblemDetails(actionContext.HttpContext, clientError.StatusCode);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ObjectResult(problemDetails)
|
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">
|
<data name="UnexpectedJsonEnd" xml:space="preserve">
|
||||||
<value>Unexcepted end when reading JSON.</value>
|
<value>Unexcepted end when reading JSON.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ApiConventions_Title_500" xml:space="preserve">
|
||||||
|
<value>An error occured while processing your request.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -9,8 +9,11 @@ using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.DataAnnotations;
|
using Microsoft.AspNetCore.Mvc.DataAnnotations;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
@ -2187,7 +2190,6 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var contentController = new ContentController();
|
var contentController = new ContentController();
|
||||||
var expected = MediaTypeHeaderValue.Parse("text/plain; charset=utf-8");
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var contentResult = (ContentResult)contentController.Content_WithNoEncoding();
|
var contentResult = (ContentResult)contentController.Content_WithNoEncoding();
|
||||||
|
|
@ -2290,6 +2292,163 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
Assert.Equal(statusCode, result.StatusCode);
|
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
|
public static IEnumerable<object[]> RedirectTestData
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -6,31 +6,18 @@ using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
{
|
{
|
||||||
public class ApiBehaviorOptionsSetupTest
|
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]
|
[Fact]
|
||||||
public void Configure_AddsClientErrorMappings()
|
public void Configure_AddsClientErrorMappings()
|
||||||
{
|
{
|
||||||
// Arrange
|
// 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 optionsSetup = new ApiBehaviorOptionsSetup();
|
||||||
var options = new ApiBehaviorOptions();
|
var options = new ApiBehaviorOptions();
|
||||||
|
|
||||||
|
|
@ -41,50 +28,15 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
Assert.Equal(expected, options.ClientErrorMapping.Keys);
|
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]
|
[Fact]
|
||||||
public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblemDetails()
|
public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblemDetails()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionContext = new ActionContext
|
var actionContext = GetActionContext();
|
||||||
{
|
var factory = GetProblemDetailsFactory();
|
||||||
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
|
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||||
|
|
@ -92,6 +44,30 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
|
||||||
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
|
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
|
||||||
Assert.Equal(400, problemDetails.Status);
|
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]
|
[Fact]
|
||||||
|
|
@ -100,13 +76,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
// Arrange
|
// Arrange
|
||||||
using (new ActivityReplacer())
|
using (new ActivityReplacer())
|
||||||
{
|
{
|
||||||
var actionContext = new ActionContext
|
var actionContext = GetActionContext();
|
||||||
{
|
var factory = GetProblemDetailsFactory();
|
||||||
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
|
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||||
|
|
@ -119,18 +93,38 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
public void ProblemDetailsInvalidModelStateResponse_SetsTraceIdFromRequest_IfActivityIsNull()
|
public void ProblemDetailsInvalidModelStateResponse_SetsTraceIdFromRequest_IfActivityIsNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionContext = new ActionContext
|
var actionContext = GetActionContext();
|
||||||
{
|
var factory = GetProblemDetailsFactory();
|
||||||
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
|
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||||
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
|
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
|
||||||
Assert.Equal("42", problemDetails.Extensions["traceId"]);
|
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(ApiBehaviorOptionsSetup),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
typeof(IPostConfigureOptions<ApiBehaviorOptions>),
|
|
||||||
new Type[]
|
|
||||||
{
|
|
||||||
typeof(ApiBehaviorOptionsSetup),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
typeof(IActionConstraintProvider),
|
typeof(IActionConstraintProvider),
|
||||||
new Type[]
|
new Type[]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
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
|
// Arrange
|
||||||
var clientError = new UnsupportedMediaTypeResult();
|
var clientError = new UnsupportedMediaTypeResult();
|
||||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||||
{
|
{
|
||||||
ClientErrorMapping =
|
ClientErrorMapping =
|
||||||
{
|
{
|
||||||
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
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);
|
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
|
||||||
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
|
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
|
||||||
Assert.Equal(415, problemDetails.Status);
|
Assert.Equal(415, problemDetails.Status);
|
||||||
Assert.Equal("about:blank", problemDetails.Type);
|
|
||||||
Assert.Null(problemDetails.Title);
|
Assert.Null(problemDetails.Title);
|
||||||
Assert.Null(problemDetails.Detail);
|
Assert.Null(problemDetails.Detail);
|
||||||
Assert.Null(problemDetails.Instance);
|
Assert.Null(problemDetails.Instance);
|
||||||
|
|
@ -42,13 +42,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var clientError = new UnsupportedMediaTypeResult();
|
var clientError = new UnsupportedMediaTypeResult();
|
||||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||||
{
|
{
|
||||||
ClientErrorMapping =
|
ClientErrorMapping =
|
||||||
{
|
{
|
||||||
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
var result = factory.GetClientError(GetActionContext(), clientError);
|
||||||
|
|
@ -71,13 +72,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
using (new ActivityReplacer())
|
using (new ActivityReplacer())
|
||||||
{
|
{
|
||||||
var clientError = new UnsupportedMediaTypeResult();
|
var clientError = new UnsupportedMediaTypeResult();
|
||||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||||
{
|
{
|
||||||
ClientErrorMapping =
|
ClientErrorMapping =
|
||||||
{
|
{
|
||||||
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
var result = factory.GetClientError(GetActionContext(), clientError);
|
||||||
|
|
@ -96,13 +98,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var clientError = new UnsupportedMediaTypeResult();
|
var clientError = new UnsupportedMediaTypeResult();
|
||||||
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions
|
||||||
{
|
{
|
||||||
ClientErrorMapping =
|
ClientErrorMapping =
|
||||||
{
|
{
|
||||||
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = factory.GetClientError(GetActionContext(), clientError);
|
var result = factory.GetClientError(GetActionContext(), clientError);
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
Converters = { new ValidationProblemDetailsConverter() }
|
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(
|
Assert.Collection(
|
||||||
problemDetails.Errors.OrderBy(kvp => kvp.Key),
|
problemDetails.Errors.OrderBy(kvp => kvp.Key),
|
||||||
kvp =>
|
kvp =>
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,11 @@ namespace BasicWebSite
|
||||||
|
|
||||||
services.Configure<ApiBehaviorOptions>(options =>
|
services.Configure<ApiBehaviorOptions>(options =>
|
||||||
{
|
{
|
||||||
var previous = options.InvalidModelStateResponseFactory;
|
|
||||||
options.InvalidModelStateResponseFactory = context =>
|
options.InvalidModelStateResponseFactory = context =>
|
||||||
{
|
{
|
||||||
var result = (BadRequestObjectResult)previous(context);
|
var result = new BadRequestObjectResult(context.ModelState);
|
||||||
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute))
|
result.ContentTypes.Clear();
|
||||||
{
|
result.ContentTypes.Add("application/vnd.error+json");
|
||||||
result.ContentTypes.Clear();
|
|
||||||
result.ContentTypes.Add("application/vnd.error+json");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue