diff --git a/src/Infrastructure/Attributes/IdempotencyAttribute.cs b/src/Infrastructure/Attributes/IdempotencyAttribute.cs new file mode 100644 index 0000000..f8d83a0 --- /dev/null +++ b/src/Infrastructure/Attributes/IdempotencyAttribute.cs @@ -0,0 +1,12 @@ +namespace Infrastructure.Attributes; + +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +public sealed class IdempotencyAttribute(string parameter, int seconds = 5, string message = "invalid request") + : Attribute +{ + public string Parameter { get; } = parameter; + + public int Seconds { get; } = seconds; + + public string Message { get; } = message; +} \ No newline at end of file diff --git a/src/Infrastructure/Filters/IdempotencyFilter.cs b/src/Infrastructure/Filters/IdempotencyFilter.cs new file mode 100644 index 0000000..0688625 --- /dev/null +++ b/src/Infrastructure/Filters/IdempotencyFilter.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using System.Text; +using Infrastructure.Attributes; +using Infrastructure.Repository; +using Infrastructure.Utils; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Mvc; + +namespace Infrastructure.Filters; + +public class IdempotencyFilter(ILogger logger, IRedisBasketRepository redis) : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + { + var idempotencyAttribute = + controllerActionDescriptor.MethodInfo.GetCustomAttribute(); + if (idempotencyAttribute is null) + { + await next(); + return; + } + + if (!context.ActionArguments.TryGetValue(idempotencyAttribute!.Parameter, out var value)) + { + logger.LogWarning("idempotency parameter key not found"); + await next(); + return; + } + + var request = context.HttpContext.Request; + var body = value!.Serialize(); + var hashBytes = MD5.HashData(Encoding.ASCII.GetBytes(body)); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + var redisKey = $"{request.Path.Value}:{hashString}"; + if (await redis.Exist(redisKey)) + { + logger.LogWarning("invalid request path: {path},remote ip address:{ip}", request.Path, + context.HttpContext.GetRequestIp()); + var message = new MessageData(false, idempotencyAttribute.Message, 409); + context.Result = new ObjectResult(message) { StatusCode = 200 }; + } + else + { + await redis.Set(redisKey, 0, TimeSpan.FromSeconds(idempotencyAttribute!.Seconds)); + await next(); + } + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/Repository/IRedisBasketRepository.cs b/src/Infrastructure/Repository/IRedisBasketRepository.cs new file mode 100644 index 0000000..3a61f96 --- /dev/null +++ b/src/Infrastructure/Repository/IRedisBasketRepository.cs @@ -0,0 +1,228 @@ +using System.Text; +using Infrastructure.Utils; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using StackExchange.Redis; + +namespace Infrastructure.Repository; + +public interface IRedisBasketRepository +{ + Task GetValue(string key); + + Task Get(string key); + + Task> GetValues(string[] keys) where T : class; + + Task Set(string key, object? value, TimeSpan cacheTime); + + Task SetValues(Dictionary valuePairs, TimeSpan cacheTime); + + Task Exist(string key); + + Task Remove(string key); + + Task Clear(); + + Task ListRangeAsync(string redisKey); + + Task ListLeftPushAsync(string redisKey, string redisValue, int db = -1); + + Task ListRightPushAsync(string redisKey, string redisValue, int db = -1); + + Task ListRightPushAsync(string redisKey, IEnumerable redisValue, int db = -1); + + Task ListLeftPopAsync(string redisKey, int db = -1) where T : class; + + Task ListRightPopAsync(string redisKey, int db = -1) where T : class; + + Task ListLeftPopAsync(string redisKey, int db = -1); + + Task ListRightPopAsync(string redisKey, int db = -1); + + Task ListLengthAsync(string redisKey, int db = -1); + + Task> ListRangeAsync(string redisKey, int db = -1); + + Task> ListRangeAsync(string redisKey, int start, int stop, int db = -1); + + Task ListDelRangeAsync(string redisKey, string redisValue, long type = 0, int db = -1); + + Task ListClearAsync(string redisKey, int db = -1); +} + +public class RedisBasketRepository(ILogger logger, ConnectionMultiplexer redis) + : IRedisBasketRepository +{ + private readonly IDatabase _database = redis.GetDatabase(); + + private IServer GetServer() + { + var endpoint = redis.GetEndPoints(); + return redis.GetServer(endpoint.First()); + } + + public async Task Clear() + { + foreach (var endPoint in redis.GetEndPoints()) + { + var server = GetServer(); + foreach (var key in server.Keys()) + { + await _database.KeyDeleteAsync(key); + } + } + } + + public async Task Exist(string key) + { + return await _database.KeyExistsAsync(key); + } + + public async Task GetValue(string key) + { + return await _database.StringGetAsync(key); + } + + public async Task Remove(string key) + { + await _database.KeyDeleteAsync(key); + } + + public async Task Set(string key, object value, TimeSpan cacheTime) + { + if (value != null) + { + if (value is string cacheValue) + { + await _database.StringSetAsync(key, cacheValue, cacheTime); + } + else + { + var jsonString = value.Serialize(); + var buffer = Encoding.UTF8.GetBytes(jsonString); + await _database.StringSetAsync(key, buffer, cacheTime); + } + } + } + + public async Task SetValues(Dictionary valuePairs, TimeSpan cacheTime) + { + var transaction = _database.CreateTransaction(); + foreach (var pair in valuePairs) + { + if (pair.Value is string value) + { + await _database.StringSetAsync(pair.Key, value, cacheTime); + } + else + { + var jsonString = pair.Value.Serialize(); + var buffer = Encoding.UTF8.GetBytes(jsonString); + await _database.StringSetAsync(pair.Key, buffer, cacheTime); + } + } + + return await transaction.ExecuteAsync(); + } + + public async Task Get(string key) + { + var value = await _database.StringGetAsync(key); + if (value.HasValue) + { + var jsonString = Encoding.UTF8.GetString(value); + return jsonString.Deserialize(); + } + else + { + return default; + } + } + + public async Task> GetValues(string[] keys) where T : class + { + var redisKeys = keys.Select(k => new RedisKey(k)).ToArray(); + var redisValues = await _database.StringGetAsync(redisKeys); + var result = new List(); + foreach (var value in redisValues) + { + if (value.HasValue) + { + result.Add(SerializeExtension.Deserialize(value)); + } + } + + return result; + } + + public async Task ListRangeAsync(string redisKey) + { + return await _database.ListRangeAsync(redisKey); + } + + public async Task ListLeftPushAsync(string redisKey, string redisValue, int db = -1) + { + return await _database.ListLeftPushAsync(redisKey, redisValue); + } + + public async Task ListRightPushAsync(string redisKey, string redisValue, int db = -1) + { + return await _database.ListRightPushAsync(redisKey, redisValue); + } + + public async Task ListRightPushAsync(string redisKey, IEnumerable redisValue, int db = -1) + { + var redislist = redisValue.Select(r => (RedisValue)r).ToArray(); + return await _database.ListRightPushAsync(redisKey, redislist); + } + + public async Task ListLeftPopAsync(string redisKey, int db = -1) where T : class + { + var value = await _database.ListLeftPopAsync(redisKey); + + return SerializeExtension.Deserialize(await _database.ListLeftPopAsync(redisKey)); + } + + public async Task ListRightPopAsync(string redisKey, int db = -1) where T : class + { + return SerializeExtension.Deserialize(await _database.ListRightPopAsync(redisKey)); + } + + public async Task ListLeftPopAsync(string redisKey, int db = -1) + { + return await _database.ListLeftPopAsync(redisKey); + } + + public async Task ListRightPopAsync(string redisKey, int db = -1) + { + return await _database.ListRightPopAsync(redisKey); + } + + public async Task ListLengthAsync(string redisKey, int db = -1) + { + return await _database.ListLengthAsync(redisKey); + } + + public async Task> ListRangeAsync(string redisKey, int db = -1) + { + var result = await _database.ListRangeAsync(redisKey); + return result.Select(o => o.ToString()); + } + + public async Task> ListRangeAsync(string redisKey, int start, int stop, int db = -1) + { + var result = await _database.ListRangeAsync(redisKey, start, stop); + return result.Select(o => o.ToString()); + } + + public async Task ListDelRangeAsync(string redisKey, string redisValue, long type = 0, int db = -1) + { + return await _database.ListRemoveAsync(redisKey, redisValue, type); + } + + public async Task ListClearAsync(string redisKey, int db = -1) + { + await _database.ListTrimAsync(redisKey, 1, 0); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Repository/IUnitOfWork.cs b/src/Infrastructure/Repository/IUnitOfWork.cs new file mode 100644 index 0000000..bb01ae1 --- /dev/null +++ b/src/Infrastructure/Repository/IUnitOfWork.cs @@ -0,0 +1,71 @@ +using SqlSugar; + +namespace Infrastructure.Repository; + +public interface IUnitOfWork +{ + SqlSugarScope DbClient { get; } + + void BeginTransaction(); + + void CommitTransaction(); + + void RollbackTransaction(); +} + +public class UnitOfWork : IUnitOfWork +{ + private int _count; + + private readonly SqlSugarScope? _dbClient; + + public SqlSugarScope DbClient => _dbClient!; + + public UnitOfWork(ISqlSugarClient sqlSugarClient) + { + if (sqlSugarClient is SqlSugarScope scope) + { + _dbClient = scope; + } + } + + public void BeginTransaction() + { + lock (this) + { + _count++; + _dbClient?.BeginTran(); + } + } + + public void CommitTransaction() + { + lock (this) + { + _count--; + if (_count != 0) + { + return; + } + + try + { + _dbClient?.CommitTran(); + } + catch (Exception e) + { + Console.WriteLine(e); + _dbClient?.RollbackTran(); + } + } + } + + public void RollbackTransaction() + { + lock (this) + { + _count--; + _dbClient?.RollbackTran(); + } + } +} \ No newline at end of file