uid | title | product | categories | summary |
---|---|---|---|---|
method-decorator |
Injecting Behaviors Before and After Method Execution |
postsharp |
PostSharp;AOP;Metaprogramming |
The document provides an in-depth guide on how to use the PostSharp.Aspects.OnMethodBoundaryAspect aspect in PostSharp for injecting behaviors before and after method execution. It covers various scenarios including handling exceptions, sharing state between advices, working with async and iterator methods. |
The xref:PostSharp.Aspects.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 xref:PostSharp.Aspects.OnMethodBoundaryAspect.
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 xref:PostSharp.Aspects.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 xref:PostSharp.Aspects.OnMethodBoundaryAspect class (see below for more advice methods available on async
and iterator methods).
Advice | Description |
---|---|
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnEntry(PostSharp.Aspects.MethodExecutionArgs) | When the method execution starts, before any user code. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnSuccess(PostSharp.Aspects.MethodExecutionArgs) | When the method execution succeeds (i.e. returns without an exception), after any user code. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.MethodExecutionArgs) | When the method execution fails with an exception, after any user code. It is equivalent to a catch block. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnExit(PostSharp.Aspects.MethodExecutionArgs) | When the method execution exits, whether successfully or with an exception. This advice runs after any user code and after the xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnSuccess(PostSharp.Aspects.MethodExecutionArgs) or xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.MethodExecutionArgs) method of the current aspect. It is equivalent to a finally block. |
-
Add a reference to the PostSharp package to your project.
-
Create an aspect class and inherit xref:PostSharp.Aspects.OnMethodBoundaryAspect.
-
Annotate the class with the [xref:PostSharp.Serialization.PSerializableAttribute] custom attribute.
-
Override the xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnEntry(PostSharp.Aspects.MethodExecutionArgs), xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnSuccess(PostSharp.Aspects.MethodExecutionArgs), xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.MethodExecutionArgs) and/or xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnExit(PostSharp.Aspects.MethodExecutionArgs) as needed, and code the logic that needs to be executed at these points.
-
Add the aspect to one or more methods. Since xref:PostSharp.Aspects.OnMethodBoundaryAspect derives from the xref:System.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 xref:applying-aspects.
The following code snippet shows a simple aspect based on xref:PostSharp.Aspects.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.
As illustrated in the example above, you can access information about the method being intercepted from the property xref:PostSharp.Aspects.MethodExecutionArgs.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, xref:PostSharp.Aspects.MethodExecutionArgs.Method gives you the proper generic method instance, so you can use this object to get generic parameters.
The xref:PostSharp.Aspects.MethodExecutionArgs object contains more information about the current execution context, as illustrated in the following table:
Property | Description |
---|---|
xref:PostSharp.Aspects.MethodExecutionArgs.Method | The method or constructor being executed (in case of generic methods, this property is set to the proper generic instance of the method). |
xref:PostSharp.Aspects.MethodExecutionArgs.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 xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnSuccess(PostSharp.Aspects.MethodExecutionArgs) or xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnExit(PostSharp.Aspects.MethodExecutionArgs) advices. |
xref:PostSharp.Aspects.AdviceArgs.Instance | The object on which the method is being executed, i.e. the value of the this keyword. |
xref:PostSharp.Aspects.MethodExecutionArgs.ReturnValue | The return value of the method. This property can be modified by the aspect. |
xref:PostSharp.Aspects.MethodExecutionArgs.Exception | The xref:System.Exception thrown by the method. This value can be modified (see below). |
Note
The properties of the xref:PostSharp.Aspects.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.
The following program illustrates how to consume the current context from the xref:PostSharp.Aspects.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.
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 xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnEntry(PostSharp.Aspects.MethodExecutionArgs) advice may want to prevent the target method from being executed. PostSharp offers this ability through the use of the xref:PostSharp.Aspects.MethodExecutionArgs.FlowBehavior property. Unless the target method is void
or is an iterator method, you will also need to set the xref:PostSharp.Aspects.MethodExecutionArgs.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 xref:PostSharp.Aspects.MethodExecutionArgs.FlowBehavior property on the xref:PostSharp.Aspects.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.
When you implement the xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.MethodExecutionArgs) class, PostSharp will generate a try/catch
block and invoke xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.MethodExecutionArgs) from the catch
block.
The default behavior of the xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.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 xref:exception-handling.
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 xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnEntry(PostSharp.Aspects.MethodExecutionArgs) and share that with xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnExit(PostSharp.Aspects.MethodExecutionArgs) to calculate the duration of the call.
To do this we use the xref:PostSharp.Aspects.MethodExecutionArgs.MethodExecutionTag property on the xref:PostSharp.Aspects.MethodExecutionArgs parameter in each of the advices. Because xref:PostSharp.Aspects.MethodExecutionArgs.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 xref:PostSharp.Aspects.MethodExecutionArgs.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 xref:PostSharp.Aspects.MethodExecutionArgs.MethodExecutionTag for state storage.
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 xref:System.Threading.Tasks.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 xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnYield(PostSharp.Aspects.MethodExecutionArgs) and xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnResume(PostSharp.Aspects.MethodExecutionArgs) methods of the xref:PostSharp.Aspects.OnMethodBoundaryAspect class.
The following table shows the advices that are specific to async
methods (and iterator methods).
Advice | Description |
---|---|
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnYield(PostSharp.Aspects.MethodExecutionArgs) | When the method execution being suspended, i.e. when the operand of the await operator is a task that has not yet completed. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnResume(PostSharp.Aspects.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 xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnYield(PostSharp.Aspects.MethodExecutionArgs) and xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnResume(PostSharp.Aspects.MethodExecutionArgs) methods are not invoked.
See xref:semantic-advising for more details regarding the behavior xref:PostSharp.Aspects.OnMethodBoundaryAspect aspect on asynchronous methods.
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.
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 xref:System.Collections.Generic.IEnumerable`1 and xref:System.Collections.Generic.IEnumerator`1 interfaces. Calling the xref:System.Collections.IEnumerator.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 xref:System.Collections.IEnumerator.MoveNext.
Just like with async
methods, you can use the xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnYield(PostSharp.Aspects.MethodExecutionArgs) and xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnResume(PostSharp.Aspects.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 xref:PostSharp.Aspects.OnMethodBoundaryAspect aspects in the context of iterator methods.
Advice | Description |
---|---|
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnEntry(PostSharp.Aspects.MethodExecutionArgs) | When the method execution starts, i.e. during the first call of the xref:System.Collections.IEnumerator.MoveNext method, before any user code. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnYield(PostSharp.Aspects.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 xref:System.Collections.IEnumerator.Current property) can be read from the xref:PostSharp.Aspects.MethodExecutionArgs.YieldValue property of the xref:PostSharp.Aspects.MethodExecutionArgs parameter. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnResume(PostSharp.Aspects.MethodExecutionArgs) | When the method execution being resumed by a call to xref:System.Collections.IEnumerator.MoveNext. Note, however, that the first call to xref:System.Collections.IEnumerator.MoveNext results in a call to xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnEntry(PostSharp.Aspects.MethodExecutionArgs). |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnSuccess(PostSharp.Aspects.MethodExecutionArgs) | When the iterator method execution succeeds (i.e. returns without an exception, causing the xref:System.Collections.IEnumerator.MoveNext method to return false ) or is interrupted (by disposing of the enumerator), after any user code. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.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. |
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnExit(PostSharp.Aspects.MethodExecutionArgs) | When the iterator method execution successfully (when xref:System.Collections.IEnumerator.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 xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnSuccess(PostSharp.Aspects.MethodExecutionArgs) or xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.MethodExecutionArgs) method of the current aspect. It is equivalent to a finally block around the whole iterator. |
See xref:semantic-advising for more details regarding the behavior xref:PostSharp.Aspects.OnMethodBoundaryAspect aspect on iterator methods.
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
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 |
---|---|
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnYield(PostSharp.Aspects.MethodExecutionArgs) | At the end of the returned enumerator's xref:System.Collections.IEnumerator.MoveNext method |
You can also enable this behavior for other methods with the return type xref:System.Collections.Generic.IEnumerable`1 or xref:System.Collections.Generic.IEnumerator`1 using semantic advising. See xref:semantic-advising for more 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 xref:System.Collections.Generic.IEnumerable`1, 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;
}
Reference
xref:PostSharp.Aspects.OnMethodBoundaryAspect
Other Resources
PostSharp Aspect Framework - Product Page
xref:PostSharp.Aspects.Arguments
xref:PostSharp.Aspects.MethodExecutionArgs.MethodExecutionTag
xref:PostSharp.Serialization.PSerializableAttribute
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnExit(PostSharp.Aspects.MethodExecutionArgs)
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnSuccess(PostSharp.Aspects.MethodExecutionArgs)
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnException(PostSharp.Aspects.MethodExecutionArgs)
xref:PostSharp.Aspects.OnMethodBoundaryAspect.OnEntry(PostSharp.Aspects.MethodExecutionArgs)
xref:PostSharp.Aspects.MethodExecutionArgs.Method
xref:PostSharp.Aspects.MethodExecutionArgs.Arguments