I want to asynchronously access DropBox API in a MonoTouch app.
I thought it would be convenient to use DropNet which itself relies on RestSharp.
Both libraries work well but DropNet overloads that return Task
s don't give you a way to associate requests with cancellation tokens.
This is how their implementation looks:
public Task<IRestResponse> GetThumbnailTask(string path, ThumbnailSize size)
{
if (!path.StartsWith("/")) path = "/" + path;
var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
return ExecuteTask(ApiType.Content, request, cancel);
}
ExecuteTask
implementation is based on TaskCompletionSource
and was originally written by Laurent Kempé:
public static class RestClientExtensions
{
public static Task<TResult> ExecuteTask<TResult>(
this IRestClient client, IRestRequest request
) where TResult : new()
{
var tcs = new TaskCompletionSource<TResult>();
WaitCallback asyncWork = _ => {
try {
client.ExecuteAsync<TResult>(request,
(response, asynchandle) => {
if (response.StatusCode != HttpStatusCode.OK) {
tcs.SetException(new DropboxException(response));
} else {
tcs.SetResult(response.Data);
}
}
);
} catch (Exception exc) {
tcs.SetException(exc);
}
};
return ExecuteTask(asyncWork, tcs);
}
public static Task<IRestResponse> ExecuteTask(
this IRestClient client, IRestRequest request
)
{
var tcs = new TaskCompletionSource<IRestResponse>();
WaitCallback asyncWork = _ => {
try {
client.ExecuteAsync(request,
(response, asynchandle) => {
if (response.StatusCode != HttpStatusCode.OK) {
tcs.SetException(new DropboxException(response));
} else {
tcs.SetResult(response);
}
}
);
} catch (Exception exc) {
tcs.SetException(exc);
}
};
return ExecuteTask(asyncWork, tcs);
}
private static Task<TResult> ExecuteTask<TResult>(
WaitCallback asyncWork, TaskCompletionSource<TResult> tcs
)
{
ThreadPool.QueueUserWorkItem(asyncWork);
return tcs.Task;
}
}
How do I change or extend this code to support cancellation with CancellationToken
?
I'd like to call it like this:
var task = dropbox.GetThumbnailTask(
"/test.jpg", ThumbnailSize.ExtraLarge2, _token
);
Because CancellationToken
is a value type, we can add it as an optional parameter to the APIs with default value and avoid null checks, which is sweet.
public Task<IRestResponse> GetThumbnailTask(
string path, ThumbnailSize size, CancellationToken cancel = default(CancellationToken)
) {
if (!path.StartsWith("/")) path = "/" + path;
var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
return ExecuteTask(ApiType.Content, request, cancel);
}
Now, RestSharp ExecuteAsync
method returns RestRequestAsyncHandle
that encapsulates underlying HttpWebRequest
, along with Abort
method. This is how we cancel things.
public static Task<TResult> ExecuteTask<TResult>(
this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken)
) where TResult : new()
{
var tcs = new TaskCompletionSource<TResult>();
try {
var async = client.ExecuteAsync<TResult>(request, (response, _) => {
if (cancel.IsCancellationRequested || response == null)
return;
if (response.StatusCode != HttpStatusCode.OK) {
tcs.TrySetException(new DropboxException(response));
} else {
tcs.TrySetResult(response.Data);
}
});
cancel.Register(() => {
async.Abort();
tcs.TrySetCanceled();
});
} catch (Exception ex) {
tcs.TrySetException(ex);
}
return tcs.Task;
}
public static Task<IRestResponse> ExecuteTask(this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken))
{
var tcs = new TaskCompletionSource<IRestResponse>();
try {
var async = client.ExecuteAsync<IRestResponse>(request, (response, _) => {
if (cancel.IsCancellationRequested || response == null)
return;
if (response.StatusCode != HttpStatusCode.OK) {
tcs.TrySetException(new DropboxException(response));
} else {
tcs.TrySetResult(response);
}
});
cancel.Register(() => {
async.Abort();
tcs.TrySetCanceled();
});
} catch (Exception ex) {
tcs.TrySetException(ex);
}
return tcs.Task;
}
Finally, Lauren's implementation puts requests in thread pool but I don't see a reason to do so—ExecuteAsync
is asynchronous itself. So I don't do that.
And that's it for cancelling DropNet operations.
I also made a few tweaks which may be useful to you.
Because I had no way of scheduling DropBox Task
s without resorting to wrapping ExecuteTask
calls into another Task
s, I decided to find an optimal concurrency level for requests, which turned out to be 4
for me, and set it explicitly:
static readonly Uri DropboxContentHost = new Uri("https://api-content.dropbox.com");
static DropboxService()
{
var point = ServicePointManager.FindServicePoint(DropboxContentHost);
point.ConnectionLimit = 4;
}
I was also content with letting unhandled task exceptions rot in hell, so that's what I did:
TaskScheduler.UnobservedTaskException += (sender, e) => {
e.SetObserved();
};
One last observation is you shouldn't call Start()
on a task returned by DropNet
because the task starts right away. If you don't like that, you'll need to wrap ExecuteTask
in yet another “real” task not backed by TaskCompletionSource
.