Search code examples
vb.netidisposableiasyncdisposable

IAsyncDisposable with Using statement in VB.NET


I have a class with a resource that ideally should be disposed of using an async method, and I'm trying to use a Using statement to do so.

Luckily, .NET added the IAsyncDisposable interface as well as a nice doc explaining how to Implement a DisposeAsync method. The doc says:

The public parameterless DisposeAsync() method is called implicitly in an await using statement

Easy! Except... there's just one problem: I'm using VB.NET, not C#. I can't find any documentation on how to use this feature in VB.NET, or anyone asking after it. VB.NET (using .NET 6) will only allow a Using statement for an instance of IDisposable, and if both IDisposable and IAsyncDisposable are implemented, it only calls Dispose, not DisposeAsync. I can't find any equivalent Await Using statement for VB.NET.

Is there any way for me to properly leverage the IAsyncDisposable interface in VB.NET with a Using statement? Or have I finally reached the point where Microsoft's abandonment of the language has caught up with me?


Solution

    • VB.NET (even as-of August 2023) does not support IAsyncDisposable in Using.
    • Nor does it support using Await inside Finally.
    • So the best you can do to try some ugly hack and/or port-over some old C# tricks...

    Approach 1: Use C# for the using part:

    Just create a new empty C# Class Library project with a single helper method which you can call from VB.NET, something like this:

    (Just don't expect to be able to use ConfigureAwait(False) - even in C# it's hard).

    public static class ArghExtensions
    {
        public static async Task UsingAsyncDisposableAsync( this IAsyncDisposable subject, CancellationToken cancellationToken, Func<CancellationToken,Task> asyncBody ) // Need this parameter order for ergonomic reasons.
        {
            await using( subject )
            {
                // Argument validation (preconditions) needs to be done from within the implicit try/finally, otherwise `subject` won't be disposed if `asyncBody` is null.
                if( subject   is null ) throw new ArgumentNullException(nameof(subject));
                if( asyncBody is null ) throw new ArgumentNullException(nameof(asyncBody));
                
                await asyncBody( cancellationToken ).ConfigureAwait(false);
            }
        }
    }
    

    Then call it from VB.NET:

    Imports ArghExtensions
    
    Async Function FoobarAsync( cancellationToken As CancellationToken ) As Task
    
       Dim d AS IAsyncDisposable = GetSomethingDisposable()
       Await d.UsingAsyncDisposableAsync( Async Function( ct As CancellationToken )
        
            ' do async stuff in here
        
       End Function )
    
    End Function
    

    An alternative way of implementing this method, without using any C# step, is to use Reflection.Emit from within VB.NET to generate equivalent (and safe) IL that reimplements that UsingAsyncDisposableAsync method - though this will be a lot of work for probably little gain tbh.

    Approach 2: Embrace VB.NET's verbosity

    Instead of using any C# code, we can actually still do it all in VB.NET, with the caveat that you need to use Try/Catch and not Try/Finally as VB.NET still can't use Await inside Finally:

    Async Function FoobarAsync( cancellationToken As CancellationToken ) As Task
    
        ' See https://stackoverflow.com/a/16626313/159145
        Dim capturedException As ExceptionDispatchInfo = Nothing
        Dim d AS IAsyncDisposable = GetSomethingDisposable()
        Try
        
            ' do async stuff in here, before the DisposeAsync call below.
            
            Await d.DisposeAsync().ConfigureAwait(False)
            d = Nothing
        
        Catch x As Exception
            
            capturedException = ExceptionDispatchInfo.Capture( x )
        
        End Try
       
        If capturedException IsNot Nothing Then
            
            If d IsNot Nothing Then
                Await d.DisposeAsync().ConfigureAwait(False)
                d = Nothing
            End If
            
            capturedException.Throw()
                
        End If
    
    End Function
    
    Function GetSomethingDisposable() As IAsyncDisposable
        
        Return New MemoryStream()
        
    End Function
    

    I haven't run this code through any static-analysis, but I'm confident this won't trigger CA2000