74 msdn magazine Async Programming
We can take the “no op” interception behavior shown in Figure 2
as a starting point.
Next, we add the code to detect the task-returning methods and
replace the returned Task with a new wrapper Task that logs the out-
come. To accomplish this, the CreateMethodReturn on the input
object is called to create a new IMethodReturn object representing
a wrapper Task created by the new CreateWrapperTask method in
the behavior, as shown in Figure 3.
Th e new CreateWrapperTask method returns a Task that waits
for the original Task to complete and logs its outcome, as shown
in Figure 4. If the task resulted in an exception, the method will
rethrow it aft er logging it. Note that this implementation doesn’t
change the original Task’s outcome, but a diff erent behavior could
replace or ignore the exceptions the original Task might introduce.
Dealing with Generics
Dealing with methods that return Task
particularly if you want to avoid taking a performance hit. Let’s
ignore for now the problem of fi guring out what “T” is and assume
it’s already known. As Figure 5 shows, we can write a generic method
that can handle Task
asynchronous language features available in C# 5.0.
As with the simple case, the method just logs without changing
the original behavior. But because the wrapped Task now returns
a value, the behavior could also replace this value, if needed.
How can we invoke this method to obtain the replacement Task?
We need to resort to refl ection, extracting the T from the intercepted
method’s generic return type, creating a closed version of this
generic method for that T and creating a delegate out of it and,
fi nally, invoking the delegate. Th is process can be quite expensive, so
it’s a good idea to cache these delegates. If the T is part of the meth-
od’s signature, we wouldn’t be able to create a delegate of a method
and invoke it without knowing the T, so we’ll split our earlier method
into two methods: one with the desired signature, and one that
benefi ts from the C# language features, as shown in Figure 6.
Next, we change the interception method so we use the correct
delegate to wrap the original task, which we get by invoking the new
GetWrapperCreator method and passing the expected task type.
We don’t need a special case for the non-generic Task, because it
can fi t the delegate approach just like the generic Task
7 shows the updated Invoke method.
All that’s left is implementing the GetWrapperCreator method.
Th is method will perform the expensive refl ection calls to create
the delegates and use a ConcurrentDictionary to cache them. Th ese
wrapper creator delegates are of type Func<Task, IMethodInvocation,
Task>; we want to get the original task and the IMethodInvocation
object representing the call to the invocation to the asynchronous
method and return a wrapper Task. Th is is shown in Figure 8.
For the non-generic Task case, no refl ection is needed and the
existing non-generic method can be used as the desired delegate
as is. When dealing with Task
private async Task CreateWrapperTask(Task task,
IMethodInvocation input)
{
try
{
await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0}",
input.MethodBase.Name);
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}",
input.MethodBase.Name, e);
throw;
}
}
Figure 4 Logging the Outcome
private async Task<T> CreateGenericWrapperTask<T>(Task<T> task,
IMethodInvocation input)
{
try
{
T value = await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0}
with value: {1}",
input.MethodBase.Name, value);
return value;
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
throw;
}
}
Figure 5 A Generic Method to Handle Task
private Task CreateGenericWrapperTask<T>(Task task, IMethodInvocation input)
{
return this.DoCreateGenericWrapperTask<T>((Task<T>)task, input);
}
private async Task<T> DoCreateGenericWrapperTask<T>(Task<T> task,
IMethodInvocation input)
{
try
{
T value = await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0}
with value: {1}",
input.MethodBase.Name, value);
return value;
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
throw;
}
}
Figure 6 Splitting the Delegate Creation Method
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
IMethodReturn value = getNext()(input, getNext);
var method = input.MethodBase as MethodInfo;
if (value.ReturnValue != null
&& method != null
&& typeof(Task).IsAssignableFrom(method.ReturnType))
{
// If this method returns a Task, override the original return value
var task = (Task)value.ReturnValue;
return input.CreateMethodReturn(
this.GetWrapperCreator(method.ReturnType)(task, input), value.Outputs);
}
return value;
}
Figure 7 The Updated Invoke Method