Implementación de un método DisposeAsync

La interfaz System.IAsyncDisposable se presentó como parte de C# 8.0. El método IAsyncDisposable.DisposeAsync() se implementa cuando se necesita realizar una limpieza de recursos, tal como se haría a la hora de implementar un método Dispose. Sin embargo, una de las principales diferencias es que esta implementación permite operaciones de limpieza asincrónicas. El elemento DisposeAsync() devuelve un elemento ValueTask que representa la operación de eliminación asincrónica.

Es habitual que, al implementar la interfaz IAsyncDisposable, las clases también implementen la interfaz IDisposable. Se debe preparar un patrón de implementación correcto de la interfaz IAsyncDisposable para la eliminación sincrónica o asincrónica. No obstante, esto no es obligatorio. Si no es posible descartar la clase de forma sincrónica, se acepta tener solo IAsyncDisposable. Todas las instrucciones para implementar el patrón de eliminación se aplican también a la implementación asincrónica. En este artículo se supone que ya se ha familiarizado con el modo de implementar un método Dispose.

Precaución

Si implementas la interfaz IAsyncDisposable, pero no la interfaz, la aplicación IDisposable puede perder recursos. Si una clase implementa IAsyncDisposable, pero no IDisposable, y un consumidor solo llama a Dispose, la implementación nunca llamaría a DisposeAsync. Esto provocaría una fuga de recursos.

Sugerencia

Con respecto a la inserción de dependencias, al registrar servicios en IServiceCollection, la duración del servicio se administra implícitamente en su nombre. El elemento IServiceProvider y el elemento IHost correspondiente orquestan la limpieza de recursos. En concreto, las implementaciones de IDisposable y IAsyncDisposable se eliminan correctamente al final de su duración especificada.

Para más información, vea Inserción de dependencias en .NET.

Exploración de los métodos DisposeAsync y DisposeAsyncCore

La interfaz IAsyncDisposable declara un único método sin parámetros: DisposeAsync(). Cualquier clase no sellada debe definir un método DisposeAsyncCore() que también devuelva un elemento ValueTask.

  • Una implementación de publicIAsyncDisposable.DisposeAsync() que no tiene parámetros.

  • Un método protected virtual ValueTask DisposeAsyncCore() cuya signatura sea:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

El método DisposeAsync

Se llama de forma implícita al método DisposeAsync() sin parámetros public en una instrucción await using, y su propósito consiste en liberar los recursos no administrados, realizar una limpieza general e indicar que el finalizador, si existe, no necesita ejecutarse. La liberación de la memoria asociada a un objeto administrado siempre corresponde al recolector de elementos no utilizados. Debido a esto, se realiza una implementación estándar:

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

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

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

Nota

Una diferencia principal en el patrón de eliminación asincrónica en comparación con el patrón de eliminación es que la llamada desde DisposeAsync() al método de sobrecarga Dispose(bool) recibe el valor false como argumento. Pero al implementar el método IDisposable.Dispose(), en su lugar se pasa el valor true. Esto ayuda a garantizar la equivalencia funcional con el patrón de eliminación sincrónico y garantiza aún más que se invoquen las rutas de acceso al código finalizador. En otras palabras, el método DisposeAsyncCore() eliminará los recursos administrados de forma asincrónica, por lo que no querrá eliminarlos también de forma sincrónica. Por tanto, llame a Dispose(false) en lugar de a Dispose(true).

El método DisposeAsyncCore

El método DisposeAsyncCore() está diseñado para realizar la limpieza asincrónica de los recursos administrados o para hacer llamadas en cascada a DisposeAsync(). Encapsula las operaciones de limpieza asincrónica comunes cuando una subclase hereda una clase base que es una implementación de IAsyncDisposable. El método DisposeAsyncCore() es virtual para que las clases derivadas puedan definir la limpieza personalizada en sus invalidaciones.

Sugerencia

Si una implementación de IAsyncDisposable es sealed, el método DisposeAsyncCore() no es necesario y la limpieza asincrónica se puede realizar directamente en el método IAsyncDisposable.DisposeAsync().

Implementación del patrón de eliminación asincrónica

Todas las clases no selladas deben considerarse una clase base potencial, ya que se podrían heredarse. Cuando se implementa el patrón de eliminación asincrónica para cualquier clase base potencial, se debe proporcionar el método protected virtual ValueTask DisposeAsyncCore(). Algunos de los ejemplos siguientes usan una clase NoopAsyncDisposable que se define de la manera siguiente:

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

Este es un ejemplo de implementación del patrón de eliminación asincrónica que usa el tipo NoopAsyncDisposable. El tipo implementa DisposeAsync devolviendo 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;
    }
}

En el ejemplo anterior:

  • ExampleAsyncDisposable es una clase no sellada que implementa la interfaz IAsyncDisposable.
  • Contiene un campo IAsyncDisposable privado, _example, que se inicializa en el constructor.
  • El método DisposeAsync delega en el método DisposeAsyncCore y llama a GC.SuppressFinalize para notificar al recolector de elementos no utilizados que el finalizador no tiene que ejecutarse.
  • Contiene un método DisposeAsyncCore() que llama al método _example.DisposeAsync() y establece el campo en null.
  • El método DisposeAsyncCore() es virtual, que permite que las subclases lo invaliden con un comportamiento personalizado.

Patrón de eliminación asincrónico alternativo sellado

Si la clase de implementación puede ser sealed, puede implementar el patrón de eliminación asincrónica reemplazando el método IAsyncDisposable.DisposeAsync(). En el ejemplo siguiente se muestra cómo implementar el patrón de eliminación asincrónica para una clase sellada:

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

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

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

En el ejemplo anterior:

  • SealedExampleAsyncDisposable es una clase sellada que implementa la interfaz IAsyncDisposable.
  • El campo contenedor _example es readonly y se inicializa en el constructor.
  • El método DisposeAsync llama al método _example.DisposeAsync(), implementando el patrón mediante el campo contenedor (eliminación en cascada).

Implementación de patrones de eliminación y eliminación asincrónica

Es posible que tenga que implementar las interfaces IDisposable y IAsyncDisposable, especialmente si el ámbito de clase contiene instancias de estas implementaciones. De este modo se asegura de poder limpiar correctamente las llamadas en cascada. A continuación se incluye una clase de ejemplo que implementa ambas interfaces y muestra las instrucciones adecuadas para la limpieza.

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

Las implementaciones de IDisposable.Dispose() y IAsyncDisposable.DisposeAsync() se llevan a cabo mediante código reutilizable sencillo.

En el método de sobrecarga Dispose(bool), la instancia de IDisposable se elimina condicionalmente si no es null. La instancia de IAsyncDisposable se convierte a IDisposable y, si tampoco es null, también se elimina. Después, se asignan ambas instancias a null.

Con el método DisposeAsyncCore(), se sigue el mismo enfoque lógico. Si la instancia de IAsyncDisposable no es null, se espera a la llamada a DisposeAsync().ConfigureAwait(false). Si la instancia de IDisposable también es una implementación de IAsyncDisposable, entonces, también se elimina de forma asincrónica. Después, se asignan ambas instancias a null.

Cada implementación se esfuerza por eliminar todos los objetos descartables posibles. Esto garantiza que la limpieza se propague en cascada correctamente.

Uso de la eliminación asincrónica

Para usar correctamente un objeto que implementa la interfaz IAsyncDisposable, utilice las palabras clave await y using juntas. Tenga en cuenta el ejemplo siguiente, donde se crea una instancia de la clase ExampleAsyncDisposable y después se encapsula en una instrucción 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 el método de extensión ConfigureAwait(IAsyncDisposable, Boolean) de la interfaz IAsyncDisposable para configurar el modo en que se serializa la continuación de la tarea en su contexto o programador original. Para obtener más información sobre ConfigureAwait, consulte las preguntas más frecuentes sobre ConfigureAwait.

En situaciones en las que no se necesita el uso de ConfigureAwait, la instrucción await using se podría simplificar de la siguiente manera:

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

        Console.ReadLine();
    }
}

Además, se podría escribir para utilizar el ámbito implícito de una declaración "using".

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Varias palabras clave await en una sola línea

A veces, la palabra clave await puede aparecer varias veces dentro de una sola línea. Por ejemplo, considere el siguiente código:

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

En el ejemplo anterior:

  • Se espera el método BeginTransactionAsync.
  • El tipo de valor devuelto es DbTransaction, que implementa IAsyncDisposable.
  • transaction se usa de forma asincrónica con async/await.

Declaraciones "using" apiladas

En situaciones en las que se crean y se usan varios objetos que implementan IAsyncDisposable, es posible que el apilamiento de instrucciones await using con ConfigureAwait en condiciones errantes pueda impedir las llamadas a DisposeAsync(). Para asegurarse de que siempre se llama a DisposeAsync(), debe evitar el apilamiento. En los tres ejemplos de código siguientes se muestran patrones aceptables para usar en su lugar.

Patrón aceptable 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();
    }
}

En el ejemplo anterior, cada operación de limpieza asincrónica tiene un ámbito explícito en el bloque await using. El ámbito externo sigue la forma en la que objOne establece sus llaves y se encierra objTwo; por tanto, primero se elimina objTwo, seguido de objOne. Ambas instancias de IAsyncDisposable tienen su método DisposeAsync() en espera, por lo que cada instancia realiza su operación de limpieza asincrónica. Las llamadas están anidadas, no apiladas.

Patrón aceptable 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();
    }
}

En el ejemplo anterior, cada operación de limpieza asincrónica tiene un ámbito explícito en el bloque await using. Al final de cada bloque, la instancia de IAsyncDisposable correspondiente tiene su método DisposeAsync() en espera, con lo que se realiza su operación de limpieza asincrónica. Las llamadas son secuenciales, no apiladas. En este escenario, primero se elimina objOne y, luego, objTwo.

Patrón aceptable 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();
    }
}

En el ejemplo anterior, cada operación de limpieza asincrónica tiene un ámbito implícito en el cuerpo del método contenedor. Al final del bloque contenedor, las instancias de IAsyncDisposable realizan sus operaciones de limpieza asincrónicas. Este ejemplo se ejecuta en orden inverso en el que se han declarado, lo que significa que objTwo se elimina antes que objOne.

Patrón no aceptable

Las líneas resaltadas en el código siguiente muestran lo que significa tener "operadores using apilados". Si se produce una excepción desde el constructor AnotherAsyncDisposable, ninguno de los objetos se elimina correctamente. La variable objTwo nunca se asigna porque el constructor no se completó correctamente. Como resultado, el constructor de AnotherAsyncDisposable es responsable de eliminar los recursos asignados antes de iniciar una excepción. Si el tipo ExampleAsyncDisposable tiene un finalizador, es apto para la finalización.

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

Sugerencia

Evite este patrón, ya que podría provocar un comportamiento inesperado. Si usa uno de los patrones aceptables, el problema de los objetos no desechados es inexistente. Las operaciones de limpieza se realizan correctamente cuando las instrucciones using no se apilan.

Consulte también

Para obtener un ejemplo de implementación dual de IDisposable y IAsyncDisposable, vea el código fuente de Utf8JsonWriteren GitHub.