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..2a00e48 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -9,6 +9,12 @@ }, "UseJsonFormat": false, "ConnectionStrings": { - "PgpoolDb": "" + "PgpoolDb": "", + "Redis": "" + }, + "CacheOptions": { + "UseDistributedCache": false, + "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..5433f8b --- /dev/null +++ b/src/Infra/Cache/CacheManager.cs @@ -0,0 +1,84 @@ +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 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; } +} + +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; + + if(!_options.UseDistributedCache) + return default; + + 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); + + if (_options.UseDistributedCache) + await _distributedCache.RemoveAsync(key); + } + + public async Task SetAsync(string key, T value) + { + this.SetMemory(key, value); + + if (_options.UseDistributedCache) + 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 + }); + } +}