PostSharp4.3/Custom Patterns/Developing Custom Aspects/Developing Simple Aspects/Injecting Behaviors into Async Methods
Injecting Behaviors into Async Methods

Async methods are methods with the async keyword in C#. Unlike normal methods, execution of async methods can be paused and resumed. Execution is paused when the method depends on a task that is being executed asynchronously, and is resumed when the dependent task has completed. An async method provides a convenient way to do potentially long-running work without blocking the caller's thread.

Async methods return a value of the Task type. At build time, the compiler performs a complex transformation of the code. The original async logic is moved to a different type called here the state machine type, and the original method body is replaced by just a few lines of code instantiated this state machine.

By default, all aspects applied on an async method are actually applied to the method instantiating the state machine. However, there are times when you want to actually add an aspect to the state machine. We will see how this is possible using the OnMethodBoundaryAspect aspect. For a more general information about using OnMethodBoundaryAspect in non-async methods, see Injecting Behaviors Before and After Method Execution.

There are two options when applying OnMethodBoundaryAspect to an async method. The first option is to apply the aspect to the method that instantiates and returns the task instance. The second option is to apply the aspect to the actual code that implements the logic inside the task. When using the second option you can also inject behaviors before and after your await statements.

This topic contains the following sections:

Applying the aspect to the method instantiating the async task

You can control how the aspect is applied to the async method by setting the boolean ApplyToStateMachine property. Set this property to false to apply the aspect only to the code instantiating the async task. You can set the property in the constructor if you don't want to set it explicitly every time the aspect is used.

In the following procedure, we demonstrate how to inject behaviors into the method instantiating the async task. For this purpose, we create a simplified caching aspect that caches the result of the async method. This aspect can be applied to any async method returning Task<object>. For example, to the TestCaching method shown below:

C#
public async Task<object> TestCaching()
{
    await Task.Wait(1000); // Some long-running operation.
    return await Task.FromResult( new {Name = "Test object"} );
}

To inject behaviors into the method instantiating the async task:

  1. Create an aspect class and inherit OnMethodBoundaryAspect. Annotate the class with the [PSerializableAttribute] custom attribute.

  2. Set the ApplyToStateMachine property to false in your constructor.

    C#
    [PSerializable]
    public class CacheTaskResultAttribute : OnMethodBoundaryAspect
    {
        public CacheTaskResultAttribute()
        {
            ApplyToStateMachine = false;
        }
    }
    Note Note

    The default value of the ApplyToStateMachine property is false unless the OnYield(MethodExecutionArgs) or OnResume(MethodExecutionArgs) advice is implemented. However, PostSharp will emit a warning whenever OnMethodBoundaryAspect is applied to an async method without setting the ApplyToStateMachine property.

  3. Implement some of the advice methods of the OnMethodBoundaryAspect class, according to your requirements. For more information about these advice methods, see Injecting Behaviors Before and After Method Execution.

    For our CacheAttribute aspect, we need to implement two advises: OnEntry(MethodExecutionArgs) and OnSuccess(MethodExecutionArgs).

    C#
    [PSerializable]
    public class CacheTaskResultAttribute : OnMethodBoundaryAspect
    {
        public CacheTaskResultAttribute()
        {
            ApplyToStateMachine = false;
        }
    
        public override void OnEntry( MethodExecutionArgs args )
        {
            object cachedValue = MemoryCache.Default[args.Method.Name];
            if ( cachedValue != null )
            {
                args.ReturnValue = Task.FromResult( cachedValue );
                args.FlowBehavior = FlowBehavior.Return;
            }
        }
    
        public override void OnSuccess( MethodExecutionArgs args )
        {
            var task = (Task<object>) args.ReturnValue;
            args.ReturnValue =
                task.ContinueWith(
                    t =>
                    {
                        MemoryCache.Default[args.Method.Name] = t.Result;
                        return t.Result;
                    });
        }
    }
  4. Apply your custom attribute to the target methods. Below you can see the [Cache] attribute applied to the TestCaching method.

    C#
    [Cache]
    public async Task<object> TestCaching()
    {
      await Task.Wait(1000); // Some long-running operation.
      return await Task.FromResult( new {Name = "Test object"} );
    }
    Note Note

    You can also set the ApplyToStateMachine property value when applying the attribute to a method, for instance using the syntax [MyAspect( ApplyToStateMachine = false )]. This can be useful if it makes sense to apply this aspect to both the state machine or the instantiation method.

Thanks to the aspect, whenever the TestCaching method is called, the CacheAttribute.OnEntry method executes first. It checks whether the result value is already stored in the cache. If the value already exists, then a new task is created from this value and immediately returned back to the caller. Otherwise, the execution continues normally and a new task instance is created to run our async method.

The CacheAttribute.OnSuccess method is called before the newly created async task is returned back to the caller. The aspect's method creates a new continuation task and returns it back to the caller instead of the original one. Once the original task completes, the continuation added by the aspect stores the resulting value in cache. Afterwards, the original caller proceeds with processing the task's result.

Applying the aspect to the async task itself

In many cases, we need to inject behaviors into the code of the async task itself. You can accomplish this by setting the OnMethodBoundaryAspect.ApplyToStateMachine property to true.

We'll see how to use this feature to create an aspect that measures the execution time of a method. In this section, we will create a simple profiling aspect and show how to apply it correctly to the async methods.

To test the profiling aspect, we will measure the execution time of the TestProfiling example method, shown below. The call to Thread.Sleep represents the work performed by the method. This is the time we want to measure. The call to Task.Delay represents the work performed outside the method, for example the asynchronous call to a web service or a database server.

C#
public async Task TestProfiling()
{
    await Task.Delay( 1000 );
    Thread.Sleep( 1000 );
}

To inject behaviors into the async task itself:

  1. Create an aspect class and inherit OnMethodBoundaryAspect. Annotate the class with the [PSerializableAttribute] custom attribute.

    C#
    [PSerializable]
    public class ProfilingAttribute : OnMethodBoundaryAspect
    {
    }
  2. Implement some of the advice methods of the OnMethodBoundaryAspect class, according to your requirements. For more information about these advice methods, see Injecting Behaviors Before and After Method Execution.

    For our profiling aspect, we need to implement two advices: OnEntry(MethodExecutionArgs) and OnExit(MethodExecutionArgs).

    C#
    [PSerializable]
    public class ProfilingAttribute : OnMethodBoundaryAspect
    {
        public override void OnEntry( MethodExecutionArgs args )
        {
            Stopwatch sw = Stopwatch.StartNew();
            args.MethodExecutionTag = sw;
        }
    
        public override void OnExit( MethodExecutionArgs args )
        {
            Stopwatch sw = (Stopwatch) args.MethodExecutionTag;
            sw.Stop();
            Console.WriteLine( "Method {0} executed for {1}ms.",
                               args.Method.Name, sw.ElapsedMilliseconds );
        }
    }
  3. Apply your custom attribute to the target methods. Set the ApplyToStateMachine property to true when applying the attribute to async methods. Below you can see the [Profiling] attribute applied to the TestProfiling method.

    C#
    [Profiling( ApplyToStateMachine = true )]
    public async Task TestProfiling()
    {
        await Task.Delay( 1000 );
        Thread.Sleep( 1000 );
    }
    Note Note

    You can set the ApplyToStateMachine property in the constructor if you don’t want to set it explicitly every time the aspect is used.

Whenever the TestProfiling method is called, it creates a new async task instance. At some later point, the task starts executing and immediately the ProfilingAttribute.OnEntry method is invoked. The method starts the stopwatch and stores it for the future use. Then the execution of the async task continues.

The ProfilingAttribute.OnExit method is called just before the async task completes. This method stops measuring the time and outputs the result to the console.

This program produces the following output:

Method TestProfiling executed for 2044ms.

As you can see, the output shows that we're not only measuring the execution time of the user code in the TestProfiling method, but also the time spent waiting for the external tasks to complete. The next section of this article will show how the ProfilingAttribute class can be improved to measure only the time spent inside the TestProfiling method.

Adding behaviors around the "await" statement

When applying the aspect to the async task itself, you can also inject behaviors before and after the await statements in that task. To achieve that, you need to override the OnYield(MethodExecutionArgs) and OnResume(MethodExecutionArgs) methods of the OnMethodBoundaryAspect class.

The OnYield(MethodExecutionArgs) method is invoked after the async task get paused and yields the control flow to the calling thread, as the result of the await statement.

The OnResume(MethodExecutionArgs) method is invoked when the async task resumes execution at the point after the await statement.

Note Note

Implementing OnYield(MethodExecutionArgs) or OnResume(MethodExecutionArgs) method also automatically sets the default value of ApplyToStateMachine property to true and suppresses the warning generated by PostSharp when the ApplyToStateMachine property is not set.

In the next steps we will add the OnYield and OnResume overrides to our ProfilingAttribute class. This will allow us to pause the stopwatch when the async method execution is paused, and to resume the stopwatch when the async method execution is resumed.

To inject behaviors around the "await" statement:

  1. Create an aspect class and inherit OnMethodBoundaryAspect. Annotate the class with the [PSerializableAttribute] custom attribute. For this example, we will reuse the ProfilingAttribute class from the previous section.

  2. Override the OnYield(MethodExecutionArgs) method to add functionality at the point where the async task is paused and yields the control flow to the calling thread.

    The following code snippet uses OnYield(MethodExecutionArgs) method to pause the stopwatch when the execution of the async method is paused.

    C#
    public override void OnYield( MethodExecutionArgs args )
    {
        Stopwatch sw = (Stopwatch) args.MethodExecutionTag;
        sw.Stop();
    }
  3. Override the OnResume(MethodExecutionArgs) method to add functionality at the point where the async task resumes its execution after the await statement. This method is invoked when another async task has completed and the control flow has returned to the original task right after the await statement.

    The following code snippet uses OnYield(MethodExecutionArgs) method to resume the stopwatch when the execution of the async method is resumed.

    C#
    public override void OnResume( MethodExecutionArgs args )
    {
        Stopwatch sw = (Stopwatch) args.MethodExecutionTag;
        sw.Start();
    }
  4. Apply your custom attribute to the target methods. Below you can see the [Profiling] attribute applied to the TestProfiling method.

    C#
    [Profiling]
    public async Task TestProfiling()
    {
        await Task.Delay( 1000 );
        Thread.Sleep( 1000 );
    }

During the code execution, the stopwatch will start upon entering the TestProfiling method. It will stop before the await statement and resume when the task awaiting is done. Finally, the time measuring is stopped again before exiting the TestProfiling method and the result is written to the console.

Method ProfilingTest executed for 1007ms.
See Also