Implementar um método DisposeAsync

A interface System.IAsyncDisposable foi introduzida como parte do C# 8.0. O método IAsyncDisposable.DisposeAsync() é implementado quando há a necessidade de limpar recursos, assim como na implementação de um método Dispose. No entanto, uma das principais diferenças é que essa implementação permite operações de limpeza assíncronas. A classe DisposeAsync() retorna uma classe ValueTask que representa a operação de eliminação assíncrona.

Geralmente, ao implementar a interface IAsyncDisposable, as classes também implementam a interface IDisposable. Um bom padrão de implementação da interface IAsyncDisposable deve ser preparado para descarte síncrono ou assíncrono. No entanto, não é um requisito. Se nenhum descartável síncrono da sua classe for possível, ter apenas IAsyncDisposable é aceitável. Todas as diretrizes para implementar o padrão de descarte também se aplicam à implementação assíncrona. Este artigo pressupõe que você já saiba como implementar um método Dispose.

Cuidado

Se você implementar a interface IAsyncDisposable mas não a interface IDisposable, seu aplicativo poderá potencialmente vazar recursos. Se uma classe implementar IAsyncDisposable, mas não IDisposable e um consumidor chama apenas Dispose, sua implementação nunca chamaria DisposeAsync. Isso resultaria em um vazamento de recursos.

Dica

Em relação à injeção de dependência, ao registrar serviços em um IServiceCollection, o tempo de vida do serviço é gerenciado implicitamente em seu nome. O IServiceProvider e o IHost correspondente orquestram a limpeza de recursos. Especificamente, as implementações de IDisposable e IAsyncDisposable são corretamente descartadas no final de seu tempo de vida especificado.

Para obter mais informações, consulte Injeção de dependência no .NET.

Explore métodos DisposeAsync e DisposeAsyncCore

A interface IAsyncDisposable declara um único método sem parâmetro DisposeAsync(). Qualquer classe não selada deve definir um método DisposeAsyncCore() que também retorna um ValueTask.

  • A implementação publicIAsyncDisposable.DisposeAsync() não tem parâmetros.

  • Um método protected virtual ValueTask DisposeAsyncCore() cuja assinatura é:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

O método DisposeAsync

O método sem parâmetros publicDisposeAsync() é chamado implicitamente em uma instrução await using e sua finalidade é liberar recursos não gerenciados, executar a limpeza geral e indicar que o finalizador, se houver, não precisa ser executado. Liberar a memória associada a um objeto gerenciado é sempre o domínio do coletor de lixo. Por isso, ele tem uma implementação padrão:

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

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

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

Observação

Uma diferença primária no padrão de descarte assíncrono em comparação com o padrão de descarte é que a chamada de DisposeAsync() para o método de sobrecarga Dispose(bool) é fornecida false como um argumento. No entanto, ao implementar o método IDisposable.Dispose(), true é passado. Isso ajuda a garantir a equivalência funcional com o padrão de descarte síncrono e garante ainda mais que os caminhos de código do finalizador sejam invocados. Em outras palavras, o método DisposeAsyncCore() descartará recursos gerenciados de forma assíncrona, portanto, não é recomendável descartá-los de forma síncrona. Portanto, chame Dispose(false) em vez de Dispose(true).

O método DisposeAsyncCore

O método DisposeAsyncCore() destina-se a executar a limpeza assíncrona de recursos gerenciados ou para chamadas em cascata para DisposeAsync(). Ele encapsula as operações de limpeza assíncronas comuns quando uma subclasse herda uma classe base que é uma implementação de IAsyncDisposable. O método DisposeAsyncCore() é virtual para que as classes derivadas possam definir uma limpeza personalizada em suas substituições.

Dica

Se uma implementação de IAsyncDisposable for sealed, o método DisposeAsyncCore() não será necessário e a limpeza assíncrona poderá ser executada diretamente no método IAsyncDisposable.DisposeAsync().

Implementar o padrão de descarte assíncrono

Todas as classes não seladas devem ser consideradas como uma classe base potencial, pois podem ser herdadas. Se você implementar o padrão de descarte assíncrono para qualquer classe base potencial, deverá fornecer o método protected virtual ValueTask DisposeAsyncCore(). Alguns dos seguintes exemplos usam uma classe NoopAsyncDisposable definida da seguinte maneira:

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

Aqui está um exemplo de implementação do padrão de descarte assíncrono que usa o tipo NoopAsyncDisposable. O tipo implementa DisposeAsync retornando 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;
    }
}

No exemplo anterior:

  • O ExampleAsyncDisposable é uma classe não selada que implementa a interface IAsyncDisposable.
  • Ele contém um campo IAsyncDisposable privado, _example, que é inicializado no construtor.
  • O método DisposeAsync delega ao método DisposeAsyncCore e chama GC.SuppressFinalize para notificar o coletor de lixo de que o finalizador não precisa ser executado.
  • Ele contém um método DisposeAsyncCore() que chama o método _example.DisposeAsync() e define o campo como null.
  • O método DisposeAsyncCore() é virtual, que permite que as subclasses o substituam por um comportamento personalizado.

Padrão de descarte assíncrono alternativo selado

Se a classe de implementação puder ser sealed, é possível implementar o padrão de descarte assíncrono substituindo o método IAsyncDisposable.DisposeAsync(). O exemplo a seguir mostra como implementar o padrão de descarte assíncrono para uma classe selada:

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

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

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

No exemplo anterior:

  • SealedExampleAsyncDisposable é uma classe não selada que implementa a interface IAsyncDisposable.
  • O campo que contém _example é readonly e é inicializado no constructo.
  • O método DisposeAsync chama o método _example.DisposeAsync() implementando o padrão por meio do campo de contenção (descarte em cascata).

Implementar padrões de descarte assíncrono e assíncrono

Talvez seja necessário implementar as interfaces IDisposable e IAsyncDisposable, especialmente quando o escopo da classe contém instâncias dessas implementações. Isso garante que você possa limpar adequadamente as chamadas em cascata. Aqui está uma classe de exemplo que implementa ambas as interfaces e demonstra as diretrizes adequadas para limpeza.

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

As implementações IDisposable.Dispose() e IAsyncDisposable.DisposeAsync() são código de texto clichê simples.

No método de sobrecarga Dispose(bool), a instância IDisposable será descartada condicionalmente se não for null. A instância IAsyncDisposable é convertida como IDisposable, e se também não for null, ela será descartada. Em seguida, ambas as instâncias são atribuídas a null.

Com o método DisposeAsyncCore(), a mesma abordagem lógica é seguida. Se a instância IAsyncDisposable não for null, sua chamada para DisposeAsync().ConfigureAwait(false) será aguardada. Se a instância IDisposable também for uma implementação de IAsyncDisposable, ela também será descartada de forma assíncrona. Em seguida, ambas as instâncias são atribuídas a null.

Cada implementação se esforça para remover todos os objetos descartáveis possíveis. Isso garante que a limpeza em cascata seja feita corretamente.

Usando descartáveis assíncronos

Para consumir corretamente um objeto que implementa a interface IAsyncDisposable, use as palavras-chave await e using juntas. Considere o exemplo a seguir, em que a classe ExampleAsyncDisposable é instanciada e, em seguida, encapsulada em uma instrução await using.

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

        Console.ReadLine();
    }
}

Importante

Use o método de extensão ConfigureAwait(IAsyncDisposable, Boolean) da interface IAsyncDisposable para configurar como a continuação da tarefa realiza marshaling em seu contexto original ou agendador. Para obter mais informações sobre ConfigureAwait, consulte FAQ do ConfigureAwait.

Para situações em que o uso de ConfigureAwait não é necessário, a instrução await using pode ser simplificada da seguinte maneira:

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

        Console.ReadLine();
    }
}

Além disso, pode ser gravado para usar o escopo implícito de uma declaração using.

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Várias palavras-chave await em uma única linha

Às vezes, a palavra-chave await pode aparecer várias vezes em uma única linha. Por exemplo, considere o seguinte código:

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

No exemplo anterior:

  • O método BeginTransactionAsync é aguardado.
  • O tipo de retorno é DbTransaction, que implementa IAsyncDisposable.
  • transaction é usado de forma assíncrona e também é aguardado.

Usos empilhados

Em situações em que você cria e usa vários objetos que implementam IAsyncDisposable, é possível que o empilhamento de instruções await using com ConfigureAwait possa impedir chamadas para DisposeAsync() em condições problemáticas. Para garantir que DisposeAsync() seja sempre chamado, evite o empilhamento. Os três exemplos de código a seguir mostram padrões aceitáveis a serem usados.

Padrão aceitável um


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

No exemplo acima, cada operação de limpeza assíncrona tem escopo explícito no bloco await using. O escopo externo segue como objOne define suas chaves, delimitando objTwo. Dessa forma, objTwo é descartado primeiro, seguido por objOne. As instâncias IAsyncDisposable têm seu método DisposeAsync() aguardado, portanto, cada instância executa sua operação de limpeza assíncrona. As chamadas estão aninhadas, não empilhadas.

Padrão aceitável dois

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

No exemplo acima, cada operação de limpeza assíncrona tem escopo explícito no bloco await using. No final de cada bloco, a instância IAsyncDisposable correspondente tem seu método DisposeAsync() aguardado, executando assim sua operação de limpeza assíncrona. As chamadas são sequenciais, não empilhadas. Nesse cenário objOne é descartado primeiro e, em seguida, objTwo é descartado.

Padrão aceitável três

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

No exemplo anterior, cada operação de limpeza assíncrona tem um escopo implícito com o corpo do método contido. No final do bloco delimitador, as instâncias IAsyncDisposable executam suas operações de limpeza assíncronas. Esse exemplo é executado em ordem inversa à declaração, o que significa que objTwo é descartado antes de objOne.

Padrão inaceitável

As linhas realçadas no código a seguir mostram o que significa ter "usos empilhados". Se uma exceção for lançada do construtor AnotherAsyncDisposable, nenhum objeto será descartado corretamente. A variável objTwo nunca é atribuída porque o construtor não foi concluído com êxito. Como resultado, o construtor de AnotherAsyncDisposable é responsável por descartar todos os recursos alocados antes de gerar uma exceção. Se o tipo ExampleAsyncDisposable tiver um finalizador, ele será qualificado para finalização.

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

Dica

Evite esse padrão, pois ele pode gerar um comportamento inesperado. Se você usar um dos padrões aceitáveis, o problema de objetos não expostos não existirá. As operações de limpeza são executadas corretamente quando as instruções using não são empilhadas.

Confira também

Para obter um exemplo de implementação dupla de IDisposable e IAsyncDisposable, consulte o código-fonte Utf8JsonWriterno GitHub.