Implémenter une méthode DisposeAsync

L’interface System.IAsyncDisposable a été introduite dans le cadre de C# 8.0. Vous implémentez la IAsyncDisposable.DisposeAsync() méthode lorsque vous devez effectuer un nettoyage des ressources, comme vous le feriez lors de l’implémentation d’une méthode Dispose. L’une des principales différences, cependant, est que cette implémentation permet des opérations de nettoyage asynchrones. DisposeAsync() retourne un ValueTask représentant l’opération de suppression asynchrone.

Lors de l’implémentation de l’interface, il IAsyncDisposable est courant que les classes implémentent également l’interface IDisposable . Un bon modèle d’implémentation de l’interface IAsyncDisposable doit être préparé pour la suppression synchrone ou asynchrone, mais ce n’est pas une exigence. Si aucun jetable synchrone de votre classe n’est possible, le fait d’avoir uniquement IAsyncDisposable est acceptable. Toutes les instructions relatives à l’implémentation du modèle de suppression s’appliquent également à l’implémentation asynchrone. Cet article suppose que vous êtes déjà familiarisé avec l’implémentation d’une méthode Dispose.

Attention

Si vous implémentez l’interface IAsyncDisposable , mais pas l’interface IDisposable , votre application peut potentiellement fuiter des ressources. Si une classe implémente IAsyncDisposable, mais pas IDisposable, et qu’un consommateur appelle Disposeuniquement , votre implémentation n’appelle DisposeAsyncjamais . Cela entraînerait une fuite de ressources.

Conseil

En ce qui concerne l’injection de dépendances, lors de l’inscription de services dans un IServiceCollection, la durée de vie du service est gérée implicitement en votre nom. Et orchestrent le IServiceProvider nettoyage des ressources correspondant IHost . Plus précisément, les implémentations de IDisposable et IAsyncDisposable sont correctement supprimées à la fin de leur durée de vie spécifiée.

Pour plus d’informations, consultez Injection de dépendances dans .NET.

Explorer DisposeAsync et DisposeAsyncCore méthodes

L’interface IAsyncDisposable déclare une seule méthode sans paramètre, DisposeAsync(). Toute classe non scellée doit avoir une méthode supplémentaire DisposeAsyncCore() qui retourne également un ValueTask.

  • Implémentation publicIAsyncDisposable.DisposeAsync() qui n’a aucun paramètre.

  • Méthode protected virtual ValueTask DisposeAsyncCore() dont la signature est :

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

Méthode DisposeAsync

La public méthode sans DisposeAsync() paramètre est appelée implicitement dans une await using instruction, et son objectif est de libérer des ressources non managées, d’effectuer un nettoyage général et d’indiquer que le finaliseur, le cas échéant, n’a pas besoin de s’exécuter. Libérer la mémoire associée à un objet managé est toujours le domaine du garbage collector. De ce fait, son implémentation standard est la suivante :

public async ValueTask DisposeAsync()
{
    // Perform async cleanup.
    await DisposeAsyncCore();

    // Dispose of unmanaged resources.
    Dispose(false);

#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
    // Suppress finalization.
    GC.SuppressFinalize(this);
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
}

Notes

Une différence principale dans le modèle de suppression asynchrone par rapport au modèle de suppression est que l’appel de à la Dispose(bool) méthode de DisposeAsync() surcharge est donné false en tant qu’argument. Lors de l’implémentation de la IDisposable.Dispose() méthode, toutefois, true est passé à la place. Cela permet de garantir l’équivalence fonctionnelle avec le modèle de suppression synchrone et garantit en outre que les chemins de code du finaliseur sont toujours appelés. En d’autres termes, la DisposeAsyncCore() méthode se débarrassera des ressources managées de manière asynchrone. Vous ne souhaitez donc pas les supprimer de manière synchrone. Par conséquent, appelez Dispose(false) au lieu de Dispose(true).

Méthode DisposeAsyncCore

La DisposeAsyncCore() méthode est destinée à effectuer le nettoyage asynchrone des ressources managées ou à des appels en cascade à DisposeAsync(). Il encapsule les opérations de nettoyage asynchrones courantes lorsqu’une sous-classe hérite d’une classe de base qui est une implémentation de IAsyncDisposable. La DisposeAsyncCore() méthode permet virtual aux classes dérivées de définir un nettoyage supplémentaire dans leurs remplacements.

Conseil

Si une implémentation de IAsyncDisposable est sealed, la DisposeAsyncCore() méthode n’est pas nécessaire et le nettoyage asynchrone peut être effectué directement dans la IAsyncDisposable.DisposeAsync() méthode .

Implémenter le modèle de suppression asynchrone

Toutes les classes non scellées doivent être considérées comme une classe de base potentielle, car elles peuvent être héritées. Si vous implémentez le modèle de suppression asynchrone pour une classe de base potentielle, vous devez fournir la protected virtual ValueTask DisposeAsyncCore() méthode . Certains des exemples suivants utilisent une NoopAsyncDisposable classe définie comme suit :

public sealed class NoopAsyncDisposable : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}

Voici un exemple d’implémentation du modèle de suppression asynchrone qui utilise un type personnalisé NoopAsyncDisposable qui implémente DisposeAsync en retournant ValueTask.CompletedTask.

public class ExampleAsyncDisposable : IAsyncDisposable
{
    private IAsyncDisposable? _example;

    public ExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        GC.SuppressFinalize(this);
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_example is not null)
        {
            await _example.DisposeAsync().ConfigureAwait(false);
        }

        _example = null;
    }
}

Dans l’exemple précédent :

  • est ExampleAsyncDisposable une classe non scellée qui implémente l’interface IAsyncDisposable .
  • Il contient un champ privé IAsyncDisposable , _example, qui est initialisé dans le constructeur.
  • La DisposeAsync méthode délègue à la DisposeAsyncCore méthode et appelle GC.SuppressFinalize pour avertir le garbage collector que le finaliseur n’a pas à s’exécuter.
  • Il contient une DisposeAsyncCore() méthode qui appelle la _example.DisposeAsync() méthode et définit le champ sur null.
  • La DisposeAsyncCore() méthode est virtual et est remplacée dans la ExampleAsyncDisposable classe .

Modèle d’élimination asynchrone alternative scellée

Si votre classe d’implémentation peut être sealed, vous pouvez implémenter le modèle de suppression asynchrone en remplaçant la IAsyncDisposable.DisposeAsync() méthode . L’exemple suivant montre comment implémenter le modèle de suppression asynchrone pour une classe scellée :

public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
    private readonly IAsyncDisposable _example;

    public SealedExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public ValueTask DisposeAsync() => _example.DisposeAsync();
}

Dans l’exemple précédent :

  • est SealedExampleAsyncDisposable une classe scellée qui implémente l’interface IAsyncDisposable .
  • Le champ contenant _example est readonly et est initialisé dans le constructeur.
  • La DisposeAsync méthode appelle la _example.DisposeAsync() méthode, implémentant le modèle via le champ contenant (suppression en cascade).

Implémenter à la fois des modèles de suppression et de suppression asynchrone

Vous devrez peut-être implémenter les IDisposable interfaces et IAsyncDisposable , en particulier lorsque l’étendue de votre classe contient des instances de ces implémentations. Cela garantit que vous pouvez effectuer des appels de nettoyage en cascade correctement. Voici un exemple de classe qui implémente les deux interfaces et illustre les conseils appropriés pour le nettoyage.

class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
    IDisposable? _disposableResource = new MemoryStream();
    IAsyncDisposable? _asyncDisposableResource = new MemoryStream();

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        Dispose(disposing: false);
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
        GC.SuppressFinalize(this);
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disposableResource?.Dispose();
            (_asyncDisposableResource as IDisposable)?.Dispose();
            _disposableResource = null;
            _asyncDisposableResource = null;
        }
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_asyncDisposableResource is not null)
        {
            await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }

        if (_disposableResource is IAsyncDisposable disposable)
        {
            await disposable.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
            _disposableResource?.Dispose();
        }

        _asyncDisposableResource = null;
        _disposableResource = null;
    }
}

Les IDisposable.Dispose() implémentations et IAsyncDisposable.DisposeAsync() sont à la fois du code réutilisable simple.

Dans la Dispose(bool) méthode de surcharge, l’instance IDisposable est supprimée de manière conditionnelle si elle n’est pas null. L’instance IAsyncDisposable est castée en tant que IDisposable, et si elle n’est pas non plus null, elle est également supprimée. Les deux instances sont ensuite affectées à null.

Avec la DisposeAsyncCore() méthode, la même approche logique est suivie. Si l’instance IAsyncDisposable n’est pas null, son appel à DisposeAsync().ConfigureAwait(false) est attendu. Si l’instance IDisposable est également une implémentation de IAsyncDisposable, elle est également supprimée de manière asynchrone. Les deux instances sont ensuite affectées à null.

Utilisation d’un jetable asynchrone

Pour consommer correctement un objet qui implémente l’interface IAsyncDisposable , vous utilisez les mots clés await et en utilisant ensemble. Prenons l’exemple suivant, où la ExampleAsyncDisposable classe est instanciée, puis encapsulée dans une await using instruction.

class ExampleConfigureAwaitProgram
{
    static async Task Main()
    {
        var exampleAsyncDisposable = new ExampleAsyncDisposable();
        await using (exampleAsyncDisposable.ConfigureAwait(false))
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

Important

Utilisez la ConfigureAwait(IAsyncDisposable, Boolean) méthode d’extension de l’interface IAsyncDisposable pour configurer la façon dont la continuation de la tâche est marshalée sur son contexte ou planificateur d’origine. Pour plus d’informations sur , consultez La faq sur ConfigureAwaitla configuration d’un objet.

Pour les situations où l’utilisation de ConfigureAwait n’est pas nécessaire, l’instruction await using peut être simplifiée comme suit :

class ExampleUsingStatementProgram
{
    static async Task Main()
    {
        await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

En outre, il peut être écrit pour utiliser l’étendue implicite d’une déclaration using.

class ExampleUsingDeclarationProgram
{
    static async Task Main()
    {
        await using var exampleAsyncDisposable = new ExampleAsyncDisposable();

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Plusieurs mots clés d’attente dans une seule ligne

Parfois, le await mot clé peut apparaître plusieurs fois dans une seule ligne. Considérons par exemple le code suivant :

await using var transaction = await context.Database.BeginTransactionAsync(token);

Dans l’exemple précédent :

  • La BeginTransactionAsync méthode est attendue.
  • Le type de retour est DbTransaction, qui implémente IAsyncDisposable.
  • Le transaction est utilisé de manière asynchrone et également attendu.

Utilisations empilées

Dans les situations où vous créez et utilisez plusieurs objets qui implémentent IAsyncDisposable, il est possible que l’empilement des await using instructions avec ConfigureAwait puisse empêcher les appels à DisposeAsync() dans des conditions errantes. Pour vous assurer que est DisposeAsync() toujours appelé, vous devez éviter l’empilement. Les trois exemples de code suivants montrent des modèles acceptables à utiliser à la place.

Modèle acceptable 1


class ExampleOneProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.

            var objTwo = new ExampleAsyncDisposable();
            await using (objTwo.ConfigureAwait(false))
            {
                // Interact with the objOne and/or objTwo instance(s).
            }
        }

        Console.ReadLine();
    }
}

Dans l’exemple précédent, chaque opération de nettoyage asynchrone est explicitement délimitée sous le await using bloc. L’étendue externe est définie par la façon dont objOne définit ses accolades, en englobant objTwo, en tant que tel objTwo est supprimé en premier, suivi de objOne. Les deux IAsyncDisposable instances ayant leur DisposeAsync() méthode attendue, chaque instance effectue son opération de nettoyage asynchrone. Les appels sont imbriqués et non empilés.

Modèle acceptable deux

class ExampleTwoProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.
        }

        var objTwo = new ExampleAsyncDisposable();
        await using (objTwo.ConfigureAwait(false))
        {
            // Interact with the objTwo instance.
        }

        Console.ReadLine();
    }
}

Dans l’exemple précédent, chaque opération de nettoyage asynchrone est explicitement délimitée sous le await using bloc. À la fin de chaque bloc, la méthode de l’instance DisposeAsync() correspondante IAsyncDisposable est attendue, effectuant ainsi son opération de nettoyage asynchrone. Les appels sont séquentiels et non empilés. Dans ce scénario objOne , est d’abord supprimé, puis objTwo est supprimé.

Modèle trois acceptable

class ExampleThreeProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using var ignored1 = objOne.ConfigureAwait(false);

        var objTwo = new ExampleAsyncDisposable();
        await using var ignored2 = objTwo.ConfigureAwait(false);

        // Interact with objOne and/or objTwo instance(s).

        Console.ReadLine();
    }
}

Dans l’exemple précédent, chaque opération de nettoyage asynchrone est implicitement délimitée avec le corps de la méthode contenant. À la fin du bloc englobant, les IAsyncDisposable instances effectuent leurs opérations de nettoyage asynchrones. Cela s’exécute dans l’ordre inverse à partir duquel ils ont été déclarés, ce qui signifie que objTwo est supprimé avant objOne.

Modèle inacceptable

Les lignes mises en surbrillance dans le code suivant montrent ce que signifie avoir des « utilisations empilées ». Si une exception est levée à partir du constructeur, aucun AnotherAsyncDisposable des objets n’est correctement supprimé. La variable objTwo n’est jamais affectée, car le constructeur n’a pas réussi. Par conséquent, le constructeur pour AnotherAsyncDisposable est chargé de supprimer toutes les ressources allouées avant de lever une exception. Si le ExampleAsyncDisposable type a un finaliseur, il peut être finalisé.

class DoNotDoThisProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        // Exception thrown on .ctor
        var objTwo = new AnotherAsyncDisposable();

        await using (objOne.ConfigureAwait(false))
        await using (objTwo.ConfigureAwait(false))
        {
            // Neither object has its DisposeAsync called.
        }

        Console.ReadLine();
    }
}

Conseil

Évitez ce modèle, car il pourrait entraîner un comportement inattendu. Si vous utilisez l’un des modèles acceptables, le problème des objets nondisposés est inexistant. Les opérations de nettoyage sont correctement effectuées lorsque using les instructions ne sont pas empilées.

Voir aussi

Pour obtenir un exemple d’implémentation double de IDisposable et IAsyncDisposable, consultez le Utf8JsonWriter code source sur GitHub.