From 44dd4cbee9aa269d3a957fb7100eccd73b27bdab Mon Sep 17 00:00:00 2001 From: cihataydin Date: Sun, 8 Jun 2025 16:34:26 +0300 Subject: [PATCH 1/2] feat(46): implements caching with Redis and in-memory cache by adding cache manager --- src/Api/Microservice.csproj | 2 + src/Api/Program.cs | 10 ++++ src/Api/appsettings.Development.json | 7 ++- src/Domain/Interfaces/ICacheManager.cs | 11 ++++ src/Domain/Services/SampleService.cs | 29 +++++++++- src/Infra/Cache/CacheManager.cs | 76 ++++++++++++++++++++++++++ 6 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/Domain/Interfaces/ICacheManager.cs create mode 100644 src/Infra/Cache/CacheManager.cs diff --git a/src/Api/Microservice.csproj b/src/Api/Microservice.csproj index 8ee4870..29fa78a 100644 --- a/src/Api/Microservice.csproj +++ b/src/Api/Microservice.csproj @@ -24,6 +24,8 @@ + + diff --git a/src/Api/Program.cs b/src/Api/Program.cs index ab2535b..f4b4290 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -6,6 +6,7 @@ using Asp.Versioning.Builder; using Domain.Interfaces; using Domain.Services; +using Infra.Cache; using Infra.Data; using Infra.Repositories; using Microsoft.EntityFrameworkCore; @@ -49,6 +50,15 @@ builder.Host.UseSerilog(); +builder.Services.AddMemoryCache(); +builder.Services.AddStackExchangeRedisCache(opts => +{ + opts.Configuration = builder.Configuration.GetConnectionString("Redis"); + opts.InstanceName = "SolutionTemplate:"; +}); +builder.Services.Configure(builder.Configuration.GetSection("CacheOptions")); +builder.Services.AddSingleton(); + var app = builder.Build(); app.UseHttpMetrics(); diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 2f2906d..96fa6d1 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -9,6 +9,11 @@ }, "UseJsonFormat": false, "ConnectionStrings": { - "PgpoolDb": "" + "PgpoolDb": "", + "Redis": "" + }, + "CacheOptions": { + "MemoryCacheDuration": "00:02:00", + "DistributedCacheDuration": "00:10:00" } } \ No newline at end of file diff --git a/src/Domain/Interfaces/ICacheManager.cs b/src/Domain/Interfaces/ICacheManager.cs new file mode 100644 index 0000000..bd4c1a4 --- /dev/null +++ b/src/Domain/Interfaces/ICacheManager.cs @@ -0,0 +1,11 @@ +using System; + +namespace Domain.Interfaces +{ + public interface ICacheManager + { + Task GetAsync(string key); + Task SetAsync(string key, T value); + Task RemoveAsync(string key); + } +} diff --git a/src/Domain/Services/SampleService.cs b/src/Domain/Services/SampleService.cs index ca90113..e462cf0 100644 --- a/src/Domain/Services/SampleService.cs +++ b/src/Domain/Services/SampleService.cs @@ -13,15 +13,26 @@ public class SampleService : ISampleService private readonly IRepository _repository; private readonly IMapper _mapper; + private readonly ICacheManager _cacheManager; + public SampleService(IRepository repository, - IMapper mapper) + IMapper mapper, + ICacheManager cacheManager) { _repository = repository; _mapper = mapper; + _cacheManager = cacheManager; } public async Task GetSampleAsync(Guid id, CancellationToken cancellationToken = default) { + var cachedSample = await _cacheManager.GetAsync($"Sample_{id}"); + + if (cachedSample is not null) + { + return _mapper.Map(cachedSample); + } + var entity = await _repository.GetByIdAsync(id, cancellationToken) ?? throw new DomainException($"Sample with ID {id} not found."); return _mapper.Map(entity); @@ -66,15 +77,24 @@ public async Task CreateSampleAsync(CreateSampleReque { var entity = _mapper.Map(request); var createdEntity = await _repository.AddAsync(entity, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + + await _cacheManager.SetAsync($"Sample_{createdEntity.Id}", createdEntity); + return _mapper.Map(createdEntity); } public async Task UpdateSampleAsync(UpdateSampleRequestModel request, CancellationToken cancellationToken = default) { var entity = _mapper.Map(request); + var updatedEntity = _repository.Update(entity); + await _repository.SaveChangesAsync(cancellationToken); + + await _cacheManager.SetAsync($"Sample_{updatedEntity.Id}", updatedEntity); + return _mapper.Map(updatedEntity); } @@ -84,6 +104,11 @@ public async Task DeleteSampleAsync(Guid id, CancellationToken cancellatio if (entity == null) return false; _repository.Remove(entity); - return true && await _repository.SaveChangesAsync(cancellationToken) > 0; + + var result = await _repository.SaveChangesAsync(cancellationToken); + + await _cacheManager.RemoveAsync($"Sample_{id}"); + + return result > 0; } } \ No newline at end of file diff --git a/src/Infra/Cache/CacheManager.cs b/src/Infra/Cache/CacheManager.cs new file mode 100644 index 0000000..7d8d142 --- /dev/null +++ b/src/Infra/Cache/CacheManager.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using Domain.Interfaces; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace Infra.Cache; + +public class CacheManagerOptions +{ + public TimeSpan MemoryCacheDuration { get; set; } = TimeSpan.FromMinutes(2); + public TimeSpan DistributedCacheDuration { get; set; } = TimeSpan.FromMinutes(10); + public JsonSerializerOptions? JsonOptions { get; set; } +} + +public class CacheManager: ICacheManager +{ + private readonly IMemoryCache _memoryCache; + private readonly IDistributedCache _distributedCache; + private readonly CacheManagerOptions _options; + + public CacheManager( + IMemoryCache memoryCache, + IDistributedCache distributedCache, + IOptions options) + { + _memoryCache = memoryCache; + _distributedCache = distributedCache; + _options = options.Value; + } + + public async Task GetAsync(string key) + { + if (_memoryCache.TryGetValue(key, out T? value)) + return value; + + var serialized = await _distributedCache.GetStringAsync(key); + if (serialized is not null) + { + value = JsonSerializer.Deserialize(serialized, _options.JsonOptions)!; + _memoryCache.Set(key, value, _options.MemoryCacheDuration); + return value; + } + + return default; + } + + public async Task RemoveAsync(string key) + { + _memoryCache.Remove(key); + await _distributedCache.RemoveAsync(key); + } + + public async Task SetAsync(string key, T value) + { + this.SetMemory(key, value); + await this.SetDistributedAsync(key, value); + } + + private void SetMemory(string key, T value, TimeSpan? duration = null) + { + _memoryCache.Set(key, value, duration ?? _options.MemoryCacheDuration); + } + + private Task SetDistributedAsync(string key, T value, TimeSpan? duration = null) + { + var serialized = JsonSerializer.Serialize(value, _options.JsonOptions); + return _distributedCache.SetStringAsync( + key, + serialized, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = duration ?? _options.DistributedCacheDuration + }); + } +} From 5869b79af069639dfccf69a99a6633c1892115ea Mon Sep 17 00:00:00 2001 From: cihataydin Date: Sun, 8 Jun 2025 16:52:37 +0300 Subject: [PATCH 2/2] feat(46): adds UseDistributedCache option to CacheManager and update appsettings --- src/Api/appsettings.Development.json | 1 + src/Infra/Cache/CacheManager.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 96fa6d1..2a00e48 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -13,6 +13,7 @@ "Redis": "" }, "CacheOptions": { + "UseDistributedCache": false, "MemoryCacheDuration": "00:02:00", "DistributedCacheDuration": "00:10:00" } diff --git a/src/Infra/Cache/CacheManager.cs b/src/Infra/Cache/CacheManager.cs index 7d8d142..5433f8b 100644 --- a/src/Infra/Cache/CacheManager.cs +++ b/src/Infra/Cache/CacheManager.cs @@ -8,6 +8,7 @@ namespace Infra.Cache; public class CacheManagerOptions { + public bool UseDistributedCache { get; set; } = true; public TimeSpan MemoryCacheDuration { get; set; } = TimeSpan.FromMinutes(2); public TimeSpan DistributedCacheDuration { get; set; } = TimeSpan.FromMinutes(10); public JsonSerializerOptions? JsonOptions { get; set; } @@ -34,6 +35,9 @@ public CacheManager( if (_memoryCache.TryGetValue(key, out T? value)) return value; + if(!_options.UseDistributedCache) + return default; + var serialized = await _distributedCache.GetStringAsync(key); if (serialized is not null) { @@ -48,13 +52,17 @@ public CacheManager( public async Task RemoveAsync(string key) { _memoryCache.Remove(key); - await _distributedCache.RemoveAsync(key); + + if (_options.UseDistributedCache) + await _distributedCache.RemoveAsync(key); } public async Task SetAsync(string key, T value) { this.SetMemory(key, value); - await this.SetDistributedAsync(key, value); + + if (_options.UseDistributedCache) + await this.SetDistributedAsync(key, value); } private void SetMemory(string key, T value, TimeSpan? duration = null)