关于 error-handling
面向 .NET 开发的错误处理指南,涵盖业务逻辑与 API。它区分预期失败和异常,并建议使用一致的 ProblemDetails 响应。
主要功能
- 01用 Result 模式表示预期失败
- 02全局异常处理
- 03ProblemDetails API 响应
- 04在边界处验证请求
- 05类型化错误模型
适用场景
- 01不用异常表示验证失败
- 02统一 API 错误格式
- 03处理意外的应用崩溃
- 04在 API 端点验证请求
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill error-handling安装后在 Claude Code 中输入/error-handling确认已加载。
详细介绍
错误处理
核心原则
- 对预期失败使用 Result 模式 — 不要因“未找到订单”或“验证失败”之类的情况抛出异常。这些是预期结果,而非异常情况。请参阅 ADR-002。
- 仅对意外失败使用异常 — 数据库连接断开、空引用错误、网络超时——这些才是真正的异常情况,应传递给全局处理程序。
- 每个 API 错误都返回 ProblemDetails — RFC 9457 是标准。每个错误响应都包含
type、title、status、detail,并可选包含errors。 - 在边界处进行验证 — 在 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