[Home](../) › [Blog](../blog) › Your Aspects, Faster: Advice Parameter Binding in PostSharp 2027.0

# Your Aspects, Faster: Advice Parameter Binding in PostSharp 2027.0

> PostSharp 2027.0 makes advice parameter binding public: advice methods declare the values they need as typed parameters, the woven code allocates nothing per call, and mismatches are caught at build time.

Published: 2026-10-23 · Author: Gaël Fraiteur · Canonical: https://postsharp.net/blog/postsharp-advice-parameter-binding

Every `OnEntry( MethodExecutionArgs args )` you have ever written pays three costs on every call: it allocates an object on the heap, it boxes the arguments and the return value into `object`, and it makes you cast them back. Our own Pattern Libraries stopped paying those costs a long time ago, because they use a mechanism we kept internal: *advice parameter binding*. With PostSharp 2027.0, this mechanism is public, documented and checked at build time.

## What MethodExecutionArgs costs you

Here is a typical logging aspect written as a simple aspect. It logs the first argument, the return value, the exception and the duration of the method:

```csharp
[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 );
    }
}
```

It works, and it has worked for many years. But look at what it asks of the runtime:

- **One allocation per call.** `MethodExecutionArgs` lives on the heap, and so does the `Arguments` collection.
- **Boxing.** The `Arguments` collection stores the arguments with their own types, but exposes them as `object`, so reading an `int` through `args.Arguments[0]` boxes it. `args.ReturnValue` is an `object` too, so an `int` return value is boxed as well.
- **Casts.** `MethodExecutionTag` is an `object` too, so the stopwatch must be cast back, and a wrong cast fails at run time.

On a method called a few times per request, nobody notices. On a hot path, the aspect becomes the most expensive part of the method.

## The same aspect with bound parameters

With advice parameter binding, the aspect derives from `MethodLevelAspect`, and each advice method declares only the values it needs, as typed parameters. A *binding attribute* on each parameter says where the value comes from:

```csharp
[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 bindings in this example:

- **`[DeclarationName]`** gives the name of the method. It is a constant, computed at build time, so there is no reflection call at run time.
- **`[State]`** declares a typed variable shared by the advices for one execution of the method. The entry advice assigns it with `out`, and the exit advice reads it. There is no `MethodExecutionTag` and no cast.
- **`[ReturnValue]`** on a generic parameter `T` receives the return value without boxing. PostSharp infers `T` for each target method.
- **`[Argument( 0 )]`** reads the first argument. Here it is still boxed, because the parameter is `object`. Make the advice generic to avoid it, as shown below.
- **`[Exception]`** gives the exception, typed as `Exception`.

The aspect is applied exactly as before, with `[Log]` on a method or with multicasting.

## What PostSharp actually generates

Bound parameters are a build-time contract, so the woven code passes the values directly. Take a smaller profiling aspect, with an entry advice that starts a `[State]` stopwatch and an exit advice that prints the `[DeclarationName]` and the duration (see [Aspect Development Concepts](https://doc.postsharp.net/postsharp/preview/framework/aspects/member-level-aspects/advices)). PostSharp transforms the target method into code equivalent to this:

```csharp
public void PlaceOrder( Order order )
{
    Stopwatch stopwatch;
    aspect.OnEntry( out stopwatch );
    try
    {
        // Method body.
    }
    finally
    {
        aspect.OnExit( "ProfileAspect.OrderService.PlaceOrder", stopwatch );
    }
}
```

The state is a local variable, the method name is a string literal, and the advices are plain method calls. Apart from the `Stopwatch` itself, the woven code allocates nothing.

## Generic advices, inferred for each target

An advice method can be generic. PostSharp infers the type arguments separately for each target method, with rules similar to the type inference of the C# compiler, and then calls a different instantiation of the advice from each target.

Take the `OnSuccess<T>` advice above. On `int Square( int value )`, PostSharp calls `OnSuccess<int>`. On `string Format( Order order )`, it calls `OnSuccess<string>`. The same source method serves every signature, and an `int` stays an `int` all the way.

Inference also works through constructed types and constraints:

```csharp
[OnMethodEntryAdvice]
public void OnEntry<T>( [DeclarationName] string methodName,
                        [Argument( 0 )] IEnumerable<T> items )
    where T : IComparable<T>
{
    Console.WriteLine( "{0} received {1} items, the first is {2}.",
                       methodName, items.Count(), items.FirstOrDefault() );
}
```

Applied to a method that takes a `List<int>`, this advice gets `T = int`: PostSharp matches `IEnumerable<T>` against the type of the argument, its base types and its interfaces, as C# would. The rules, in short:

- **No boxing.** A value of a value type is passed as it is, and a `ref`, `out` or `in` parameter receives the location itself.
- **Common type.** When several bound values give a type argument, it is the type to which all of them can be assigned. `OnEntry<T>( [Argument( 0 )] T a, [Argument( 1 )] T b )` on `M( string, object )` gives `T = object`.
- **Constructed types.** `T[]`, `T?`, `IList<T>` and the like are matched against the value's type, its base types and its interfaces.
- **Constraints are checked.** Including `unmanaged`, `new()` and constraints that refer to other type parameters.

When a type argument cannot be inferred for a target, or violates a constraint, the build fails with the error LA0220, which states the reason. Apply the advice above to a method that takes a `List<object>`, and you learn at build time that `object` does not implement `IComparable<object>`.

The type of a parameter can also use the type parameters of a generic aspect class. They are replaced by the type arguments of the aspect instance. See [Generic advice methods](https://doc.postsharp.net/postsharp/preview/framework/aspects/member-level-aspects/advice-parameter-binding#generic-advice-methods) for the complete rules.

## Errors at build time, not in production

A simple aspect accepts any method, then fails at run time if it makes a wrong assumption, for instance by reading `args.Arguments[0]` on a method that has no parameters.

With bound parameters, PostSharp checks each advice against each target when it weaves the aspect. An advice that does not fit a target is a build error with a message code. Apply the `LogAttribute` above to a method without parameters, and `[Argument( 0 )]` causes the error LA0225: the target method has no parameter at this index. A parameter of the wrong type, a binding used in an advice that does not support it, or a type argument that cannot be inferred all have their own codes. PostSharp reports all the errors of an aspect in one build.

The [complete list of build messages](https://doc.postsharp.net/postsharp/preview/framework/aspects/member-level-aspects/advice-parameter-binding#build-messages) is in the reference.

## Beyond the basics

The logging example uses five bindings. The [Advice Parameter Reference](https://doc.postsharp.net/postsharp/preview/framework/aspects/member-level-aspects/advice-parameter-binding) lists them all. Among them:

- **Write access with `ref` and `out`.** Replace an argument, set or replace the return value, or replace the exception.
- **`[FlowBehavior]`** to return without executing the method, or to swallow an exception.
- **Async methods.** `[CurrentTask]` gives the task that the caller observes, including for `ValueTask` methods, and `[AsyncCallId]` identifies one execution of the async method.
- **Interception.** `[Binding]` gives an object that invokes the original method, field or property.
- **`[This]`, `[Declaration]` and `[DeclarationIdentifier]`** for the instance and the reflection object of the target.

Grouping also got simpler. `[SelfPointcut]` and the `Master` property are optional when the grouping of the advices is unambiguous, so an aspect that applies to a single method, field, property or event needs neither.

## Should you modernize your aspects?

Not necessarily. Simple aspects remain fully supported, and you can keep the `AdviceArgs` signature in the following cases:

- The aspect intercepts events, validates fields or parameters, or uses `OnInvokeAsync`. These advices do not accept bound parameters yet.
- The aspect reads the value yielded by an iterator, which only `MethodExecutionArgs.YieldValue` gives.
- The aspect is not on a performance-sensitive path, and you prefer the simpler programming model.

The other [limitations](https://doc.postsharp.net/postsharp/preview/framework/aspects/member-level-aspects/advice-parameter-binding#limitations) are listed in the reference.

For the aspects that do run on hot paths, the conversion is mechanical. Change the base class to `MethodLevelAspect` or `LocationLevelAspect`, turn each override into an advice method with the matching attribute, replace each `args` member with a bound parameter, and build. PostSharp tells you what does not fit. The [Modernizing Your Custom Aspects](https://doc.postsharp.net/postsharp/preview/framework/aspects/member-level-aspects/modernizing-aspects) article gives the steps and a table that maps each member of `MethodExecutionArgs`, `MethodInterceptionArgs` and `LocationInterceptionArgs` to its binding.

## Try it

Advice parameter binding is available from PostSharp 2027.0, which also adds support for .NET 11 and C# 15. See [What's New in PostSharp 2027.0](https://doc.postsharp.net/postsharp/preview/introduction/whats-new/2027-0) for the full list. Pick one aspect on your hottest path, convert it, and tell us how it went on [GitHub](https://github.com/postsharp/PostSharp.Public/issues).

Happy meta-programming with PostSharp!

---

## Site navigation

- Index of the whole site: [llms.txt](../llms.txt)

