parent
17d72c2b94
commit
667ad4daff
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
|
public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
|
||||||
{
|
{
|
||||||
private readonly CompatibilitySwitch<bool> _suppressUseClientErrorFactory;
|
private readonly CompatibilitySwitch<bool> _suppressMapClientErrors;
|
||||||
private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses;
|
private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses;
|
||||||
private readonly ICompatibilitySwitch[] _switches;
|
private readonly ICompatibilitySwitch[] _switches;
|
||||||
|
|
||||||
|
|
@ -26,11 +26,11 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiBehaviorOptions()
|
public ApiBehaviorOptions()
|
||||||
{
|
{
|
||||||
_suppressUseClientErrorFactory = new CompatibilitySwitch<bool>(nameof(SuppressUseClientErrorFactory));
|
_suppressMapClientErrors = new CompatibilitySwitch<bool>(nameof(SuppressMapClientErrors));
|
||||||
_suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses));
|
_suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses));
|
||||||
_switches = new[]
|
_switches = new[]
|
||||||
{
|
{
|
||||||
_suppressUseClientErrorFactory,
|
_suppressMapClientErrors,
|
||||||
_suppressUseValidationProblemDetailsForInvalidModelStateResponses,
|
_suppressUseValidationProblemDetailsForInvalidModelStateResponses,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -71,12 +71,16 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
public bool SuppressConsumesConstraintForFormFileParameters { get; set; }
|
public bool SuppressConsumesConstraintForFormFileParameters { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/> use <see cref="ClientErrorFactory"/>
|
/// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/>
|
||||||
/// to transform certain certain client errors.
|
/// transform certain certain client errors.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// When <c>false</c>, <see cref="ClientErrorFactory"/> is used to transform <see cref="IClientErrorActionResult"/> to the value
|
/// When <c>false</c>, a result filter is added to API controller actions that transforms <see cref="IClientErrorActionResult"/>.
|
||||||
/// specified by the factory. In the default case, this converts <see cref="StatusCodeResult"/> instances to an <see cref="ObjectResult"/>
|
/// By default, <see cref="ClientErrorMapping"/> is used to map <see cref="IClientErrorActionResult"/> to a
|
||||||
/// with <see cref="ProblemDetails"/>.
|
/// <see cref="ProblemDetails"/> instance (returned as the value for <see cref="ObjectResult"/>).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// To customize the output of the filter (for e.g. to return a different error type), register a custom
|
||||||
|
/// implementation of of <see cref="IClientErrorFactory"/> in the service collection.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
|
|
@ -102,11 +106,11 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
|
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool SuppressUseClientErrorFactory
|
public bool SuppressMapClientErrors
|
||||||
{
|
{
|
||||||
// Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property.
|
// Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property.
|
||||||
get => _suppressUseClientErrorFactory.Value;
|
get => _suppressMapClientErrors.Value;
|
||||||
set => _suppressUseClientErrorFactory.Value = value;
|
set => _suppressMapClientErrors.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -148,11 +152,15 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a map of HTTP status codes to <see cref="IActionResult"/> factories.
|
/// Gets a map of HTTP status codes to <see cref="ClientErrorData"/>. Configured values
|
||||||
/// Configured factories are used when <see cref="SuppressUseClientErrorFactory"/> is <see langword="false"/>.
|
/// are used to transform <see cref="IClientErrorActionResult"/> to an <see cref="ObjectResult"/>
|
||||||
|
/// instance where the <see cref="ObjectResult.Value"/> is <see cref="ProblemDetails"/>.
|
||||||
|
/// <para>
|
||||||
|
/// Use of this feature can be disabled by resetting <see cref="SuppressMapClientErrors"/>.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDictionary<int, Func<ActionContext, IActionResult>> ClientErrorFactory { get; } =
|
public IDictionary<int, ClientErrorData> ClientErrorMapping { get; } =
|
||||||
new Dictionary<int, Func<ActionContext, IActionResult>>();
|
new Dictionary<int, ClientErrorData>();
|
||||||
|
|
||||||
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
|
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Information for producing client errors. This type is used to configure client errors
|
||||||
|
/// produced by consumers of <see cref="ApiBehaviorOptions.ClientErrorMapping"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ClientErrorData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a link (URI) that describes the client error.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// By default, this maps to <see cref="ProblemDetails.Type"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public string Link { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the summary of the client error.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// By default, this maps to <see cref="ProblemDetails.Title"/> and should not change
|
||||||
|
/// between multiple occurences of the same error.
|
||||||
|
/// </remarks>
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -257,6 +257,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
|
services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
|
||||||
services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
|
services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
|
||||||
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
|
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
|
||||||
|
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Route Handlers
|
// Route Handlers
|
||||||
|
|
|
||||||
|
|
@ -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 System.Collections.Generic;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -11,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter
|
internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter
|
||||||
{
|
{
|
||||||
private readonly IDictionary<int, Func<ActionContext, IActionResult>> _clientErrorFactory;
|
private readonly IClientErrorFactory _clientErrorFactory;
|
||||||
private readonly ILogger<ClientErrorResultFilter> _logger;
|
private readonly ILogger<ClientErrorResultFilter> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -20,10 +19,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
public int Order => -2000;
|
public int Order => -2000;
|
||||||
|
|
||||||
public ClientErrorResultFilter(
|
public ClientErrorResultFilter(
|
||||||
ApiBehaviorOptions apiBehaviorOptions,
|
IClientErrorFactory clientErrorFactory,
|
||||||
ILogger<ClientErrorResultFilter> logger)
|
ILogger<ClientErrorResultFilter> logger)
|
||||||
{
|
{
|
||||||
_clientErrorFactory = apiBehaviorOptions?.ClientErrorFactory ?? throw new ArgumentNullException(nameof(apiBehaviorOptions));
|
_clientErrorFactory = clientErrorFactory ?? throw new ArgumentNullException(nameof(clientErrorFactory));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,16 +37,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.Result is IClientErrorActionResult clientErrorActionResult &&
|
if (!(context.Result is IClientErrorActionResult clientError))
|
||||||
clientErrorActionResult.StatusCode is int statusCode &&
|
|
||||||
_clientErrorFactory.TryGetValue(statusCode, out var factory))
|
|
||||||
{
|
{
|
||||||
var result = factory(context);
|
return;
|
||||||
|
|
||||||
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), statusCode);
|
|
||||||
|
|
||||||
context.Result = factory(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var result = _clientErrorFactory.GetClientError(context, clientError);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), clientError.StatusCode);
|
||||||
|
context.Result = result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A factory for producing client errors. This contract is used by controllers annotated
|
||||||
|
/// with <see cref="ApiControllerAttribute"/> to transform <see cref="IClientErrorActionResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IClientErrorFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms <paramref name="clientError"/> for the specified <paramref name="actionContext"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
|
||||||
|
/// <param name="clientError">The <see cref="IClientErrorActionResult"/>.</param>
|
||||||
|
/// <returns>THe <see cref="IActionResult"/> that would be returned to the client.</returns>
|
||||||
|
IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// 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 Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
internal class ProblemDetailsClientErrorFactory : IClientErrorFactory
|
||||||
|
{
|
||||||
|
private readonly ApiBehaviorOptions _options;
|
||||||
|
|
||||||
|
public ProblemDetailsClientErrorFactory(IOptions<ApiBehaviorOptions> options)
|
||||||
|
{
|
||||||
|
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ObjectResult(problemDetails)
|
||||||
|
{
|
||||||
|
StatusCode = problemDetails.Status,
|
||||||
|
ContentTypes =
|
||||||
|
{
|
||||||
|
"application/problem+json",
|
||||||
|
"application/problem+xml",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public ApiBehaviorApplicationModelProvider(
|
public ApiBehaviorApplicationModelProvider(
|
||||||
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
|
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
|
||||||
IModelMetadataProvider modelMetadataProvider,
|
IModelMetadataProvider modelMetadataProvider,
|
||||||
|
IClientErrorFactory clientErrorFactory,
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_apiBehaviorOptions = apiBehaviorOptions.Value;
|
_apiBehaviorOptions = apiBehaviorOptions.Value;
|
||||||
|
|
@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
loggerFactory.CreateLogger<ModelStateInvalidFilter>());
|
loggerFactory.CreateLogger<ModelStateInvalidFilter>());
|
||||||
|
|
||||||
_clientErrorResultFilter = new ClientErrorResultFilter(
|
_clientErrorResultFilter = new ClientErrorResultFilter(
|
||||||
_apiBehaviorOptions,
|
clientErrorFactory,
|
||||||
loggerFactory.CreateLogger<ClientErrorResultFilter>());
|
loggerFactory.CreateLogger<ClientErrorResultFilter>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
private void AddClientErrorFilter(ActionModel actionModel)
|
private void AddClientErrorFilter(ActionModel actionModel)
|
||||||
{
|
{
|
||||||
if (_apiBehaviorOptions.SuppressUseClientErrorFactory)
|
if (_apiBehaviorOptions.SuppressMapClientErrors)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
if (Version < CompatibilityVersion.Version_2_2)
|
if (Version < CompatibilityVersion.Version_2_2)
|
||||||
{
|
{
|
||||||
dictionary[nameof(ApiBehaviorOptions.SuppressUseClientErrorFactory)] = true;
|
dictionary[nameof(ApiBehaviorOptions.SuppressMapClientErrors)] = true;
|
||||||
dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true;
|
dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
options.InvalidModelStateResponseFactory = DefaultFactory;
|
options.InvalidModelStateResponseFactory = DefaultFactory;
|
||||||
ConfigureClientErrorFactories(options);
|
ConfigureClientErrorMapping(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PostConfigure(string name, ApiBehaviorOptions options)
|
public override void PostConfigure(string name, ApiBehaviorOptions options)
|
||||||
|
|
@ -57,9 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
base.PostConfigure(name, options);
|
base.PostConfigure(name, options);
|
||||||
|
|
||||||
// We want to use problem details factory only if
|
// We want to use problem details factory only if
|
||||||
// (a) it has not been opted out of (SuppressUseClientErrorFactory = true)
|
// (a) it has not been opted out of (SuppressMapClientErrors = true)
|
||||||
// (b) a different factory was configured
|
// (b) a different factory was configured
|
||||||
if (!options.SuppressUseClientErrorFactory &&
|
if (!options.SuppressMapClientErrors &&
|
||||||
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
|
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
|
||||||
{
|
{
|
||||||
options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
|
options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
|
||||||
|
|
@ -67,77 +67,55 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal for unit testing
|
// Internal for unit testing
|
||||||
internal static void ConfigureClientErrorFactories(ApiBehaviorOptions options)
|
internal static void ConfigureClientErrorMapping(ApiBehaviorOptions options)
|
||||||
{
|
{
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[400] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 400,
|
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
|
||||||
Title = Resources.ApiConventions_Title_400,
|
Title = Resources.ApiConventions_Title_400,
|
||||||
});
|
};
|
||||||
|
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[401] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 401,
|
Link = "https://tools.ietf.org/html/rfc7235#section-3.1",
|
||||||
Type = "https://tools.ietf.org/html/rfc7235#section-3.1",
|
|
||||||
Title = Resources.ApiConventions_Title_401,
|
Title = Resources.ApiConventions_Title_401,
|
||||||
});
|
};
|
||||||
|
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[403] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 403,
|
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
|
||||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
|
|
||||||
Title = Resources.ApiConventions_Title_403,
|
Title = Resources.ApiConventions_Title_403,
|
||||||
});
|
};
|
||||||
|
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[404] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 404,
|
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
|
||||||
Title = Resources.ApiConventions_Title_404,
|
Title = Resources.ApiConventions_Title_404,
|
||||||
});
|
};
|
||||||
|
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[406] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 406,
|
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
|
||||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
|
|
||||||
Title = Resources.ApiConventions_Title_406,
|
Title = Resources.ApiConventions_Title_406,
|
||||||
});
|
};
|
||||||
|
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[409] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 409,
|
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
|
||||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
|
|
||||||
Title = Resources.ApiConventions_Title_409,
|
Title = Resources.ApiConventions_Title_409,
|
||||||
});
|
};
|
||||||
|
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[415] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 415,
|
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
|
||||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
|
|
||||||
Title = Resources.ApiConventions_Title_415,
|
Title = Resources.ApiConventions_Title_415,
|
||||||
});
|
};
|
||||||
|
|
||||||
AddClientErrorFactory(new ProblemDetails
|
options.ClientErrorMapping[422] = new ClientErrorData
|
||||||
{
|
{
|
||||||
Status = 422,
|
Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
|
||||||
Type = "https://tools.ietf.org/html/rfc4918#section-11.2",
|
|
||||||
Title = Resources.ApiConventions_Title_422,
|
Title = Resources.ApiConventions_Title_422,
|
||||||
});
|
};
|
||||||
|
|
||||||
void AddClientErrorFactory(ProblemDetails problemDetails)
|
|
||||||
{
|
|
||||||
var statusCode = problemDetails.Status.Value;
|
|
||||||
options.ClientErrorFactory[statusCode] = _ => new ObjectResult(problemDetails)
|
|
||||||
{
|
|
||||||
StatusCode = statusCode,
|
|
||||||
ContentTypes =
|
|
||||||
{
|
|
||||||
"application/problem+json",
|
|
||||||
"application/problem+xml",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
|
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
private static readonly Action<ILogger, Type, Type, Type, Exception> _notMostEffectiveFilter;
|
private static readonly Action<ILogger, Type, Type, Type, Exception> _notMostEffectiveFilter;
|
||||||
private static readonly Action<ILogger, IEnumerable<IOutputFormatter>, Exception> _registeredOutputFormatters;
|
private static readonly Action<ILogger, IEnumerable<IOutputFormatter>, Exception> _registeredOutputFormatters;
|
||||||
|
|
||||||
private static readonly Action<ILogger, Type, Type, int, Exception> _transformingClientError;
|
private static readonly Action<ILogger, Type, int?, Type, Exception> _transformingClientError;
|
||||||
|
|
||||||
static MvcCoreLoggerExtensions()
|
static MvcCoreLoggerExtensions()
|
||||||
{
|
{
|
||||||
|
|
@ -651,10 +651,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
48,
|
48,
|
||||||
"Skipped binding parameter '{ParameterName}' since its binding information disallowed it for the current request.");
|
"Skipped binding parameter '{ParameterName}' since its binding information disallowed it for the current request.");
|
||||||
|
|
||||||
_transformingClientError = LoggerMessage.Define<Type, Type, int>(
|
_transformingClientError = LoggerMessage.Define<Type, int?, Type>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(49, nameof(Infrastructure.ClientErrorResultFilter)),
|
new EventId(49, nameof(Infrastructure.ClientErrorResultFilter)),
|
||||||
"Replacing {InitialActionResultType} with status code {StatusCode} with {ReplacedActionResultType} produced from ClientErrorFactory'.");
|
"Replacing {InitialActionResultType} with status code {StatusCode} with {ReplacedActionResultType}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters)
|
public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters)
|
||||||
|
|
@ -1585,9 +1585,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void TransformingClientError(this ILogger logger, Type initialType, Type replacedType, int statusCode)
|
public static void TransformingClientError(this ILogger logger, Type initialType, Type replacedType, int? statusCode)
|
||||||
{
|
{
|
||||||
_transformingClientError(logger, initialType, replacedType, statusCode, null);
|
_transformingClientError(logger, initialType, statusCode, replacedType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogFilterExecutionPlan(
|
private static void LogFilterExecutionPlan(
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence
|
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
|
||||||
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
||||||
/// see[RFC7231], Section 3.4).
|
/// see[RFC7231], Section 3.4).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
public string Detail { get; set; }
|
public string Detail { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
|
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Instance { get; set; }
|
public string Instance { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,35 +32,25 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnResultExecuting_DoesNothing_IfStatusCodeDoesNotExistInApiBehaviorOptions()
|
public void OnResultExecuting_DoesNothing_IfTransformedValueIsNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionResult = new NotFoundResult();
|
var actionResult = new NotFoundResult();
|
||||||
var context = GetContext(actionResult);
|
var context = GetContext(actionResult);
|
||||||
var filter = GetFilter(new ApiBehaviorOptions());
|
var factory = new Mock<IClientErrorFactory>();
|
||||||
|
factory
|
||||||
// Act
|
.Setup(f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()))
|
||||||
filter.OnResultExecuting(context);
|
.Returns((IActionResult)null)
|
||||||
|
.Verifiable();
|
||||||
// Assert
|
|
||||||
Assert.Same(actionResult, context.Result);
|
var filter = new ClientErrorResultFilter(factory.Object, NullLogger<ClientErrorResultFilter>.Instance);
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OnResultExecuting_DoesNothing_IfResultDoesNotHaveStatusCode()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var actionResult = new Mock<IActionResult>()
|
|
||||||
.As<IClientErrorActionResult>()
|
|
||||||
.Object;
|
|
||||||
var context = GetContext(actionResult);
|
|
||||||
var filter = GetFilter(new ApiBehaviorOptions());
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
filter.OnResultExecuting(context);
|
filter.OnResultExecuting(context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(actionResult, context.Result);
|
Assert.Same(actionResult, context.Result);
|
||||||
|
factory.Verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -78,18 +68,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
Assert.Same(Result, context.Result);
|
Assert.Same(Result, context.Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClientErrorResultFilter GetFilter(ApiBehaviorOptions options = null)
|
private static ClientErrorResultFilter GetFilter()
|
||||||
{
|
{
|
||||||
var apiBehaviorOptions = options ?? GetOptions();
|
var factory = Mock.Of<IClientErrorFactory>(
|
||||||
var filter = new ClientErrorResultFilter(apiBehaviorOptions, NullLogger<ClientErrorResultFilter>.Instance);
|
f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()) == Result);
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ApiBehaviorOptions GetOptions()
|
return new ClientErrorResultFilter(factory, NullLogger<ClientErrorResultFilter>.Instance);
|
||||||
{
|
|
||||||
var apiBehaviorOptions = new ApiBehaviorOptions();
|
|
||||||
apiBehaviorOptions.ClientErrorFactory[404] = _ => Result;
|
|
||||||
return apiBehaviorOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ResultExecutingContext GetContext(IActionResult actionResult)
|
private static ResultExecutingContext GetContext(IActionResult actionResult)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
// 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.Extensions.Options;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
public class ProblemDetalsClientErrorFactoryTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetClientError_ReturnsProblemDetails_IfNoMappingWasFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var clientError = new UnsupportedMediaTypeResult();
|
||||||
|
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
||||||
|
{
|
||||||
|
ClientErrorMapping =
|
||||||
|
{
|
||||||
|
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = factory.GetClientError(new ActionContext(), clientError);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var objectResult = Assert.IsType<ObjectResult>(result);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetClientError_ReturnsProblemDetails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var clientError = new UnsupportedMediaTypeResult();
|
||||||
|
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
|
||||||
|
{
|
||||||
|
ClientErrorMapping =
|
||||||
|
{
|
||||||
|
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = factory.GetClientError(new ActionContext(), clientError);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var objectResult = Assert.IsType<ObjectResult>(result);
|
||||||
|
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("Some link", problemDetails.Type);
|
||||||
|
Assert.Equal("Summary", problemDetails.Title);
|
||||||
|
Assert.Null(problemDetails.Detail);
|
||||||
|
Assert.Null(problemDetails.Instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
@ -1081,7 +1082,7 @@ Environment.NewLine + "int b";
|
||||||
var context = GetContext(typeof(TestApiController));
|
var context = GetContext(typeof(TestApiController));
|
||||||
var options = new ApiBehaviorOptions
|
var options = new ApiBehaviorOptions
|
||||||
{
|
{
|
||||||
SuppressUseClientErrorFactory = true,
|
SuppressMapClientErrors = true,
|
||||||
InvalidModelStateResponseFactory = _ => null,
|
InvalidModelStateResponseFactory = _ => null,
|
||||||
};
|
};
|
||||||
var provider = GetProvider(options);
|
var provider = GetProvider(options);
|
||||||
|
|
@ -1122,7 +1123,11 @@ Environment.NewLine + "int b";
|
||||||
|
|
||||||
var loggerFactory = NullLoggerFactory.Instance;
|
var loggerFactory = NullLoggerFactory.Instance;
|
||||||
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
|
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
|
||||||
return new ApiBehaviorApplicationModelProvider(optionsAccessor, modelMetadataProvider, loggerFactory);
|
return new ApiBehaviorApplicationModelProvider(
|
||||||
|
optionsAccessor,
|
||||||
|
modelMetadataProvider,
|
||||||
|
Mock.Of<IClientErrorFactory>(),
|
||||||
|
loggerFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ApplicationModelProviderContext GetContext(
|
private static ApplicationModelProviderContext GetContext(
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Configure_AddsClientErrorFactories()
|
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, };
|
||||||
|
|
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
optionsSetup.Configure(options);
|
optionsSetup.Configure(options);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expected, options.ClientErrorFactory.Keys);
|
Assert.Equal(expected, options.ClientErrorMapping.Keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
|
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
|
||||||
|
Assert.Equal((int)HttpStatusCode.UnsupportedMediaType, problemDetails.Status);
|
||||||
|
Assert.Equal("Unsupported Media Type", problemDetails.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||||
Assert.False(mvcOptions.EnableEndpointRouting);
|
Assert.False(mvcOptions.EnableEndpointRouting);
|
||||||
Assert.Null(mvcOptions.MaxValidationDepth);
|
Assert.Null(mvcOptions.MaxValidationDepth);
|
||||||
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
||||||
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory);
|
Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||||
Assert.False(mvcOptions.EnableEndpointRouting);
|
Assert.False(mvcOptions.EnableEndpointRouting);
|
||||||
Assert.Null(mvcOptions.MaxValidationDepth);
|
Assert.Null(mvcOptions.MaxValidationDepth);
|
||||||
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
||||||
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory);
|
Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||||
Assert.True(mvcOptions.EnableEndpointRouting);
|
Assert.True(mvcOptions.EnableEndpointRouting);
|
||||||
Assert.Equal(32, mvcOptions.MaxValidationDepth);
|
Assert.Equal(32, mvcOptions.MaxValidationDepth);
|
||||||
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
||||||
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory);
|
Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||||
Assert.True(mvcOptions.EnableEndpointRouting);
|
Assert.True(mvcOptions.EnableEndpointRouting);
|
||||||
Assert.Equal(32, mvcOptions.MaxValidationDepth);
|
Assert.Equal(32, mvcOptions.MaxValidationDepth);
|
||||||
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
|
||||||
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory);
|
Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This just does the minimum needed to be able to resolve these options.
|
// This just does the minimum needed to be able to resolve these options.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue