From 6886d01cc9d6c7bb1d5c8f93b069ce7489fc964d Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 16:25:35 +0200 Subject: [PATCH 1/8] added redis to project --- TickAPI/TickAPI/TickAPI.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index aeff0ec..2f1a9cc 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -18,6 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + From af6dbcb6e4aebb1e204be95cdaa0d2b76439b9f3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 16:49:00 +0200 Subject: [PATCH 2/8] added connection multiplexer, connecting to appropriate Redis address --- TickAPI/TickAPI/Program.cs | 5 +++++ TickAPI/TickAPI/appsettings.example.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 9e7e07b..685704d 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis; using TickAPI.Admins.Abstractions; using TickAPI.Admins.Repositories; using TickAPI.Admins.Services; @@ -154,6 +155,10 @@ errorNumbersToAdd: null)); }); +builder.Services.AddSingleton(sp => + ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("ResellioRedisCache")) +); + // Create CORS policy builder.Services.AddCors(options => { diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 9070fd3..04cae82 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -11,7 +11,8 @@ "https://another-site.com" ], "ConnectionStrings": { - "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=resellioDB;Persist Security Info=False;User ID=sa;Password=Rese11io;TrustServerCertificate=True;Encrypt=False" + "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=resellioDB;Persist Security Info=False;User ID=sa;Password=Rese11io;TrustServerCertificate=True;Encrypt=False", + "ResellioRedisCache": "localhost:6379" }, "Authentication": { "Google": { From 1db8acdff78bbb397f15ad8dd99f01e8b1b7ea70 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 17:14:14 +0200 Subject: [PATCH 3/8] added redis service with basic method declarations --- .../Redis/Abstractions/IRedisService.cs | 10 ++++ .../Common/Redis/Services/RedisService.cs | 56 +++++++++++++++++++ TickAPI/TickAPI/Program.cs | 3 + 3 files changed, 69 insertions(+) create mode 100644 TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs create mode 100644 TickAPI/TickAPI/Common/Redis/Services/RedisService.cs diff --git a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs new file mode 100644 index 0000000..8dd6d0d --- /dev/null +++ b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Common.Redis.Abstractions; + +public interface IRedisService +{ + public Task GetStringAsync(string key); + public Task SetStringAsync(string key, string value, TimeSpan? expiry = null); + public Task DeleteKeyAsync(string key); + public Task GetObjectAsync(string key); + public Task SetObjectAsync(string key, T value, TimeSpan? expiry = null); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs new file mode 100644 index 0000000..ed87395 --- /dev/null +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -0,0 +1,56 @@ +using StackExchange.Redis; +using TickAPI.Common.Redis.Abstractions; + +namespace TickAPI.Common.Redis.Services; + +public class RedisService : IRedisService +{ + private readonly IDatabase _database; + + public RedisService(IConnectionMultiplexer connectionMultiplexer) + { + _database = connectionMultiplexer.GetDatabase(); + } + + public Task GetStringAsync(string key) + { + throw new NotImplementedException(); + } + + public Task SetStringAsync(string key, string value, TimeSpan? expiry = null) + { + throw new NotImplementedException(); + } + + public Task DeleteKeyAsync(string key) + { + throw new NotImplementedException(); + } + + public Task GetObjectAsync(string key) + { + throw new NotImplementedException(); + } + + public Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) + { + throw new NotImplementedException(); + } + + private async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) + { + var attempt = 0; + while (true) + { + try + { + return await action(); + } + catch (RedisConnectionException) when (attempt < retryCount) + { + attempt++; + await Task.Delay(millisecondsDelay); + } + } + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 685704d..59b7d54 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -36,6 +36,8 @@ using TickAPI.Categories.Services; using TickAPI.Common.Claims.Abstractions; using TickAPI.Common.Claims.Services; +using TickAPI.Common.Redis.Abstractions; +using TickAPI.Common.Redis.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -117,6 +119,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); From e14337d8a8c3f2ee6047ce60373475ab339dd93f Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 17:36:47 +0200 Subject: [PATCH 4/8] implemented basic RedisService methods --- .../Common/Redis/Services/RedisService.cs | 64 +++++++++++++++---- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs index ed87395..630cf19 100644 --- a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -1,4 +1,5 @@ -using StackExchange.Redis; +using System.Text.Json; +using StackExchange.Redis; using TickAPI.Common.Redis.Abstractions; namespace TickAPI.Common.Redis.Services; @@ -12,31 +13,68 @@ public RedisService(IConnectionMultiplexer connectionMultiplexer) _database = connectionMultiplexer.GetDatabase(); } - public Task GetStringAsync(string key) + public async Task GetStringAsync(string key) { - throw new NotImplementedException(); + return await RetryAsync(async () => + { + var value = await _database.StringGetAsync(key); + return value.HasValue ? value.ToString() : null; + }); } - public Task SetStringAsync(string key, string value, TimeSpan? expiry = null) + public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) { - throw new NotImplementedException(); + await RetryAsync(async () => + { + await _database.StringSetAsync(key, value, expiry); + }); } - public Task DeleteKeyAsync(string key) + public async Task DeleteKeyAsync(string key) { - throw new NotImplementedException(); + return await RetryAsync(async () => + { + return await _database.KeyDeleteAsync(key); + }); } - public Task GetObjectAsync(string key) + public async Task GetObjectAsync(string key) { - throw new NotImplementedException(); + var json = await GetStringAsync(key); + + if (string.IsNullOrEmpty(json)) + { + return default; + } + + return JsonSerializer.Deserialize(json); } - public Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) + public async Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) { - throw new NotImplementedException(); + var json = JsonSerializer.Serialize(value); + + await SetStringAsync(key, json, expiry); } - + + private async Task RetryAsync(Func action, int retryCount = 3, int millisecondsDelay = 100) + { + var attempt = 0; + while (true) + { + try + { + await action(); + return; + } + catch (Exception ex) when (ex is RedisConnectionException or RedisTimeoutException && attempt < retryCount) + { + attempt++; + await Task.Delay(millisecondsDelay); + } + } + } + private async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) { var attempt = 0; @@ -46,7 +84,7 @@ private async Task RetryAsync(Func> action, int retryCount = 3, in { return await action(); } - catch (RedisConnectionException) when (attempt < retryCount) + catch (Exception ex) when (ex is RedisConnectionException or RedisTimeoutException && attempt < retryCount) { attempt++; await Task.Delay(millisecondsDelay); From 8328b649226954db4dc99fa5acd34b5f45456a17 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 20:36:42 +0200 Subject: [PATCH 5/8] added json serializer options and made the RetryAsync methods static --- TickAPI/TickAPI/Common/Redis/Services/RedisService.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs index 630cf19..8a0d86e 100644 --- a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -7,6 +7,7 @@ namespace TickAPI.Common.Redis.Services; public class RedisService : IRedisService { private readonly IDatabase _database; + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); public RedisService(IConnectionMultiplexer connectionMultiplexer) { @@ -47,17 +48,17 @@ public async Task DeleteKeyAsync(string key) return default; } - return JsonSerializer.Deserialize(json); + return JsonSerializer.Deserialize(json, _jsonOptions); } public async Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) { - var json = JsonSerializer.Serialize(value); + var json = JsonSerializer.Serialize(value, _jsonOptions); await SetStringAsync(key, json, expiry); } - private async Task RetryAsync(Func action, int retryCount = 3, int millisecondsDelay = 100) + private static async Task RetryAsync(Func action, int retryCount = 3, int millisecondsDelay = 100) { var attempt = 0; while (true) @@ -75,7 +76,7 @@ private async Task RetryAsync(Func action, int retryCount = 3, int millise } } - private async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) + private static async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) { var attempt = 0; while (true) From bc74f8e3c09475b94aa1d7be77c84a54e2fadfc9 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 20:46:05 +0200 Subject: [PATCH 6/8] Made Set methods of the Redis service return bool; added two additional utility methods --- .../Redis/Abstractions/IRedisService.cs | 6 ++- .../Common/Redis/Services/RedisService.cs | 38 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs index 8dd6d0d..6db23c5 100644 --- a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs @@ -3,8 +3,10 @@ public interface IRedisService { public Task GetStringAsync(string key); - public Task SetStringAsync(string key, string value, TimeSpan? expiry = null); + public Task SetStringAsync(string key, string value, TimeSpan? expiry = null); public Task DeleteKeyAsync(string key); public Task GetObjectAsync(string key); - public Task SetObjectAsync(string key, T value, TimeSpan? expiry = null); + public Task SetObjectAsync(string key, T value, TimeSpan? expiry = null); + public Task KeyExistsAsync(string key); + public Task KeyExpireAsync(string key, TimeSpan expiry); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs index 8a0d86e..cdb8c01 100644 --- a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -23,11 +23,11 @@ public RedisService(IConnectionMultiplexer connectionMultiplexer) }); } - public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) + public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) { - await RetryAsync(async () => + return await RetryAsync(async () => { - await _database.StringSetAsync(key, value, expiry); + return await _database.StringSetAsync(key, value, expiry); }); } @@ -51,29 +51,27 @@ public async Task DeleteKeyAsync(string key) return JsonSerializer.Deserialize(json, _jsonOptions); } - public async Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) + public async Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) { var json = JsonSerializer.Serialize(value, _jsonOptions); - await SetStringAsync(key, json, expiry); + return await SetStringAsync(key, json, expiry); } - - private static async Task RetryAsync(Func action, int retryCount = 3, int millisecondsDelay = 100) + + public async Task KeyExistsAsync(string key) { - var attempt = 0; - while (true) + return await RetryAsync(async () => { - try - { - await action(); - return; - } - catch (Exception ex) when (ex is RedisConnectionException or RedisTimeoutException && attempt < retryCount) - { - attempt++; - await Task.Delay(millisecondsDelay); - } - } + return await _database.KeyExistsAsync(key); + }); + } + + public async Task KeyExpireAsync(string key, TimeSpan expiry) + { + return await RetryAsync(async () => + { + return await _database.KeyExpireAsync(key, expiry); + }); } private static async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) From e4896782125a32c8fa875159c8b96bafdebe3938 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 22:02:56 +0200 Subject: [PATCH 7/8] added Redis service methods for manipulating counters --- .../Redis/Abstractions/IRedisService.cs | 4 ++ .../Common/Redis/Services/RedisService.cs | 47 ++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs index 6db23c5..194a7d5 100644 --- a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs @@ -9,4 +9,8 @@ public interface IRedisService public Task SetObjectAsync(string key, T value, TimeSpan? expiry = null); public Task KeyExistsAsync(string key); public Task KeyExpireAsync(string key, TimeSpan expiry); + public Task IncrementValueAsync(string key, long value = 1); + public Task DecrementValueAsync(string key, long value = 1); + public Task GetLongValueAsync(string key); + public Task SetLongValueAsync(string key, long value, TimeSpan? expiry = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs index cdb8c01..34dba4c 100644 --- a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -25,29 +25,21 @@ public RedisService(IConnectionMultiplexer connectionMultiplexer) public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) { - return await RetryAsync(async () => - { - return await _database.StringSetAsync(key, value, expiry); - }); + return await RetryAsync(async () => await _database.StringSetAsync(key, value, expiry)); } public async Task DeleteKeyAsync(string key) { - return await RetryAsync(async () => - { - return await _database.KeyDeleteAsync(key); - }); + return await RetryAsync(async () => await _database.KeyDeleteAsync(key)); } public async Task GetObjectAsync(string key) { var json = await GetStringAsync(key); - if (string.IsNullOrEmpty(json)) { return default; } - return JsonSerializer.Deserialize(json, _jsonOptions); } @@ -60,20 +52,39 @@ public async Task SetObjectAsync(string key, T value, TimeSpan? expiry public async Task KeyExistsAsync(string key) { - return await RetryAsync(async () => - { - return await _database.KeyExistsAsync(key); - }); + return await RetryAsync(async () => await _database.KeyExistsAsync(key)); } public async Task KeyExpireAsync(string key, TimeSpan expiry) { - return await RetryAsync(async () => + return await RetryAsync(async () => await _database.KeyExpireAsync(key, expiry)); + } + + public async Task IncrementValueAsync(string key, long value = 1) + { + return await RetryAsync(async () => await _database.StringIncrementAsync(key, value)); + } + + public async Task DecrementValueAsync(string key, long value = 1) + { + return await RetryAsync(async () => await _database.StringDecrementAsync(key, value)); + } + + public async Task GetLongValueAsync(string key) + { + var value = await RetryAsync(() => _database.StringGetAsync(key)); + if (value.HasValue && long.TryParse(value, out var result)) { - return await _database.KeyExpireAsync(key, expiry); - }); + return result; + } + return null; } - + + public async Task SetLongValueAsync(string key, long value, TimeSpan? expiry = null) + { + return await RetryAsync(async () => await _database.StringSetAsync(key, value.ToString(), expiry)); + } + private static async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) { var attempt = 0; From 8e19596bc34f8309cee8bc21bc20e34ccb43e83b Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 26 Apr 2025 22:14:13 +0200 Subject: [PATCH 8/8] private static field rename for consistency purposes --- TickAPI/TickAPI/Common/Redis/Services/RedisService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs index 34dba4c..bba9b34 100644 --- a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -7,7 +7,7 @@ namespace TickAPI.Common.Redis.Services; public class RedisService : IRedisService { private readonly IDatabase _database; - private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); public RedisService(IConnectionMultiplexer connectionMultiplexer) { @@ -40,12 +40,12 @@ public async Task DeleteKeyAsync(string key) { return default; } - return JsonSerializer.Deserialize(json, _jsonOptions); + return JsonSerializer.Deserialize(json, JsonOptions); } public async Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) { - var json = JsonSerializer.Serialize(value, _jsonOptions); + var json = JsonSerializer.Serialize(value, JsonOptions); return await SetStringAsync(key, json, expiry); }