我想以 application/problem+json 格式返回 API 中的所有错误响应。默认情况下,返回空的 NotFound() 或 BadRequest() 已经导致这种格式。然而,当它们传递值时(例如 BadRequest("blah")),它们会丢失这种格式。
有没有办法返回带有附加属性的 ProblemDetails 对象,而不必手动填充默认的 ProblemDetails 属性?我想避免为此使用异常处理程序,因为我不想仅仅为了响应格式化而抛出异常。
响应应如下所示:
{
// should be auto-populated with values that an empty NotFound() generates
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-7d554354b54a8e6be652c2ea65434e55-a453edeb85b9eb80-00",
// what i want to add
"additionalProperties": {
"example": "blah"
}
}
您可以使用 ProblemDetailsFactory 从 DI 解析它来创建
ProblemDetails
的实例。参数之一是状态代码,您可以从操作中返回Problem(_factory.CreateProbelmDetails(…))
好吧,离开 Giacomo De Liberali 的回答,我做了一些挖掘并找到了一个不错的解决方案。
我查找了 ProblemDetailsFactory (v6.0.1) 默认实现的源代码,并编写了一个工作原理类似的服务。这样,当我返回完全预期的错误响应时,我可以避免抛出异常。
我使用了
IHttpAccessor
服务,而不是传递 HttpContext 作为参数。这项服务需要提前注册,如下Problem.cs
:
builder.Services.AddHttpContextAccessor();
我在默认注册的
ProblemDetails
服务的帮助下填充 IOptions<ApiBehaviorOptions>
实例的默认字段。
最后,我在
Create(..)
方法中传递一个可选对象作为参数,并使用 problemDetails.Extensions.Add(...)
方法添加它的属性。
这是我当前使用的完整实现:
public class ProblemService
{
private readonly IHttpContextAccessor _contextAccessor;
private readonly ApiBehaviorOptions _options;
public ProblemService(IOptions<ApiBehaviorOptions> options, IHttpContextAccessor contextAccessor)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor));
}
public ProblemDetails Create(int? statusCode = null, object? extensions = null)
{
var context = _contextAccessor.HttpContext ?? throw new NullReferenceException();
statusCode ??= 500;
var problemDetails = new ProblemDetails
{
Status = statusCode,
Instance = context.Request.Path
};
if (extensions != null)
{
foreach (var extension in extensions.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
problemDetails.Extensions.Add(extension.Name, extension.GetValue(extensions, null));
}
}
ApplyProblemDetailsDefaults(context, 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;
}
}
}
注入控制器后的使用方法如下:
return Unauthorized(_problem.Create(StatusCodes.Status401Unauthorized, new
{
I18nKey = LoginFailureTranslationKey.AccessFailed
));
另一种方法是创建一个
IResultFilter
实现,为 ProblemDetails
响应返回 StatusCodeResult
。
通过这种方式,您可以为 StatusCode 和 ObjectResult 响应全局生成
ProblemDetails
。
从这里的默认实现
ClientErrorResultFilter
中获得灵感https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastruct/ClientErrorResultFilter.cs
public class CodeResultErrorFilter : IAlwaysRunResultFilter
{
private readonly ProblemDetailsFactory _problemDetailsFactory;
private readonly ILogger<CodeResultErrorFilter> _logger;
public CodeResultErrorFilter(
ProblemDetailsFactory problemDetailsFactory,
ILogger<CodeResultErrorFilter> logger)
{
_problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory));;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
ArgumentNullException.ThrowIfNull(context);
if (!(context.Result is ObjectResult codeResultError))
{
return;
}
// We do not have an upper bound on the allowed status code. This allows this filter to be used
// for 5xx and later status codes.
if (codeResultError.StatusCode < 400)
{
return;
}
var problemDetails = _problemDetailsFactory.CreateProblemDetails(
httpContext: context.HttpContext,
statusCode: codeResultError.StatusCode
);
//customize problem details here
problemDetails.Extensions.Add("error", codeResultError.Value);
var result = new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status,
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
if (result == null)
{
return;
}
context.Result = result;
}
}
将其添加到 DI 管道
ClientErrorResultFilter
services.AddControllers(options =>
{
options.Filters.Add(typeof(CodeResultErrorFilter), -3000);
});