.NET 6 API 使用默认响应值填充扩展 ProblemDetails 类

问题描述 投票:0回答:3

我想以 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"
  }
}
c# api .net-core .net-6.0 asp.net-apicontroller
3个回答
3
投票

您可以使用 ProblemDetailsFactory 从 DI 解析它来创建

ProblemDetails
的实例。参数之一是状态代码,您可以从操作中返回
Problem(_factory.CreateProbelmDetails(…))


1
投票

好吧,离开 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
));

0
投票

另一种方法是创建一个

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
  • 为此,请将阶数设置为 -3000
services.AddControllers(options =>
{
    options.Filters.Add(typeof(CodeResultErrorFilter), -3000); 
});
© www.soinside.com 2019 - 2024. All rights reserved.