前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >在 ASP.NET Core 中实现幂等 REST API

在 ASP.NET Core 中实现幂等 REST API

作者头像
郑子铭
发布2024-12-24 14:18:44
发布2024-12-24 14:18:44
10200
代码可运行
举报
运行总次数:0
代码可运行

幂等性是 REST API 的一个关键概念,可确保系统的可靠性和一致性。幂等操作可以重复多次,而不会更改初始 API 请求之外的结果。此属性在分布式系统中尤其重要,因为网络故障或超时可能会导致重复请求。

在 API 中实现幂等性有几个好处:

  • 它可以防止意外的重复操作
  • 它提高了分布式系统的可靠性
  • 它有助于处理网络问题并正常重试

在本周的期刊中,我们将探讨如何在 ASP.NET Core API 中实现幂等性,以确保您的系统保持稳健可靠。

什么是幂等性?

在 Web API 的上下文中,幂等意味着发出多个相同的请求应具有与发出单个请求相同的效果。换句话说,无论客户端发送同一请求多少次,服务器端效果都应该只发生一次。

关于 HTTP 语义的 RFC 9110 标准提供了我们可以使用的定义。以下是它对幂等方法的描述:

如果使用该方法的多个相同请求对服务器的预期效果与单个此类请求的效果相同,则认为该请求方法是“幂等的”。 在本规范定义的请求方法中,PUT、DELETE 和安全请求方法 [(GET、HEAD、OPTIONS 和 TRACE) — 作者注] 是幂等的。

- RFC 9110(HTTP 语义),第 9.2.2 节,第 1 段

但是,以下段落非常有趣。它阐明了服务器可以实现不适用于资源的“其他非幂等副作用”。

…幂等属性仅适用于用户请求的内容;服务器可以自由地单独记录每个请求,保留修订控制历史记录,或为每个幂等请求实现其他非幂等副作用。

- RFC 9110(HTTP 语义),第 9.2.2 节,第 2 段

实现幂等性的好处不仅限于遵守 HTTP 方法语义。它显著提高了 API 的可靠性,尤其是在网络问题可能导致重试请求的分布式系统中。通过实施幂等性,可以防止由于客户端重试而发生的重复操作。

哪些 HTTP 方法是幂等的?

几种 HTTP 方法本质上是幂等的:

  • GET, : 在不修改服务器状态的情况下检索数据。HEAD
  • PUT:更新资源,无论是否重复,都会产生相同的状态。
  • DELETE:删除多个请求具有相同结果的资源。
  • OPTIONS:检索通信选项信息。

POST本身并不是幂等的,因为它通常会创建资源或处理数据。重复请求可能会创建多个资源或触发多个操作。POST

但是,我们可以为使用自定义逻辑的方法实现幂等性。POST

注意:虽然请求不是天生的幂等的,但我们可以将它们设计为幂等的。例如,在创建之前检查现有资源可确保重复请求不会导致重复的操作或资源。POSTPOST

在 ASP.NET Core 中实现幂等性

为了实现幂等性,我们将使用涉及幂等性键的策略:

  1. 客户端为每个操作生成一个唯一密钥,并在自定义标头中发送该密钥。
  2. 服务器检查之前是否见过此键:
  • 对于新密钥,请处理请求并存储结果。
  • 对于已知键,返回存储的结果而不重新处理。

这可确保重试的请求(例如,由于网络问题)在服务器上仅处理一次。

我们可以通过组合 an 和 来实现控制器的幂等性。现在,我们可以指定将幂等性应用于控制器终端节点。AttributeIAsyncActionFilterIdempotentAttribute

注意:当请求失败(返回 4xx/5xx)时,我们不会缓存响应。这允许客户端使用相同的幂等密钥重试。但是,这意味着失败的请求后跟具有相同键的成功请求将成功 - 请确保这符合您的业务需求。

代码语言:javascript
代码运行次数:0
复制
[AttributeUsage(AttributeTargets.Method)]
internalsealedclassIdempotentAttribute:Attribute, IAsyncActionFilter
{
    privateconstint DefaultCacheTimeInMinutes =;
    privatereadonlyTimeSpan _cacheDuration;

    publicIdempotentAttribute(int cacheTimeInMinutes = DefaultCacheTimeInMinutes)
    {
        _cacheDuration = TimeSpan.FromMinutes(minutes);
    }

    publicasyncTaskOnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // Parse the Idempotence-Key header from the request
        if(!context.HttpContext.Request.Headers.TryGetValue(
                "Idempotence-Key",
                outStringValues idempotenceKeyValue)||
            !Guid.TryParse(idempotenceKeyValue,outGuid idempotenceKey))
        {
            context.Result =newBadRequestObjectResult("Invalid or missing Idempotence-Key header");
            return;
        }

        IDistributedCache cache = context.HttpContext
            .RequestServices.GetRequiredService<IDistributedCache>();

        // Check if we already processed this request and return a cached response (if it exists)
        string cacheKey =$"Idempotent_{idempotenceKey}";
        string? cachedResult =await cache.GetStringAsync(cacheKey);
        if(cachedResult isnotnull)
        {
            IdempotentResponse response = JsonSerializer.Deserialize<IdempotentResponse>(cachedResult)!;

            var result =newObjectResult(response.Value){ StatusCode = response.StatusCode };
            context.Result = result;

            return;
        }

        // Execute the request and cache the response for the specified duration
        ActionExecutedContext executedContext =awaitnext();

        if(executedContext.Result isObjectResult{ StatusCode:>=and<} objectResult)
        {
            int statusCode = objectResult.StatusCode ?? StatusCodes.Status200OK;
            IdempotentResponse response =new(statusCode, objectResult.Value);

            await cache.SetStringAsync(
                cacheKey,
                JsonSerializer.Serialize(response),
                newDistributedCacheEntryOptions{ AbsoluteExpirationRelativeToNow = _cacheDuration }
            );
        }
    }
}

internalsealedclassIdempotentResponse
{
    [JsonConstructor]
    publicIdempotentResponse(int statusCode,object?value)
    {
        StatusCode = statusCode;
        Value =value;
    }

    publicint StatusCode {get;}
    publicobject? Value {get;}
}

注意:在检查和设置缓存之间有一个小的争用条件窗口。为了实现绝对一致性,我们应该考虑使用分布式锁模式,尽管这会增加复杂性和延迟。

现在,我们可以将此属性应用于我们的控制器操作:

代码语言:javascript
代码运行次数:0
复制
[ApiController]
[Route("api/[controller]")]
publicclassOrdersController:ControllerBase
{
    [HttpPost]
    [Idempotent(cacheTimeInMinutes: )]
    publicIActionResultCreateOrder([FromBody]CreateOrderRequest request)
    {
        // Process the order...

        returnCreatedAtAction(nameof(GetOrder),new{ id = orderDto.Id }, orderDto);
    }
}

最少 API 的幂等性

要使用 Minimal API 实现幂等性,我们可以使用 .IEndpointFilter

代码语言:javascript
代码运行次数:0
复制
internal sealedclassIdempotencyFilter(int cacheTimeInMinutes =)
    : IEndpointFilter
{
    publicasyncValueTask<object?>InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        // Parse the Idempotence-Key header from the request
        if(TryGetIdempotenceKey(outGuid idempotenceKey))
        {
            return Results.BadRequest("Invalid or missing Idempotence-Key header");
        }

        IDistributedCache cache = context.HttpContext
            .RequestServices.GetRequiredService<IDistributedCache>();

        // Check if we already processed this request and return a cached response (if it exists)
        string cacheKey =$"Idempotent_{idempotenceKey}";
        string? cachedResult =await cache.GetStringAsync(cacheKey);
        if(cachedResult isnotnull)
        {
            IdempotentResponse response = JsonSerializer.Deserialize<IdempotentResponse>(cachedResult)!;
            returnnewIdempotentResult(response.StatusCode, response.Value);
        }

        object? result =awaitnext(context);

        // Execute the request and cache the response for the specified duration
        if(result isIStatusCodeHttpResult{ StatusCode:>=and<} statusCodeResult
            andIValueHttpResult valueResult)
        {
            int statusCode = statusCodeResult.StatusCode ?? StatusCodes.Status200OK;
            IdempotentResponse response =new(statusCode, valueResult.Value);

            await cache.SetStringAsync(
                cacheKey,
                JsonSerializer.Serialize(response),
                newDistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(cacheTimeInMinutes)
                }
            );
        }

        return result;
    }
}

// We have to implement a custom result to write the status code
internalsealedclassIdempotentResult:IResult
{
    privatereadonlyint _statusCode;
    privatereadonlyobject? _value;

    publicIdempotentResult(int statusCode,object?value)
    {
        _statusCode = statusCode;
        _value =value;
    }

    publicTaskExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.StatusCode = _statusCode;

        return httpContext.Response.WriteAsJsonAsync(_value);
    }
}

现在,我们可以将此终端节点过滤器应用于我们的 Minimal API 终端节点:

代码语言:javascript
代码运行次数:0
复制
app.MapPost("/api/orders", CreateOrder)  
    .RequireAuthorization()  
    .WithOpenApi()  
    .AddEndpointFilter<IdempotencyFilter>();

前两个实现的替代方案是在自定义中间件中实现幂等逻辑。

最佳实践和注意事项

以下是我在实现幂等性时始终牢记的关键事项。

缓存持续时间很棘手。我的目标是在不保留过时数据的情况下覆盖合理的重试窗口。合理的缓存时间通常从几分钟到 24-48 小时不等,具体取决于您的具体使用案例。

并发可能很痛苦,尤其是在高流量 API 中。使用分布式锁的线程安全实现效果很好。当同时收到多个请求时,它可以控制事情。但这应该是罕见的。

对于分布式设置,Redis 是我的首选。它非常适合作为共享缓存,在所有 API 实例之间保持幂等性一致。此外,它还处理分布式锁定。

如果客户端将幂等性密钥重新用于不同的请求正文,该怎么办?在这种情况下,我返回一个错误。我的方法是对请求正文进行哈希处理,并使用幂等键存储它。当收到请求时,我会比较请求正文的哈希值。如果它们不同,我将返回一个错误。这可以防止滥用幂等密钥并保持 API 的完整性。

在 REST API 中实现幂等性可以提高服务的可靠性和一致性。它确保相同的请求产生相同的结果,防止意外的重复并妥善处理网络问题。

虽然我们的实施提供了一个基础,但我建议您根据自己的需求进行调整。专注于 API 中的关键操作,尤其是那些修改系统状态或触发重要业务流程的操作。

通过采用幂等性,您可以构建更强大且用户友好的 API。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是幂等性?
  • 哪些 HTTP 方法是幂等的?
  • 在 ASP.NET Core 中实现幂等性
  • 最佳实践和注意事项
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档