Before version 3.1, PostSharp did not understand the state machine transformation operated by the compiler. Adding an aspect to an iterator would produce surprising results, because the aspect would actually be applied only to the code that instantiates the state machine class. For instance, an OnException advise would never get invoked because instantiating the state machine is unlikely to throw any exception. Conversely, any exception thrown by the iterator would never be captured because the exception was technically (at MSIL level) thrown by the MoveNext method of the state machine class. The workaround was easy: add an aspect to this MoveNext method. The availability of this workaround was perhaps the reason why users did not complain that PostSharp lacks this ability.
Things became more problematic with async methods. The compiler does the same kind of magic as with iterators, but a more advanced one. With async methods, exceptions thrown by user code would get “transparently” intercepted from within the MoveNext method and assigned to the Task object – before they get any chance to get intercepted by our aspect. Thus, the need for proper support from PostSharp became more urgent. No wonder if this became our number-one feature request on User Voice. We were not able to ship the feature in PostSharp 3.0 for a lot of embarrassing reasons, but now it’s out!
The old, backward-compatible way
PostSharp has been around for 9 years. One of our major concerns has always been to never break backward compatibility. The problem is actually not to ensure that your old code still builds after you upgrade to PostSharp. The real challenge is to guarantee that your code will behave identically.
To ensure behavioral backward compatibility, we had to take this design decision: if you apply an OnMethodBoundaryAspect to an iterator or async method, by default, it won’t be applied to the state machine.
However, we still think that the backward-compatible behavior is odd and that new users would really expect the aspect to be applied to the state machine. Therefore, a warning will be emitted whenever an OnMethodBoundaryAspect is applied to an async or iterator method. To turn off the warning, you have to set the aspect property ApplyToStateMachine to false if you want to maintain the backward-compatible behavior. You can set the property in the constructor if you don’t want to set it explicitly every time the aspect is used.
The following aspect exhibits the backward-compatible behavior of the OnMethodBoundary aspect:
[PSerializable] class MyAspect : OnMethodBoundaryAspect { public override void OnEntry(MethodExecutionArgs args) { Console.WriteLine("OnEntry"); } public override void OnSuccess(MethodExecutionArgs args) { Console.WriteLine("OnSuccess({0})", args.ReturnValue); } public override void OnExit(MethodExecutionArgs args) { Console.WriteLine("OnExit"); } public override void OnException(MethodExecutionArgs args) { Console.WriteLine("OnException({0})", args.Exception.Message); } }
Let’s apply this aspect to an iterator:
static void PrintFruits(bool throwException) { try { foreach (string fruit in GetFruits(throwException)) { Console.WriteLine("Received: " + fruit); } } catch (Exception e) { Console.WriteLine("Exception: " + e.Message); } } [MyAspect(ApplyToStateMachine = false)] static IEnumerable GetFruits(bool throwException) { yield return "blackcurrant"; yield return "pomegranate"; if ( throwException ) throw new Exception("Rotten fruit."); yield return "pineapple"; }
Here is the output of this code with the success flow:
OnEntry OnSuccess(Program+<GetFruits>d__0) OnExit Received: blackcurrant Received: pomegranate Received: pineapple
You can see that the OnSuccess advice is called before the enumerator has produced the first result, and that the return value printed by the OnSuccess advice does not make sense.
And here is the output with the exception flow:
OnEntry OnSuccess(Program+<GetFruits>d__0) OnExit Received: blackcurrant Received: pomegranate Exception: Rotten fruit.
As you can see, the OnException handler is never hit.
Applying the aspect to the state machine
Things get very different when you set the ApplyToStateMachine property to true. Just modify the property and the code above will produce the following output:
OnEntry Received: blackcurrant Received: pomegranate Received: pineapple OnSuccess() OnExit
The exception flow is the following:
OnEntry Received: blackcurrant Received: pomegranate OnException(Rotten fruit.) Exception: Rotten fruit.
As you can see, the behavior is now “as expected”, i.e. it is consistent with the level of abstraction of the source code. The previous behavior was consistent with the level of abstraction of MSIL, and this is why it was less useful.
New advices: OnYield and OnResume
Additionally to applying the aspect to the state machine instead of the method that merely instantiates it, PostSharp 3.1 brings two new advices: OnYield and OnResume. These advices are defined on the new interface IOnStateMachineBoundaryAspect. If you want to use them, you need to have your aspect class implement this interface.
Note that implementing the IOnStateMachineBoundaryAspect has the side effect of settings the default value of the ApplyToStateMachine property to true and to quiet the warning that is otherwise displayed then this property is not set. This is because, if your code implements IOnStateMachineBoundaryAspect, we trust we can put usability prior to backward compatibility.
But let’s go back to the advices themselves. Interestingly, they apply identically – with exactly the same semantics – to both iterator and async methods. In short, OnYield is invoked when the state machine yields the control flow, i.e. when the control flow temporary leaves the state machine to the caller. OnResume is invoked when the state machine gains back the control flow.
With iterators
Let’s see this more concretely, first on iterators. OnYield is invoked after the yield return statement, before the control flow gets back to the consumer of the iterator. OnResume is then invoked when the consumer calls MoveNext. Note that the first time the consumer calls MoveNext, the OnEntry advice is invoked. Also, when the iterators terminates using yield break or simply by letting the control flow fall back, the OnSuccess advice is invoked after OnYield.
What if you want to know which value has just been yielded? Simply read the MethodExecutionArgs.YieldValue property. You can also write this property if you want to change the returned value.
Let’s update our aspect to add tracing of OnYield and OnResume events:
[PSerializable] class MyAspect : OnMethodBoundaryAspect, IOnStateMachineBoundaryAspect { public override void OnEntry(MethodExecutionArgs args) { Console.WriteLine("OnEntry"); } public override void OnSuccess(MethodExecutionArgs args) { Console.WriteLine("OnSuccess({0})", args.ReturnValue); } public override void OnExit(MethodExecutionArgs args) { Console.WriteLine("OnExit"); } public override void OnException(MethodExecutionArgs args) { Console.WriteLine("OnException({0})", args.Exception.Message); } public void OnResume(MethodExecutionArgs args) { Console.WriteLine("OnResume"); } public void OnYield(MethodExecutionArgs args) { Console.WriteLine("OnYield({0})", args.YieldValue); } }
The program output is now the following:
OnEntry OnYield(blackcurrant) Received: blackcurrant OnResume OnYield(pomegranate) Received: pomegranate OnResume OnYield(pineapple) Received: pineapple OnResume OnSuccess() OnExit ------------- OnEntry OnYield(blackcurrant) Received: blackcurrant OnResume OnYield(pomegranate) Received: pomegranate OnResume OnException(Rotten fruit.) Exception: Rotten fruit.
With async methods
The behavior of OnYield and OnResume is similar for async methods than for iterators. OnYield and OnResume are invoked upon execution of the await keyword: OnYield gets called when a wait begins, OnResume when a wait ends.
Note there are cases where the await keyword does not result into an execution of the OnYield/OnResume sequence: when the awaited-for task completes synchronously, the execution of the method continues without going through OnYield and OnResume. This is logical if you count that in this case the state machine really does not yield the control flow.
The YieldValue property is not available for async methods.
Let’s test our aspect on the following code:
[MyAspect] static async Task<string> TimerMethod() { for (int i = 3; i >= 0; i--) { Console.WriteLine(i + " green bottles"); await Task.Delay(100); } return "Done"; }
The output of this program is the following:
OnEntry 3 green bottles OnYield() OnResume 2 green bottles OnYield() OnResume 1 green bottles OnYield() OnResume 0 green bottles OnYield() OnResume OnSuccess() OnExit
Limitations
Note that the following limitations apply when an aspect is applied to a state machine (whether the additional advices OnYield and OnResume are applied or not):
- The control flow cannot be changed (the MethodExecutionArgs.FlowBehavior property is ignored).
- The return value cannot be read or changed (the MethodExecutionArgs.ReturnValue is ignored).
Aspect Composition
Perhaps it goes without saying, but this is never trivial to implement: you can apply many aspects to state machines, apply state-machine-aware and -unaware aspects to a method, and do strange combinations of aspects. This is all supposed to work – and tested.
Use Cases
I believe the new features are very useful in the following use cases:
- Call stack reconstruction: remember the stack call on entry so that it can be meaningfully displayed if an exception occurs after resume of the async method. Otherwise, you would just see that the call stack of the exception comes from the thread pool.
- Iterator logging: you can now log the values returned by the iterator.
- Profiling: you can now accurately compute the time taken by an iterator or async method to complete.
- Context switching: ensure that the value of some thread-static fields are preserved and meaningful in all parts of a state machine.
Summary
The OnMethodBoundaryAspect can now be applied to state machines like async and iterator methods, and your aspect will not be applied to the state machine itself instead of just to instructions that instantiate the state machine. There is a new interface IOnStateMachineBoundaryAspect with two new advices: OnYield and OnResume. The new feature is designed to be backward compatible with the old (and odd) behavior. You will need to manually set the ApplyToStateMachine property if you want to get rid of the attention-to-odd-but-backward-compatible-behavior warning.
I think this is an exciting feature, and it required a lot of engineering work just to make it work and integrate well with other features of PostSharp.
A last word: PostSharp 3.1 is still beta and there’s still room to improve the API. I’m anxious to hear your feedback so we can take into account before sealing the new feature.
Happy PostSharping!
-gael