adding cache
All checks were successful
API and ETL Build / build_etl (push) Successful in 39s
API and ETL Build / build_api (push) Successful in 11s

This commit is contained in:
Jose Henrique 2025-06-09 23:31:21 -03:00
parent 39faab6483
commit 673cda6408
6 changed files with 199 additions and 42 deletions

View File

@ -1,3 +1,4 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using OpenCand.API.Config; using OpenCand.API.Config;
using OpenCand.API.Repository; using OpenCand.API.Repository;
@ -51,15 +52,21 @@ namespace OpenCand.API
app.Run(); app.Run();
} } private static void SetupServices(WebApplicationBuilder builder)
private static void SetupServices(WebApplicationBuilder builder)
{ {
builder.Services.Configure<FotosSettings>(builder.Configuration.GetSection("FotosSettings")); builder.Services.Configure<FotosSettings>(builder.Configuration.GetSection("FotosSettings"));
builder.Services.AddScoped<OpenCandRepository>(); builder.Services.AddMemoryCache();
builder.Services.AddScoped<CandidatoRepository>();
builder.Services.AddScoped<BemCandidatoRepository>(); // Register repositories with IMemoryCache
builder.Services.AddScoped<DespesaReceitaRepository>(); builder.Services.AddScoped<OpenCandRepository>(provider =>
new OpenCandRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped<CandidatoRepository>(provider =>
new CandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped<BemCandidatoRepository>(provider =>
new BemCandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped<DespesaReceitaRepository>(provider =>
new DespesaReceitaRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped<OpenCandService>(); builder.Services.AddScoped<OpenCandService>();
} }
} }

View File

@ -1,4 +1,4 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Caching.Memory;
using Npgsql; using Npgsql;
namespace OpenCand.Repository namespace OpenCand.Repository
@ -7,11 +7,131 @@ namespace OpenCand.Repository
{ {
protected string ConnectionString { get; private set; } protected string ConnectionString { get; private set; }
protected NpgsqlConnection? Connection { 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"] ?? ConnectionString = configuration["DatabaseSettings:ConnectionString"] ??
throw new ArgumentNullException("Connection string not found in configuration"); throw new ArgumentNullException("Connection string not found in configuration");
_cache = cache;
}
/// <summary>
/// Generic method to get data from cache or execute a factory function if not cached
/// </summary>
/// <typeparam name="T">Type of data to cache</typeparam>
/// <param name="cacheKey">Unique cache key</param>
/// <param name="factory">Function to execute if data is not in cache</param>
/// <param name="expiration">Cache expiration time (optional, uses default if not provided)</param>
/// <param name="priority">Cache priority (optional, uses default if not provided)</param>
/// <returns>Cached or freshly retrieved data</returns>
protected async Task<T?> GetOrSetCacheAsync<T>(
string cacheKey,
Func<Task<T?>> 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;
}
/// <summary>
/// Generic method to get data from cache or execute a synchronous factory function if not cached
/// </summary>
/// <typeparam name="T">Type of data to cache</typeparam>
/// <param name="cacheKey">Unique cache key</param>
/// <param name="factory">Function to execute if data is not in cache</param>
/// <param name="expiration">Cache expiration time (optional, uses default if not provided)</param>
/// <param name="priority">Cache priority (optional, uses default if not provided)</param>
/// <returns>Cached or freshly retrieved data</returns>
protected T? GetOrSetCache<T>(
string cacheKey,
Func<T?> 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;
}
/// <summary>
/// Removes an item from cache by key
/// </summary>
/// <param name="cacheKey">Cache key to remove</param>
protected void ClearCache(string cacheKey)
{
_cache?.Remove(cacheKey);
}
/// <summary>
/// Checks if an item exists in cache
/// </summary>
/// <param name="cacheKey">Cache key to check</param>
/// <returns>True if item exists in cache, false otherwise</returns>
protected bool IsCached(string cacheKey)
{
return _cache?.TryGetValue(cacheKey, out _) ?? false;
} /// <summary>
/// Generates a standardized cache key for entity-based caching
/// </summary>
/// <param name="entityName">Name of the entity (e.g., "Candidato", "Stats")</param>
/// <param name="identifier">Unique identifier for the entity (optional)</param>
/// <returns>Formatted cache key</returns>
protected static string GenerateCacheKey(string entityName, object? identifier = null)
{
return identifier != null ? $"{entityName}_{identifier}" : entityName;
} }
} }
} }

View File

@ -1,4 +1,5 @@
using Dapper; using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql; using Npgsql;
using OpenCand.Core.Models; using OpenCand.Core.Models;
@ -6,7 +7,7 @@ namespace OpenCand.Repository
{ {
public class BemCandidatoRepository : BaseRepository public class BemCandidatoRepository : BaseRepository
{ {
public BemCandidatoRepository(IConfiguration configuration) : base(configuration) public BemCandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{ {
} }

View File

@ -1,4 +1,5 @@
using Dapper; using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql; using Npgsql;
using OpenCand.Core.Models; using OpenCand.Core.Models;
@ -6,11 +7,15 @@ namespace OpenCand.Repository
{ {
public class CandidatoRepository : BaseRepository public class CandidatoRepository : BaseRepository
{ {
public CandidatoRepository(IConfiguration configuration) : base(configuration) public CandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{ {
} }
public async Task<List<Candidato>> SearchCandidatosAsync(string query) public async Task<List<Candidato>?> SearchCandidatosAsync(string query)
{
string cacheKey = GenerateCacheKey("Search", query);
return await GetOrSetCacheAsync(cacheKey, async () =>
{ {
using var connection = new NpgsqlConnection(ConnectionString); using var connection = new NpgsqlConnection(ConnectionString);
return (await connection.QueryAsync<Candidato>(@" return (await connection.QueryAsync<Candidato>(@"
@ -22,9 +27,15 @@ namespace OpenCand.Repository
ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC
LIMIT 10; LIMIT 10;
", new { q = query })).AsList(); ", new { q = query })).AsList();
});
} }
public async Task<Candidato?> GetCandidatoAsync(Guid idcandidato) public async Task<Candidato?> GetCandidatoAsync(Guid idcandidato)
{
string cacheKey = GenerateCacheKey("Candidato", idcandidato);
return await GetOrSetCacheAsync(cacheKey, async () =>
{ {
using (var connection = new NpgsqlConnection(ConnectionString)) using (var connection = new NpgsqlConnection(ConnectionString))
{ {
@ -33,6 +44,7 @@ namespace OpenCand.Repository
WHERE idcandidato = @idcandidato;", WHERE idcandidato = @idcandidato;",
new { idcandidato }); new { idcandidato });
} }
});
} }
public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato) public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato)

View File

@ -1,4 +1,5 @@
using Dapper; using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Npgsql; using Npgsql;
using OpenCand.Core.Models; using OpenCand.Core.Models;
@ -7,7 +8,7 @@ namespace OpenCand.Repository
{ {
public class DespesaReceitaRepository : BaseRepository public class DespesaReceitaRepository : BaseRepository
{ {
public DespesaReceitaRepository(IConfiguration configuration) : base(configuration) public DespesaReceitaRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{ {
} }

View File

@ -1,17 +1,21 @@
using Dapper; using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql; using Npgsql;
using OpenCand.Core.Models; using OpenCand.Core.Models;
using OpenCand.Repository; using OpenCand.Repository;
namespace OpenCand.API.Repository namespace OpenCand.API.Repository
{ public class OpenCandRepository : BaseRepository
{ {
public class OpenCandRepository : BaseRepository public OpenCandRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{
public OpenCandRepository(IConfiguration configuration) : base(configuration)
{ {
} }
public async Task<OpenCandStats> GetOpenCandStatsAsync() public async Task<OpenCandStats> GetOpenCandStatsAsync()
{
string cacheKey = GenerateCacheKey("OpenCandStats");
var result = await GetOrSetCacheAsync(cacheKey, async () =>
{ {
using (var connection = new NpgsqlConnection(ConnectionString)) using (var connection = new NpgsqlConnection(ConnectionString))
{ {
@ -22,8 +26,20 @@ namespace OpenCand.API.Repository
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos, (SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
(SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais, (SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais,
(SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;"); (SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;");
return stats ?? new OpenCandStats(); return stats ?? new OpenCandStats();
} }
});
return result ?? new OpenCandStats();
}
/// <summary>
/// Clears the cached OpenCand statistics, forcing a fresh fetch on the next call
/// </summary>
public void ClearStatsCache()
{
ClearCache(GenerateCacheKey("OpenCandStats"));
} }
} }
} }