How to Make Any Operation Asynchronous in .NET
I’m in the middle of writing some updates to Quick and Dirty Feed Parser for use in a new personal project of mine; namely, I need to make QD Feed Parser work asynchronously so I can use it in conjunction with asynchronous controllers in ASP.NET MVC3, and eventually Silverlight + Windows Phone 7.
Asynchronous network IO is a breeze in every version of .NET since 1.1 – WebRequest and many others have supported asynchronous methods for as long as I’ve been using them. However, asynchronous disk IO, which I need to support in QD Feed Parser, is not something that comes so easily in .NET 4.0 and below.
StreamReader is the most user-friendly tool for reading files off of local disk because you don’t have to manage any buffers or do any of the sort of accounting that a class like FileStream requires[footnote:It should be noted that FileStream does support asynchronous operations out of the box, but they’re a pain in the ass to use due to the reasons I just described.]. Here’s how nice and easy life is using StreamReader, using actual code from QD Feed Parser as an example:
protected static FeedTuple DownloadXmlFromUri(Uri feeduri) { string xmlContent; using (var reader = new StreamReader(feeduri.LocalPath)) { xmlContent = reader.ReadToEnd(); } return new FeedTuple{FeedContent = xmlContent, FeedUri = feeduri}; }
Given that StreamReader doesn’t have any built-in methods for making this asynchronous, what’s the best way to do this, given that the async and await keywords coming down the pipe in .NET 5.0 aren’t fully available yet? The answer is: we’re going to wrap our disc IO operations using delegates.
Out of the box delegates have both a BeginInvoke and EndInvoke methods, which behave like any other standard asynchronous method call and you, the developer get to decide what functions are invoked on the background thread.
Here are the steps:
1. Create a delegate and a static worker method
protected delegate FeedTuple BackGroundWorker(Uri feeduri); protected static FeedTuple DownloadXmlFromUri(Uri feeduri) { string xmlContent; using (var reader = new StreamReader(feeduri.LocalPath)) { xmlContent = reader.ReadToEnd(); } return new FeedTuple { FeedContent = xmlContent, FeedUri = feeduri }; }
The delegate and the static worker method need to have the same signatures.
2. Define your own Begin[Operation] and End[Operation] methods
For Quick and Dirty Feed Parser, here is what I created, minus some integrity checks in order to make the code more readable:
public override IAsyncResult BeginDownloadXml(Uri feeduri, AsyncCallback callback) { return FeedWorkerDelegate.BeginInvoke(feeduri, callback, new FeedTuple()); } public override FeedTuple EndDownloadXml(IAsyncResult asyncResult) { var result = FeedWorkerDelegate.EndInvoke(asyncResult); return result; }
Your begin method should return the IAsyncResult from your delegate – this way your caller can pass the asynchronous result back to the End[Operation] method and return your result. You can also use the IAsyncResult’s WaitHandle to put a block a calling thread if you wanted.
When dealing with delegates you have to be careful with your accounting – for every BeginInvoke there should be exactly one EndInvoke.
3. Calling the asynchronous methods
Here’s a quick unit test I wrote for testing this method in Quick and Dirty Feed Parser.
[Test, TestCaseSource("TestCases")] public void CanDownloadXmlStreamAsync(string rsslocation) { var feeduri = new Uri(rsslocation); var result = Factory.BeginDownloadXml(feeduri, null); var resultantTuple = Factory.EndDownloadXml(result); Assert.IsNotNull(resultantTuple); Assert.That(resultantTuple.FeedContent != String.Empty); Assert.AreEqual(feeduri, resultantTuple.FeedUri); }
You should note that I’m effectively blocking the calling thread until the asynchronous worker thread returns a result in this method, and that’s because it’s easier to write unit tests this way.
The best practice for taking advantage of asynchronous method calls is to utilize a callback function, which you can do like so:
public void DownloadXmlStreamAsync(string rsslocation) { var feeduri = new Uri(rsslocation); var result = Factory.BeginDownloadXml(feeduri, async => { var callbackTuple = Factory.EndDownloadXml(async); Dispatcher.BeginInvoke(() => { //... Do some client-side stuff with the data... }); }); }
And that’s it!