From 673cda640895fc518f0d0385967be583d5c9c406 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Mon, 9 Jun 2025 23:31:21 -0300 Subject: [PATCH] adding cache --- OpenCand.API/Program.cs | 21 ++- OpenCand.API/Repository/BaseRepository.cs | 124 +++++++++++++++++- .../Repository/BemCandidatoRepository.cs | 3 +- .../Repository/CandidatoRepository.cs | 48 ++++--- .../Repository/DespesaReceitaRepository.cs | 3 +- OpenCand.API/Repository/OpenCandRepository.cs | 42 ++++-- 6 files changed, 199 insertions(+), 42 deletions(-) diff --git a/OpenCand.API/Program.cs b/OpenCand.API/Program.cs index 2e39980..4cf1cc6 100644 --- a/OpenCand.API/Program.cs +++ b/OpenCand.API/Program.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; using OpenCand.API.Config; using OpenCand.API.Repository; @@ -51,15 +52,21 @@ namespace OpenCand.API app.Run(); - } - - private static void SetupServices(WebApplicationBuilder builder) + } private static void SetupServices(WebApplicationBuilder builder) { builder.Services.Configure(builder.Configuration.GetSection("FotosSettings")); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddMemoryCache(); + + // Register repositories with IMemoryCache + builder.Services.AddScoped(provider => + new OpenCandRepository(provider.GetRequiredService(), provider.GetService())); + builder.Services.AddScoped(provider => + new CandidatoRepository(provider.GetRequiredService(), provider.GetService())); + builder.Services.AddScoped(provider => + new BemCandidatoRepository(provider.GetRequiredService(), provider.GetService())); + builder.Services.AddScoped(provider => + new DespesaReceitaRepository(provider.GetRequiredService(), provider.GetService())); + builder.Services.AddScoped(); } } diff --git a/OpenCand.API/Repository/BaseRepository.cs b/OpenCand.API/Repository/BaseRepository.cs index 8575077..df9c2be 100644 --- a/OpenCand.API/Repository/BaseRepository.cs +++ b/OpenCand.API/Repository/BaseRepository.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Caching.Memory; using Npgsql; namespace OpenCand.Repository @@ -7,11 +7,131 @@ namespace OpenCand.Repository { protected string ConnectionString { get; private set; } protected NpgsqlConnection? Connection { get; private set; } + protected readonly IMemoryCache? _cache; - public BaseRepository(IConfiguration configuration) + // Default cache settings + protected static readonly TimeSpan DefaultCacheExpiration = TimeSpan.FromDays(7); + protected static readonly CacheItemPriority DefaultCachePriority = CacheItemPriority.Normal; + + public BaseRepository(IConfiguration configuration, IMemoryCache? cache = null) { ConnectionString = configuration["DatabaseSettings:ConnectionString"] ?? throw new ArgumentNullException("Connection string not found in configuration"); + _cache = cache; + } + + /// + /// Generic method to get data from cache or execute a factory function if not cached + /// + /// Type of data to cache + /// Unique cache key + /// Function to execute if data is not in cache + /// Cache expiration time (optional, uses default if not provided) + /// Cache priority (optional, uses default if not provided) + /// Cached or freshly retrieved data + protected async Task GetOrSetCacheAsync( + string cacheKey, + Func> factory, + TimeSpan? expiration = null, + CacheItemPriority? priority = null) where T : class + { + // If caching is not available, execute factory directly + if (_cache == null) + { + return await factory(); + } + + // Try to get cached data first + if (_cache.TryGetValue(cacheKey, out T? cachedData) && cachedData != null) + { + return cachedData; + } + + // If not in cache, execute factory function + var result = await factory(); + + // Only cache non-null results + if (result != null) + { + _cache.Set(cacheKey, result, new MemoryCacheEntryOptions + { + SlidingExpiration = expiration ?? DefaultCacheExpiration, + Priority = priority ?? DefaultCachePriority + }); + } + + return result; + } + + /// + /// Generic method to get data from cache or execute a synchronous factory function if not cached + /// + /// Type of data to cache + /// Unique cache key + /// Function to execute if data is not in cache + /// Cache expiration time (optional, uses default if not provided) + /// Cache priority (optional, uses default if not provided) + /// Cached or freshly retrieved data + protected T? GetOrSetCache( + string cacheKey, + Func factory, + TimeSpan? expiration = null, + CacheItemPriority? priority = null) where T : class + { + // If caching is not available, execute factory directly + if (_cache == null) + { + return factory(); + } + + // Try to get cached data first + if (_cache.TryGetValue(cacheKey, out T? cachedData) && cachedData != null) + { + return cachedData; + } + + // If not in cache, execute factory function + var result = factory(); + + // Only cache non-null results + if (result != null) + { + _cache.Set(cacheKey, result, new MemoryCacheEntryOptions + { + SlidingExpiration = expiration ?? DefaultCacheExpiration, + Priority = priority ?? DefaultCachePriority + }); + } + + return result; + } + + /// + /// Removes an item from cache by key + /// + /// Cache key to remove + protected void ClearCache(string cacheKey) + { + _cache?.Remove(cacheKey); + } + + /// + /// Checks if an item exists in cache + /// + /// Cache key to check + /// True if item exists in cache, false otherwise + protected bool IsCached(string cacheKey) + { + return _cache?.TryGetValue(cacheKey, out _) ?? false; + } /// + /// Generates a standardized cache key for entity-based caching + /// + /// Name of the entity (e.g., "Candidato", "Stats") + /// Unique identifier for the entity (optional) + /// Formatted cache key + protected static string GenerateCacheKey(string entityName, object? identifier = null) + { + return identifier != null ? $"{entityName}_{identifier}" : entityName; } } } diff --git a/OpenCand.API/Repository/BemCandidatoRepository.cs b/OpenCand.API/Repository/BemCandidatoRepository.cs index 08dfb53..a4b2651 100644 --- a/OpenCand.API/Repository/BemCandidatoRepository.cs +++ b/OpenCand.API/Repository/BemCandidatoRepository.cs @@ -1,4 +1,5 @@ using Dapper; +using Microsoft.Extensions.Caching.Memory; using Npgsql; using OpenCand.Core.Models; @@ -6,7 +7,7 @@ namespace OpenCand.Repository { public class BemCandidatoRepository : BaseRepository { - public BemCandidatoRepository(IConfiguration configuration) : base(configuration) + public BemCandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache) { } diff --git a/OpenCand.API/Repository/CandidatoRepository.cs b/OpenCand.API/Repository/CandidatoRepository.cs index 51af5cc..46f710f 100644 --- a/OpenCand.API/Repository/CandidatoRepository.cs +++ b/OpenCand.API/Repository/CandidatoRepository.cs @@ -1,4 +1,5 @@ using Dapper; +using Microsoft.Extensions.Caching.Memory; using Npgsql; using OpenCand.Core.Models; @@ -6,33 +7,44 @@ namespace OpenCand.Repository { public class CandidatoRepository : BaseRepository { - public CandidatoRepository(IConfiguration configuration) : base(configuration) + public CandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache) { } - public async Task> SearchCandidatosAsync(string query) + public async Task?> SearchCandidatosAsync(string query) { - using var connection = new NpgsqlConnection(ConnectionString); - return (await connection.QueryAsync(@" - SELECT c.*, - GREATEST(similarity(c.apelido, @q), similarity(c.nome, @q)) AS sim - FROM candidato c - WHERE c.apelido % @q - OR c.nome % @q - ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC - LIMIT 10; - ", new { q = query })).AsList(); + string cacheKey = GenerateCacheKey("Search", query); + + return await GetOrSetCacheAsync(cacheKey, async () => + { + using var connection = new NpgsqlConnection(ConnectionString); + return (await connection.QueryAsync(@" + SELECT c.*, + GREATEST(similarity(c.apelido, @q), similarity(c.nome, @q)) AS sim + FROM candidato c + WHERE c.apelido % @q + OR c.nome % @q + ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC + LIMIT 10; + ", new { q = query })).AsList(); + }); + } public async Task GetCandidatoAsync(Guid idcandidato) { - using (var connection = new NpgsqlConnection(ConnectionString)) + string cacheKey = GenerateCacheKey("Candidato", idcandidato); + + return await GetOrSetCacheAsync(cacheKey, async () => { - return await connection.QueryFirstOrDefaultAsync(@" - SELECT * FROM candidato - WHERE idcandidato = @idcandidato;", - new { idcandidato }); - } + using (var connection = new NpgsqlConnection(ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(@" + SELECT * FROM candidato + WHERE idcandidato = @idcandidato;", + new { idcandidato }); + } + }); } public async Task GetCandidatoCpfAsync(Guid idcandidato) diff --git a/OpenCand.API/Repository/DespesaReceitaRepository.cs b/OpenCand.API/Repository/DespesaReceitaRepository.cs index b961d85..4542e0f 100644 --- a/OpenCand.API/Repository/DespesaReceitaRepository.cs +++ b/OpenCand.API/Repository/DespesaReceitaRepository.cs @@ -1,4 +1,5 @@ using Dapper; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Npgsql; using OpenCand.Core.Models; @@ -7,7 +8,7 @@ namespace OpenCand.Repository { public class DespesaReceitaRepository : BaseRepository { - public DespesaReceitaRepository(IConfiguration configuration) : base(configuration) + public DespesaReceitaRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache) { } diff --git a/OpenCand.API/Repository/OpenCandRepository.cs b/OpenCand.API/Repository/OpenCandRepository.cs index 1a7d3bb..7176b06 100644 --- a/OpenCand.API/Repository/OpenCandRepository.cs +++ b/OpenCand.API/Repository/OpenCandRepository.cs @@ -1,29 +1,45 @@ using Dapper; +using Microsoft.Extensions.Caching.Memory; using Npgsql; using OpenCand.Core.Models; using OpenCand.Repository; namespace OpenCand.API.Repository -{ - public class OpenCandRepository : BaseRepository +{ public class OpenCandRepository : BaseRepository { - public OpenCandRepository(IConfiguration configuration) : base(configuration) + public OpenCandRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache) { } public async Task GetOpenCandStatsAsync() { - using (var connection = new NpgsqlConnection(ConnectionString)) + string cacheKey = GenerateCacheKey("OpenCandStats"); + + var result = await GetOrSetCacheAsync(cacheKey, async () => { - var stats = await connection.QueryFirstOrDefaultAsync(@" - SELECT - (SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos, - (SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos, - (SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos, - (SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais, - (SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;"); - return stats ?? new OpenCandStats(); - } + using (var connection = new NpgsqlConnection(ConnectionString)) + { + var stats = await connection.QueryFirstOrDefaultAsync(@" + SELECT + (SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos, + (SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos, + (SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos, + (SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais, + (SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;"); + + return stats ?? new OpenCandStats(); + } + }); + + return result ?? new OpenCandStats(); + } + + /// + /// Clears the cached OpenCand statistics, forcing a fresh fetch on the next call + /// + public void ClearStatsCache() + { + ClearCache(GenerateCacheKey("OpenCandStats")); } } }