.net - application/problem+json 和 .net 6 最小 API?

标签 .net asp.net-core-webapi asp.net-core-6.0

我创建了一个非常基本的项目,它只是抛出一个异常,期望得到一个 application/problem+json 响应,但我得到了一个包含错误详细信息的 html 页面。 我猜这是我读过的一些帖子谈论的“开发人员异常页面”,但我没有将其添加到启动中。

完整代码示例:

using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => {
    throw new System.Exception("Hello, World!");
    return new string[] { };
});

app.Run();

我在谷歌上找不到任何相关的内容,我认为这是因为它非常新鲜。

如果不在 Visual Studio 中运行,行为会改变吗? 我可以更改它,以便在 Visual Studio 中运行时也获得 application/problem+json 吗?

最佳答案

在“开发”环境中运行时(即 IWebHostEnvironment.IsDevelopment() 为 true),开发人员异常页面现在默认打开(在 .NET 6 中)。

通常,在 ASP.NET Core 应用程序中配置异常处理时,在开发与生产(或更严格地说,非开发)期间将采用单独的路径进行异常处理。开发人员异常页面(顾名思义)仅用于开发,而异常处理程序中间件 (app.UseExceptionHandler) 适用于非开发场景。

要返回异常的问题详细信息响应,您需要分别配置两个路径。开发人员异常页面有一个通过 IDeveloperPageExceptionFilter 接口(interface)的插件模型,可用于控制异常的呈现方式。 Here's an example当客户端指示它支持 JSON 时,过滤器将异常呈现为问题详细信息:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Diagnostics;

/// <summary>
/// Formats <see cref="DeveloperExceptionPageMiddleware"/> exceptions as JSON Problem Details if the client indicates it accepts JSON.
/// </summary>
public class ProblemDetailsDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter
{
    private static readonly object ErrorContextItemsKey = new object();
    private static readonly MediaTypeHeaderValue _jsonMediaType = new MediaTypeHeaderValue("application/json");

    private static readonly RequestDelegate _respondWithProblemDetails = RequestDelegateFactory.Create((HttpContext context) =>
    {
        if (context.Items.TryGetValue(ErrorContextItemsKey, out var errorContextItem) && errorContextItem is ErrorContext errorContext)
        {
            return new ErrorProblemDetailsResult(errorContext.Exception);
        }

        return null;
    }).RequestDelegate;

    public async Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next)
    {
        var headers = errorContext.HttpContext.Request.GetTypedHeaders();
        var acceptHeader = headers.Accept;

        if (acceptHeader?.Any(h => h.IsSubsetOf(_jsonMediaType)) == true)
        {
            errorContext.HttpContext.Items.Add(ErrorContextItemsKey, errorContext);
            await _respondWithProblemDetails(errorContext.HttpContext);
        }
        else
        {
            await next(errorContext);
        }
    }
}

internal class ErrorProblemDetailsResult : IResult
{
    private readonly Exception _ex;

    public ErrorProblemDetailsResult(Exception ex)
    {
        _ex = ex;
    }

    public async Task ExecuteAsync(HttpContext httpContext)
    {
        var problemDetails = new ProblemDetails
        {
            Title = $"An unhandled exception occurred while processing the request",
            Detail = $"{_ex.GetType().Name}: {_ex.Message}",
            Status = _ex switch
            {
                BadHttpRequestException ex => ex.StatusCode,
                _ => StatusCodes.Status500InternalServerError
            }
        };
        problemDetails.Extensions.Add("exception", _ex.GetType().FullName);
        problemDetails.Extensions.Add("stack", _ex.StackTrace);
        problemDetails.Extensions.Add("headers", httpContext.Request.Headers.ToDictionary(kvp => kvp.Key, kvp => (string)kvp.Value));
        problemDetails.Extensions.Add("routeValues", httpContext.GetRouteData().Values);
        problemDetails.Extensions.Add("query", httpContext.Request.Query);
        var endpoint = httpContext.GetEndpoint();
        if (endpoint != null)
        {
            var routeEndpoint = endpoint as RouteEndpoint;
            var httpMethods = endpoint?.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods;
            problemDetails.Extensions.Add("endpoint", new {
                endpoint?.DisplayName,
                routePattern = routeEndpoint?.RoutePattern.RawText,
                routeOrder = routeEndpoint?.Order,
                httpMethods = httpMethods != null ? string.Join(", ", httpMethods) : ""
            });
        }

        var result = Results.Json(problemDetails, statusCode: problemDetails.Status, contentType: "application/problem+json");

        await result.ExecuteAsync(httpContext);
    }
}

public static class ProblemDetailsDeveloperPageExtensions
{
    /// <summary>
    /// Adds a <see cref="IDeveloperPageExceptionFilter"/> that formats all exceptions as JSON Problem Details to clients
    /// that indicate they support JSON via the Accepts header.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection"/></param>
    /// <returns>The <see cref="IServiceCollection"/></returns>
    public static IServiceCollection AddProblemDetailsDeveloperPageExceptionFilter(this IServiceCollection services) =>
        services.AddSingleton<IDeveloperPageExceptionFilter, ProblemDetailsDeveloperPageExceptionFilter>();
}

你像这样在 DI 中注册它:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetailsDeveloperPageExceptionFilter();

var app = builder.Build();

对于非开发场景,您可以注册自己的端点来处理异常并在那里实现所需的行为,或者您可以使用像this one这样的中间件。 .

要自己执行此操作,您需要注册异常处理程序中间件并将其指向错误端点,该端点被写入以返回问题详细信息,like this :

...
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error");
}

var problemJsonMediaType = new MediaTypeHeaderValue("application/problem+json");
app.MapGet("/error", (HttpContext context) =>
    {
        var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        var badRequestEx = error as BadHttpRequestException;
        var statusCode = badRequestEx?.StatusCode ?? StatusCodes.Status500InternalServerError;

        if (context.Request.GetTypedHeaders().Accept?.Any(h => problemJsonMediaType.IsSubsetOf(h)) == true)
        {
            // JSON Problem Details
            return error switch
            {
                BadHttpRequestException ex => Results.Extensions.Problem(detail: ex.Message, statusCode: ex.StatusCode),
                _ => Results.Extensions.Problem()
            };
        }

        // Plain text
        context.Response.StatusCode = statusCode;
        return Results.Text(badRequestEx?.Message ?? "An unhandled exception occurred while processing the request.");
    })
   .ExcludeFromDescription();
...

请注意,此示例中的某些部分使用 custom IResult implementation目前,由于即将发布的 ASP.NET Core 6.0 rc.2 版本中正在修复一个问题

关于.net - application/problem+json 和 .net 6 最小 API?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69351956/

相关文章:

c# - Visual Studio 2010 中的 "Browse To Find Source"

c# - 无法访问已处置的上下文实例

mongodb - 单元测试 Net core - 与 MongoDB 内存数据库的集成测试

c# - DBContext Builder 不包含 .NET 6 中 UseSqlServer 的定义

c# - 从非托管进程卸载 .NET DLL

c# - 与 "always on top"相反

c# - 如何在 C# 上为包装的 C++ 方法编写签名,该方法具有指向函数及其参数的指针?

asp.net-core - 从 asp.net web api 核心返回图像作为 IActionResult

c# - 无法从远程设备连接到.Net Core API 服务器

c# - 如何在asp.net core 6中添加全局路由前缀