I'm using the Enterprise Library Transient Fault Handling Application Block in a Windows 8 Store App with the WCF Data Services Client for ODATA. I want to use retry logic for transient errors occurring when calling the ODATA service. I built a custom transient error detection strategy.
I have also built a LoadTaskAsync extension method for the DataServiceCollection as the LoadAsync method does not return a Task (instead the DataServiceCollection raises the LoadCompleted event).
So I can load data into a DataServiceCollection as follows:
var query = this.DataContext.Products.Where(item => item.Modified >= anchor);
var products = new DataServiceCollection<Product>(this.DataContext);
await this.retryPolicy.ExecuteAsync(() => products.LoadTaskAsync(query));
Now the documentation for the Enterprise Library Transient Fault Handling Application Block states that
The taskFunc argument you pass to the ExecuteAsync method is not necessarily invoked in the same synchronization context used when calling ExecuteAsync originally; so if you need to start the task from within the UI thread for example, be sure to schedule it explicitly within the delegate.
I need to invoke the LoadTaskAsync method on the UI thread as the load operation may update products that were already tracked by the data context and that are data bound to the UI.
Question is how? Preferably without modifying the LoadTaskAsync extension method (what if it was not my code to change). I was thinking of creating an extension method for the RetryPolicy that calls the ExecuteAsync method while making sure the taskFunc is invoked on the UI thread.
The easiest way perhaps is to modify the LoadTaskAsync extension method to pass in a TaskCreationOptions.AttachedToParent, so I could create an extension method for the RetryPolicy as follows:
public static Task<TResult> ExecuteCurrentSynchronizationContextAsync<TResult>(
this RetryPolicy retryPolicy,
Func<TaskCreationOptions, Task<TResult>> taskFunc)
{
var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
return
retryPolicy.ExecuteAsync(
() =>
Task.Factory.StartNew(
() => taskFunc(TaskCreationOptions.AttachedToParent),
CancellationToken.None,
TaskCreationOptions.None,
scheduler).Unwrap());
}
Note that taskFunc must now be a Func<TaskCreationOptions, Task<TResult>>.
I would then call this as follows:
await
this.retryPolicy.ExecuteCurrentSynchronizationContextAsync(
creationOptions => products.LoadTaskAsync(query, creationOptions));
As I prefer not to change the LoadTaskAsync extension method, how could I change this RetryPolicy ExecuteCurrentSynchronizationContextAsync extension method so that taskFunc can be a Func<Task<TResult>> again while making sure taskFunc is invoked on the UI thread?
I would not recommend using AttachedToParent
with asynchronous tasks. In fact, most promise-style tasks will specify DenyChildAttach
which prevents AttachedToParent
from working.
Instead, you just need to capture the synchronization context itself and use that. There is a wrinkle: Windows Store apps don't allow synchronous invocation on the synchronization context, so you'll need to either use CoreDispatcher.RunAsync
instead of SynchronizationContext
, or build your own async
-aware extension method for SynchronizationContext
. Of those two, I prefer using the SynchronizationContext
approach; it's a bit more code in this case, but it means you don't have to tie your code (presumably service-layer code) to this specific UI framework.
So, first we define a RunAsync
on SynchronizationContext
, which will (asynchronously) execute asynchronous code on a specified synchronization context:
public static Task<TResult> RunAsync<TResult>(this SynchronizationContext context, Func<Task<TResult>> func)
{
var tcs = new TaskCompletionSource<TResult>();
context.Post(async _ =>
{
try
{
tcs.TrySetResult(await func());
}
catch (OperationCanceledException)
{
tcs.TrySetCanceled();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}, null);
return tcs.Task;
}
Then we can capture and use the SynchronizationContext
:
public static Task<TResult> ExecuteOnCurrentSynchronizationContextAsync<TResult>(
this RetryPolicy retryPolicy,
Func<Task<TResult>> taskFunc)
{
var context = SynchronizationContext.Current ?? new SynchronizationContext();
return retryPolicy.ExecuteAsync(() => context.RunAsync(taskFunc));
}