Aller au contenu principal

Remote Progress & Background Task

Le Remote Progress permet de lancer une tâche longue côté serveur, de libérer rapidement la requête HTTP, puis de suivre sa progression côté Blazor via SignalR. Côté serveur, le service central est BackgroundTaskService. Côté client, RemoteTaskService crée l'appel HTTP, ouvre la connexion au hub et expose un RemoteTaskState contenant la progression.

Ce mécanisme évite les expirations de délai sur les traitements longs tout en conservant un retour utilisateur en temps réel.

Fonctionnement & prérequis​

Packages et services​

Côté client, ajoutez le package Lagoon.UI.RemoteTasks puis enregistrez le service :

Registry.cs
builder.Services.AddScoped<RemoteTaskService>();

Côté serveur, activez les background tasks :

Registry.cs
services.AddBackgroundTasks();

La méthode d'extension réelle est :

public static IServiceCollection AddBackgroundTasks(this IServiceCollection services)

Elle ajoute SignalR avec le protocole MessagePack et enregistre BackgroundTaskService en singleton. Le hub Remote Task est mappé automatiquement par le fallback Lagoon lorsque le service est enregistré.

Principe d'exécution​

  1. Le client appelle RemoteTaskService.TryPostAsync(...).
  2. Le service client initialise la connexion SignalR, génère un identifiant de tâche et ajoute TaskId / HubCoId à la requête HTTP.
  3. Le contrôleur serveur appelle BackgroundTaskService.RunTask(...).
  4. La tâche s'exécute dans un nouveau scope de dépendances.
  5. Le serveur transmet les changements de Progress au client.
  6. À la fin, le serveur renvoie le résultat ou le message d'erreur au RemoteTaskState.

Utilisation simple​

Lancer une tâche serveur​

BackgroundTaskService expose les signatures publiques suivantes :

public void RunTask<TService, TIn, TResult>(ControllerBase controller, Func<TService, TIn, Progress, CancellationToken, Task<TResult>> func, TIn arg)
public void RunTask<TService, TResult>(ControllerBase controller, Func<TService, Progress, CancellationToken, Task<TResult>> func)
public void RunTask<TService>(ControllerBase controller, Func<TService, Progress, CancellationToken, Task> func)
attention

La méthode passée à RunTask doit être statique. Si vous passez un paramètre (TIn), il ne doit pas référencer de service scoped, car la tâche de fond s'exécute dans un scope différent de celui de la requête HTTP.

Exemple sans résultat :

ImportsController.cs
[ApiController]
[Route("api/imports")]
public class ImportsController : LgControllerBase
{
private readonly BackgroundTaskService _backgroundTaskService;

public ImportsController(BackgroundTaskService backgroundTaskService)
{
_backgroundTaskService = backgroundTaskService;
}

[HttpPost("orders")]
public void ImportOrders(ImportOrdersVm model)
{
// Lance la tâche dans un scope serveur dédié et termine immédiatement la requête HTTP.
_backgroundTaskService.RunTask<ImportService, string, int>(this, ImportOrdersAsync, model.ImportId);
}

private static async Task<int> ImportOrdersAsync(ImportService service, string importId, Progress progress, CancellationToken cancellationToken)
{
progress.Reset(100, "#ImportStarting");

for (int index = 0; index < 100; index++)
{
cancellationToken.ThrowIfCancellationRequested();

// Exécute une étape métier puis notifie le client de l'avancement.
await service.ImportNextOrderAsync(importId, cancellationToken);
progress.Increment(1, "#ImportInProgress");
}

progress.ReportEnd("#ImportFinished");
return 100;
}
}

Suivre la progression (Remote Progress)​

Côté client, RemoteTaskService expose :

public Task<RemoteTaskState> TryPostAsync<TArg>(string requestUri, TArg model)
public Task<RemoteTaskState> TryPostAsync<TArg>(string requestUri, TArg model, Func<RemoteTaskState, Task> onEnd)
public async Task<RemoteTaskState<TResult>> TryPostAsync<TArg, TResult>(string requestUri, TArg model, Func<RemoteTaskState<TResult>, Task> onEnd)

RemoteTaskState expose notamment :

public string ErrorMessage { get; }
public bool HasError { get; }
public Progress Progress { get; }
public void ThrowIfErrorOccured();
public void Cancel();

RemoteTaskState<TResult> ajoute :

public TResult Result { get; }

Exemple d'écran client avec affichage de la barre de progression :

ImportOrdersPage.razor
@inherits LgPage

<LgButton Text="#StartImport" OnClick="LaunchImportAsync" />

@if (_state is not null)
{
<LgLoaderView Progress="_state.Progress" ProgressType="ProgressType.Bar">
<LgLabel Text="#ImportDone" />
</LgLoaderView>
<LgButton Text="#CancelImport" Kind="ButtonKind.Secondary" OnClick="CancelImportAsync" />
}
ImportOrdersPage.razor.cs
public partial class ImportOrdersPage : LgPage
{
[Inject]
public RemoteTaskService RemoteTaskService { get; set; }

private RemoteTaskState<int> _state;

private async Task LaunchImportAsync(ActionEventArgs args)
{
try
{
ImportOrdersVm model = new()
{
ImportId = "orders-2024"
};

// Démarre l'appel HTTP puis conserve l'état pour afficher la progression.
_state = await RemoteTaskService.TryPostAsync<ImportOrdersVm, int>("api/imports/orders", model, OnImportEndedAsync);
}
catch (Exception ex)
{
ShowException(ex);
}
}

private Task CancelImportAsync(ActionEventArgs args)
{
// Demande l'annulation côté serveur via le hub Remote Task.
_state?.Cancel();
return Task.CompletedTask;
}

private Task OnImportEndedAsync(RemoteTaskState<int> state)
{
try
{
// Convertit l'erreur serveur éventuelle en exception utilisateur Lagoon.
state.ThrowIfErrorOccured();
ShowSuccess("#ImportFinished");
}
catch (Exception ex)
{
ShowException(ex);
}
return Task.CompletedTask;
}
}

La progression transite par Progress. Les méthodes publiques les plus utiles sont :

public void Reset(string message = null)
public void Reset(int endPosition, string message = null)
public void Reset(int startPosition, int endPosition, string message = null)
public void Increment()
public void Increment(string message)
public void Increment(int value)
public void Increment(int value, string message)
public void Report(int position)
public void Report(string message)
public void Report(int position, string message)
public void ReportEnd()
public void ReportEnd(string message)
public Progress OpenSubProgress(int subPartLength, bool ignoreMessages = false)
public Progress OpenSubProgress(int subPartLength, string message, bool ignoreMessages = false)
public Progress OpenSubProgress(int subPartLength, int startPosition, int endPosition, bool ignoreMessages = false)
public Progress OpenSubProgress(int subPartLength, int startPosition, int endPosition, string message, bool ignoreMessages = false)
public static Progress operator ++(Progress progress)

BackgroundTaskProgress relaie automatiquement Reset, Report, Increment et ReportEnd vers le client. Les doublons de pourcentage/message sont filtrés pour éviter des notifications SignalR inutiles.

Utilisation avancée​

Retourner un résultat​

Utilisez la signature RunTask<TService, TIn, TResult> côté serveur et TryPostAsync<TArg, TResult> côté client. Le résultat est disponible dans RemoteTaskState<TResult>.Result lorsque le callback de fin est appelé.

ImportsController.cs
[HttpPost("preview")]
public void BuildPreview(ImportOrdersVm model)
{
// Retourne le nombre de lignes analysées au client à la fin de la tâche.
_backgroundTaskService.RunTask<ImportService, string, int>(this, BuildPreviewAsync, model.ImportId);
}

private static async Task<int> BuildPreviewAsync(ImportService service, string importId, Progress progress, CancellationToken cancellationToken)
{
progress.Reset(10, "#PreviewStarting");
int count = await service.BuildPreviewAsync(importId, progress, cancellationToken);
progress.ReportEnd("#PreviewFinished");
return count;
}

Annulation​

Côté client, appelez :

_state.Cancel();

Côté serveur, la méthode de tâche reçoit un CancellationToken. Vérifiez-le régulièrement :

cancellationToken.ThrowIfCancellationRequested();

Erreurs​

Si la tâche serveur échoue, BackgroundTaskService capture l'exception, trace l'erreur et renvoie un message d'erreur au client. Dans le callback de fin, utilisez la méthode réelle :

state.ThrowIfErrorOccured();

Vous pouvez aussi consulter :

state.HasError
state.ErrorMessage

Reconnexion et nettoyage​

En cas de reconnexion SignalR, le client demande l'état courant de chaque tâche suivie. Si la tâche est terminée, le serveur renvoie son résultat ; si elle n'existe plus, un message d'erreur est renvoyé. Une fois le résultat reçu, le client demande au serveur de libérer le résultat conservé en mémoire.

Voir aussi​