Implémenter une méthode DisposeAsync

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

Lors de l’implémentation de l’interface IAsyncDisposable, il 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êt pour la suppression synchrone ou asynchrone ; mais cela n’est pas une obligation. Si aucune suppression synchrone de votre classe n’est possible, avoir seulement 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 de suppression.

Attention

Si vous implémentez l’interface IAsyncDisposable, mais pas l’interface IDisposable, votre application peut potentiellement laisser fuiter des ressources. Si une classe implémente IAsyncDisposable mais pas IDisposable, et qu’un consommateur appelle seulement Dispose, votre implémentation n’appelle jamais DisposeAsync. Ceci 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. Le IServiceProvider et le IHost correspondant orchestrent le nettoyage des ressources. 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 les méthodes DisposeAsync et DisposeAsyncCore

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

  • Une implémentation de publicIAsyncDisposable.DisposeAsync() qui n’a pas de paramètres.

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

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

Méthode DisposeAsync

La méthode DisposeAsync() sans paramètre public est appelée implicitement dans une instruction await using, 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 d’être exécuté. La libération de la mémoire associée à un objet managé est toujours du domaine du récupérateur de mémoire. 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);

    // Suppress finalization.
    GC.SuppressFinalize(this);
}

Notes

Une différence principale dans le modèle de suppression asynchrone par rapport au modèle de suppression, est que l’appel depuis DisposeAsync() à la méthode surchargée Dispose(bool) reçoit false comme argument. Cependant, lors de l’implémentation de la méthode IDisposable.Dispose(), c’est true qui est passé à la place. Ceci permet de garantir l’équivalence fonctionnelle avec le modèle de suppression synchrone et de garantir que les chemins de code du finaliseur sont néanmoins toujours appelés. En d’autres termes, la méthode DisposeAsyncCore() va supprimer les ressources managées de façon asynchrone : vous ne voulez donc pas les supprimer aussi de façon synchrone. Par conséquent, appelez Dispose(false) au lieu de Dispose(true).

Méthode DisposeAsyncCore

La méthode DisposeAsyncCore() est destinée à effectuer le nettoyage asynchrone des ressources managées ou pour les appels en cascade à DisposeAsync(). Elle encapsule les opérations de nettoyage asynchrones courantes quand une sous-classe hérite d’une classe de base qui est une implémentation de IAsyncDisposable. La méthode DisposeAsyncCore() est virtual : les classes dérivées peuvent donc définir un nettoyage personnalisé dans leurs surcharges.

Conseil

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

Implémenter le modèle de supplémentaires 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 toute classe de base potentielle, vous devez fournir la méthode protected virtual ValueTask DisposeAsyncCore(). Certains des exemples suivants utilisent une classe NoopAsyncDisposable 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 le type NoopAsyncDisposable. Le type implémente en DisposeAsync renvoyant 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 :

  • Le ExampleAsyncDisposable est 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 méthode DisposeAsync délègue à la méthode DisposeAsyncCore et appelle GC.SuppressFinalize pour avertir le récupérateur de mémoire que le finaliseur ne doit pas s’exécuter.
  • Elle contient une méthode DisposeAsyncCore() qui appelle la méthode _example.DisposeAsync() et définit le champ sur null.
  • La méthode DisposeAsyncCore() est virtual, ce qui permet aux sous-classes de la remplacer par un comportement personnalisé.

Autre modèle de suppression asynchrone scellé

Si votre classe d’implémentation peut être sealed, vous pouvez implémenter le modèle de suppression asynchrone en surchargeant la méthode IAsyncDisposable.DisposeAsync(). 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 :

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

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

Il peut être nécessaire d’implémenter à la fois les interfaces IDisposable et IAsyncDisposable, en particulier quand l’étendue de votre classe contient des instances de ces implémentations. Ceci garantit que vous pouvez correctement effectuer des appels de nettoyage en cascade. Voici un exemple de classe qui implémente les deux interfaces et illustre les instructions appropriées 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);
        GC.SuppressFinalize(this);
    }

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

            if (_asyncDisposableResource is IDisposable disposable)
            {
                disposable.Dispose();
                _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 implémentations de IDisposable.Dispose() et de IAsyncDisposable.DisposeAsync() sont toutes deux du code réutilisable simple.

Dans la méthode de surcharge Dispose(bool), l’instance IDisposable est supprimée de façon 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 de la valeur null.

Avec la méthode DisposeAsyncCore(), 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 façon asynchrone. Les deux instances sont ensuite affectées de la valeur null.

Chaque implémentation s’efforce de supprimer tous les objets jetables possibles. Cela garantit que le nettoyage est correctement mis en cascade .

Utilisation d’un supprimable asynchrone

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

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 méthode d’extension ConfigureAwait(IAsyncDisposable, Boolean) de l’interface IAsyncDisposable pour configurer la façon dont la continuation de la tâche est marshalée sur son contexte ou son planificateur d’origine. Pour plus d’informations sur ConfigureAwait, consultez Questions fréquentes (FAQ) sur ConfigureAwait.

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, elle peut être écrite de façon à 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 await dans une même ligne

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

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

Dans l’exemple précédent :

  • La méthode BeginTransactionAsync est attendue.
  • Le type de retour est DbTransaction, qui implémente IAsyncDisposable.
  • transaction est utilisée de façon asynchrone et également attendue.

Using empilés

Dans les situations où vous créez et où vous utilisez plusieurs objets qui implémentent IAsyncDisposable, il est possible que l’empilement d’instructions await using avec ConfigureAwait puisse empêcher les appels à DisposeAsync() dans des conditions irrégulières. Pour garantir que DisposeAsync() est 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 bloc await using. L’étendue externe suit la façon dont objOne définit ses accolades, englobant objTwo, de sorte que objTwo est supprimé en premier, suivi de objOne. La méthode DisposeAsync() des deux instances IAsyncDisposable étant attendue, chaque instance effectue son opération de nettoyage asynchrone. Les appels sont imbriqués et non pas empilés.

Modèle acceptable 2

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 bloc await using. À la fin de chaque bloc, la méthode DisposeAsync() de l’instance IAsyncDisposable correspondante est attendue, effectuant ainsi son opération de nettoyage asynchrone. Les appels sont séquentiels et non pas empilés. Dans ce scénario, objOne est supprimé en premier, puis objTwo est supprimé.

Modèle acceptable 3

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 par le corps de méthode qu’elle contient. À la fin du bloc englobant, les instances IAsyncDisposable effectuent leurs opérations de nettoyage asynchrones. Cet exemple s’exécute dans l’ordre inverse de celui dont ils ont été déclarés, ce qui signifie que objTwo est supprimé avant objOne.

Modèle non acceptable

Les lignes mises en surbrillance dans le code suivant montrent ce que cela signifie d’avoir des « usings empilés ». Si une exception est levée depuis le constructeur AnotherAsyncDisposable, aucun objet n’est correctement supprimé. La variable objTwo n’est jamais affectée, car le constructeur ne s’est pas terminé correctement. Par conséquent, le constructeur pour AnotherAsyncDisposable est responsable de l’élimination des ressources allouées avant de lever une exception. Si le type ExampleAsyncDisposable a un finaliseur, il est éligible pour la finalisation.

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 un des modèles acceptables, le problème des objets non supprimés n’existe pas. Les opérations de nettoyage sont effectuées correctement quand les instructions using ne sont pas empilées.

Voir aussi

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