ASP.NET Core Web API 幂等性

API的幂等性(Idempotent),是指调用某个方法1次或N次对资源产生的影响结果都是相同的。
GET请求默认是幂等的,因为它只是查询资源,而不会修改资源。
而POST请求默认是不幂等的,多次调用POST方法可能会产生不同的结果,并会创建多个资源。
想象一下,你在扫码支付时,输入金额后点击了2次“确定”按钮,肯定不希望扣2次款。
幂等性保证了操作只会执行一次。

1、思路
使用ASP.NET Core过滤器来处理POST请求,检查请求头【Headers】中的幂等键(IdempotencyKey)。
如果在缓存中未检查到IdempotencyKey,则真实执行操作并缓存响应数据,否则直接返回缓存的响应数据。
这样,操作只能对资源产生一次影响。

2、IdempotentAttributeFilter
创建自定义Filter,使用OnActionExecuting方法在执行操作前检查缓存,如有缓存直接返回context.Result;使用OnResultExecuted方法在执行操作后缓存响应。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace WebApi
{
    /// <summary>
    /// 
    /// </summary>
    public class IdempotentAttributeFilter : IActionFilter, IResultFilter
    {
        private readonly IDistributedCache _distributedCache;
        private bool _isIdempotencyCache = false;
        const string IdempotencyKeyHeaderName = "IdempotencyKey";
        private string _idempotencyKey;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="distributedCache"></param>
        public IdempotentAttributeFilter(IDistributedCache distributedCache)
        {
            _distributedCache = distributedCache;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public void OnActionExecuting(ActionExecutingContext context)
        {
            Microsoft.Extensions.Primitives.StringValues idempotencyKeys;
            context.HttpContext.Request.Headers.TryGetValue(IdempotencyKeyHeaderName, out idempotencyKeys);
            _idempotencyKey = idempotencyKeys.ToString();

            var cacheData = _distributedCache.GetString(GetDistributedCacheKey());
            if (cacheData != null)
            {
                context.Result = JsonConvert.DeserializeObject<ObjectResult>(cacheData);
                _isIdempotencyCache = true;
                return;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public void OnResultExecuted(ResultExecutedContext context)
        {
            //已缓存
            if (_isIdempotencyCache)
            {
                return;
            }

            var contextResult = context.Result;

            DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions();

            //相对过期时间
            //cacheOptions.SlidingExpiration = TimeSpan.FromSeconds(10);
            //绝对过期时间
            cacheOptions.AbsoluteExpirationRelativeToNow = new TimeSpan(24, 0, 0);

            //缓存:
            _distributedCache.SetString(GetDistributedCacheKey(), JsonConvert.SerializeObject(contextResult), cacheOptions);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public void OnResultExecuting(ResultExecutingContext context)
        {
        }

        private string GetDistributedCacheKey()
        {
            return "Idempotency:" + _idempotencyKey;
        }
    }
}

3、创建自定义Attribute
声明了IdempotentAttribute的Class或者Method,在运行时会创建IdempotentAttributeFilter。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace WebApi
{
    /// <summary>
    /// 
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    public class IdempotentAttribute : Attribute, IFilterFactory
    {
        /// <summary>
        /// 
        /// </summary>
        public bool IsReusable => false;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="serviceProvider"></param>
        /// <returns></returns>
        public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
        {
            var distributedCache = (IDistributedCache)serviceProvider.GetService(typeof(IDistributedCache));

            var filter = new IdempotentAttributeFilter(distributedCache);
            return filter;
        }
    }
}

4、新建ASP.NET Core Web API项目
创建 WeatherForecastController 控制器,为Post方法加上【Idempotent】
这里用一个静态变量模拟数据库,POST请求写入数据,GET请求读取数据

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace WebApi.Controllers
{
    /// <summary>
    /// 
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class WeatherForecastController : ControllerBase
    {
        private static List<WeatherForecast> _db = new List<WeatherForecast>();

        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        /// <summary>
        /// 
        /// </summary>
        public WeatherForecastController()
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="temperature"></param>
        /// <returns></returns>
        [Idempotent]
        [HttpPost]
        public WeatherForecast Post(int temperature)
        {
            var data = new WeatherForecast { TemperatureC = temperature };
            _db.Add(data);

            return data;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        [HttpGet()]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return _db.Select(p => new WeatherForecast
            {
                TemperatureC = p.TemperatureC,
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }

    public class WeatherForecast
    {
        public DateTime Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string Summary { get; set; }
    }
}

5、注册分布式缓存
必须增加分布式缓存,用于保存幂等键的值和响应数据。
管理 NuGet 程序包(N)...

Microsoft.Extensions.Caching.SqlServer
Microsoft.Extensions.Caching.Redis
Microsoft.Extensions.Caching.StackExchangeRedis

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    //分布式 SQL Server 缓存
    services.AddDistributedSqlServerCache(opt =>
    {
        opt.ConnectionString = Configuration.GetConnectionString("DefaultConnection");
        opt.SchemaName = "dbo";
        opt.TableName = "sys_distributed_cache";
        opt.DefaultSlidingExpiration = TimeSpan.FromMinutes(10);
        opt.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5);
    });

    //分布式 Redis 缓存
    services.AddDistributedRedisCache(cfg =>
    {
        cfg.Configuration = Configuration.GetConnectionString("RedisConnection");
    });

    //分布式 StackExchangeRedis 缓存 
    services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = "localhost";
        options.InstanceName = "SampleInstance";
    });

    //分布式缓存
    services.AddDistributedMemoryCache();
}

在数据库中新建一个名叫“CacheDB”的数据库,然后以管理员身份cmd运行下面指令,会创建一张名叫“CacheTable”表,相应的缓存信息都存在于这张表中。

dotnet sql-cache create <connection string> <schema> <table>

dotnet sql-cache create "Server=localhost;User=sa;Password=000000;Database=CacheDB" dbo CacheTable

成功后会提示【Table and index were created successfully】
表结构

CREATE TABLE [dbo].[CacheTable](
    [Id] [nvarchar](449) NOT NULL,
    [Value] [varbinary](max) NOT NULL,
    [ExpiresAtTime] [datetimeoffset](7) NOT NULL,
    [SlidingExpirationInSeconds] [bigint] NULL,
    [AbsoluteExpiration] [datetimeoffset](7) NULL,
 CONSTRAINT [pk_Id] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
 
CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] ON [dbo].[CacheTable]
(
    [ExpiresAtTime] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

6、测试
运行Web API,使用不同IdempotencyKey执行POST请求,然后获取数据

code

POST /api/WeatherForecast?temperature=1000 HTTP/1.1
Host: localhost:8001
IdempotencyKey: 1000

*
*

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值