PostSharp4.3/Custom Patterns/Developing Custom Aspects/Developing Simple Aspects/Injecting Behaviors Before and After Method Execution
Injecting Behaviors Before and After Method Execution

There are two ways to inject behaviors into methods. The first is the method decorator: it allows you to add instructions before and after method execution. The second is method interception: the hook gets invoked instead of the method. Decorators are faster than interceptors, but interceptors are more powerful. The current article covers decorators. For the other aspect, see Intercepting Methods.

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.

Injection points

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.

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

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

  2. To add functionality prior to the execution of the target method, override the method and code the functionality you desire.

    C#
    [PSerializable]
    public class LoggingAspect : OnMethodBoundaryAspect
    {
      public override void OnEntry(MethodExecutionArgs args)
      {
         Console.WriteLine("The {0} method has been entered.", args.Method.Name);
      }
    }
  3. Inject functionality immediately after the method executes by overriding the OnExit(MethodExecutionArgs) method.

    Note Note

    It's important to remember that the OnExit(MethodExecutionArgs) method will execute every time that the target method completes its execution regardless of if the target method completed successfully or threw an exception.

    C#
     public override void OnExit(MethodExecutionArgs args)
    {
      Console.WriteLine("The {0} method has exited", args.Method.Name);
    }
  4. To add functionality that only executes when the target method has completed successfully, you will override the OnSuccess(MethodExecutionArgs) method in your aspect. The OnSuccess(MethodExecutionArgs) method will be executed every time that the target method completes successfully. If the target method throws an exception OnSuccess(MethodExecutionArgs) will not execute.

    C#
     public override void OnSuccess(MethodExecutionArgs args)
    {
      Console.WriteLine("The {0} method executed successfully.", args.Method.Name);
    }
  5. The final location that you can intercept requires you to override the OnException(MethodExecutionArgs) method. As the name of the overrode method suggests, this is where you can inject functionality that should execute when the target method throws and exception.

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

The four methods (OnEntry(MethodExecutionArgs), OnExit(MethodExecutionArgs), OnSuccess(MethodExecutionArgs) and OnException(MethodExecutionArgs)) that you overrode are the locations that you are able to intercept method execution. Between these four location you are able to implement many different infrastructure patterns with minimal effort.

Accessing the method

As illustrated in the examples above, you can access information about the method being intercepted from the property 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.

Accessing parameters

It's rare that you will intercept method execution and not interact with the parameters that were passed to the target method. For example, when you implement method interception for logging you will probably want to log the parameter values that were passed to the target method.

Each of the interception locations that were outlined earlier has access to that information. If you look at the OnEntry(MethodExecutionArgs) method in your aspect you will see that it has a MethodExecutionArgs parameter. That parameter is used for OnExit(MethodExecutionArgs), OnSuccess(MethodExecutionArgs) and OnException(MethodExecutionArgs) as well. The collection Arguments gives access to parameter values.

Let's modify the OnEntry(MethodExecutionArgs) method and include the parameter values in the log message.

To include argument values to the logged text:

  1. Create a foreach loop to gather each of the parameter values in the Arguments property of the args parameter.

  2. In the loop concatenate the parameter values into a string.

  3. Pass that string of argument values to the logging tool.

    C#
     public override void OnEntry(MethodExecutionArgs args)
    {
      var argValues = new StringBuilder();
      foreach (var argument in args.Arguments)
      {
                argValues.Append(argument.ToString()).Append(",");
      }
    
      Console.WriteLine("The {0} method was entered with the parameter values: {1}",
                        args.Method.Name, argValues.ToString());
    }
    Note Note

    A production implementation of this aspect would need to take re-entrance into account. See the article Logging for a ready-made logging aspect.

It's also possible to modify the parameter values inside your aspect methods. All you need to do is modify the value of the item in the Arguments collection. Remember that all items in the Arguments collection are object types so you will need to be careful with how you change values. If the value you are modifying was originally a string, you will want to ensure it stays a string type. It's especially true that when you change the parameter type in the OnEntry(MethodExecutionArgs) method you may cause the system to be unable to execute the target method due to a parameter type mismatch.

Note Note

The only parameter types that you can modify are those defined as either out or ref. If you need to modify input arguments, you should use Intercepting Methods.

Accessing the target objects

In combination with the parameters you will probably interact with the target code instance that the aspect is attached to. The Instance property provides you with the instance of the object that the aspect is currently operating against. It is an object type so you will need to cast it to the correct type to be able to interact with it. If you debug your aspect and that aspect doesn't make use of Instance, it will be set to null. It's also set to null if the target code is defined as static.

Accessing the return value

Like target method parameters you also have access to the return value for those target methods. It's possible to both read the return value as well as modify it. The return value can be found at ReturnValue in all four of the aspect methods covered earlier.

C#
 public override void OnExit(MethodExecutionArgs args)
{
    args.ReturnValue = false;
}
Note Note

If a target method is defined as void, the ReturnValue property will be set to null. ReturnValue is an object type so you must be careful how you modify the return value with respect to the return value type of the target code.

Changing execution flow

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, you may want to exit the execution of the target code at some point in the OnEntry(MethodExecutionArgs) advice. PostSharp offers this ability through the use of FlowBehavior.

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

  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.

Note Note

Using flow control to exit the target code execution will return back to the code that called the target code. As a result, you need to be considerate to the target code's return value. In the example above, the target code will always return null. This may or may not be the behavior that you want. If it isn't, you can set the ReturnValue on the MethodExecutionArgs and that value will be returned from the target code.

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.

Rethrowing an existing exception

To rethrow an existing exception, you will set the FlowBehavior property to RethrowException. Whatever exception that was caught in the OnException(MethodExecutionArgs) advice will be rethrown to the code that is calling the target code.

C#
 public override void OnException(MethodExecutionArgs args)
{
    if (args.Exception.GetType() == typeof(DivideByZeroException))
    {
        args.FlowBehavior = FlowBehavior.RethrowException;
    }
}

Throwing a new exception

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 ExecutionDurationAspect : 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.

See Also