C# Minimal API:保持端点整洁的实用方法

发布: (2025年12月22日 GMT+8 22:09)
8 min read
原文: Dev.to

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 状态码(例如 NoContentOk)。

权衡: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 方法。

Back to Blog

相关文章

阅读更多 »

C# Minimal API:输出缓存

Minimal API:输出缓存 将生成的响应存储在服务器上,并直接提供,而无需重新执行端点。Microsoft Docs https://learn.m...