mcpField
E
error-handling

error-handling

github.com/codewithmukesh开发7913.0k 次安装12 周前更新
代码仓库

关于 error-handling

面向 .NET 开发的错误处理指南,涵盖业务逻辑与 API。它区分预期失败和异常,并建议使用一致的 ProblemDetails 响应。

主要功能

  1. 01用 Result 模式表示预期失败
  2. 02全局异常处理
  3. 03ProblemDetails API 响应
  4. 04在边界处验证请求
  5. 05类型化错误模型

适用场景

  1. 01不用异常表示验证失败
  2. 02统一 API 错误格式
  3. 03处理意外的应用崩溃
  4. 04在 API 端点验证请求

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill error-handling

安装后在 Claude Code 中输入/error-handling确认已加载。

详细介绍

错误处理

核心原则

  1. 对预期失败使用 Result 模式 — 不要因“未找到订单”或“验证失败”之类的情况抛出异常。这些是预期结果,而非异常情况。请参阅 ADR-002。
  2. 仅对意外失败使用异常 — 数据库连接断开、空引用错误、网络超时——这些才是真正的异常情况,应传递给全局处理程序。
  3. 每个 API 错误都返回 ProblemDetails — RFC 9457 是标准。每个错误响应都包含 type、title、status、detail,并可选包含 errors。
  4. 在边界处进行验证 — 在 API 层验证传入请求,而不是在业务逻辑深处验证。

模式

Result 模式

一种简单的泛型结果类型,可携带值或错误。

public class Result
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public List<string> Errors { get; }

    protected Result(bool isSuccess, List<string>? errors = null)
    {
        IsSuccess = isSuccess;
        Errors = errors ?? [];
    }

    public static Result Success() => new(true);
    public static Result Failure(params string[] errors) => new(false, [..errors]);
    public static Result<T> Success<T>(T value) => new(value);
    public static Result<T> Failure<T>(params string[] errors) => new(errors);
}

public class Result<T> : Result
{
    public T Value { get; }

    internal Result(T value) : base(true) => Value = value;
    internal Result(IEnumerable<string> errors) : base(false, [..errors]) => Value = default!;
}
将 Result 映射为 ProblemDetails
public static class ResultExtensions
{
    public static IResult ToProblemDetails(this Result result, int statusCode = 400)
    {
        return TypedResults.Problem(
            title: "One or more errors occurred",
            statusCode: statusCode,
            extensions: new Dictionary<string, object?>
            {
                ["errors"] = result.Errors
            });
    }
}

// Usage in endpoint
group.MapPost("/", async (CreateOrder.Command command, ISender sender, CancellationToken ct) =>
{
    var result = await sender.Send(command, ct);
    return result.IsSuccess
        ? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
        : result.ToProblemDetails();
});
全局异常处理程序

捕获意外异常并将其转换为 ProblemDetails。关于现代的 IExceptionHandler 方式(推荐),请参阅 knowledge/common-infrastructure.md。下面的内联 lambda 适用于简单场景:

// Program.cs
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();

        logger.LogError(exception, "Unhandled exception for {Method} {Path}",
            context.Request.Method, context.Request.Path);

        var problem = new ProblemDetails
        {
            Title = "An unexpected error occurred",
            Status = StatusCodes.Status500InternalServerError,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1"
        };

        // Don't leak details in production
        if (context.RequestServices.GetRequiredService<IHostEnvironment>().IsDevelopment())
        {
            problem.Detail = exception?.Message;
        }

        context.Response.StatusCode = problem.Status.Value;
        await context.Response.WriteAsJsonAsync(problem);
    });
});
使用 Endpoint Filter 的 FluentValidation
// Validator
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty().WithMessage("Customer ID is required");

        RuleFor(x => x.Items)
            .NotEmpty().WithMessage("At least one item is required");

        RuleForEach(x => x.Items).ChildRules(item =>
        {
            item.RuleFor(x => x.ProductId).NotEmpty();
            item.RuleFor(x => x.Quantity).GreaterThan(0);
        });
    }
}

// Generic validation filter
public class ValidationFilter<TRequest> : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var validator = context.HttpContext.RequestServices.GetService<IValidator<TRequest>>();
        if (validator is null)
            return await next(context);

        var request = context.Arguments.OfType<TRequest>().FirstOrDefault();
        if (request is null)
            return await next(context);

        var result = await validator.ValidateAsync(request);
        if (!result.IsValid)
        {
            return TypedResults.ValidationProblem(result.ToDictionary());
        }

        return await next(context);
    }
}

// Registration
group.MapPost("/", CreateOrder)
    .AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
类型化错误结果

如需更丰富的错误处理,请使用类型化错误枚举或错误对象。

public abstract record Error(string Code, string Message);
public record NotFoundError(string Entity, object Id)
    : Error("not_found", $"{Entity} with ID {Id} was not found");
public record ValidationError(string Field, string Message)
    : Error("validation", Message);
public record ConflictError(string Message)
    : Error("conflict", Message);

// Map to HTTP status codes
public static IResult ToHttpResult(this Error error) => error switch
{
    NotFoundError => TypedResults.Problem(title: error.Message, statusCode: 404),
    ValidationError => TypedResults.Problem(title: error.Message, statusCode: 400),
    ConflictError => TypedResults.Problem(title: error.Message, statusCode: 409),
    _ => TypedResults.Problem(title: error.Message, statusCode: 500)
};

反模式

不要将异常用于流程控制
// BAD — exceptions for expected outcomes
public Order GetOrder(Guid id)
{
    var order = db.Orders.Find(id)
        ?? throw new NotFoundException($"Order {id} not found");
    return order;
}

// GOOD — Result pattern
public Result<Order> GetOrder(Guid id)
{
    var order = db.Orders.Find(id);
    return order is not null
        ? Result.Success(order)
        : Result.Failure<Order>($"Order {id} not found");
}
不要从 API 返回原始错误字符串
// BAD — inconsistent error format
return Results.BadRequest("Something went wrong");
return Results.BadRequest(new { error = "Invalid input" });

// GOOD — always ProblemDetails
return TypedResults.Problem(title: "Invalid input", statusCode: 400);
return TypedResults.ValidationProblem(validationResult.ToDictionary());
不要捕获并吞掉异常
// BAD — silently swallowing
try { await ProcessOrder(order); }
catch (Exception) { /* ignore */ }

// GOOD — log and handle appropriately
try { await ProcessOrder(order); }
catch (PaymentException ex)
{
    logger.LogWarning(ex, "Payment failed for order {OrderId}", order.Id);
    return Result.Failure<Order>("Payment processing failed");
}

决策指南

场景建议
预期的业务失败Result 模式
输入验证使用 endpoint filter 的 FluentValidation
意外崩溃全局异常处理程序 → ProblemDetails
API 错误格式始终使用 RFC 9457 ProblemDetails
处理程序中的验证返回 Result.Failure,不要抛出异常
外部服务故障捕获特定异常,返回 Result.Failure
错误日志记录使用关联 ID 的结构化日志记录

评论

支持 Markdown