6

Disposable.Create require an Action as parameter. The Action is run when the Rx subscription is being disposed.

When disposing a Rx subscription I’d like to run some asynchronous clean up code, however using async () => with Action is identical to async void, which I’d like to avoid. For more details on why I want to avoid this, see here.

Is it possible to create something like a Disposable.AsyncCreate, which accepts Func<Task> rather than Action. If so how should I use it as part of a CompositeDisposable?

Or are there other patterns for dealing with asynchronous Disposal?

1iveowl
  • 1,622
  • 1
  • 18
  • 31
  • 2
    The IDisposable interface does not have any method returning a Task so how should calling Dispose wait for a task? – Peter Bons Jul 24 '17 at 18:20
  • How this question is different from your previous question: https://stackoverflow.com/questions/45205132/alternative-to-using-async-in-rx-finally ? – VMAtm Jul 24 '17 at 18:22
  • @Peter that is my understanding too. Still wondering if there is some way to accomplish this anyhow. – 1iveowl Jul 24 '17 at 18:32
  • @JasperHBojsen - Why do you want to avoid `async () =>`? – Enigmativity Jul 24 '17 at 23:41
  • @Enigmativity I've updated the question with a link explaining why I want to avoid `async void` which is the consequence of using `async () =>` with `Action`. – 1iveowl Jul 25 '17 at 03:26
  • 1
    @JasperHBojsen - It doesn't have the same problem. I'll post some sample code to show. – Enigmativity Jul 25 '17 at 04:57
  • 1
    I cover a couple of patterns [on my blog](https://blog.stephencleary.com/2013/03/async-oop-6-disposal.html). – Stephen Cleary Jul 25 '17 at 15:03

2 Answers2

3

You could do something like this. I'm still not sure how good an idea it is:

public class DisposableAsync
{
    private readonly IDisposable _disposable; 
    private readonly Func<Task> _asyncDisposalAction;
    public DisposableAsync(IDisposable disposable, Func<Task> asyncDisposalAction)
    {
        _disposable = disposable;
        _asyncDisposalAction = asyncDisposalAction;
    }

    public Task DisposeAsync()
    {
        _disposable.Dispose();
        return _asyncDisposalAction();
    }
}

public static class DisposableAsyncExtensions
{
    public static DisposableAsync ToAsync(this IDisposable disposable, Func<Task> asyncDisposalAction)
    {
        return new DisposableAsync(disposable, asyncDisposalAction);
    }
}

You could then use it like this:

async Task Go()
{

    var o = Observable.Interval(TimeSpan.FromMilliseconds(100));
    var d = o
        .Subscribe(i => Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {i}"))
        .ToAsync(async () =>
        {
            Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: Dispose Beginning");
            await Task.Delay(1000);
            Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: Dispose Complete");
        });
    Console.Read();
    var t = d.DisposeAsync();
    Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: Outside task, waiting for dispose to complete");
    await t;
    Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: Task Complete");

}

This solution wouldn't work with using() statements, and the class DisposableAsync should be robustified. Outside of that, I can't think of anything wrong with it, but I'm predisposed (ahem) against it though. Just feels kind of hacky.

Shlomo
  • 14,102
  • 3
  • 28
  • 43
  • Since you're not using a `using` you should still use a `try/finally` (as a `using` would) to ensure that the disposal happens even if there is an exception. – Servy Jul 25 '17 at 14:33
  • It's not exactly how I'd imagine a solution either but it's the closest to an answer I've come to too. – 1iveowl Jul 25 '17 at 16:18
0

I don't think async () => has the problems you think it does.

Try this:

Action x = async () =>
{
    try
    {
        Console.WriteLine("Hello");
        await Task.Delay(TimeSpan.FromSeconds(2.0));
        throw new Exception("Wait");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    Console.WriteLine("Goodbye");
};

var d = Disposable.Create(x);

d.Dispose();

It produces:

Hello
Wait
Goodbye
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • 1
    I suppose this is because everything inside the `Action` is wrapped around a try/catch, hence no exception risk being thrown outside the `Action` and hence there is no risk of such an exception crashing the process. I guess this could work, but it seems to me that it is not the most intuitively esthetical solution. – 1iveowl Jul 25 '17 at 09:10
  • 1
    If you move the try catch outside the delegate to around the dispose, you get an unhandled exception on the process. – Shlomo Jul 25 '17 at 09:39
  • 1
    In the interests of fostering good coding practices, would you consider moving the `await` inside the `try { }` body? It's hard to overstate the importance of comprehensive exception handling in non-`Task` returning methods and delegates. – Kirill Shlenskiy Jul 25 '17 at 10:03
  • 1
    @KirillShlenskiy - I was trying to make the error handling part of the code very distinct, but I would generally do it as you suggest. – Enigmativity Jul 25 '17 at 13:06
  • 1
    It's not just about error handling semantics though. Now the code is continuing on before finishing disposing of the resource. That may well be problematic. – Servy Jul 25 '17 at 13:39