This article explains how to modernize an aspect written for an earlier version of PostSharp in two ways:
- Advice parameter binding. Aspects derived from OnMethodBoundaryAspect, MethodInterceptionAspect or LocationInterceptionAspect receive their context through an AdviceArgs object, such as MethodExecutionArgs. This object is allocated on the heap on each call, so it adds pressure on the garbage collector. It is also weakly typed: arguments and return values are exposed as object, so the aspect must cast them, and values of value types are boxed. With advice parameter binding, an advice instead declares the values it needs as typed parameters (see Advice Parameters).
- Dependency injection. Aspects that get their services from a global container or a service locator can now receive them from the constructors of the target class, which a dependency injection container calls.
Advice parameter binding
Why use bound parameters
Bound parameters have the following benefits:
- Performance. The woven code does not allocate an AdviceArgs object on each call, and it does not allocate an Arguments object unless the advice asks for all the arguments. Arguments and return values of value types are not boxed when the parameter has their type or is generic.
- Type safety. The advice receives typed values instead of object values that it must cast.
- Build-time validation. PostSharp checks the signature of the advice against each target. An argument that is missing or that has an incompatible type causes a build error, not an InvalidCastException or an ArgumentOutOfRangeException at run time. With MatchPointcut, the advice can instead skip the targets that do not match.
- Readable code. The signature of the advice shows which values the advice uses.
Keep the AdviceArgs signature in the following cases:
- The aspect intercepts events, validates locations or uses OnInvokeAsync(MethodInterceptionArgs). These advices do not accept bound parameters.
- The aspect reads the value yielded by an iterator, which only YieldValue gives.
- The aspect is not on a performance-sensitive path, and you prefer the simpler programming model of a simple aspect.
For the complete list, see the limitations.
How to convert an aspect to bound parameters
To convert a simple aspect to an aspect with bound parameters:
Change the base class of the aspect from the simple aspect class to the corresponding level aspect class:
- OnMethodBoundaryAspect and MethodInterceptionAspect become MethodLevelAspect.
- LocationInterceptionAspect becomes LocationLevelAspect.
The multicasting properties and the
CompileTimeValidate,CompileTimeInitializeandRuntimeInitializemethods are defined on these classes too, so they do not change.Change each overridden method into an advice method. Remove the
overridekeyword, and add the advice attribute that corresponds to the method, for instance OnMethodEntryAdvice forOnEntry. See the table of advice kinds.Do not add pointcuts. Advices without pointcut apply to the target of the aspect, and the advices of one family form one group, which is woven as a single
try/catch/finallyconstruct, like the simple aspect (see Developing Member-Level Aspects).Move the properties of the simple aspect that control the weaving to one of the advices: they apply to the whole group. For a method boundary aspect, these properties are SemanticallyAdvisedMethodKinds and UnsupportedTargetAction. A method interception aspect has the same two properties, which move to the OnMethodInvokeAdvice. Set each property on one advice only: when two advices of a group set it to different values, PostSharp reports the error LA0231.
Replace the AdviceArgs parameter of each advice with bound parameters, following the table below.
Build the project and fix the errors that PostSharp reports on the advices. For the list of messages, see Build messages.
Mapping AdviceArgs members to bound parameters
MethodExecutionArgs
| Member of MethodExecutionArgs | Bound parameter |
|---|---|
Arguments[i] |
[Argument( i )] T value, or ref to change the argument. The argument can also be selected by its name or its type. |
| Arguments | [Arguments] Arguments arguments. This allocates an object on each call. |
| Instance | [This] object instance, or the type of the target class. |
| Method | [Declaration] MethodBase method |
Method.Name |
[DeclarationName] string name |
| DeclarationIdentifier | [DeclarationIdentifier] DeclarationIdentifier identifier |
| MethodExecutionTag | [State( StateScope.MethodInvocation )] T state: out in the entry advice, by value in the other advices. |
| ReturnValue | [ReturnValue] T value: by value to read it, ref to replace it in the success and exit advices, out to set it in the entry and exception advices. |
| Exception | [Exception] Exception exception in the exception advice, or ref to replace it. |
| FlowBehavior | [FlowBehavior] out FlowBehavior flowBehavior in the entry, exception and resume advices. |
| YieldValue | No equivalent. |
For async methods, the method boundary advices also accept [AsyncCallId], [CurrentTask], [AwaitedTask], [AwaitedMethod] and [Awaiter]. These values are not available through MethodExecutionArgs.
MethodInterceptionArgs
| Member of MethodInterceptionArgs | Bound parameter |
|---|---|
| Proceed() | returnValue = (T) binding.Invoke( ref instance, arguments ), with the parameters [This] object instance, [Binding] IMethodBinding binding, [Arguments] Arguments arguments and [ReturnValue] out T returnValue. |
| Invoke(Arguments) | binding.Invoke( ref instance, otherArguments ) |
| Arguments | [Arguments] Arguments arguments, or [Argument( i )] T value to read one argument. |
| ReturnValue | [ReturnValue] out T returnValue |
| Method | [Declaration] MethodBase method |
| Binding | [Binding] IMethodBinding binding |
| ProceedAsync() | No equivalent. OnMethodInvokeAsyncAdvice does not accept bound parameters. |
LocationInterceptionArgs
In the following table, binding is a [Binding] ILocationBinding<T> parameter, and instance is a [This] object parameter. On an instance field or property of a struct, the instance parameter receives a boxed copy, so the value is stored in the copy and lost. Apply such an aspect only to classes and static members, or keep the LocationInterceptionArgs signature for structs.
| Member of LocationInterceptionArgs | Bound parameter |
|---|---|
Value |
[LocationValue] out T value in a get advice, [LocationValue] T value in a set advice. |
| ProceedGetValue() | value = binding.GetValue( instance ) |
| ProceedSetValue() | binding.SetValue( instance, value ) |
| GetCurrentValue() | binding.GetValue( instance ) |
| SetNewValue(object) | binding.SetValue( instance, newValue ) |
Location |
[Declaration] LocationInfo location |
LocationName |
[DeclarationName] string name |
| Binding | [Binding] ILocationBinding<T> binding |
| Index | No equivalent. |
Example of a method boundary aspect
The following aspect logs the execution of a method with an OnMethodBoundaryAspect. It reads the first argument and the return value as objects, and it stores a Stopwatch in MethodExecutionTag.
[PSerializable]
public sealed class LogAttribute : OnMethodBoundaryAspect
{
public override void OnEntry( MethodExecutionArgs args )
{
Console.WriteLine( "Entering {0}({1}).", args.Method.Name, args.Arguments[0] );
args.MethodExecutionTag = Stopwatch.StartNew();
}
public override void OnSuccess( MethodExecutionArgs args )
{
Console.WriteLine( "{0} returned {1}.", args.Method.Name, args.ReturnValue );
}
public override void OnException( MethodExecutionArgs args )
{
Console.WriteLine( "{0} failed: {1}", args.Method.Name, args.Exception.Message );
}
public override void OnExit( MethodExecutionArgs args )
{
var stopwatch = (Stopwatch) args.MethodExecutionTag;
Console.WriteLine( "{0} took {1} ms.", args.Method.Name, stopwatch.ElapsedMilliseconds );
}
}
The following code is the same aspect with bound parameters. The name of the method is a constant, the stopwatch is a typed variable shared by the advices, and the success advice is generic, so the return value is passed without boxing. The first argument is still boxed because the parameter is of type object. To avoid this, make the advice generic and bind the argument to a parameter of type T.
[PSerializable]
public sealed class LogAttribute : MethodLevelAspect
{
[OnMethodEntryAdvice]
public void OnEntry( [DeclarationName] string methodName,
[Argument( 0 )] object argument,
[State( StateScope.MethodInvocation )] out Stopwatch stopwatch )
{
Console.WriteLine( "Entering {0}({1}).", methodName, argument );
stopwatch = Stopwatch.StartNew();
}
[OnMethodSuccessAdvice]
public void OnSuccess<T>( [DeclarationName] string methodName, [ReturnValue] T returnValue )
{
Console.WriteLine( "{0} returned {1}.", methodName, returnValue );
}
[OnMethodExceptionAdvice]
public void OnException( [DeclarationName] string methodName, [Exception] Exception exception )
{
Console.WriteLine( "{0} failed: {1}", methodName, exception.Message );
}
[OnMethodExitAdvice]
public void OnExit( [DeclarationName] string methodName,
[State( StateScope.MethodInvocation )] Stopwatch stopwatch )
{
Console.WriteLine( "{0} took {1} ms.", methodName, stopwatch.ElapsedMilliseconds );
}
}
The original aspect could be applied to any method. The modernized aspect requires a method with at least one parameter: on a method without parameters, [Argument( 0 )] causes the error LA0225. On a method that returns void, the returnValue parameter receives the default value.
Example of a location interception aspect
The following aspect converts the value assigned to a string property of a class to uppercase, first with a LocationInterceptionAspect:
[PSerializable]
public sealed class UpperCaseAttribute : LocationInterceptionAspect
{
public override void OnSetValue( LocationInterceptionArgs args )
{
args.Value = ((string) args.Value)?.ToUpperInvariant();
args.ProceedSetValue();
}
}
And then with bound parameters:
[PSerializable]
public sealed class UpperCaseAttribute : LocationLevelAspect
{
[OnLocationSetValueAdvice]
public void OnSetValue( [This] object instance,
[LocationValue] string value,
[Binding] ILocationBinding<string> binding )
{
binding.SetValue( instance, value?.ToUpperInvariant() );
}
}
Differences in behavior
Check the following points after the conversion:
- An advice is checked against the signature of each target. A target that the original aspect accepted can cause an error, for instance when an argument or a return value has an incompatible type. Restrict the targets with multicasting or
CompileTimeValidate, or use MatchPointcut to skip them. [CurrentTask]isnulluntil the async method yields.- When an advice keeps its MethodExecutionArgs parameter and adds a
[ReturnValue],[FlowBehavior]or[Exception]parameter that writes the same value, the bound parameter takes precedence, and PostSharp reports a warning (LA0234, LA0243 or LA0248). - In a method interception advice, a value written to an
outorref[Argument]parameter does not reach the intercepted method (warning LA0235). Change the Arguments object passed to the binding instead. - Advices of a simple aspect are always grouped. In a member-level aspect, the advices of one family are grouped too. Advices of different families, such as a method boundary advice and a method interception advice, are separate transformations, and their order determines the generated code (see Ordering Advices).
Dependency injection
Why use dependency injection
PostSharp was designed when dependency injection was not yet mainstream in .NET. Aspects are instantiated at build time, and unless they implement IInstanceScopedAspect, one aspect instance serves all the objects of the target class: the fields of the aspect are statically scoped, with one aspect instance per target declaration. An instance-scoped aspect can read a dependency from the target object with ImportMemberAttribute (see Importing Dependencies from the Target Object), but it creates an aspect instance for each object, which is expensive. Other aspects get their dependencies from a global container or a service locator (see Consuming Dependencies from an Aspect).
Since then, dependency injection has become the standard way to build .NET applications. Modern containers, starting with Microsoft.Extensions.DependencyInjection, pass the dependencies of a class to its constructor. An aspect could import a member that the target class already had, but it could not make the constructor of the class receive a new dependency. For teams that moved to an architecture centered on dependency injection, aspects were therefore an obstacle.
PostSharp 2027.0 removes this obstacle: an aspect can now declare the services it needs, and PostSharp adds them as parameters to the constructors of the target class, so the class receives them from the container. The source code of the class does not mention the service, and the aspect does not need to be instance-scoped. Objects created with new get the service from RunTimeServiceLocator.
How to move an aspect to dependency injection
Dependencies reach the advices through bound parameters, so first convert the aspect to advice parameter binding, as described above. Then:
Implement the IAdviceProvider interface in the aspect, and return an ImportDependencyAdviceInstance for each service from the ProvideAdvices(object) method.
In the advices that use a service, add a parameter of the service type annotated with ImportMemberAttribute, and remove the calls to the global container or the service locator, for instance from the
RuntimeInitializemethod.If the aspect implements IInstanceScopedAspect only to hold its dependencies, make it statically scoped.
Register the services in the container. For the objects that the code creates with
new, also register them in RunTimeServiceLocator.
A dependency request adds parameters to the public constructors of the target class, which is a binary breaking change for the assemblies that use the class. For the complete description, see Receiving Dependencies from the Constructors of the Target Class.
See Also
Reference
AdviceParameterAttribute
MethodLevelAspect
LocationLevelAspect
ImportDependencyAdviceInstance
Other Resources
Advice Parameters
Advice Methods
Developing Simple Aspects
Receiving Dependencies from the Constructors of the Target Class