3

I've got a helper method that takes a lambda to execute within a try/catch/finally block, e.g.

public void ProcessSpreadsheet(string filename, Action<object[,]> process) {
    try {
        // Open MS Excel workbook
        // Open sheet & extract data into valueArray
        // ... more boiler plate ...

        process(valueArray);
    }
    catch (FooException e) {
        LogFoo(e.Message);
        throw;
    }
    catch (BarException e) {
        LogBar(e.Message);
        throw;
    }
    finally {
        // Close workbook, release resources, etc..
    }
}

I would now like to create an async version that accepts an async lambda.

(Note, I've read and agree with Stephen Toub's posts on async over sync, but that doesn't apply here as the crux of the method, where asynchrony is or isn't beneficial, is supplied by the consumer via a lambda.)

I can copy and paste the above, mark it as async, change the return type to Task, add "Async" to it's name, change the type of the process paramter to Func<object[,],Task> and change

process(valueArray);

to

await process(valueArray);

and everything works fine. But I'd like to avoid repeating all the code in the try/catch/finally. Is there a clean way to achieve this without repeated code?

So far the best solution I've got is:

public async Task ProcessSpreadsheetAsync(string filename, Func<object[,],Task> process) {
    var asyncTaskComplete = new ManualResetEvent(false);
    ProcessSpreadsheet(filename, async valueArray => {
        await process(valueArray);
        asyncTaskComplete.Set();
    });
    await Task.Run(() => asyncTaskComplete.WaitOne());
}

but it's messy and the exceptions aren't handled. I'm wondering if there's another way?

DanO
  • 2,526
  • 2
  • 32
  • 38
eoinmullan
  • 1,157
  • 1
  • 9
  • 32
  • Which code are you trying to avoid duplicating? the try/catch/finally code? Why are you running the code via `Task.Run` in the async version? What exactly are you trying to achieve with the async version? – Yacoub Massad May 02 '16 at 21:40
  • @YacoubMassad yes, that should have been Func, I fixed that, thanks. – eoinmullan May 02 '16 at 21:43
  • Does this code really work? Do exceptions thrown by `process` get caught by the catch blocks? – Yacoub Massad May 02 '16 at 21:50
  • Yeah, I'd like to avoid duplicating the try/catch/finally code. I'd like an async version for the case where the lambda being passed in would benefit from being async. For example, I use the synchronous version to calculate totals from the spreadsheet, but I'd like an asynchronous version where I make a network call for each row in the spreadsheet. This would be slow, and I'd like to keep the UI responsive, so I'd like to do it in an async lambda. – eoinmullan May 02 '16 at 21:55
  • @YacoubMassad, no, you're correct again, the exceptions aren't caught :(. I guess I need more help than I thought. – eoinmullan May 02 '16 at 21:56
  • I don't know how committed you already are to this concept of receiving a lambda, but if you're already dealing with async you should seriously consider inverting the chain of command. Instead of your function receiving lambdas or async lambdas or whatever, you should modify this function to have the following signature `public Task ProcessSpreadsheet(string filename) { ... }`. If the consuming code is using `async/await`, it can deal with the `Task` that way, if it isn't, it can register its lambda using `ContinueWith`. – Asad Saeeduddin May 02 '16 at 22:04
  • Why not extract the duplicate code from the try/catch/finally blocks that is synchronous (using excel, logging, closing excel) into their own methods, and use such methods from the two versions? – Yacoub Massad May 02 '16 at 22:11
  • @AsadSaeeduddin, that's a possibility for sure. I could also just return object[,] and let the consumer use it however they wish. However, I'm still curious to understand if what I have in mind is possible. – eoinmullan May 02 '16 at 22:14
  • @YacoubMassad, I could, but that's not completely DRY, e.g. if I wanted to catch another exception type I'd have to add it in two places. I guess I'm also just plain curious about whether it's possible to wrap one such function by another. – eoinmullan May 02 '16 at 22:18
  • @eoinmullan It probably is, but accepting this unnecessary burden into your code is the primary issue with your code. You don't need to do any of these gymnastics, simply writing the synchronous version, and giving the user the freedom to use `await Task.Run(ProcessSpreadsheet)` or `try { var results = ProcessSpreadSheet("..."); process(results) }` as they wish kills many birds with one stone. – Asad Saeeduddin May 02 '16 at 22:23
  • I don't think you can do it this way. There is one approach tough, but it would probably change your design in a significant way. You can make your methods *possibly-async*. They return a `Task` in their signature. Real asynchronous methods in this case would return an incomplete Task, and synchronous ones would return a completed task (e.g. `Task.FromResult`). This will force all your code to look like asynchronous code, and it might introduce a very small performance hit. Take a look at [this answer](http://stackoverflow.com/a/20985704/2290059). – Yacoub Massad May 02 '16 at 22:24
  • Thanks for the link, I'll check that out. – eoinmullan May 02 '16 at 22:26
  • I don't wish to use any gymnastics and I'd certainly prefer changing the API and using one of the suggested solutions to something overly complex. I've been thinking about this for a while, though, and wanted to ask to ask on SO to make sure I'm not missing something simple first. – eoinmullan May 02 '16 at 22:31

3 Answers3

1

I've read and agree with Stephen Toub's posts on async over sync, but that doesn't apply here as the crux of the method, where asynchrony is or isn't beneficial, is supplied by the consumer via a lambda.

Well, yes and no. I can see where you'd say the consuming code will determine whether it's asynchronous or not. But to avoid duplication, you still have to beware the sync-over-async and async-over-sync antipatterns.

In terms of solutions, the first one that comes to mind is to only accept asynchronous lambdas. This is conceptually similar to defining a Task-returning method in an interface - the implementation may be asynchronous, or it may also be synchronous.

In your case, it would look like:

public async Task ProcessSpreadsheetAsync(string filename, Func<object[,],Task> process) {
  try {
    // Open MS Excel workbook
    // Open sheet & extract data into valueArray
    // ... more boiler plate ...

    await process(valueArray);
  }
  catch (FooException e) {
    LogFoo(e.Message);
    throw;
  }
  catch (BarException e) {
    LogBar(e.Message);
    throw;
  }
  finally {
    // Close workbook, release resources, etc..
  }
}

I would leave it at that. Any synchronous processes would be able to do:

await ProcessSpreadsheetAsync(filename, data => { ...; return Task.FromResult(true); });

In this particular case, you can also get away with writing a wrapper like this:

public void ProcessSpreadsheet(string filename, Action<object[,]> process)
{
  ProcessSpreadsheetAsync(filename, async data => { process(data); }).GetAwaiter().GetResult();
}

However, this is only safe because ProcessSpreadsheetAsync only has one await, which is on its process. If ProcessSpreadsheetAsync is ever changed to have another await, then the wrapper can easily cause a deadlock.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Isn't it better to duplicate the code here rather than forcing a sync method to run async ? Since it comes with an additional cost of extra allocations on the heap. – naltun Jan 08 '20 at 10:16
  • 1
    @NazmiAltun: The synchronous methods still run synchronously; they return a completed task. If you want to avoid the allocation, you can provide a `static` field for `Task.FromResult(true)` or use `ValueTask`. – Stephen Cleary Jan 08 '20 at 16:40
1

To answer your actual question instead of telling you to do something else, just extract the try/catch blocks into functions. This is advised generally as part of separation of concerns. Littering logic with error handling is bad form.

bodangly
  • 2,473
  • 17
  • 28
0

I would drop the idea of accepting a lambda. This is accepting responsibility in your code that really doesn't belong. Instead, simply provide a single overload that looks like this:

public object[,] ProcessSpreadsheet(string filename) {
    try {
        // Open MS Excel workbook
        // Open sheet & extract data into valueArray
        // ... more boiler plate ...

        return valueArray;
    }
    catch (FooException e) {
        LogFoo(e.Message);
        throw;
    }
    catch (BarException e) {
        LogBar(e.Message);
        throw;
    }
    finally {
        // Close workbook, release resources, etc..
    }
}

If this is being consumed by synchronous code, it will simply do:

try {
    var results = ProcessSpreadsheet("...");
    DoSomethingElse(results);
} catch (FooException e) {
    // ...
} catch (CanHappenWhileDoingSomethingElseException e) {
    // ...
}

The consumer of your code has infinite flexibility as far as dealing with exceptions is concerned.

If someone is using async/await, all they have to do is:

try {
    var results = await Task.Run(() => ProcessSpreadsheet("..."));
    DoSomethingElse(results);
} catch (FooException e) {
    // ...
} catch (CanHappenWhileDoingSomethingElseException e) {
    // ...
}

All of their error handling works exactly the same way.

Asad Saeeduddin
  • 46,193
  • 6
  • 90
  • 139