Fix Captura de Datos Bancas
This commit is contained in:
@@ -163,17 +163,24 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
{
|
{
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
// --- CORRECCIÓN FINAL ---
|
|
||||||
// Eliminamos la comprobación de ContentLength. Confiamos en el try-catch.
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
// Verificamos si la respuesta realmente tiene contenido.
|
||||||
|
// ContentLength puede ser null, así que lo verificamos primero.
|
||||||
|
if (response.Content.Headers.ContentLength == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("La API devolvió 200 OK pero con cuerpo vacío para getBancas. URI: {uri}", requestUri);
|
||||||
|
return null; // Tratamos un cuerpo vacío como "sin datos".
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Solo intentamos deserializar si sabemos que hay contenido.
|
||||||
return await response.Content.ReadFromJsonAsync<RepartoBancasDto>();
|
return await response.Content.ReadFromJsonAsync<RepartoBancasDto>();
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
// Esto se activará si el cuerpo está vacío o no es un JSON válido.
|
// Si aún así falla (ej. el contenido es "[]" en lugar de "{}"), lo capturamos como una advertencia.
|
||||||
_logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {uri}", requestUri);
|
_logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {uri}", requestUri);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,15 +109,15 @@ public class CriticalDataWorker : BackgroundService
|
|||||||
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
|
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
var provincia = await dbContext.AmbitosGeograficos
|
// --- MODIFICACIÓN 1: Obtener todos los ámbitos en una sola consulta ---
|
||||||
|
var ambitosASondear = await dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
|
.Where(a => (a.NivelId == 10 || a.NivelId == 20) && a.DistritoId != null)
|
||||||
|
|
||||||
var seccionesElectorales = await dbContext.AmbitosGeograficos
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null)
|
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
|
var provincia = ambitosASondear.FirstOrDefault(a => a.NivelId == 10);
|
||||||
|
var seccionesElectorales = ambitosASondear.Where(a => a.NivelId == 20).ToList();
|
||||||
|
|
||||||
if (!categoriasDeBancas.Any() || provincia == null)
|
if (!categoriasDeBancas.Any() || provincia == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas.");
|
_logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas.");
|
||||||
@@ -129,62 +129,29 @@ public class CriticalDataWorker : BackgroundService
|
|||||||
var todasLasProyecciones = new List<ProyeccionBanca>();
|
var todasLasProyecciones = new List<ProyeccionBanca>();
|
||||||
bool hasReceivedAnyNewData = false;
|
bool hasReceivedAnyNewData = false;
|
||||||
|
|
||||||
// Bucle para el nivel Provincial
|
// --- MODIFICACIÓN 2: Usar un diccionario para no buscar repetidamente en la BD ---
|
||||||
foreach (var categoria in categoriasDeBancas)
|
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
||||||
|
|
||||||
|
// Bucle combinado para todos los ámbitos
|
||||||
|
foreach (var ambito in ambitosASondear)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id);
|
|
||||||
|
|
||||||
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
|
|
||||||
{
|
|
||||||
hasReceivedAnyNewData = true;
|
|
||||||
|
|
||||||
// --- SEGURIDAD: Usar TryParse para la fecha ---
|
|
||||||
DateTime fechaTotalizacion;
|
|
||||||
if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
|
|
||||||
{
|
|
||||||
// Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo.
|
|
||||||
_logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
|
|
||||||
fechaTotalizacion = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fechaTotalizacion = parsedDate.ToUniversalTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var banca in bancas)
|
|
||||||
{
|
|
||||||
todasLasProyecciones.Add(new ProyeccionBanca
|
|
||||||
{
|
|
||||||
EleccionId = EleccionId,
|
|
||||||
AmbitoGeograficoId = provincia.Id,
|
|
||||||
AgrupacionPoliticaId = banca.IdAgrupacion,
|
|
||||||
NroBancas = banca.NroBancas,
|
|
||||||
CategoriaId = categoria.Id,
|
|
||||||
FechaTotalizacion = fechaTotalizacion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bucle para el nivel de Sección Electoral
|
|
||||||
foreach (var seccion in seccionesElectorales)
|
|
||||||
{
|
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
|
||||||
foreach (var categoria in categoriasDeBancas)
|
foreach (var categoria in categoriasDeBancas)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id);
|
|
||||||
|
// Llamada a la API (lógica adaptada para ambos niveles)
|
||||||
|
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id);
|
||||||
|
|
||||||
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
|
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
|
||||||
{
|
{
|
||||||
hasReceivedAnyNewData = true;
|
hasReceivedAnyNewData = true;
|
||||||
|
|
||||||
// --- APLICAMOS LA MISMA SEGURIDAD AQUÍ ---
|
|
||||||
DateTime fechaTotalizacion;
|
DateTime fechaTotalizacion;
|
||||||
if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
|
if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
|
_logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
|
||||||
fechaTotalizacion = DateTime.UtcNow;
|
fechaTotalizacion = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -194,10 +161,26 @@ public class CriticalDataWorker : BackgroundService
|
|||||||
|
|
||||||
foreach (var banca in bancas)
|
foreach (var banca in bancas)
|
||||||
{
|
{
|
||||||
|
// --- MODIFICACIÓN 3: Lógica de "Upsert" para Agrupaciones ---
|
||||||
|
if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion);
|
||||||
|
|
||||||
|
var nuevaAgrupacion = new AgrupacionPolitica
|
||||||
|
{
|
||||||
|
Id = banca.IdAgrupacion,
|
||||||
|
Nombre = banca.NombreAgrupacion,
|
||||||
|
IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken);
|
||||||
|
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para no volver a crearla
|
||||||
|
}
|
||||||
|
|
||||||
todasLasProyecciones.Add(new ProyeccionBanca
|
todasLasProyecciones.Add(new ProyeccionBanca
|
||||||
{
|
{
|
||||||
EleccionId = EleccionId,
|
EleccionId = EleccionId,
|
||||||
AmbitoGeograficoId = seccion.Id,
|
AmbitoGeograficoId = ambito.Id,
|
||||||
AgrupacionPoliticaId = banca.IdAgrupacion,
|
AgrupacionPoliticaId = banca.IdAgrupacion,
|
||||||
NroBancas = banca.NroBancas,
|
NroBancas = banca.NroBancas,
|
||||||
CategoriaId = categoria.Id,
|
CategoriaId = categoria.Id,
|
||||||
@@ -213,6 +196,10 @@ public class CriticalDataWorker : BackgroundService
|
|||||||
_logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos...");
|
_logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos...");
|
||||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
|
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
|
||||||
|
|
||||||
|
// Si se crearon nuevas agrupaciones, se guardarán aquí primero.
|
||||||
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
|
||||||
|
// Luego, procedemos con las proyecciones.
|
||||||
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
||||||
await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken);
|
await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken);
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
@@ -222,7 +209,7 @@ public class CriticalDataWorker : BackgroundService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada.");
|
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos, la tabla no fue modificada.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|||||||
@@ -600,15 +600,15 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
|
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
var provincia = await dbContext.AmbitosGeograficos
|
// --- MODIFICACIÓN 1: Obtener todos los ámbitos en una sola consulta ---
|
||||||
|
var ambitosASondear = await dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
|
.Where(a => (a.NivelId == 10 || a.NivelId == 20) && a.DistritoId != null)
|
||||||
|
|
||||||
var seccionesElectorales = await dbContext.AmbitosGeograficos
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null)
|
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
|
var provincia = ambitosASondear.FirstOrDefault(a => a.NivelId == 10);
|
||||||
|
var seccionesElectorales = ambitosASondear.Where(a => a.NivelId == 20).ToList();
|
||||||
|
|
||||||
if (!categoriasDeBancas.Any() || provincia == null)
|
if (!categoriasDeBancas.Any() || provincia == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas.");
|
_logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas.");
|
||||||
@@ -620,62 +620,29 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
var todasLasProyecciones = new List<ProyeccionBanca>();
|
var todasLasProyecciones = new List<ProyeccionBanca>();
|
||||||
bool hasReceivedAnyNewData = false;
|
bool hasReceivedAnyNewData = false;
|
||||||
|
|
||||||
// Bucle para el nivel Provincial
|
// --- MODIFICACIÓN 2: Usar un diccionario para no buscar repetidamente en la BD ---
|
||||||
foreach (var categoria in categoriasDeBancas)
|
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
||||||
|
|
||||||
|
// Bucle combinado para todos los ámbitos
|
||||||
|
foreach (var ambito in ambitosASondear)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id);
|
|
||||||
|
|
||||||
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
|
|
||||||
{
|
|
||||||
hasReceivedAnyNewData = true;
|
|
||||||
|
|
||||||
// --- SEGURIDAD: Usar TryParse para la fecha ---
|
|
||||||
DateTime fechaTotalizacion;
|
|
||||||
if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
|
|
||||||
{
|
|
||||||
// Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo.
|
|
||||||
_logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
|
|
||||||
fechaTotalizacion = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fechaTotalizacion = parsedDate.ToUniversalTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var banca in bancas)
|
|
||||||
{
|
|
||||||
todasLasProyecciones.Add(new ProyeccionBanca
|
|
||||||
{
|
|
||||||
EleccionId = EleccionId,
|
|
||||||
AmbitoGeograficoId = provincia.Id,
|
|
||||||
AgrupacionPoliticaId = banca.IdAgrupacion,
|
|
||||||
NroBancas = banca.NroBancas,
|
|
||||||
CategoriaId = categoria.Id,
|
|
||||||
FechaTotalizacion = fechaTotalizacion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bucle para el nivel de Sección Electoral
|
|
||||||
foreach (var seccion in seccionesElectorales)
|
|
||||||
{
|
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
|
||||||
foreach (var categoria in categoriasDeBancas)
|
foreach (var categoria in categoriasDeBancas)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id);
|
|
||||||
|
// Llamada a la API (lógica adaptada para ambos niveles)
|
||||||
|
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id);
|
||||||
|
|
||||||
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
|
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
|
||||||
{
|
{
|
||||||
hasReceivedAnyNewData = true;
|
hasReceivedAnyNewData = true;
|
||||||
|
|
||||||
// --- APLICAMOS SEGURIDAD AQUÍ ---
|
|
||||||
DateTime fechaTotalizacion;
|
DateTime fechaTotalizacion;
|
||||||
if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
|
if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
|
_logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
|
||||||
fechaTotalizacion = DateTime.UtcNow;
|
fechaTotalizacion = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -685,10 +652,26 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
|
|
||||||
foreach (var banca in bancas)
|
foreach (var banca in bancas)
|
||||||
{
|
{
|
||||||
|
// --- MODIFICACIÓN 3: Lógica de "Upsert" para Agrupaciones ---
|
||||||
|
if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion);
|
||||||
|
|
||||||
|
var nuevaAgrupacion = new AgrupacionPolitica
|
||||||
|
{
|
||||||
|
Id = banca.IdAgrupacion,
|
||||||
|
Nombre = banca.NombreAgrupacion,
|
||||||
|
IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken);
|
||||||
|
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para no volver a crearla
|
||||||
|
}
|
||||||
|
|
||||||
todasLasProyecciones.Add(new ProyeccionBanca
|
todasLasProyecciones.Add(new ProyeccionBanca
|
||||||
{
|
{
|
||||||
EleccionId = EleccionId,
|
EleccionId = EleccionId,
|
||||||
AmbitoGeograficoId = seccion.Id,
|
AmbitoGeograficoId = ambito.Id,
|
||||||
AgrupacionPoliticaId = banca.IdAgrupacion,
|
AgrupacionPoliticaId = banca.IdAgrupacion,
|
||||||
NroBancas = banca.NroBancas,
|
NroBancas = banca.NroBancas,
|
||||||
CategoriaId = categoria.Id,
|
CategoriaId = categoria.Id,
|
||||||
@@ -704,6 +687,10 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
_logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos...");
|
_logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos...");
|
||||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
|
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
|
||||||
|
|
||||||
|
// Si se crearon nuevas agrupaciones, se guardarán aquí primero.
|
||||||
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
|
||||||
|
// Luego, procedemos con las proyecciones.
|
||||||
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
||||||
await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken);
|
await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken);
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
@@ -713,7 +700,7 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada.");
|
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos, la tabla no fue modificada.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|||||||
Reference in New Issue
Block a user