Compare commits
6 Commits
68d91b8151
...
main
Author | SHA1 | Date | |
---|---|---|---|
579517a1d4 | |||
d4ce3b2577 | |||
a1440baf3d | |||
ddd99ec703 | |||
965b693a19 | |||
ecbf2f07d6 |
@@ -8,6 +8,7 @@ namespace OpenCand.API.Config
|
|||||||
public const string DefaultPolicy = "DefaultPolicy";
|
public const string DefaultPolicy = "DefaultPolicy";
|
||||||
public const string CandidatoSearchPolicy = "CandidatoSearchPolicy";
|
public const string CandidatoSearchPolicy = "CandidatoSearchPolicy";
|
||||||
public const string CpfRevealPolicy = "CpfRevealPolicy";
|
public const string CpfRevealPolicy = "CpfRevealPolicy";
|
||||||
|
public const string EstatisticaPolicy = "EstatisticaPolicy";
|
||||||
|
|
||||||
public static void ConfigureRateLimiting(this IServiceCollection services)
|
public static void ConfigureRateLimiting(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
@@ -26,13 +27,12 @@ namespace OpenCand.API.Config
|
|||||||
// Default policy: 200 requests per minute with burst of 100
|
// Default policy: 200 requests per minute with burst of 100
|
||||||
options.AddFixedWindowLimiter(policyName: DefaultPolicy, options =>
|
options.AddFixedWindowLimiter(policyName: DefaultPolicy, options =>
|
||||||
{
|
{
|
||||||
options.PermitLimit = 200;
|
options.PermitLimit = 400;
|
||||||
options.Window = TimeSpan.FromMinutes(1);
|
options.Window = TimeSpan.FromMinutes(1);
|
||||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
options.QueueLimit = 100; // Burst capacity
|
options.QueueLimit = 100; // Burst capacity
|
||||||
});
|
});
|
||||||
|
|
||||||
// Candidato Search policy: 300 requests per minute with burst of 200
|
|
||||||
options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options =>
|
options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options =>
|
||||||
{
|
{
|
||||||
options.PermitLimit = 300;
|
options.PermitLimit = 300;
|
||||||
@@ -41,23 +41,27 @@ namespace OpenCand.API.Config
|
|||||||
options.QueueLimit = 200; // Burst capacity
|
options.QueueLimit = 200; // Burst capacity
|
||||||
});
|
});
|
||||||
|
|
||||||
// CPF Reveal policy: 15 requests per minute without burst
|
|
||||||
options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options =>
|
options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options =>
|
||||||
{
|
{
|
||||||
options.PermitLimit = 15;
|
options.PermitLimit = 20;
|
||||||
options.Window = TimeSpan.FromMinutes(1);
|
options.Window = TimeSpan.FromMinutes(1);
|
||||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
options.QueueLimit = 0; // No burst
|
options.QueueLimit = 5; // Burst capacity
|
||||||
});
|
});
|
||||||
|
|
||||||
options.OnRejected = async (context, token) =>
|
options.OnRejected = async (context, token) =>
|
||||||
{
|
{
|
||||||
|
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||||
|
var logger = loggerFactory.CreateLogger("RateLimitingConfig");
|
||||||
|
var clientIdentifier = GetClientIdentifier(context.HttpContext);
|
||||||
|
logger.LogWarning("Rate limit exceeded for client {ClientIdentifier}", clientIdentifier);
|
||||||
|
|
||||||
context.HttpContext.Response.StatusCode = 429;
|
context.HttpContext.Response.StatusCode = 429;
|
||||||
|
|
||||||
var retryAfter = GetRetryAfter(context);
|
var retryAfter = GetRetryAfter(context);
|
||||||
if (retryAfter.HasValue)
|
if (retryAfter.HasValue)
|
||||||
{
|
{
|
||||||
context.HttpContext.Response.Headers.Add("Retry-After", retryAfter.Value.ToString());
|
context.HttpContext.Response.Headers.Append("Retry-After", retryAfter.Value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.HttpContext.Response.WriteAsync(
|
await context.HttpContext.Response.WriteAsync(
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using OpenCand.API.Config;
|
||||||
|
|
||||||
namespace OpenCand.API.Controllers
|
namespace OpenCand.API.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("v1/[controller]")]
|
[Route("v1/[controller]")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
|
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
||||||
public class BaseController : Controller
|
public class BaseController : Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.VisualBasic;
|
|
||||||
using OpenCand.API.Config;
|
using OpenCand.API.Config;
|
||||||
using OpenCand.API.Model;
|
using OpenCand.API.Model;
|
||||||
using OpenCand.API.Services;
|
using OpenCand.API.Services;
|
||||||
using OpenCand.Core.Models;
|
using OpenCand.Core.Models;
|
||||||
|
using OpenCand.Core.Utils;
|
||||||
|
|
||||||
namespace OpenCand.API.Controllers
|
namespace OpenCand.API.Controllers
|
||||||
{
|
{
|
||||||
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
|
||||||
public class CandidatoController : BaseController
|
public class CandidatoController : BaseController
|
||||||
{
|
{
|
||||||
private readonly OpenCandService openCandService;
|
private readonly OpenCandService openCandService;
|
||||||
@@ -22,12 +21,16 @@ namespace OpenCand.API.Controllers
|
|||||||
[EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)]
|
[EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)]
|
||||||
public async Task<CandidatoSearchResult> CandidatoSearch([FromQuery] string q)
|
public async Task<CandidatoSearchResult> CandidatoSearch([FromQuery] string q)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(q) || q.Length == 1)
|
if (string.IsNullOrEmpty(q) || q.Length < 3)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Query parameter 'q' cannot be null/empty.", nameof(q));
|
throw new ArgumentException("Query parameter 'q' cannot be null/empty.", nameof(q));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await openCandService.SearchCandidatosAsync(q);
|
var result = await openCandService.SearchCandidatosAsync(q);
|
||||||
|
|
||||||
|
result.Candidatos.ForEach(c => c.Cpf = CpfMasking.MaskCpf(c.Cpf));
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("random")]
|
[HttpGet("random")]
|
||||||
@@ -42,7 +45,10 @@ namespace OpenCand.API.Controllers
|
|||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<Candidato> GetCandidatoById([FromRoute] Guid id)
|
public async Task<Candidato> GetCandidatoById([FromRoute] Guid id)
|
||||||
{
|
{
|
||||||
return await openCandService.GetCandidatoAsync(id);
|
var result = await openCandService.GetCandidatoAsync(id);
|
||||||
|
result.Cpf = CpfMasking.MaskCpf(result.Cpf);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/bens")]
|
[HttpGet("{id}/bens")]
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using OpenCand.API.Config;
|
|
||||||
using OpenCand.API.Model;
|
using OpenCand.API.Model;
|
||||||
using OpenCand.API.Services;
|
using OpenCand.API.Services;
|
||||||
|
using static OpenCand.API.Model.GetValueSumRequest;
|
||||||
|
|
||||||
namespace OpenCand.API.Controllers
|
namespace OpenCand.API.Controllers
|
||||||
{
|
{
|
||||||
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
|
||||||
public class EstatisticaController : BaseController
|
public class EstatisticaController : BaseController
|
||||||
{
|
{
|
||||||
private readonly EstatisticaService estatisticaService;
|
private readonly EstatisticaService estatisticaService;
|
||||||
@@ -23,9 +21,9 @@ namespace OpenCand.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("enriquecimento")]
|
[HttpGet("enriquecimento")]
|
||||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos()
|
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos([FromQuery] GetValueSumRequestFilter requestFilter)
|
||||||
{
|
{
|
||||||
return await estatisticaService.GetMaioresEnriquecimentos();
|
return await estatisticaService.GetMaioresEnriquecimentos(requestFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("values-sum")]
|
[HttpPost("values-sum")]
|
||||||
|
@@ -27,6 +27,12 @@ namespace OpenCand.API.Controllers
|
|||||||
{
|
{
|
||||||
return await openCandService.GetDataAvailabilityStatsAsync();
|
return await openCandService.GetDataAvailabilityStatsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("tech")]
|
||||||
|
public async Task<DatabaseTechStats> GetDatabaseTechStats()
|
||||||
|
{
|
||||||
|
return await openCandService.GetDatabaseTechStatsAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -36,7 +36,8 @@ namespace OpenCand.API
|
|||||||
{
|
{
|
||||||
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
|
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
|
||||||
RequestPath = "/assets/fotos"
|
RequestPath = "/assets/fotos"
|
||||||
}); app.UseHttpsRedirection();
|
});
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
// Use rate limiting middleware
|
// Use rate limiting middleware
|
||||||
app.UseRateLimiter();
|
app.UseRateLimiter();
|
||||||
@@ -53,10 +54,17 @@ 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.AddMemoryCache();
|
|
||||||
|
// Configure memory cache with size limit from appsettings
|
||||||
|
var sizeLimitMB = builder.Configuration.GetValue<int>("CacheSettings:SizeLimitMB", 15);
|
||||||
|
builder.Services.AddMemoryCache(options =>
|
||||||
|
{
|
||||||
|
options.SizeLimit = sizeLimitMB * 1024L * 1024L; // Convert MB to bytes
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddScoped(provider => new OpenCandRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
builder.Services.AddScoped(provider => new OpenCandRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||||
builder.Services.AddScoped(provider => new CandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
builder.Services.AddScoped(provider => new CandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace OpenCand.Repository
|
namespace OpenCand.Repository
|
||||||
{
|
{
|
||||||
@@ -56,7 +58,8 @@ namespace OpenCand.Repository
|
|||||||
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
||||||
{
|
{
|
||||||
SlidingExpiration = expiration ?? DefaultCacheExpiration,
|
SlidingExpiration = expiration ?? DefaultCacheExpiration,
|
||||||
Priority = priority ?? DefaultCachePriority
|
Priority = priority ?? DefaultCachePriority,
|
||||||
|
Size = EstimateSize(result)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +102,8 @@ namespace OpenCand.Repository
|
|||||||
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
||||||
{
|
{
|
||||||
SlidingExpiration = expiration ?? DefaultCacheExpiration,
|
SlidingExpiration = expiration ?? DefaultCacheExpiration,
|
||||||
Priority = priority ?? DefaultCachePriority
|
Priority = priority ?? DefaultCachePriority,
|
||||||
|
Size = EstimateSize(result)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,5 +137,25 @@ namespace OpenCand.Repository
|
|||||||
{
|
{
|
||||||
return identifier != null ? $"{entityName}_{identifier}" : entityName;
|
return identifier != null ? $"{entityName}_{identifier}" : entityName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates the memory size of an object by serializing it to JSON
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of the object</typeparam>
|
||||||
|
/// <param name="obj">The object to estimate size for</param>
|
||||||
|
/// <returns>Estimated size in bytes</returns>
|
||||||
|
private static long EstimateSize<T>(T obj)
|
||||||
|
{
|
||||||
|
if (obj == null) return 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(obj);
|
||||||
|
return Encoding.UTF8.GetByteCount(json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 1024; // Default estimate if serialization fails
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -102,6 +102,16 @@ namespace OpenCand.Repository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task IncreaseCandidatoPopularity(Guid idcandidato)
|
||||||
|
{
|
||||||
|
using var connection = new NpgsqlConnection(ConnectionString);
|
||||||
|
await connection.ExecuteAsync(@"
|
||||||
|
UPDATE candidato
|
||||||
|
SET popularidade = popularidade + 1
|
||||||
|
WHERE idcandidato = @idcandidato;
|
||||||
|
", new { idcandidato });
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Guid?> GetRandomCandidatoIdAsync()
|
public async Task<Guid?> GetRandomCandidatoIdAsync()
|
||||||
{
|
{
|
||||||
using var connection = new NpgsqlConnection(ConnectionString);
|
using var connection = new NpgsqlConnection(ConnectionString);
|
||||||
|
@@ -2,8 +2,9 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using OpenCand.API.Model;
|
using OpenCand.API.Model;
|
||||||
using OpenCand.Core.Models;
|
|
||||||
using OpenCand.Repository;
|
using OpenCand.Repository;
|
||||||
|
using System.Text.Json;
|
||||||
|
using static OpenCand.API.Model.GetValueSumRequest;
|
||||||
|
|
||||||
namespace OpenCand.API.Repository
|
namespace OpenCand.API.Repository
|
||||||
{
|
{
|
||||||
@@ -16,9 +17,39 @@ namespace OpenCand.API.Repository
|
|||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos()
|
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
|
||||||
{
|
{
|
||||||
string cacheKey = GenerateCacheKey("GetMaioresEnriquecimento");
|
var joinBase = string.Empty;
|
||||||
|
if (requestFilter == null) requestFilter = new GetValueSumRequestFilter();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
joinBase = " JOIN candidato_mapping cm ON ed.idcandidato = cm.idcandidato ";
|
||||||
|
var whereConditions = new List<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(requestFilter.Partido))
|
||||||
|
{
|
||||||
|
whereConditions.Add($"cm.sgpartido = '{requestFilter.Partido}'");
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(requestFilter.Uf))
|
||||||
|
{
|
||||||
|
whereConditions.Add($"cm.siglauf = '{requestFilter.Uf.ToUpper()}'");
|
||||||
|
}
|
||||||
|
if (requestFilter.Ano != null)
|
||||||
|
{
|
||||||
|
whereConditions.Add($"cm.ano = '{requestFilter.Ano}'");
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(requestFilter.Cargo))
|
||||||
|
{
|
||||||
|
whereConditions.Add($"cm.cargo = '{requestFilter.Cargo}'");
|
||||||
|
}
|
||||||
|
if (whereConditions.Count > 0)
|
||||||
|
{
|
||||||
|
joinBase += " WHERE " + string.Join(" AND ", whereConditions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestJson = JsonSerializer.Serialize(requestFilter);
|
||||||
|
|
||||||
|
string cacheKey = GenerateCacheKey("GetMaioresEnriquecimento", requestJson);
|
||||||
|
|
||||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||||
{
|
{
|
||||||
@@ -46,6 +77,7 @@ namespace OpenCand.API.Repository
|
|||||||
JOIN candidato c ON ed.idcandidato = c.idcandidato
|
JOIN candidato c ON ed.idcandidato = c.idcandidato
|
||||||
JOIN mv_bem_candidato pi ON ed.idcandidato = pi.idcandidato AND ed.anoInicial = pi.ano
|
JOIN mv_bem_candidato pi ON ed.idcandidato = pi.idcandidato AND ed.anoInicial = pi.ano
|
||||||
JOIN mv_bem_candidato pf ON ed.idcandidato = pf.idcandidato AND ed.anoFinal = pf.ano
|
JOIN mv_bem_candidato pf ON ed.idcandidato = pf.idcandidato AND ed.anoFinal = pf.ano
|
||||||
|
" + joinBase + @"
|
||||||
ORDER BY
|
ORDER BY
|
||||||
enriquecimento DESC
|
enriquecimento DESC
|
||||||
LIMIT 25;")
|
LIMIT 25;")
|
||||||
@@ -67,7 +99,7 @@ namespace OpenCand.API.Repository
|
|||||||
{
|
{
|
||||||
result.Partidos = (await connection.QueryAsync<string>(@"SELECT DISTINCT sigla FROM partido ORDER BY sigla ASC;")).AsList();
|
result.Partidos = (await connection.QueryAsync<string>(@"SELECT DISTINCT sigla FROM partido ORDER BY sigla ASC;")).AsList();
|
||||||
result.SiglasUF = (await connection.QueryAsync<string>(@"SELECT DISTINCT siglauf FROM candidato_mapping ORDER BY siglauf ASC;")).AsList();
|
result.SiglasUF = (await connection.QueryAsync<string>(@"SELECT DISTINCT siglauf FROM candidato_mapping ORDER BY siglauf ASC;")).AsList();
|
||||||
result.Anos = (await connection.QueryAsync<int>(@"SELECT DISTINCT ano FROM candidato_mapping ORDER BY ano ASC;")).AsList();
|
result.Anos = (await connection.QueryAsync<int>(@"SELECT DISTINCT ano FROM candidato_mapping ORDER BY ano DESC;")).AsList();
|
||||||
result.Cargos = (await connection.QueryAsync<string>(@"SELECT DISTINCT cargo FROM candidato_mapping ORDER BY cargo ASC;")).AsList();
|
result.Cargos = (await connection.QueryAsync<string>(@"SELECT DISTINCT cargo FROM candidato_mapping ORDER BY cargo ASC;")).AsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -81,5 +81,63 @@ namespace OpenCand.API.Repository
|
|||||||
|
|
||||||
return result ?? new DataAvailabilityStats();
|
return result ?? new DataAvailabilityStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
|
||||||
|
{
|
||||||
|
string cacheKey = GenerateCacheKey("DatabaseTechStats");
|
||||||
|
|
||||||
|
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||||
|
{
|
||||||
|
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var stats = new DatabaseTechStats();
|
||||||
|
|
||||||
|
|
||||||
|
// Get table stats using reltuples for entries
|
||||||
|
var tableStats = await connection.QueryAsync<TableStats>(@"
|
||||||
|
SELECT
|
||||||
|
pt.schemaname||'.'||pt.tablename as Name,
|
||||||
|
pg_total_relation_size(pt.schemaname||'.'||pt.tablename) as TotalSize,
|
||||||
|
COALESCE(c.reltuples,0)::bigint as Entries
|
||||||
|
FROM pg_tables pt
|
||||||
|
JOIN pg_class c ON c.relname = pt.tablename AND c.relkind = 'r'
|
||||||
|
WHERE pt.schemaname = 'public'
|
||||||
|
ORDER BY pg_total_relation_size(pt.schemaname||'.'||pt.tablename) DESC;");
|
||||||
|
|
||||||
|
|
||||||
|
var tableStatsList = tableStats.ToList();
|
||||||
|
stats.Tables = tableStatsList;
|
||||||
|
stats.TotalSize = tableStatsList.Sum(t => t.TotalSize);
|
||||||
|
stats.TotalEntries = tableStatsList.Sum(t => t.Entries);
|
||||||
|
|
||||||
|
// Get materialized view stats using reltuples for entries
|
||||||
|
var materializedViewStats = await connection.QueryAsync<TableStats>(@"
|
||||||
|
SELECT
|
||||||
|
pmv.schemaname||'.'||pmv.matviewname as Name,
|
||||||
|
pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) as TotalSize,
|
||||||
|
COALESCE(c.reltuples,0)::bigint as Entries
|
||||||
|
FROM pg_matviews pmv
|
||||||
|
JOIN pg_class c ON c.relname = pmv.matviewname AND c.relkind = 'm'
|
||||||
|
WHERE pmv.schemaname = 'public'
|
||||||
|
ORDER BY pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) DESC;");
|
||||||
|
|
||||||
|
stats.MaterializedViews = materializedViewStats.ToList();
|
||||||
|
|
||||||
|
// Get index stats
|
||||||
|
var indexStats = await connection.QueryFirstOrDefaultAsync<IndexStats>(@"
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as Amount,
|
||||||
|
SUM(pg_relation_size(indexrelid)) as Size
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname = 'public';");
|
||||||
|
|
||||||
|
stats.Indexes = indexStats ?? new IndexStats();
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result ?? new DatabaseTechStats();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -58,14 +58,16 @@ namespace OpenCand.API.Services
|
|||||||
await PerformPreLoad("GetValueSum", () => estatisticaService.GetValueSum(request));
|
await PerformPreLoad("GetValueSum", () => estatisticaService.GetValueSum(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await PerformPreLoad("GetMaioresEnriquecimentos", () => estatisticaService.GetMaioresEnriquecimentos(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PreloadSingleEndpoints(EstatisticaService estatisticaService, OpenCandService openCandService)
|
private async Task PreloadSingleEndpoints(EstatisticaService estatisticaService, OpenCandService openCandService)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Preloading single-call endpoints...");
|
logger.LogInformation("Preloading single-call endpoints...");
|
||||||
|
|
||||||
await PerformPreLoad("GetOpenCandStatsAsync", estatisticaService.GetMaioresEnriquecimentos);
|
|
||||||
await PerformPreLoad("GetOpenCandStatsAsync", openCandService.GetOpenCandStatsAsync);
|
await PerformPreLoad("GetOpenCandStatsAsync", openCandService.GetOpenCandStatsAsync);
|
||||||
|
await PerformPreLoad("GetDatabaseTechStatsAsync", openCandService.GetDatabaseTechStatsAsync);
|
||||||
await PerformPreLoad("GetDataAvailabilityStatsAsync", openCandService.GetDataAvailabilityStatsAsync);
|
await PerformPreLoad("GetDataAvailabilityStatsAsync", openCandService.GetDataAvailabilityStatsAsync);
|
||||||
|
|
||||||
logger.LogInformation("Single-call endpoints preload completed");
|
logger.LogInformation("Single-call endpoints preload completed");
|
||||||
|
@@ -1,56 +1,54 @@
|
|||||||
using OpenCand.API.Model;
|
using OpenCand.API.Model;
|
||||||
using OpenCand.API.Repository;
|
using OpenCand.API.Repository;
|
||||||
using OpenCand.Repository;
|
using static OpenCand.API.Model.GetValueSumRequest;
|
||||||
|
|
||||||
namespace OpenCand.API.Services
|
namespace OpenCand.API.Services
|
||||||
{
|
{
|
||||||
public class EstatisticaService
|
public class EstatisticaService
|
||||||
{
|
{
|
||||||
private readonly OpenCandRepository openCandRepository;
|
|
||||||
private readonly CandidatoRepository candidatoRepository;
|
|
||||||
private readonly BemCandidatoRepository bemCandidatoRepository;
|
|
||||||
private readonly DespesaReceitaRepository despesaReceitaRepository;
|
|
||||||
private readonly EstatisticaRepository estatisticaRepository;
|
private readonly EstatisticaRepository estatisticaRepository;
|
||||||
private readonly IConfiguration configuration;
|
|
||||||
private readonly ILogger<OpenCandService> logger;
|
private readonly ILogger<OpenCandService> logger;
|
||||||
|
|
||||||
public EstatisticaService(
|
public EstatisticaService(
|
||||||
OpenCandRepository openCandRepository,
|
|
||||||
CandidatoRepository candidatoRepository,
|
|
||||||
BemCandidatoRepository bemCandidatoRepository,
|
|
||||||
DespesaReceitaRepository despesaReceitaRepository,
|
|
||||||
EstatisticaRepository estatisticaRepository,
|
EstatisticaRepository estatisticaRepository,
|
||||||
IConfiguration configuration,
|
|
||||||
ILogger<OpenCandService> logger)
|
ILogger<OpenCandService> logger)
|
||||||
{
|
{
|
||||||
this.openCandRepository = openCandRepository;
|
|
||||||
this.candidatoRepository = candidatoRepository;
|
|
||||||
this.bemCandidatoRepository = bemCandidatoRepository;
|
|
||||||
this.despesaReceitaRepository = despesaReceitaRepository;
|
|
||||||
this.estatisticaRepository = estatisticaRepository;
|
this.estatisticaRepository = estatisticaRepository;
|
||||||
this.configuration = configuration;
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ConfigurationModel> GetConfigurationModel()
|
public async Task<ConfigurationModel> GetConfigurationModel()
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Getting configuration model");
|
||||||
|
|
||||||
return await estatisticaRepository.GetConfiguration();
|
return await estatisticaRepository.GetConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos()
|
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
|
||||||
{
|
{
|
||||||
return await estatisticaRepository.GetMaioresEnriquecimentos();
|
logger.LogInformation($"Getting maiores enriquecimentos. Filters: Partido={requestFilter?.Partido}, Uf={requestFilter?.Uf}, Ano={requestFilter?.Ano}, Cargo={requestFilter?.Cargo}");
|
||||||
|
|
||||||
|
return await estatisticaRepository.GetMaioresEnriquecimentos(requestFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<GetValueSumResponse>> GetValueSum(GetValueSumRequest request)
|
public async Task<List<GetValueSumResponse>> GetValueSum(GetValueSumRequest request)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Getting value sum for {request.Type} grouped by {request.GroupBy}. Filters: Partido={request.Filter?.Partido}, Uf={request.Filter?.Uf}, Ano={request.Filter?.Ano}, Cargo={request.Filter?.Cargo}");
|
||||||
|
// count exec time
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
ValidateRequest(request);
|
ValidateRequest(request);
|
||||||
|
|
||||||
var sqlBuilder = new SqlQueryBuilder();
|
var sqlBuilder = new SqlQueryBuilder();
|
||||||
var query = sqlBuilder.BuildQuery(request);
|
var query = sqlBuilder.BuildQuery(request);
|
||||||
var parameters = sqlBuilder.GetParameters();
|
var parameters = sqlBuilder.GetParameters();
|
||||||
|
|
||||||
return await estatisticaRepository.GetValueSum(query, parameters);
|
var result = await estatisticaRepository.GetValueSum(query, parameters);
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
logger.LogInformation($"GetValueSum - Execution time: {stopwatch.ElapsedMilliseconds} ms");
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateRequest(GetValueSumRequest request)
|
private void ValidateRequest(GetValueSumRequest request)
|
||||||
|
@@ -37,26 +37,46 @@ namespace OpenCand.API.Services
|
|||||||
|
|
||||||
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Getting OpenCand stats");
|
||||||
|
|
||||||
return await openCandRepository.GetOpenCandStatsAsync();
|
return await openCandRepository.GetOpenCandStatsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DataAvailabilityStats> GetDataAvailabilityStatsAsync()
|
public async Task<DataAvailabilityStats> GetDataAvailabilityStatsAsync()
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Getting data availability stats");
|
||||||
|
|
||||||
var stats = await openCandRepository.GetDataAvailabilityAsync();
|
var stats = await openCandRepository.GetDataAvailabilityAsync();
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
|
||||||
|
{
|
||||||
|
logger.LogInformation("Getting database tech stats");
|
||||||
|
|
||||||
|
var stats = await openCandRepository.GetDatabaseTechStatsAsync();
|
||||||
|
|
||||||
|
stats.Tables = stats.Tables.OrderBy(t => t.Name).ToList();
|
||||||
|
stats.MaterializedViews = stats.MaterializedViews.OrderBy(mv => mv.Name).ToList();
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<CandidatoSearchResult> SearchCandidatosAsync(string query)
|
public async Task<CandidatoSearchResult> SearchCandidatosAsync(string query)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Searching candidatos with query: {query}");
|
||||||
|
|
||||||
return new CandidatoSearchResult()
|
return new CandidatoSearchResult()
|
||||||
{
|
{
|
||||||
Candidatos = await candidatoRepository.SearchCandidatosAsync(query)
|
Candidatos = await candidatoRepository.SearchCandidatosAsync(query) ?? new List<Candidato>()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Guid?> GetRandomCandidato()
|
public async Task<Guid?> GetRandomCandidato()
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Getting random candidato");
|
||||||
|
|
||||||
return await candidatoRepository.GetRandomCandidatoIdAsync();
|
return await candidatoRepository.GetRandomCandidatoIdAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +86,15 @@ namespace OpenCand.API.Services
|
|||||||
var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato);
|
var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato);
|
||||||
var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato);
|
var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await candidatoRepository.IncreaseCandidatoPopularity(idcandidato);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, $"Error increasing popularity for Candidato ID {idcandidato}");
|
||||||
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
throw new KeyNotFoundException($"Candidato with ID {idcandidato} not found.");
|
throw new KeyNotFoundException($"Candidato with ID {idcandidato} not found.");
|
||||||
@@ -74,6 +103,10 @@ namespace OpenCand.API.Services
|
|||||||
{
|
{
|
||||||
throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found.");
|
throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found.");
|
||||||
}
|
}
|
||||||
|
if (candidatoExt == null)
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException($"CandidatoExt for ID {idcandidato} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var eleicao in eleicoes)
|
foreach (var eleicao in eleicoes)
|
||||||
{
|
{
|
||||||
@@ -86,6 +119,8 @@ namespace OpenCand.API.Services
|
|||||||
result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList();
|
result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList();
|
||||||
result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList();
|
result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList();
|
||||||
|
|
||||||
|
logger.LogDebug($"Found Candidato: {result.Nome}, ID: {result.IdCandidato}, CPF: {result.Cpf}, FotoUrl: {result.FotoUrl}");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +132,8 @@ namespace OpenCand.API.Services
|
|||||||
result = new List<BemCandidato>();
|
result = new List<BemCandidato>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Found {result.Count} bens for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new BemCandidatoResult()
|
return new BemCandidatoResult()
|
||||||
{
|
{
|
||||||
Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList()
|
Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList()
|
||||||
@@ -105,12 +142,16 @@ namespace OpenCand.API.Services
|
|||||||
|
|
||||||
public async Task<RedeSocialResult> GetCandidatoRedeSocialById(Guid idcandidato)
|
public async Task<RedeSocialResult> GetCandidatoRedeSocialById(Guid idcandidato)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Getting redes sociais for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
|
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
result = new List<RedeSocial>();
|
result = new List<RedeSocial>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogDebug($"Found {result.Count} redes sociais for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new RedeSocialResult()
|
return new RedeSocialResult()
|
||||||
{
|
{
|
||||||
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
|
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
|
||||||
@@ -119,12 +160,16 @@ namespace OpenCand.API.Services
|
|||||||
|
|
||||||
public async Task<CpfRevealResult> GetCandidatoCpfById(Guid idcandidato)
|
public async Task<CpfRevealResult> GetCandidatoCpfById(Guid idcandidato)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Getting CPF for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato);
|
var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
return new CpfRevealResult();
|
return new CpfRevealResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogDebug($"Found CPF {result} for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new CpfRevealResult()
|
return new CpfRevealResult()
|
||||||
{
|
{
|
||||||
Cpf = result
|
Cpf = result
|
||||||
@@ -133,12 +178,16 @@ namespace OpenCand.API.Services
|
|||||||
|
|
||||||
public async Task<DespesasResult> GetDespesasByIdAndYear(Guid idcandidato)
|
public async Task<DespesasResult> GetDespesasByIdAndYear(Guid idcandidato)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Getting despesas for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
var result = await despesaReceitaRepository.GetDespesasByCandidatoIdYearAsync(idcandidato);
|
var result = await despesaReceitaRepository.GetDespesasByCandidatoIdYearAsync(idcandidato);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
return new DespesasResult();
|
return new DespesasResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogDebug($"Found {result.Count} despesas for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new DespesasResult()
|
return new DespesasResult()
|
||||||
{
|
{
|
||||||
Despesas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
Despesas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||||
@@ -147,12 +196,16 @@ namespace OpenCand.API.Services
|
|||||||
|
|
||||||
public async Task<ReceitaResult> GetReceitasByIdAndYear(Guid idcandidato)
|
public async Task<ReceitaResult> GetReceitasByIdAndYear(Guid idcandidato)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Getting receitas for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
var result = await despesaReceitaRepository.GetReceitasByCandidatoIdYearAsync(idcandidato);
|
var result = await despesaReceitaRepository.GetReceitasByCandidatoIdYearAsync(idcandidato);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
return new ReceitaResult();
|
return new ReceitaResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogDebug($"Found {result.Count} receitas for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new ReceitaResult()
|
return new ReceitaResult()
|
||||||
{
|
{
|
||||||
Receitas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
Receitas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||||
|
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DatabaseSettings": {
|
"DatabaseSettings": {
|
||||||
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Include Error Detail=true;CommandTimeout=300"
|
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Pooling=true;Minimum Pool Size=1;Maximum Pool Size=20;Connection Lifetime=300;Command Timeout=30;Application Name=OpenCand.API;Include Error Detail=true"
|
||||||
},
|
},
|
||||||
"FotosSettings": {
|
"FotosSettings": {
|
||||||
"Path": "./fotos_cand",
|
"Path": "./fotos_cand",
|
||||||
"ApiBasePath": "http://localhost:5299/assets/fotos"
|
"ApiBasePath": "http://localhost:5299/assets/fotos"
|
||||||
},
|
},
|
||||||
|
"CacheSettings": {
|
||||||
|
"SizeLimitMB": 15
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
@@ -11,12 +11,34 @@
|
|||||||
|
|
||||||
public class DataAvailabilityStats
|
public class DataAvailabilityStats
|
||||||
{
|
{
|
||||||
public List<int> Candidatos { get; set; }
|
public List<int> Candidatos { get; set; } = new List<int>();
|
||||||
public List<int> BemCandidatos { get; set; }
|
public List<int> BemCandidatos { get; set; } = new List<int>();
|
||||||
public List<int> DespesaCandidatos { get; set; }
|
public List<int> DespesaCandidatos { get; set; } = new List<int>();
|
||||||
public List<int> ReceitaCandidatos { get; set; }
|
public List<int> ReceitaCandidatos { get; set; } = new List<int>();
|
||||||
public List<int> RedeSocialCandidatos { get; set; }
|
public List<int> RedeSocialCandidatos { get; set; } = new List<int>();
|
||||||
|
|
||||||
public List<int> FotosCandidatos { get; set; }
|
public List<int> FotosCandidatos { get; set; } = new List<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DatabaseTechStats
|
||||||
|
{
|
||||||
|
public List<TableStats> Tables { get; set; } = new List<TableStats>();
|
||||||
|
public List<TableStats> MaterializedViews { get; set; } = new List<TableStats>();
|
||||||
|
public IndexStats Indexes { get; set; } = new IndexStats();
|
||||||
|
public long TotalSize { get; set; }
|
||||||
|
public long TotalEntries { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TableStats
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public long TotalSize { get; set; }
|
||||||
|
public long Entries { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexStats
|
||||||
|
{
|
||||||
|
public int Amount { get; set; }
|
||||||
|
public long Size { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
OpenCand.Core/Utils/CpfMasking.cs
Normal file
21
OpenCand.Core/Utils/CpfMasking.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace OpenCand.Core.Utils
|
||||||
|
{
|
||||||
|
public static class CpfMasking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Masks a CPF number by replacing the middle 3 digits with '*'
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cpf">The CPF number to mask.</param>
|
||||||
|
/// <returns>The masked CPF number.</returns>
|
||||||
|
public static string MaskCpf(string cpf)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cpf) || cpf.Length != 11)
|
||||||
|
{
|
||||||
|
return cpf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mask the middle 3 digits
|
||||||
|
return $"{cpf.Substring(0, 3)}***{cpf.Substring(6)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user