Open sandboxFocusImprove this doc

Providing Advices Dynamically

An aspect usually declares its import and introduction advices with custom attributes, such as ImportMemberAttribute, IntroduceMemberAttribute and IntroduceInterfaceAttribute (see Introducing Interfaces, Methods, Properties and Events into Existing Classes and Accessing Members of the Target Class). A custom attribute is the same for every target. When the advices depend on the target, for instance when the aspect must import every field of a given type, the aspect implements the IAdviceProvider interface instead.

PostSharp calls the ProvideAdvices(object) method at build time, for each target of the aspect. The method receives the target as a reflection object, typically a Type, and returns the advices to add to the aspect instance, as AdviceInstance objects.

Advice instances

The following classes represent the advices that an aspect can provide:

Class Equivalent attribute Advice
ImportLocationAdviceInstance ImportMemberAttribute on a field of type Property<TValue> Imports a field or property of the target class into an aspect field of type ILocationBinding, or of a collection type such as List<ILocationBinding>, with one advice per member. A property can also be imported into a Property<TValue> field, as with the attribute, but a field cannot.
ImportMethodAdviceInstance ImportMemberAttribute on a field of a delegate type Imports a method of the target class into a delegate field of the aspect.
IntroduceMethodAdviceInstance IntroduceMemberAttribute Introduces a public method of the aspect class into the target class.
IntroduceInterfaceAdviceInstance IntroduceInterfaceAttribute Introduces an interface, which the aspect class implements, into the target class.
IntroduceFieldAdviceInstance None Requests a field in the target type: an existing field, or a new one. The advices of the aspect read and write it with [ImportMember] parameters. See Requesting fields.
ImportDependencyAdviceInstance None Requests a service that the target class receives from its constructors, for instance from a dependency injection container. The advices of the aspect read it with [ImportMember] parameters. See Receiving Dependencies from the Constructors of the Target Class.

Import advices and interface introductions require an aspect derived from InstanceLevelAspect (errors LA0079 and LA0069). Field and dependency requests do not: any aspect can provide them, including a statically scoped aspect such as a MethodLevelAspect, because the advices receive the field through their parameters, not through a field of the aspect. The aspect fields and methods that these advices refer to are public members of the aspect class, as with the custom attributes, and the import fields are instance fields. An aspect field that receives imported members is set at run time, so mark it with PNonSerializedAttribute when the aspect is serialized with PSerializableAttribute.

Behaviors such as method boundary and interception advices cannot be provided this way. To apply them to members selected with code, use a MethodPointcut (see Selecting Members with Pointcuts), or add aspects with IAspectProvider (see Adding Aspects Dynamically).

Example

The following aspect implements IDisposable on its target class, and disposes all the fields of the class whose type implements IDisposable. ProvideAdvices imports each of these fields into the DisposableFields list, and introduces the interface only when the class has such fields.

[PSerializable]
public sealed class DisposeFieldsAttribute : InstanceLevelAspect, IAdviceProvider, IDisposable
{
    [PNonSerialized]
    public List<ILocationBinding> DisposableFields;

    public IEnumerable<AdviceInstance> ProvideAdvices( object targetElement )
    {
        Type targetType = (Type) targetElement;
        FieldInfo aspectField = typeof(DisposeFieldsAttribute).GetField( nameof(DisposableFields) );

        List<FieldInfo> fields = targetType
            .GetFields( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly )
            .Where( field => typeof(IDisposable).IsAssignableFrom( field.FieldType ) )
            .ToList();

        foreach ( FieldInfo field in fields )
        {
            yield return new ImportLocationAdviceInstance( aspectField, new LocationInfo( field ) );
        }

        if ( fields.Count > 0 )
        {
            yield return new IntroduceInterfaceAdviceInstance( typeof(IDisposable), InterfaceOverrideAction.Ignore );
        }
    }

    public void Dispose()
    {
        foreach ( ILocationBinding binding in this.DisposableFields )
        {
            ((IDisposable) binding.GetValue( this.Instance ))?.Dispose();
        }
    }
}

When the aspect is applied to a class that has two fields of a disposable Connection type, disposing an instance of the class prints the following output:

Disposing the read connection.
Disposing the write connection.

With InterfaceOverrideAction.Ignore, a class that already implements IDisposable, for instance through a base class, keeps its own Dispose method, and the aspect does not dispose its fields.

Requesting fields

An aspect may need to store a value for each target object, for instance a flag, a counter or a cache. A statically scoped aspect has one instance for all the objects of a class, so it cannot keep this value itself. An IntroduceFieldAdviceInstance requests a field in the target type instead: the type to which the aspect is applied, or the type that declares the member to which it is applied. The target type can be a class or a struct.

The request does not always introduce a field. A compatible field may already exist, for instance because the source code declares it or because another aspect introduced it. The request then says what to do: fail, skip the introduction, or introduce another field. The request does not initialize the field: a new field has the default value of its type.

The advices of the aspect instance that made the request receive the field through a parameter annotated with ImportMemberAttribute, by value, as in, ref or out, or as an ILocationBinding<T> (see Importing members of the target type):

  • When the request has a Name, the parameter imports the field by this name, whatever the real name of the field. The requested field wins over a member of the target type that has the same name.
  • When the request has no name, the parameter imports the field by its type, without a name. The fields that the aspect instance requested are then the only candidates of this type.

The requested name is visible only to the aspect instance that made the request. Other aspects see an introduced field as a member of the type, by its real name or by its type.

Example

The following aspect makes a method run only once for each object. It requests one new bool field for each method to which it is applied, without a name, and its entry advice imports this field by type with a ref parameter. The aspect is a MethodLevelAspect, so it is statically scoped, but the flag is stored in each object.

[PSerializable]
public sealed class RunOnceAttribute : MethodLevelAspect, IAdviceProvider
{
    public IEnumerable<AdviceInstance> ProvideAdvices( object targetElement )
    {
        // One new field per target method, in the class that declares the method.
        yield return new IntroduceFieldAdviceInstance( typeof(bool) )
        {
            WhenCompatibleFieldExists = IntroduceFieldCompatibleAction.IntroduceAnother,
            IsSerialized = false
        };
    }

    [OnMethodEntryAdvice]
    public static void OnEntry( [ImportMember] ref bool hasRun, [FlowBehavior] out FlowBehavior flowBehavior )
    {
        if ( hasRun )
        {
            flowBehavior = FlowBehavior.Return;
        }
        else
        {
            hasRun = true;
            flowBehavior = FlowBehavior.Default;
        }
    }
}

The aspect is applied to the Open and Close methods of a class that already has a bool field. IntroduceFieldCompatibleAction.IntroduceAnother gives each method a field of its own, next to the isOpen field, which the aspect does not import:

public class Connection
{
    private readonly string name;
    private bool isOpen;

    public Connection( string name )
    {
        this.name = name;
    }

    public bool IsOpen => this.isOpen;

    [RunOnce]
    public void Open()
    {
        this.isOpen = true;
        Console.WriteLine( "Opening {0}.", this.name );
    }

    [RunOnce]
    public void Close()
    {
        this.isOpen = false;
        Console.WriteLine( "Closing {0}.", this.name );
    }
}

Each object opens and closes its connection only once:

Opening the first connection.
Opening the second connection.
Closing the second connection.

Configuring the request

The IntroduceFieldAdviceInstance class has the following properties:

Property Description
FieldType The type of the field, passed to the constructor. It can depend on the target, and it can be a closed generic type.
Name The requested name, or null. A name finds an existing field of this name, and it is the name of a new field. Without a name, an existing field is found by its type, and a new field gets a generated name that cannot conflict with the source code.
WhenCompatibleFieldExists What the request does when the target type already has a compatible field. See below. The default value is Fail.
WhenIncompatibleMemberExists What a request with a name does when a member has this name but is not a compatible field. See below. The default value is Fail.
Visibility The visibility of a new field. The default value is Private.
IsStatic Whether the field is static. A static field of a generic type exists once for each closed generic type.
IsReadOnly Whether a new field is read-only (initonly). The advices then import it by value or as in only. The default value is false.
IsSerialized Whether a new field is serialized with the object. The default value is true. When it is false, the field is marked NotSerialized, which a serializer such as BinaryFormatter skips.
CustomAttributes The custom attributes added to a new field, as ObjectConstruction objects. They are not added to an existing field.

A new field has the CompilerGeneratedAttribute custom attribute.

A field is compatible with the request when it has exactly the type of the request and the same kind (static or instance), and when it has the requested name, or any name when the request has no name. The candidates are the fields of the target type, the fields of its base types that the target type can access, and the fields introduced by earlier requests. Constants and the fields generated by the compiler are not candidates.

WhenCompatibleFieldExists takes one of the following IntroduceFieldCompatibleAction values:

  • Fail, the default value: the build fails with the error LA0271.
  • Ignore: the introduction is skipped, and the advices of the aspect import the compatible field. This is how several aspects share one field.
  • IntroduceAnother: a new field is introduced next to the compatible field. Its name is the requested name followed by a number, such as handlers2, or a generated name when the request has no name.

A member that has the requested name but is not a compatible field, such as a property, a constant or a field of another type, is an incompatible member. WhenIncompatibleMemberExists takes one of the following IntroduceFieldIncompatibleAction values:

  • Fail, the default value: the build fails with the error LA0272.
  • IntroduceAnother: a new field is introduced, with the requested name followed by a number. The existing member is not renamed.

WhenIncompatibleMemberExists applies only to a request that has a name: a request without a name finds fields by their type, and its new field gets a generated name that cannot conflict with another member.

When no field is compatible and no member is incompatible, the request introduces a new field with the requested name, or with a generated name. A request without a name that finds several compatible fields causes the error LA0273, unless WhenCompatibleFieldExists is IntroduceAnother. Give it a name.

The number appended to a name depends on the order of the aspects. A field that other code must find by its name should therefore not set WhenCompatibleFieldExists or WhenIncompatibleMemberExists to IntroduceAnother.

Ordering several requests

PostSharp resolves the requests of a type after those of its base types, in the order of the aspects (see Coping with Several Aspects on the Same Target). When two aspects request fields that can resolve to the same field (the same name, a numbered name, or no name with the same type and kind), the result depends on their order. Order these aspects with aspect dependencies, otherwise PostSharp reports the warning PS0114. A request declares the effect StandardEffects.MemberIntroduction, and the effect returned by GetMemberIntroductionEffect for its name, or for its type when it has no name.

The following requests do not need to be ordered:

  • The requests of the same aspect instance, which are resolved in the order in which the aspect provides them.
  • Equivalent requests, which have the same name, type, kind, visibility, serialization, read-only flag, custom attributes, WhenCompatibleFieldExists and WhenIncompatibleMemberExists, when WhenCompatibleFieldExists is not Fail. With Ignore, they share one field whatever their order. With IntroduceAnother, only the numbers appended to the names depend on the order. This is the case of one aspect applied to several members of a class.

Build messages

Code Severity Meaning
LA0268 Error A field cannot be introduced into the target type, which is an interface, an enum or a delegate, or the aspect is applied to an assembly.
LA0269 Error The field type is not valid: a by-reference type, a pointer, void, or a type that contains generic parameters. A byref-like type, such as Span<T>, is valid only as an instance field of a ref struct.
LA0270 Error The requested name is empty or contains only white space. Set Name to null to request a field by its type.
LA0271 Error A compatible field exists, and WhenCompatibleFieldExists is Fail.
LA0272 Error An incompatible member has the requested name, and WhenIncompatibleMemberExists is Fail.
LA0273 Error The request has no name, several compatible fields exist, and WhenCompatibleFieldExists is not IntroduceAnother.
LA0279 Error An instance field is requested in a type that has an explicit layout, where a new field would have no offset. Request a static field, or declare the field with its offset.
PS0114 Warning Two aspects request fields that can resolve to the same field, and they are not ordered.

Extension blocks

ProvideAdvices receives reflection objects, which represent the code that the compiler generates. For C# 14 extension blocks, this code includes metadata types and members that do not exist in the source code. Do not select them: use IsExtensionBlockMetadata to recognize them (see C# 14 Extension Blocks and Multicasting).

See Also

Reference

IAdviceProvider
AdviceInstance
ImportLocationAdviceInstance
ImportMethodAdviceInstance
IntroduceMethodAdviceInstance
IntroduceInterfaceAdviceInstance
IntroduceFieldAdviceInstance
IntroduceFieldCompatibleAction
IntroduceFieldIncompatibleAction
ImportDependencyAdviceInstance

Other Resources

Developing Type- and Assembly-Level Aspects
Selecting Members with Pointcuts
Introducing Interfaces, Methods, Properties and Events into Existing Classes
Accessing Members of the Target Class
Adding Aspects Dynamically
Advice Parameters
Receiving Dependencies from the Constructors of the Target Class