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 .