Implementieren einer DisposeAsync-Methode

Dies System.IAsyncDisposable-Schnittstelle wurde als Teil von C# 8.0 eingeführt. Sie implementieren die IAsyncDisposable.DisposeAsync() -Methode, wenn Sie eine Ressourcenbereinigung durchführen müssen, genau wie beim IAsyncDisposable.DisposeAsync(). Einer der Hauptunterschiede besteht jedoch darin, dass diese Implementierung asynchrone Bereinigungsvorgänge zulässt. DisposeAsync() gibt eine ValueTask zurück, die den asynchronen Dispose-Vorgang darstellt.

Bei der Implementierung der IAsyncDisposable-Schnittstelle ist es typisch, dass Klassen auch die IDisposable-Schnittstelle implementieren. Ein gutes Implementierungsmuster der IAsyncDisposable-Schnittstelle besteht darin, sowohl auf den synchronen als auch auf den asynchronen Dispose-Vorgang vorbereitet zu sein. Alle Anleitungen zum Implementieren des Dispose-Musters gelten auch für die asynchrone Implementierung. In diesem Artikel wird davon ausgegangen, dass Sie bereits mit der Implementierung einer Dispose-Methode vertraut sind.

Tipp

Im Hinblick auf die Abhängigkeitsinjektion wird die IServiceCollection beim Registrieren von Diensten in einem IServiceCollectionimplizit in Ihrem Namen verwaltet. Die IServiceProvider und die entsprechende IHost Orchestrierungsressourcenbereinigung. Insbesondere Implementierungen von IDisposable und IAsyncDisposable werden am Ende ihrer angegebenen Lebensdauer ordnungsgemäß verworfen.

Weitere Informationen finden Sie unter Abhängigkeitsinjektion in .NET.

DisposeAsync() und DisposeAsyncCore()

Die IAsyncDisposable-Schnittstelle deklariert eine einzelne parameterlose Methode: DisposeAsync(). Jede nicht versiegelte Klasse sollte über eine zusätzliche DisposeAsyncCore()-Methode verfügen, die ebenfalls eine ValueTask zurückgibt.

  • Eine publicIAsyncDisposable.DisposeAsync() -Implementierung, die keine Parameter aufweist.

  • Eine protected virtual ValueTask DisposeAsyncCore()-Methode mit der folgenden Signatur:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

Die DisposeAsync()-Methode

Die public parameterlose DisposeAsync()-Methode wird implizit in einer await using-Anweisung aufgerufen, und ihr Zweck ist es, nicht verwaltete Ressourcen freizugeben, eine generelle Bereinigung durchzuführen und anzugeben, dass der Finalizer, sofern vorhanden, nicht ausgeführt werden muss. Das Freigeben des Speichers, der einem verwalteten Objekt zugeordnet ist, ist immer die Domäne des Garbage Collectors. Daher weist sie eine Standardimplementierung auf:

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
}

Hinweis

Ein primärer Unterschied im asynchronen Dispose-Muster im Vergleich zum Dispose-Muster besteht darin, dass dem Aufruf von DisposeAsync() an die Dispose(bool)-Überladungsmethode false als Argument übergeben wird. Beim Implementieren der IDisposable.Dispose()-Methode wird jedoch stattdessen true übergeben. Dadurch wird die funktionale Äquivalenz mit dem synchronen Dispose-Muster sichergestellt, und es wird weiterhin sichergestellt, dass Finalizer-Codepfade auch noch aufgerufen werden. Mit anderen Worten: Die DisposeAsyncCore()-Methode gibt verwaltete Ressourcen asynchron frei, sodass Sie diese nicht auch noch synchron löschen sollten. Rufen Sie daher Dispose(false) anstelle von Dispose(true) auf.

DisposeAsyncCore()-Methode

Die DisposeAsyncCore()-Methode ist dafür vorgesehen, die asynchrone Bereinigung verwalteter Ressourcen oder kaskadierende Aufrufe von DisposeAsync() auszuführen. Sie kapselt die allgemeinen asynchronen Bereinigungsvorgänge, wenn eine Unterklasse eine Basisklasse erbt, die eine Implementierung von IAsyncDisposable ist. Die DisposeAsyncCore()-Methode ist virtual, damit abgeleitete Klassen zusätzliche Bereinigungen in ihren Außerkraftsetzungen definieren können.

Tipp

Wenn eine Implementierung von IAsyncDisposablesealed ist, wird die DisposeAsyncCore()-Methode nicht benötigt, und die asynchrone Bereinigung kann direkt in der IAsyncDisposable.DisposeAsync()-Methode ausgeführt werden.

Implementieren des asynchronen Dispose-Musters

Alle nicht versiegelten Klassen sollten als potenzielle Basisklasse angesehen werden, da sie geerbt werden könnten. Wenn Sie das asynchrone Dispose-Muster für eine potenzielle Basisklasse implementieren, müssen Sie die protected virtual ValueTask DisposeAsyncCore()-Methode bereitstellen. Im Folgenden finden Sie eine Beispielimplementierung des asynchronen Dispose-Musters, das eine System.Text.Json.Utf8JsonWriter verwendet.

using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

public class ExampleAsyncDisposable : IAsyncDisposable, IDisposable
{
    private Utf8JsonWriter? _jsonWriter = new(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)
        {
            _jsonWriter?.Dispose();
            _jsonWriter = null;
        }
    }

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

        _jsonWriter = null;
    }
}

Im vorangehenden Beispiel wird Utf8JsonWriter verwendet. Weitere Informationen zu System.Text.Jsonfinden Sie unter System.Text.Json.

Implementieren von Dispose-Mustern und asynchronen Dispose-Mustern

Möglicherweise müssen Sie sowohl die IDisposable- als auch die IAsyncDisposable-Schnittstelle implementieren, insbesondere wenn der Klassenbereich Instanzen dieser Implementierungen enthält. Dadurch wird sichergestellt, dass Bereinigungsaufrufe ordnungsgemäß kaskadiert werden können. Unten sehen Sie eine Beispielklasse, die beide Schnittstellen implementiert und die richtige Vorgehensweise beim Bereinigen veranschaulicht.

using System;
using System.IO;
using System.Threading.Tasks;

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;
    }
}

Bei der IDisposable.Dispose()- und der IAsyncDisposable.DisposeAsync()-Implementierung handelt es sich um einfache Codebausteine.

In der Überladungsmethode Dispose(bool) wird die IDisposable-Instanz bedingt verworfen, wenn sie nicht den Wert null aufweist. Die IAsyncDisposable-Instanz wird in IDisposable umgewandelt. Wenn der Wert dieser Instanz auch nicht null ist, wird sie ebenfalls verworfen. Beiden Instanzen wird dann der Wert null zugewiesen.

Bei der DisposeAsyncCore()-Methode wird der gleiche logische Ansatz verfolgt. Wenn die IAsyncDisposable-Instanz nicht null ist, wird auf ihren Aufruf von DisposeAsync().ConfigureAwait(false) gewartet. Wenn die IDisposable-Instanz auch eine Implementierung von IAsyncDisposable ist, wird sie auch asynchron verworfen. Beiden Instanzen wird dann der Wert null zugewiesen.

Verwenden von asynchron verwerfbar

Um ein Objekt, das die IAsyncDisposable -Schnittstelle implementiert, ordnungsgemäß zu nutzen, verwenden Sie die IAsyncDisposable und using zusammen. Sehen Sie sich das folgende Beispiel an, in dem die ExampleAsyncDisposable-Klasse instanziiert und dann von einer await using-Anweisung umschlossen wird.

using System;
using System.Threading.Tasks;

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

        Console.ReadLine();
    }
}

Wichtig

Verwenden Sie die ConfigureAwait(IAsyncDisposable, Boolean)-Erweiterungsmethode der IAsyncDisposable-Schnittstelle, um zu konfigurieren, wie die Fortsetzung der Aufgabe in ihrem ursprünglichen Kontext oder Scheduler gemarshallt wird. Weitere Informationen zu ConfigureAwaitfinden Sie unter ConfigureAwait.

In Situationen, in denen die Verwendung von ConfigureAwait nicht erforderlich ist, könnte die await using-Anweisung wie folgt vereinfacht werden:

using System;
using System.Threading.Tasks;

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

        Console.ReadLine();
    }
}

Darüber hinaus könnte sie so geschrieben werden, dass Sie den impliziten Bereich einer using-Deklaration verwendet.

using System;
using System.Threading.Tasks;

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Schlüsselwörter mit mehreren Await-Stichwörtern in einer zeile

Manchmal wird das await Schlüsselwort mehrmals innerhalb einer einzelnen Zeile angezeigt. Beachten Sie z. B. folgenden Code:

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

Im vorherigen Beispiel:

  • Die BeginTransactionAsync -Methode wird erwartet.
  • Der Rückgabetyp ist DbTransaction, der implementiert IAsyncDisposable.
  • Wird transaction asynchron verwendet und wird ebenfalls erwartet.

Gestapelte using-Anweisungen

In Situationen, in denen Sie mehrere Objekte erstellen und verwenden, die IAsyncDisposable implementieren, kann das Stapeln von await using-Anweisungen mit ConfigureAwait in fehlgeleiteten Bedingungen Aufrufe von DisposeAsync() verhindern. Sie sollten das Stapeln vermeiden, um sicherzustellen, dass DisposeAsync() immer aufgerufen wird. Die folgenden drei Codebeispiele zeigen akzeptable Muster, die stattdessen verwendet werden können.

Akzeptables Muster 1

using System;
using System.Threading.Tasks;

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();
    }
}

Im vorherigen Beispiel ist der Bereich für die einzelnen asynchronen Bereinigungsvorgänge jeweils explizit unter dem await using-Block festgelegt. Der äußere Bereich wird dadurch definiert, wie objOne Klammern setzt und dabei objTwo umschließt. Insofern wird also zuerst objTwo und danach objOne gelöscht. Auf beide IAsyncDisposable Instanzen wird die DisposeAsync() -Methode gewartet, sodass jede Instanz ihren asynchronen Bereinigungsvorgang ausführt. Die Aufrufe werden geschachtelt, nicht gestapelt.

Akzeptables Muster 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();
    }
}

Im vorherigen Beispiel ist der Bereich für die einzelnen asynchronen Bereinigungsvorgänge jeweils explizit unter dem await using-Block festgelegt. Am Ende jedes Blocks wartet die entsprechende IAsyncDisposable-Instanz auf ihre DisposeAsync()-Methode, führt also den asynchronen Bereinigungsvorgang durch. Die Aufrufe erfolgen sequenziell, nicht gestapelt. In diesem Szenario wird zunächst objOne und dann objTwo gelöscht.

Akzeptables Muster 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();
    }
}

Im vorherigen Beispiel wird der Bereich für die einzelnen asynchronen Bereinigungsvorgänge implizit mit dem Methodenkörper festgelegt, in dem die Vorgänge jeweils enthalten sind. Am Ende des umschließenden Blocks führen die IAsyncDisposable-Instanzen die asynchronen Bereinigungsvorgänge durch. Die Ausführung erfolgt in umgekehrter Reihenfolge dazu, wie sie deklariert wurden, d. h. objTwo wird vor objOne gelöscht.

Unzulässiges Muster

Die hervorgehobenen Zeilen im folgenden Code zeigen, was es bedeutet, "gestapelte Usings" zu verwenden. Wenn eine Ausnahme vom AnotherAsyncDisposable Konstruktor ausgelöst wird, wird kein Objekt ordnungsgemäß verworfen. Die Variable objTwo wird nie zugewiesen, da der Konstruktor nicht erfolgreich abgeschlossen wurde. Daher ist der Konstruktor für dafür AnotherAsyncDisposable verantwortlich, alle zugeordneten Ressourcen zu verwerfen, bevor eine Ausnahme ausgelöst wird.

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();
    }
}

Tipp

Vermeiden Sie dieses Muster, da es zu unerwartetem Verhalten führen kann.

Weitere Informationen

Ein Beispiel für eine duale Implementierung von IDisposable und IAsyncDisposablefinden Sie im Utf8JsonWriter Quellcode IDisposable.