diff --git a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs new file mode 100644 index 0000000..194a7d5 --- /dev/null +++ b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs @@ -0,0 +1,16 @@ +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); + 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 new file mode 100644 index 0000000..bba9b34 --- /dev/null +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -0,0 +1,104 @@ +using System.Text.Json; +using StackExchange.Redis; +using TickAPI.Common.Redis.Abstractions; + +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) + { + _database = connectionMultiplexer.GetDatabase(); + } + + public async Task GetStringAsync(string key) + { + return await RetryAsync(async () => + { + var value = await _database.StringGetAsync(key); + return value.HasValue ? value.ToString() : null; + }); + } + + public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) + { + return await RetryAsync(async () => await _database.StringSetAsync(key, value, expiry)); + } + + public async Task DeleteKeyAsync(string 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); + } + + public async Task SetObjectAsync(string key, T value, TimeSpan? expiry = null) + { + var json = JsonSerializer.Serialize(value, JsonOptions); + + return await SetStringAsync(key, json, expiry); + } + + public async Task KeyExistsAsync(string key) + { + return await RetryAsync(async () => await _database.KeyExistsAsync(key)); + } + + public async Task KeyExpireAsync(string key, TimeSpan expiry) + { + 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 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; + while (true) + { + try + { + return await action(); + } + catch (Exception ex) when (ex is RedisConnectionException or RedisTimeoutException && 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 9e7e07b..59b7d54 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; @@ -35,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"; @@ -116,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(); @@ -154,6 +158,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/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 + 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": {