PostSharpDeveloping Custom AspectsDeveloping Simple AspectsInjecting Behaviors Before and After Method Execution
Open sandboxFocusImprove this doc

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.

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.

[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 Method, which gives you a reflection object <xref:System.Reflection.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 implementation of the OnSuccess(MethodExecutionArgs) or OnExit(MethodExecutionArgs) advices.
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

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 MethodExecutionArgs parameter:

[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) returned 3.

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 FlowBehavior property. Unless the target method is void or is an iterator method, you will also need to set the ReturnValue property.

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.

Handling exceptions

When you implement the OnException(MethodExecutionArgs) class, PostSharp will generate a try/catch block and invoke OnException(MethodExecutionArgs) from the catch block.

The default behavior of the OnException(MethodExecutionArgs) advice is to rethrow the exception after the execution of the advice. You can also choose to ignore the exception, to replace it with another. For details, see Handling Exceptions.

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.

[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

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 (and iterator 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

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.

[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.

[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 <xref:System.Collections.Generic.IEnumerator`1> 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 code.
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 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 in 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.

[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

Working with other enumerable methods

The behavior described above is also true when there is a method interception aspect ordered after the method boundary aspect. In that case, the method interception aspect could return any enumerable object, not just the iterator from the target iterator method. This means that the OnYield advice behaves differently:

Advice Description
OnYield(MethodExecutionArgs) At the end of the returned enumerator's MoveNext() method

You can also enable this behavior for other methods with the return type IEnumerable<T> or <xref:System.Collections.Generic.IEnumerator`1> using semantic advising. See Semantic Advising of Iterator and Async Methods for more details.

Technical details

When there is a non-semantically-advised method boundary aspect or a method interception aspect ordered after a semantically-advised method boundary aspect on an iterator method, or when a semantically-advised method boundary aspect is applied to a method that returns IEnumerable<T>, <xref:System.Collections.Generic.IEnumerator`1> or their non-generic variants, PostSharp transforms the target method equivalently to this example:

Suppose the original target method is:

[MySemanticBoundaryAspect, MyInterceptionAspect] // in that order
static IEnumerable<int> Return2()
{
    yield return 2;
}

Then that piece of code is equivalent to this code:

[MySemanticBoundaryAspect]
static IEnumerable<int> Return2()
{
    var innerEnumerable = Return2__OriginalMethod();
    var innerEnumerator = innerEnumerable?.GetEnumerator();
    if (innerEnumerator == null) 
    {
        yield break;
    }
    foreach(var element in innerEnumerator)
    {
        yield return element;
    }
}
[MyInterceptionAspect]
static IEnumerable<int> Return2__OriginalMethod()
{
    yield return 2;
}

See Also

Reference

OnMethodBoundaryAspect
Other Resources

PostSharp Aspect Framework - Product Page
Arguments
MethodExecutionTag
PSerializableAttribute
OnExit(MethodExecutionArgs)
OnSuccess(MethodExecutionArgs)
OnException(MethodExecutionArgs)
OnEntry(MethodExecutionArgs)
Method
Arguments