feat: Refactor RaceUploadCommunicationClient and RaceUploadTabView to improve HttpClient management and resource disposal

This commit is contained in:
MaddoScientisto 2026-03-08 14:30:37 +01:00
commit bdf503c627
3 changed files with 145 additions and 81 deletions

View file

@ -17,18 +17,21 @@ namespace ImageCatalog_2.AvaloniaViews;
public partial class RaceUploadTabView : Avalonia.Controls.UserControl
{
private readonly IRaceUploadCommunicationClient _apiClient;
private readonly ILogger<RaceUploadTabView> _logger;
public RaceUploadTabView()
{
InitializeComponent();
_apiClient = Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
_logger = Program.ServiceProvider.GetService(typeof(ILogger<RaceUploadTabView>)) as ILogger<RaceUploadTabView>
?? NullLogger<RaceUploadTabView>.Instance;
}
private static IRaceUploadCommunicationClient CreateClient()
{
return Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
}
private async void CreateRace_Click(object? sender, RoutedEventArgs e)
{
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
@ -76,32 +79,43 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
var saveResponse = await _apiClient.SaveRaceAsync(
new RaceSaveRequest
{
IdGara = 0,
Description = sanitizedDescription,
StartDate = startDate,
EndDate = endDate,
TipoGaraId = tipoGaraId,
EventoInLinea = model.ApiEventoInLineaIndex,
TipoIndicizzazione = model.ApiTipoIndexValue,
FreeEvent = model.ApiFreeEventIndex,
PathBase = model.ApiPathBase?.Trim(),
Localita = model.ApiLocalita?.Trim(),
},
CancellationToken.None);
RawEndpointResponse saveResponse;
RawEndpointResponse createPointsResponse;
long raceId;
var raceId = ExtractRaceId(saveResponse.Body);
if (raceId <= 0)
var client = CreateClient();
try
{
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
saveResponse = await client.SaveRaceAsync(
new RaceSaveRequest
{
IdGara = 0,
Description = sanitizedDescription,
StartDate = startDate,
EndDate = endDate,
TipoGaraId = tipoGaraId,
EventoInLinea = model.ApiEventoInLineaIndex,
TipoIndicizzazione = model.ApiTipoIndexValue,
FreeEvent = model.ApiFreeEventIndex,
PathBase = model.ApiPathBase?.Trim(),
Localita = model.ApiLocalita?.Trim(),
},
CancellationToken.None).ConfigureAwait(false);
raceId = ExtractRaceId(saveResponse.Body);
if (raceId <= 0)
{
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
}
model.ApiRaceId = raceId.ToString();
createPointsResponse = await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(false);
}
finally
{
(client as IDisposable)?.Dispose();
}
model.ApiRaceId = raceId.ToString();
var createPointsResponse = await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None);
var sb = new StringBuilder();
sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}");
@ -197,41 +211,50 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
var sb = new StringBuilder();
sb.AppendLine($"File da inviare: {files.Count}");
foreach (var file in files)
List<long> pointIds;
var client = CreateClient();
try
{
var relativePath = Path.GetRelativePath(model.DestinationPath, file);
var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
await using var stream = File.OpenRead(file);
await _apiClient.UploadFileToReceiverAsync(
new ReceiveFileUploadRequest
{
FileName = Path.GetFileName(file),
FileStream = stream,
DestinationPath = remotePath,
OverwriteRemoteFile = true,
},
CancellationToken.None).ConfigureAwait(true);
uploaded++;
if (uploaded % 20 == 0 || uploaded == files.Count)
foreach (var file in files)
{
statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
var relativePath = Path.GetRelativePath(model.DestinationPath, file);
var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
await using var stream = File.OpenRead(file);
await client.UploadFileToReceiverAsync(
new ReceiveFileUploadRequest
{
FileName = Path.GetFileName(file),
FileStream = stream,
DestinationPath = remotePath,
OverwriteRemoteFile = true,
},
CancellationToken.None).ConfigureAwait(true);
uploaded++;
if (uploaded % 20 == 0 || uploaded == files.Count)
{
statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
}
}
sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
foreach (var pointId in pointIds)
{
await client.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
}
}
sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
var pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
foreach (var pointId in pointIds)
finally
{
await _apiClient.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
(client as IDisposable)?.Dispose();
}
sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
outputBox.Text = sb.ToString();
statusBlock.Text = "Upload e indicizzazione completati.";
@ -251,31 +274,49 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
private async Task<RawEndpointResponse> LoginAsync(string login, string password)
{
return await _apiClient.LoginAdminAsync(
new AdminLoginRequest
{
Login = login,
Password = password,
Command = "check",
},
CancellationToken.None).ConfigureAwait(false);
var client = CreateClient();
try
{
return await client.LoginAdminAsync(
new AdminLoginRequest
{
Login = login,
Password = password,
Command = "check",
},
CancellationToken.None).ConfigureAwait(false);
}
finally
{
(client as IDisposable)?.Dispose();
}
}
private async Task<List<long>> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
{
const int maxAttempts = 10;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
var client = CreateClient();
try
{
var response = await _apiClient.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
var ids = ExtractPointIds(response.Body);
if (ids.Count > 0)
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
return ids;
var response = await client.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
var ids = ExtractPointIds(response.Body);
if (ids.Count > 0)
{
return ids;
}
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
}
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
return new List<long>();
}
finally
{
(client as IDisposable)?.Dispose();
}
return [];