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, genauso wie bei der Implementierung einer Dispose-Methode. Einer der Hauptunterschiede besteht jedoch darin, dass diese Implementierung asynchrone Bereinigungsvorgänge zulässt. DisposeAsync() gibt eine ValueTask zurück, die den asynchronen Bereinigungsvorgang darstellt.

Bei der Implementierung der IAsyncDisposable-Schnittstelle implementieren die Klassen in der Regel auch die IDisposable-Schnittstelle. Ein gutes Implementierungsmuster der IAsyncDisposable-Schnittstelle besteht darin, entweder für das synchrone oder asynchrone Bereinigen vorbereitet zu sein, dies ist jedoch keine Voraussetzung. Ist kein synchrones Verwerfen Ihrer Klasse möglich, ist es akzeptabel, wenn nur IAsyncDisposable verfügbar ist. Alle Leitfäden zum Implementieren des Bereinigungsmusters gelten auch für die asynchrone Implementierung. In diesem Artikel wird davon ausgegangen, dass Sie bereits mit der Implementierung einer Dispose-Methode vertraut sind.

Achtung

Wenn Sie die Schnittstelle IAsyncDisposable, aber nicht die Schnittstelle IDisposable implementieren, kann es in Ihrer App möglicherweise zu Ressourcenverlusten kommen. Wenn eine Klasse IAsyncDisposable implementiert, aber nicht IDisposable, und ein Consumer nur Dispose aufruft, würde Ihre Implementierung niemals DisposeAsync aufrufen. Dies würde zu Ressourcenverlusten führen.

Tipp

In Bezug auf die Abhängigkeitsinjektion (DI) wird die Dienstlebensdauer beim Registrieren von Diensten in einer IServiceCollection implizit in Ihrem Namen verwaltet. Der IServiceProvider und die entsprechende IHost-Orchestrierungsressourcenbereinigung. Insbesondere Implementierungen von IDisposable und IAsyncDisposable werden am Ende ihrer angegebenen Lebensdauer ordnungsgemäß gelöscht.

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

Erkunden der DisposeAsync- und DisposeAsyncCore-Methoden

Die IAsyncDisposable-Schnittstelle deklariert eine einzelne parameterlose Methode: DisposeAsync(). Jede nicht versiegelte Klasse sollte eine DisposeAsyncCore()-Methode definieren, die auch einen ValueTask zurückgibt.

  • Eine public IAsyncDisposable.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);

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

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.

Die 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 benutzerdefinierte 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. Einige der folgenden Beispiele verwenden eine NoopAsyncDisposable-Klasse, die wie folgt definiert ist:

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

Im Folgenden finden Sie eine Beispielimplementierung des asynchronen Bereinigungsmusters, das den Typ NoopAsyncDisposable verwendet. Der Typ implementiert DisposeAsync, indem ValueTask.CompletedTask zurückgegeben wird.

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

Im vorherigen Beispiel:

  • ExampleAsyncDisposable ist eine nicht versiegelte Klasse, die die IAsyncDisposable-Schnittstelle implementiert.
  • Sie enthält ein privates IAsyncDisposable-Feld _example, das im Konstruktor initialisiert wird.
  • Die DisposeAsync-Methode delegiert an die DisposeAsyncCore-Methode und ruft GC.SuppressFinalize auf, um den Garbage Collector zu benachrichtigen, dass der Finalizer nicht ausgeführt werden muss.
  • Sie enthält eine DisposeAsyncCore()-Methode, die die _example.DisposeAsync()-Methode aufruft, und legt das Feld auf null fest.
  • Die DisposeAsyncCore()-Methode ist virtual, damit Unterklassen sie mit benutzerdefiniertem Verhalten außer Kraft setzen können.

Versiegeltes alternatives asynchrones Dispose-Muster

Wenn Ihre implementierende Klasse sealed sein kann, können Sie das asynchrone Dispose-Muster implementieren, indem Sie die IAsyncDisposable.DisposeAsync()-Methode außer Kraft setzen. Das folgende Beispiel zeigt, wie das asynchrone Dispose-Muster für eine versiegelte Klasse implementiert wird:

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

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

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

Im vorherigen Beispiel:

  • SealedExampleAsyncDisposable ist eine versiegelte Klasse, die die IAsyncDisposable-Schnittstelle implementiert.
  • Das enthaltende _example-Feld ist readonly und wird im Konstruktor initialisiert.
  • Die DisposeAsync-Methode ruft die _example.DisposeAsync()-Methode auf und implementiert das Muster über das enthaltende Feld (kaskadierendes Verwerfen).

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. Im Folgenden finden Sie eine Beispielklasse, die beide Schnittstellen implementiert und die richtige Vorgehensweise beim Bereinigen veranschaulicht.

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

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 bereinigt, 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 bereinigt. 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.

Jede Implementierung ist bestrebt, alle möglichen verwerfbaren Objekte zu löschen. Dadurch wird sichergestellt, dass die Bereinigung ordnungsgemäß kaskadiert wird.

Verwenden von asynchron verwerfbar

Wenn Sie ein Objekt ordnungsgemäß nutzen können möchten, das die IAsyncDisposable-Schnittstelle implementiert, verwenden Sie die Schlüsselwörter await 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.

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 ConfigureAwait finden Sie in den Häufig gestellten Fragen zu ConfigureAwait.

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

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.

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Mehrere await-Schlüsselwörter in einer einzelnen Zeile

Manchmal kann das Schlüsselwort await mehrmals innerhalb einer einzelnen Zeile angezeigt werden. 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 IAsyncDisposable implementiert.
  • Die transaction wird asynchron verwendet und 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


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 bereinigt. Beide IAsyncDisposable-Instanzen haben ihre DisposeAsync()-Methode erwartet, 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 im Beispiel erfolgt in umgekehrter Reihenfolge dazu, wie sie deklariert wurden, d. h. objTwo wird vor objOne bereinigt.

Unzulässiges Muster

Die hervorgehobenen Zeilen im folgenden Code zeigen, was es bedeutet, dass „gestapelte using-Instanzen“ verwendet werden. Wenn eine Ausnahme aus dem AnotherAsyncDisposable-Konstruktor ausgelöst wird, wird kein Objekt ordnungsgemäß verworfen. Die Variable objTwo wird nie zugewiesen, da der Konstruktor nicht erfolgreich abgeschlossen wurde. Folglich ist der Konstruktor für AnotherAsyncDisposable dafür verantwortlich, alle zugeordneten Ressourcen zu verwerfen, bevor er eine Ausnahme auslöst. Wenn der ExampleAsyncDisposable-Typ über einen Finalizer verfügt, ist er zur Finalisierung berechtigt.

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. Wenn Sie eines der akzeptablen Muster verwenden, gibt es das Problem der nicht verworfenen Objekte nicht. Die Bereinigungsvorgänge werden ordnungsgemäß ausgeführt, wenn using-Anweisungen nicht gestapelt werden.

Weitere Informationen

Ein Beispiel für eine Implementierung von IDisposable und IAsyncDisposable finden Sie im Quellcode von Utf8JsonWriterauf GitHub.