C# Minimal API:保持端点整洁的实用方法
Source: Dev.to
Minimal APIs make it tempting to write everything inline, but this quickly becomes unmaintainable.
当端点在同一位置处理验证、业务逻辑、错误处理和响应格式化时,虽然使用 Minimal API 很容易把所有代码写在一起,但这很快就会变得难以维护。解决方案是将业务逻辑提取到专用的处理程序中,让端点仅负责路由。
Source: …
实用方法保持端点整洁
为了保持 Minimal API 端点的整洁和可维护,业务逻辑应在端点之外实现,通常放在专用的处理程序类中。
端点只应负责:
- 接收输入
- 将工作委派给处理程序
- 返回 HTTP 响应
基本处理程序示例
public record WeatherForecastPayload(string Location, int Days);
public class WeatherForecastRequestHandler
{
public Task HandleAsync(
WeatherForecastPayload request,
CancellationToken cancellationToken)
{
return Task.FromResult(new[]
{
"Freezing", "Bracing", "Chilly", "Cool",
"Mild", "Warm", "Balmy", "Hot",
"Sweltering", "Scorching"
});
}
}
使用处理程序的端点
app.MapPost("/create-forecast",
async (
[FromBody] WeatherForecastPayload payload,
WeatherForecastRequestHandler handler,
CancellationToken cancellationToken) =>
{
var result = await handler.HandleAsync(payload, cancellationToken);
if (result is not null)
{
return Results.Ok(result);
}
return Results.BadRequest();
});
这样可以工作,但一旦响应逻辑变得更复杂,端点就会膨胀并失去可读性。
Source: …
引入统一的处理器抽象
大多数处理器:
- 接受请求
- 返回响应
我们可以使用通用接口和统一的响应模型来形式化这一过程。
处理器接口
public interface IHttpRequestHandler<TRequest, TResponse>
{
Task<TResponse> HandleAsync(
TRequest request,
CancellationToken cancellationToken);
}
统一的响应模型
public class DataResponse<T>
{
public T? Data { get; init; }
public IEnumerable<string> Errors { get; init; } = [];
}
public class HttpDataResponse<T> : DataResponse<T>
{
[JsonIgnore]
public HttpStatusCode StatusCode { get; init; }
}
HttpStatusCode 在内部使用,并不会在 API 响应体中暴露;这就是 HttpDataResponse 继承自 DataResponse 的原因。
使用处理器驱动响应的更简洁端点
app.MapPost("/create-forecast",
async (
[FromBody] WeatherForecastPayload payload,
IHttpRequestHandler<WeatherForecastPayload, HttpDataResponse<string[]>> handler,
CancellationToken cancellationToken) =>
{
var response = await handler.HandleAsync(payload, cancellationToken);
return Results.Json(
response,
statusCode: (int)response.StatusCode);
});
端点现在更加简洁,但我们可以通过扩展方法进一步提升。
使用扩展简化
public static class HandlerExtensions
{
public static async Task<IResult> SendAsync<TRequest, TResponse>(
this IHttpRequestHandler<TRequest, HttpDataResponse<TResponse>> handler,
TRequest request,
CancellationToken cancellationToken)
{
var response = await handler.HandleAsync(request, cancellationToken);
return Results.Json(
response,
statusCode: (int)response.StatusCode);
}
}
有了这个扩展方法,端点逻辑被简化为一次方法调用。端点不再负责构建响应;它仅仅委托执行。
目标模式:简易端点
app.MapPost("/create-forecast",
async (
[FromBody] WeatherForecastPayload payload,
IHttpRequestHandler<WeatherForecastPayload, HttpDataResponse<string[]>> handler,
CancellationToken cancellationToken)
=> await handler.SendAsync(payload, cancellationToken));
这就是我们的目标模式 —— 端点变成了简洁的一行代码。此时,端点不再包含业务或响应逻辑。然而,使用这种实现方式时,IHttpRequestHandler 总是需要同时指定请求类型和响应类型,因此我们需要处理以下情况:
没有输入/负载的请求
对于没有请求体的端点,可以使用一个简单的标记类型,例如一个空记录:
public sealed record EmptyRequest;
空响应
同样的做法也可以用于响应,使 SendAsync 扩展方法能够确定合适的 HTTP 状态码(例如 NoContent、Ok)。
权衡:HTTP 感知的处理程序
使用 HttpDataResponse 时,处理程序是 HTTP‑aware ——它们直接返回 HttpStatusCode。这会使你的处理程序层与 HTTP 传输耦合。
对于许多应用来说,这 务实且足够。处理程序层 就是 你的 HTTP 边界。只需确保:
- 只有
IHttpRequestHandler的实现返回HttpDataResponse - 更低层(领域服务、业务逻辑)保持对传输方式的无感
如果你对这种耦合感到担忧,还有另一种做法。
替代方案:传输无关的处理程序
如果您需要更严格的分离(例如,在 gRPC、消息队列、REST 之间共享处理程序),可以使用传输无关的状态码,这些状态码在 API 层映射到 HTTP 状态码。这使得核心处理程序逻辑完全独立于任何传输关注点。
(进一步的实现细节已省略,以保持简洁。)
统一响应模型(传输无关)
public enum HandlerStatusCode
{
Success = 0,
SuccessWithEmptyResult = 1,
ValidationError = 2,
InternalError = 4
}
public class HandlerResponse<T> : DataResponse<T>
{
[JsonIgnore]
public HandlerStatusCode StatusCode { get; init; }
}
严格处理程序接口
IHttpRequestHandler 已重命名为 IStatusRequestHandler,因为它现在有自己的状态类型,而不是 HTTP 状态。
public interface IStatusRequestHandler<TRequest, TResponse>
{
Task<HandlerResponse<TResponse>> HandleAsync(
TRequest request,
CancellationToken cancellationToken);
}
在扩展方法中映射状态码
public static class HandlerExtensions
{
/// <summary>
/// Executes a request handler and maps the response to an appropriate HTTP result.
/// </summary>
public static async Task<IResult> SendAsync<TRequest, TResponse>(
this IStatusRequestHandler<TRequest, TResponse> requestHandler,
TRequest request,
CancellationToken cancellationToken)
{
var response = await requestHandler.HandleAsync(request, cancellationToken);
return response.StatusCode switch
{
HandlerStatusCode.SuccessWithEmptyResult => Results.NoContent(),
HandlerStatusCode.Success => Results.Json(response, statusCode: (int)HttpStatusCode.OK),
HandlerStatusCode.ValidationError => Results.Json(response, statusCode: (int)HttpStatusCode.BadRequest),
HandlerStatusCode.InternalError => Results.Json(response, statusCode: (int)HttpStatusCode.InternalServerError),
_ => throw new InvalidOperationException($"Unknown HandlerStatusCode: {response.StatusCode}"),
};
}
}
何时使用此方法
- 您正在构建多个传输层(REST、gRPC、消息队列)
- 您在层之间设有严格的架构边界
- 您希望处理程序完全与 HTTP 解耦
解决方案示例
以下示例项目展示了两种方法:
- Using HTTP‑aware handlers – HTTP 响应码直接在处理层处理。
- Using transport‑agnostic handlers – 响应模型独立于 HTTP。
结论
此模式将 Minimal API 端点从多行路由函数转换为单行声明,使其保持一致、可测试且易于维护。
核心洞察: 端点应只负责路由,而不实现业务逻辑。通过统一使用 IHttpRequestHandler/IStatusRequestHandler 及其对应的响应模型,你可以获得:
- 简单的端点可写在一行
- 业务逻辑在可测试的处理程序中隔离
- 在整个 API 中保持一致的错误处理和响应格式
权衡: 处理程序会变为 HTTP 感知。对大多数应用来说,这是正确的选择,因为处理程序位于 HTTP 边界。如果需要与传输层无关的处理程序(用于 gRPC、消息队列等),请使用上文展示的 HandlerStatusCode 方法。