IDisposable and/or IAsyncDisposable pattern for IDisposable T in Task<T>

116 views Asked by At

If I have a class where a field is an instance of a Task, and that task returns a class which implements IDisposable, what is a pattern to implement IDisposable (+/- IAsyncDisposable) on the instance wrapped by the Task, bearing in mind the Task may have completed, may still be in progress and require cancellation, or may have failed by the time Dispose is called. A quick go in pseudo code:

public sealed Class MyClass : IDisposable
{
  private readonly CancellationTokenSource _cancelTokenSrc;
  private readonly Task<SecurityTokens> _securityTask;
  private bool _isDisposed;
  public MyClass(Uri endPoint) {
     _cancelTokenSrc= new CancellationTokenSource;
     _securityTask = GetServerSecurityData(endpoint, _cancelTokenSrc.Token);
  }
  public Task<IEnumerable<File>> GetContainedFiles(string folderName) {
    var data = await GetPrivate(...);
    ...
  }
  public Task UploadFile(File file) {
    var data = await GetPrivate(...);
    ...
  }
  public Task<Stream> DowloadFiles(string fileId) {
   var data = await GetPrivate(...);
   ...
  }
  private async Task<HttpRepsonse> GetPrivate(uri uri) {
    var s = await _securityTask;
    // now send get request & return response
    // include security data contained in s with every request
    ...
  }
///!!! This is where I would appreciate peoples thoughts & expertise!!!
  public void Dispose() {
    if (!_isDisposed) {
      if (_securityTask.IsCompleted) { 
        _securityTask.Result.Dispose();
      }
      else if (!_securityTask.IsFaulted && !_securityTask.IsCanceled)
      {
         _cancelTokenSrc.Cancel();
      }
      disposed = true;
      GC.SupressFinalize()
    }
  }
}

In summary can you help me implement a more robust dispose() implementation to ensure the Stream returned by the Task is always disposed correctly, plus possibly how to also implement IAsyncDisposable?

Edit

in response to the comments - it is always hard to create a simple example which is easy to follow but which conveys the complexity. In this case I am envisaging the instantiation to begin negotiations with a server to obtain bearer tokens and claims etc. Once this data is available, I don't want to go through the whole authentication/authorization process each time a method is called. Each method will of its own right be asynchronous, but if the security negotiation is ongoing, will await completion of that before initiating a range of different but related methods.

Edit 2

I have changed the example code a little as comments were concentrating on the intention of the pseudocode, which is not really what the question is about - I have tried to make the example a little more concrete. The intent is to create 3 classes all implementing the same interface but negotiating different APIs for accessing the users MS Sharepoint, Google drive or Dropbox files & folders). I can make code to do this - this question is not about working around a specific problem. I would really like to know a useful pattern for disposing the IDisposable object wrapped by a Task, in a class implementing IDisposable. It may be this is too messy an approach which leads to anti-patterns and that is fine as an answer.

2

There are 2 answers

0
Enigmativity On BEST ANSWER

My understanding is that you have a class like this:

public class MyClass
{
    public Task<MyDisposable> MyDisposable;
}

And you want to properly manage the IDisposable lifetime of MyDisposable.

My standard approach for this is to write methods that let me inject in the code I want to perform on the disposable and then call .Dispose() before the method ends.

Here are the three that you perhaps need:

public class MyClass
{
    private Task<MyDisposable> CreateMyDisposableAsync() => Task.Run(() => new MyDisposable());
    
    public async Task<R> UsingMyDisposableAsync<R>(Func<MyDisposable, Task<R>> consumeAsync)
    {
        using (var myDisposable = await this.CreateMyDisposableAsync())
        {
            return await consumeAsync(myDisposable);
        }
    }

    public async IAsyncEnumerable<R> UsingMyDisposableAsync<T, R>(IEnumerable<T> source, Func<MyDisposable, T, Task<R>> consumeAsync)
    {
        using (var myDisposable = await this.CreateMyDisposableAsync())
        {
            foreach (var r in source)
            {
                yield return await consumeAsync(myDisposable, r);
            }
        }
    }

    public async Task UsingMyDisposableAsync(Func<MyDisposable, Task> actionAsync)
    {
        using (var myDisposable = await this.CreateMyDisposableAsync())
        {
            await actionAsync(myDisposable);
        }
    }
}

I've implemented this class to show you how these work:

public class MyDisposable : IDisposable
{
    public Task<int> GetValueAsync(int t) => Task.Run(() => t * 2);

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                Console.WriteLine("MyDisposable Disposed!");
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            disposedValue = true;
        }
    }

    bool disposedValue;

    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
    // ~MyResource()
    // {
    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

Note that GetValueAsync simply doubles the input number.

Now I can write this:

var myClass = new MyClass();

var v = await myClass.UsingMyDisposableAsync<int>(mr => mr.GetValueAsync(42));
Console.WriteLine(v);

await foreach (var x in myClass.UsingMyDisposableAsync<int, int>(
    new[] { 1, 2 },
    (mr, t) => mr.GetValueAsync(t)))
{
    Console.WriteLine(x);
}

int j = 2;
int k = 3;
await myClass.UsingMyDisposableAsync(async mr =>
{
    j = await mr.GetValueAsync(j);
    k = await mr.GetValueAsync(j + k);
});
Console.WriteLine(j);
Console.WriteLine(k);

That outputs:

MyDisposable Disposed!
84
2
4
MyDisposable Disposed!
MyDisposable Disposed!
4
14

It would have been great to have seen real code from your side so that I could show how this might work for you.

0
Brent On

The method I ended up using was to:

  • Have no publicly accessible constructor
  • Have a publicly accessible static factory method which accesses the private constructor
internal sealed class GoogleDriveIcsFiles: IDisposable, ICloudFileAccessor
    {
    private readonly DriveService _driveService;
    private readonly string _rootFolderId;
    private GoogleDriveIcsFiles(DriveService driveService, string rootFolderId)
    {
       _driveService = driveService;
       _rootFolderId = rootFolderId;
    }
    ... properties and methods
    public void Dispose()
    {
        // note as the only way we can instantiate this class is through the static create method
        // we own (and can therefore dispose) driveservice
        ((IDisposable)_driveService).Dispose();
    }
    public static async Task<GoogleDriveIcsFiles> Create(string user)
    {
       UserCredential usrCred;
       using (var strm = System.IO.File.OpenRead("client_secret.json"))
       {
           GoogleClientSecrets fromStream = await GoogleClientSecrets.FromStreamAsync(strm);
           usrCred = await GoogleWebAuthorizationBroker.AuthorizeAsync(fromStream.Secrets,
               new[] { DriveService.Scope.Drive, DriveService.Scope.DriveFile },
               user,
               CancellationToken.None,
               new FileDataStore("Drive.Auth.Store"));
       }
       var driveService = new DriveService(new BaseClientService.Initializer()
       {
           HttpClientInitializer = usrCred,
           ApplicationName = "Roster to Calendar",
       });
       var request = driveService.Files.Get("root");
       request.Fields = "id";
       return new GoogleDriveIcsFiles(driveService, (await request.ExecuteAsync()).Id);
    }
}