No creo que esta pregunta sea un duplicado de "Forma correcta de tratar las excepciones en DisposeAsync" .
Digamos que mi clase implementa IAsynsDisposable
porque tiene una tarea en segundo plano de ejecución prolongada y DisposeAsync
finaliza esa tarea. Un patrón familiar podría ser la propiedad Completion
, por ejemplo, ChannelReader<T>.Completion
(a pesar de que ChannelReader
no implementa IAsynsDisposable
).
¿Se considera una buena práctica propagar las excepciones de la tarea de Completion
fuera de DisposeAsync
?
Aquí hay un ejemplo completo que se puede copiar/pegar en un dotnet new console
. Tenga en cuenta await this.Completion
. Finalización dentro DisposeAsync
:
try { await using var service = new BackgroundService(TimeSpan.FromSeconds(2)); await Task.Delay(TimeSpan.FromSeconds(3)); } catch (Exception ex) { Console.WriteLine(ex); Console.ReadLine(); } class BackgroundService: IAsyncDisposable { public Task Completion { get; } private CancellationTokenSource _diposalCts = new(); public BackgroundService(TimeSpan timeSpan) { this.Completion = Run(timeSpan); } public async ValueTask DisposeAsync() { _diposalCts.Cancel(); try { await this.Completion; } finally { _diposalCts.Dispose(); } } private async Task Run(TimeSpan timeSpan) { try { await Task.Delay(timeSpan, _diposalCts.Token); throw new InvalidOperationException("Boo!"); } catch (OperationCanceledException) { } } }
Alternativamente, puedo observar service.Completion
explícitamente en el código del cliente (e ignorar sus excepciones dentro de DiposeAsync
para evitar que se arrojen dos veces), como a continuación:
try { await using var service = new BackgroundService(TimeSpan.FromSeconds(2)); await Task.Delay(TimeSpan.FromSeconds(3)); await service.Completion; } catch (Exception ex) { Console.WriteLine(ex); Console.ReadLine(); } class BackgroundService: IAsyncDisposable { public Task Completion { get; } private CancellationTokenSource _diposalCts = new(); public BackgroundService(TimeSpan timeSpan) { this.Completion = Run(timeSpan); } public async ValueTask DisposeAsync() { _diposalCts.Cancel(); try { await this.Completion; } catch { // the client should observe this.Completion } finally { _diposalCts.Dispose(); } } private async Task Run(TimeSpan timeSpan) { try { await Task.Delay(timeSpan, _diposalCts.Token); throw new InvalidOperationException("Boo!"); } catch (OperationCanceledException) { } } }
¿Hay consenso sobre qué opción es mejor?
Por ahora, me he decidido por una clase de ayuda reutilizable LongRunningAsyncDisposable
( aquí hay una esencia , advertencia: apenas probada todavía), que permite:
IAsyncDisposable.DisposeAsync
en cualquier momento, de una manera segura para subprocesos y amigable con la concurrencia ;DisposeAsync
debe volver a lanzar las excepciones de la tarea ( DisposeAsync
esperará la finalización de la tarea de cualquier manera, antes de realizar una limpieza);LongRunningAsyncDisposable.Completion
.