Post­Sharp Documentation / Developing Custom Aspects / Developing Simple Aspects / Injecting Behaviors Before and After Method Execution

Injecting Behaviors Before and After Method Execution

The OnMethodBoundaryAspect aspect implements the so-called decorator pattern: it allows you to execute logic before and after the execution of a target method.

You may want to use method decorators to perform logging, monitor performance, initialize database transactions or any one of many other infrastructure related tasks. PostSharp provides you with an easy to use framework for all of these tasks in the form of the OnMethodBoundaryAspect.

This topic contains the following sections:

Executing code before and after method execution

When you are decorating methods, there are different locations that you may wish to inject functionality to. You may want to perform a task prior to the method executing or just before it finishes execution. There are situations where you may want to inject functionality only when the method has successfully executed or when it has thrown an exception. All of these injection points are structured and available to you in the OnMethodBoundaryAspect class as virtual methods (called advices) that you can implement if you need them.

The following table shows the advice methods available in the OnMethodBoundaryAspect class (see below for more advice methods available on async and iterator methods).

Advice

Description

OnEntry(MethodExecutionArgs)

When the method execution starts, before any user code.

OnSuccess(MethodExecutionArgs)

When the method execution succeeds (i.e. returns without an exception), after any user code.

OnException(MethodExecutionArgs)

When the method execution fails with an exception, after any user code. It is equivalent to a catch block.

OnExit(MethodExecutionArgs)

When the method execution exits, whether successfully or with an exception. This advice runs after any user code and after the OnSuccess(MethodExecutionArgs) or OnException(MethodExecutionArgs) method of the current aspect. It is equivalent to a finally block.

To create a simple aspect that writes some text whenever a method enters, succeeds, or fails:

  1. Add a reference to the PostSharp package to your project.

  2. Create an aspect class and inherit OnMethodBoundaryAspect.

  3. Annotate the class with the [PSerializableAttribute] custom attribute.

  4. Override the OnEntry(MethodExecutionArgs), OnSuccess(MethodExecutionArgs), OnException(MethodExecutionArgs) and/or OnExit(MethodExecutionArgs) as needed, and code the logic that needs to be executed at these points.

  5. Add the aspect to one or more methods. Since OnMethodBoundaryAspect derives from the Attribute class, you can just add the aspect custom attribute to the methods you need. If you need to add the aspect to more methods (for instance all public methods in a namespace), you can learn about more advanced techniques in Adding Aspects to Code.

Example

The following code snippet shows a simple aspect based on OnMethodBoundaryAspect which writes a line to the console during each of the four events. The aspect is applied to the Program.Main method.

C#
[PSerializable]
public class LoggingAspect : OnMethodBoundaryAspect
{

  public override void OnEntry(MethodExecutionArgs args)
  {
     Console.WriteLine("The {0} method has been entered.", args.Method.Name);
  }

  public override void OnSuccess(MethodExecutionArgs args)
  {
      Console.WriteLine("The {0} method executed successfully.", args.Method.Name);
  }

  public override void OnExit(MethodExecutionArgs args)
  {
     Console.WriteLine("The {0} method has exited.", args.Method.Name);
  }     

  public override void OnException(MethodExecutionArgs args)
  {
      Console.WriteLine("An exception was thrown in {0}.", args.Method.Name);
  }

}

static class Program
{
   [LoggingAspect]
   static void Main()
   {
     Console.WriteLine("Hello, world.");
   }
}

Executing the program prints the following lines to the console:

The Main method has been entered.
Hello, world.
The Main method executed successfully.
The Main method has exited.
Accessing the current execution context

As illustrated in the example above, you can access information about the method being intercepted from the property MethodExecutionArgs.Method, which gives you a reflection object MethodBase. This object gives you access to parameters, return type, declaring type, and other characteristics. In case of generic methods or generic types, Method gives you the proper generic method instance, so you can use this object to get generic parameters.

The MethodExecutionArgs object contains more information about the current execution context, as illustrated in the following table:

Property

Description

Method

The method or constructor being executed (in case of generic methods, this property is set to the proper generic instance of the method).

Arguments

The arguments passed to the method. In case of out and ref arguments, the argument values can be modified by the aspect.

Instance

The object on which the method is being executed, i.e. the value of the this keyword.

ReturnValue

The return value of the method. This property can be modified by the aspect.

Exception

The Exception thrown by the method. This value can be modified (see below).

Note Note

The properties of the MethodExecutionArgs class cannot be directly viewed in the debugger. Because of optimizations, the properties must be referenced in your source code in order to be viewable in the debugger.

Example

The following program illustrates how to consume the current context from the MethodExecutionArg parameter:

C#
[PSerializable]
public class LoggingAspect : OnMethodBoundaryAspect
{

  public override void OnEntry(MethodExecutionArgs args)
  {
     Console.WriteLine("Method {0}({1}) started.", args.Method.Name, string.Join( ", ", args.Arguments ) );
  }

  public override void OnSuccess(MethodExecutionArgs args)
  {
      Console.WriteLine("Method {0}({1}) returned {2}.", args.Method.Name, string.Join( ", ", args.Arguments ), args.ReturnValue );
  }


}

static class Program
{
   static void Main()
   {
     Foo( 1, 2 );
   }

   static int Foo(int a, int b)
   {
       Console.WriteLine("Hello, world.");
       return 3;
   }
}

When this program executes, it prints the following output:

Method Foo(1, 2) started.
Hello, world.
Method Foo(1, 2) return 3.
Changing execution flow

There are several ways you can affect the execution of a method using the OnMethodBoundaryAspect aspect:

This section contains the following subsections:

Returning without executing the method

When your aspect is interacting with the target code, there are situations where you will need to alter the execution flow behavior. For example, your OnEntry(MethodExecutionArgs) advice may want to prevent the target method from being executed. PostSharp offers this ability through the use of the MethodExecutionArgs.FlowBehavior property. Unless the target method is void, you will also need to set the MethodExecutionArgs.ReturnValue property.

C#
public override void OnEntry(MethodExecutionArgs args)
{
    if (args.Arguments.Count > 0 && args.Arguments[0] == null)
    {
        args.FlowBehavior = FlowBehavior.Return;
        args.ReturnValue = -1;
    }

  Console.WriteLine("The {0} method was entered with the parameter values: {1}",
                    args.Method.Name, argValues.ToString());
}

As you can see, all that is needed to exit the execution of the target code is setting the FlowBehavior property on the MethodExecutionArgs to Return.

Managing execution flow control when dealing with exceptions there are two primary situations that you need to consider: re-throwing the exception and throwing a new exception.

Throwing a different exception than the caught one

To throw a new exception you will have to perform two tasks. First, you will need to assign the new exception to the Exception property. This is the exception that will be thrown as part of the flow behavior. After that, you will need to set the FlowBehavior property to ThrowException.

C#
public override void OnException(MethodExecutionArgs args)
{
    if (args.Exception.GetType() == typeof(IndexOutOfRangeException))
    {
        args.Exception = new CustomArrayIndexException("This was thrown from an aspect",
                                                        args.Exception);
        args.FlowBehavior = FlowBehavior.ThrowException;
    }
}
Note Note

The remaining FlowBehavior enumeration value is Continue. In OnException(MethodExecutionArgs), this behavior will not rethrow the caught exception. In OnEntry(MethodExecutionArgs), OnSuccess(MethodExecutionArgs) and OnExit(MethodExecutionArgs) the target code execution will continue with no interruption.

Note Note

The default FlowBehavior value for OnEntry(MethodExecutionArgs), OnSuccess(MethodExecutionArgs) and OnExit(MethodExecutionArgs) is Continue. For OnException(MethodExecutionArgs) the default value is RethrowException.

Sharing state between advices

When you are working with multiple advices on a single aspect, you will encounter the need to share state between these advices. For example, if you have created an aspect that times the execution of a method, you will need to track the starting time at OnEntry(MethodExecutionArgs) and share that with OnExit(MethodExecutionArgs) to calculate the duration of the call.

To do this we use the MethodExecutionTag property on the MethodExecutionArgs parameter in each of the advices. Because MethodExecutionTag is an object type, you will need to cast the value stored in it while retrieving it and before using it.

C#
[PSerializable]
public class ProfilingAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        args.MethodExecutionTag = Stopwatch.StartNew();
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        var sw = (Stopwatch)args.MethodExecutionTag;
        sw.Stop();

        System.Diagnostics.Debug.WriteLine("{0} executed in {1} seconds", args.Method.Name,
                                           sw.ElapsedMilliseconds / 1000);
    }
}
Note Note

The value stored in MethodExecutionTag will not be shared between different instances of the aspect. If the aspect is attached to two different pieces of target code, each attachment will have its own unshared MethodExecutionTag for state storage.

Working with async methods

The specificity of async methods is that their execution can be suspended while they are awaiting a dependent operation (typically another async method or a Task). While an async method is suspended, it does not block any thread. When the dependent operation has completed, the execution of the async method can be resumed, possibly on a different thread than the one the method was previously executing on.

There are many situations in which you may want to execute some logic when an async method is being suspended or resumed. For instance, a profiling aspect may exclude the time when the method is waiting for a dependency. You can achieve this by overriding the OnYield(MethodExecutionArgs) and OnResume(MethodExecutionArgs) methods of the OnMethodBoundaryAspect class.

The following table shows the advices that are specific to async methods.

Advice

Description

OnYield(MethodExecutionArgs)

When the method execution being suspended, i.e. when the operand of the await operator is a task that has not yet completed.

OnResume(MethodExecutionArgs)

When the method execution being resumed, i.e. when the operand of the await operator has completed.

Note Note

When the operand of the await is a task that has already completed, the OnYield(MethodExecutionArgs) and OnResume(MethodExecutionArgs) methods are not invoked.

See Semantic Advising of Iterator and Async Methods for more details regarding the behavior OnMethodBoundaryAspect aspect on asynchronous methods.

Example

In this example, we will reuse the ProfilingAttribute class from the previous section and will extend it to exclude the time spent while waiting for dependent operations.

C#
[PSerializable]
public class ProfilingAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
      args.MethodExecutionTag = Stopwatch.StartNew();
    }

    public override void OnExit(MethodExecutionArgs args)
    {
      var sw = (Stopwatch)args.MethodExecutionTag;
      sw.Stop();

      System.Diagnostics.Debug.WriteLine("{0} executed in {1} seconds", args.Method.Name,
                                         sw.ElapsedMilliseconds / 1000);
    }

    public override void OnYield( MethodExecutionArgs args )
    {
      Stopwatch sw = (Stopwatch) args.MethodExecutionTag;
      sw.Stop();
    }

    public override void OnResume( MethodExecutionArgs args )
    {
        Stopwatch sw = (Stopwatch) args.MethodExecutionTag;
        sw.Start();
    }

}

Let's apply the [Profiling] attribute to the TestProfiling method.

C#
[Profiling]
public async Task TestProfiling()
{
    await Task.Delay( 3000 );
    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.
Working with iterator methods

Iterator methods are methods that contain the yield keyword. Under the hood, the C# or VB compiler transforms the iterator method into a state machine class that implements the IEnumerable<T> and IEnumerator<T> interfaces. Calling the MoveNext() method causes the method to execute until the next yield keyword. The keyword causes the method execution to be suspended, and it is resumed by the next call to MoveNext().

Just like with async methods, you can use the OnYield(MethodExecutionArgs) and OnResume(MethodExecutionArgs) methods to inject behaviors when an iterator method is suspended or resumed.

The following table explains the behavior of the different advices of the OnMethodBoundaryAspect aspects in the context of iterator methods.

Advice

Description

OnEntry(MethodExecutionArgs)

When the method execution starts, i.e. during the first call of the MoveNext() method, before any user call.

OnYield(MethodExecutionArgs)

When the iterator method emits a result using the yield return statement (and the iterator execution is therefore suspended). The operand of the yield return statement (i.e. the value of the IEnumerator.Current property) can be read from the YieldValue property of the MethodExecutionArgs parameter.

OnResume(MethodExecutionArgs)

When the method execution being resumed by a call to MoveNext(). Note, however, that the first call to MoveNext() results into a call to OnEntry(MethodExecutionArgs).

OnSuccess(MethodExecutionArgs)

When the iterator method execution succeeds (i.e. returns without an exception, causing the MoveNext() method to return false) or is interrupted (by disposing of the enumerator), after any user code.

OnException(MethodExecutionArgs)

When the method execution fails with an exception, after any user code. It is equivalent to a catch block around the whole iterator method.

OnExit(MethodExecutionArgs)

When the iterator method execution successfully (when MoveNext() method returns false), is interrupted (by disposing of the enumerator) or fails with an exception. This advice runs after any user code and after the OnSuccess(MethodExecutionArgs) or OnException(MethodExecutionArgs) method of the current aspect. It is equivalent to a finally block around the whole iterator.

See Semantic Advising of Iterator and Async Methods for more details regarding the behavior OnMethodBoundaryAspect aspect on iterator methods.

Example

The following program illustrates the timing of different advices in the context of an iterator.

C#
[PSerializable]
class MyAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        Console.WriteLine("! entry");
    }

    public override void OnResume(MethodExecutionArgs args)
    {
        Console.WriteLine("! resume");
    }
    public override void OnYield(MethodExecutionArgs args)
    {
        Console.WriteLine($"! yield return {args.YieldValue}");
    }

    public override void OnSuccess(MethodExecutionArgs args)
    {
        Console.WriteLine("! success");
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        Console.WriteLine("! exit");
    }
}

class Program
{
    static void Main(string[] args)
    {
        foreach (var i in Foo())
        {
            Console.WriteLine($"# received {i}");
        }

        Console.WriteLine("# done");
    }

    [MyAspect]
    static IEnumerable<int> Foo()
    {
        Console.WriteLine("@ part 1");
        yield return 1;
        Console.WriteLine("@ part 2");
        yield return 2;
        Console.WriteLine("@ part 3");
        yield return 3;
        Console.WriteLine("@ part 4");
    }
}

Executing the program prints the following output:

! entry
@ part 1
! yield return 1
# received 1
! resume
@ part 2
! yield return 2
# received 2
! resume
@ part 3
! yield return 3
# received 3
! resume
@ part 4
! success
! exit
# done
See Also