Open sandboxFocusImprove this doc

Semantic Advising of Iterator and Async Methods

In most C# and VB methods, the source code is very similar to the way how the method is actually executed by the .NET runtime. However, with async and iterator methods, mapping between source code and assembly code is far from being straightforward. The compiler performs a complex transformation of the source code and generates a state machine type. If you disassemble an async or iterator method, you would just find some instructions that instantiate this state machine. When you apply an aspect to an async or iterator method, it leads to an ambiguity whether the aspect should be applied semantically at the abstraction level of the source code, or whether it should be applied non-semantically at the abstraction level of the assembly code.

By default, PostSharp applies semantic advising for async and iterator methods. It also uses semantic advising for all methods returning a Task.

This article discusses all details of semantic advising.

Semantic advising for asynchronous code

Consider the following code snippet:

public class FlowerService
{
  [MyAspect]
  public Task<Flower> GetFlowerAsync1( int flowerId, string connectionString )
  {
    var connection = ConnectionManager.GetConnection( connectionString );
    return this.GetFlowerAsync2( flowerId, connection );
  }

  [MyAspect]
  public async Task<Flower> GetFlowerAsync2( int flowerId, Connection connection )
  {
      var flowerData = await connection.GetFlowerAsync( flowerId );
      var familyData = await connection.GetFlowerFamilyAsync( flowerData.FamilyId );
      return new Flower( flowerId, flowerData.Name, familyData.Name );
  }
}

The GetFlowerAsync1 method returns a Task but is not async (it would be useless and hurt performance to make it async). The GetFlowerAsync2 method both returns a Task and is async.

Both methods are enhanced by [MyAspect]. For different behaviors implemented by MyAspect, how would you expect MyAspect to work?

  • If MyAspect was an exception handler, you would probably expect all exceptions to be caught by MyAspect, including exceptions thrown by the GetFlowerAsync and GetFlowerFamilyAsync methods and the constructor of the Flower class. You would be deceived to realize that the aspect only handles exceptions thrown in the process of instantiating Task<Flower> and execute the part of the task that can run synchronously.

  • If MyAspect was a profiling aspect, you would probably want to measure the time taken by the whole method to execute. That is, you will probably be interested in the time of the whole Task<Flower> to run to completion, not just the time to instantiate it and run to the first waiting point.

  • If MyAspect was a caching aspect, you would probably want to cache the Flower object, not the Task<Flower> itself.

That is, most of the time, you want the aspect to apply to the semantic of the method, not to its implementation (how it is implemented in MSIL and executed by the .NET runtime).

We use the term semantic advising when an aspect or advice is applied to the level of abstraction of the programming language (C# or VB). Non-semantic advising or low-level advising means that PostSharp applies the aspect to the level of abstraction of MSIL.

Semantic advising is the default behavior for all methods returning a Task (or any other awaitable type such as ValueTask) and all async methods.

Synchronization context

In method boundary aspects, all advices are executed in the synchronization context of the caller of the advised method. For example, if you await an async method from the event handler of an event in a Windows Forms application, all advices (such as OnEntry(MethodExecutionArgs) and OnExit(MethodExecutionArgs)) are also executed on the Windows Forms UI thread.

It is generally a good choice to execute the advices in the current synchronization context. However, it can cause a deadlock in a situation where the synchronization context is blocked until the advised method completes.

Example of the deadlock:

[OnMethodBoundaryAspect1]
Task Return4()
{
  return Task.Delay(500);
} 
void button1_Clicked(object sender, EventArgs args)
{
  Return4().Wait(); // blocks the Windows Forms UI thread
  // without PostSharp, the thread would get unblocked when the delay completes
  // with PostSharp, there is a deadlock because OnMethodBoundaryAspect1's OnExit advice needs to happen in the Windows Forms UI thread
}

If you do not want to execute advices in the current synchronization context, use MethodInterceptionAspect instead of OnMethodBoundaryAspect. The interception aspect gives you more control over the synchronization context.

Semantic advising for iterators

The notion of semantic advising also applies to iterator methods, i.e. methods that include the yield return statement.

Consider the following code snippet:

public class PostcardService
{

  [MyAspect]
  public IEnumerable<Postcard> GetPostcards1( )
  {
      return new [] { new Postcard("Hello from Alaska"), new Postcard("Hello from Siberia") };
  }

  [MyAspect]
  public IEnumerable<Postcard> GetPostcards2( int flowerId, Connection connection )
  {
      yield return new Postcard("Hello from Alaska");
      yield return new Postcard("Hello from Siberia");
  }

}

The GetPostcards2 method requires special attention. Under the hood, the C# or VB compiler generates a new class implementing the IEnumerable<T> and IEnumerator<T> interfaces, called the enumerator class. At runtime, calling the GetPostcards2 method only instantiates the enumerator class. The initial logic of GetPostcards2 is moved to the MoveNext method of the enumerator.

Let's do the same exercise as for asynchronous methods. For different behaviors implemented by MyAspect, how would you expect MyAspect to work?

  • If MyAspect was an exception handler, you would probably want the aspect to catch any exception thrown by the C# code that you can see. That is, in GetPostcards2, you actually want to catch exceptions in the MoveNext method of the enumerator class. In this case, you need semantic advising.

  • If MyAspect was a profiling aspect, you may want to only measure the time when GetPostcards2 is actually executing, but exclude the time when the caller is processing the data returned by the enumerator. Therefore, you will also want to add behaviors to the MoveNext method of the enumerator class. In this case again, you need semantic advising.

  • If MyAspect was a caching aspect, however, you will want to cache a copy of the enumerator itself, therefore you will need to enhance the GetPostcards2 method and not the MoveNext method. In this case, you don't need semantic advising.

Semantic advising vs non-semantic advising

The following table compares semantic advising with non-semantic advising in several situations.

Semantic Advising Non-Semantic Advising
Async methods:
Code covered or intercepted by the aspect The whole async method. The part of the async method before the first await operator whose operand (typically a Task) has not yet completed.
Return value The operand of the return statement, i.e. Result. The Task<TResult> object itself.
Non-async methods returning a T:System.Threading.Tasks.Task:
Code covered or intercepted by the aspect Both the code that instantiates or gets the Task and the whole execution of the Task. The compiler-generated code that instantiates or gets the Task (and if the Task represents an async method, plus the first segment of the method that runs synchronously).
Return value The value of the Result property. The Task<TResult> object itself.
Iterator methods:
Code covered or intercepted by the aspect The whole method. The compiler-generated code that instantiates the enumerator class (no user code is covered).
Return value None. The enumerator.
Non-iterator methods returning T:System.Collections.Generic.IEnumerable`1:
Code covered or intercepted by the aspect The returned enumerator's MoveNext method. The method that is being enhanced by the aspect.
Return value None. The enumerator or enumerable object.
Async iterator methods:
Code covered or intercepted by the aspect (not supported) The compiler-generated code that instantiates the async enumerator class (no user code is covered).
Return value (not supported) The async enumerator.

Supported and default advising modes

Semantic advising is available for the OnMethodBoundaryAspect, OnExceptionAspect and MethodInterceptionAspect aspects. Whenever semantic advising makes sense, it is the default advising mode, except for normal methods returning IEnumerable<T> or IEnumerator<T> which are not semantically advised by default because we expect that non-semantic advising is more often what you need for those methods.

The following table specifies where semantic advising is supported and where it is the default advising mode.

Target methods Advising modes supported by OnMethodBoundaryAspect and OnExceptionAspect Advising modes supported by MethodInterceptionAspect Default advising mode
async method returning void Semantic and non-semantic Non-semantic Semantic
async method returning a Task Semantic and non-semantic Semantic and non-semantic Semantic
async method returning something else than void or a Task Semantic and non-semantic Non-semantic Semantic
Iterator method Semantic and non-semantic Non-semantic Semantic
Normal method returning IEnumerable<T> or IEnumerator<T> Semantic and non-semantic Non-semantic Non-semantic
Async iterator method (C# 8) Non-semantic Non-semantic Semantic
Warning

With MethodInterceptionAspect, the default advising mode is always non-semantic when the OnInvokeAsync(MethodInterceptionArgs) advice is not implemented by the aspect.

The default mode is always semantic, even in situations where semantic advising is not available. This design allows us to implement support for semantic advising in future versions of PostSharp without breaking backward compatibility. However, PostSharp will emit a build-time error if you try to use semantic advising on a method that is not supported.

The next sections explain how to opt out from semantic advising and how to cope with situations when semantic advising is not available.

Enabling and disabling semantic advising

You can disable or enable semantic advising by setting the SemanticallyAdvisedMethodKinds property of your aspect. You would typically set this property in the constructor or in the CompileTimeInitialize(MethodBase, AspectInfo) method.

If you want to disable semantic advising in all situations, set the SemanticallyAdvisedMethodKinds property to None. Otherwise, you can select individual situations in which semantic advising should be applied by setting the property to a bitwise combination of the values of the SemanticallyAdvisedMethodKinds enumeration. For instance, the Async | Iterator value instructs PostSharp to use semantic advising for async methods and iterators but not for other methods returning a Task or an enumerable.

Example

The following code snippet shows how to configure a caching aspect so that semantic advising is used for methods returning a Task but not for methods returning an enumerable.

[PSerializable]
public class CacheAttribute : MethodInterceptionAspect
{

  public CacheAttribute()
  {
      this.SemanticallyAdvisedMethodKinds = SemanticallyAdvisedMethodKinds.ReturnsAwaitable;
  }

  // Detailed skipped.

}

Coping with situations where semantic advising is not available

By default, PostSharp will emit a build-time error if you're applying a semantically advising aspect to a method that does not support it (for instance, an asynchronous MethodInterceptionAspect cannot be applied to an async void method).

Instead of failing with an error, you can change the behavior by setting the UnsupportedTargetAction aspect property. The default value is Fail. You can choose Ignore to silently skip applying the aspect or advice to the target method, or Fallback to apply non-semantic advising.

If you are using composite aspects, you can change the attribute property UnsupportedTargetAction and similarly named properties on other advices.

See Also

Reference

OnMethodBoundaryAspect
OnYield(MethodExecutionArgs)
OnResume(MethodExecutionArgs)
OnEntry(MethodExecutionArgs)
OnExit(MethodExecutionArgs)
OnSuccess(MethodExecutionArgs)
MethodExecutionArgs
SemanticallyAdvisedMethodKinds
MethodExecutionTag
PSerializableAttribute
Other Resources

Injecting Behaviors Before and After Method Execution
PostSharp Aspect Framework - Product Page