An aspect can introduce other aspects to child declarations. These aspects are known as child aspects. Child aspects must adhere to two conditions:
- The child aspect class should be processed after the parent aspect class. In other words, the child aspect class must be listed before the parent class in the AspectOrderAttribute ordering definition. Please refer to Ordering aspects for more details.
- The target declaration of the child aspect class must be contained in the target declaration of the parent aspect. For example, a type-level aspect can introduce aspects to methods of the current type, but not to methods of a different type. A parameter-level aspect can add a child aspect to the parent method or another method of the same type, but not of a different type.
An aspect can add child aspects from the BuildAspect method by using the builder.Outbound property, followed by calling the AddAspect method. To introduce child aspects on members of the current declaration, use the Select or SelectMany method, and then invoke AddAspect or AddAspectIfEligible.
Overriding a child aspect with an attribute.
Note that any aspect added "manually" as a custom attribute takes precedence over aspects added by the parent aspect. In this case, the primary instance of the aspect is the one created by the custom attribute, and the secondary instance is the one added by the parent aspect. Secondary aspect instances are exposed on the IAspectInstance.SecondaryInstances property, which you can access from meta.AspectInstance or builder.AspectInstance.
Example: audited object and audited member
The following example contains two aspects: [AuditedObject]
and [AuditedMember]
. The [AuditedObject]
aspect automatically audits all public methods and properties. [AuditedMember]
only audits the target method or property. To avoid duplicating logic between AuditedObjectAttribute
and AuditedMemberAttribute
, AuditedObjectAttribute
adds instances of the AuditedObjectAttribute
class as child aspects.
[AuditedMember]
also allows developers to opt out of auditing. If the developer adds [AuditedMember(false)]
to a member, this aspect instance will take precedence over the one added by [AuditedObject]
.
1using Doc.ChildAspect;
2using Metalama.Framework.Aspects;
3
4[assembly:
5 AspectOrder(
6 AspectOrderDirection.CompileTime,
7 typeof(AuditedObjectAttribute),
8 typeof(AuditedMemberAttribute) )]
1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Advising;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5using System;
6
7#pragma warning disable CS8618
8
9namespace Doc.ChildAspect;
10
11public class AuditedMemberAttribute : Attribute, IAspect<IMethod>, IAspect<IProperty>
12{
13 [IntroduceDependency]
14 private readonly IAuditSink _auditSink;
15
16 public AuditedMemberAttribute() : this( true ) { }
17
18 public AuditedMemberAttribute( bool isEnabled )
19 {
20 this.IsEnabled = isEnabled;
21 }
22
23 public bool IsEnabled { get; }
24
25 void IAspect<IMethod>.BuildAspect( IAspectBuilder<IMethod> builder )
26 {
27 if ( !this.IsEnabled )
28 {
29 builder.SkipAspect();
30
31 return;
32 }
33
34 builder.Override( nameof(this.MethodTemplate) );
35 }
36
37 void IAspect<IProperty>.BuildAspect( IAspectBuilder<IProperty> builder )
38 {
39 if ( !this.IsEnabled )
40 {
41 builder.SkipAspect();
42
43 return;
44 }
45
46 builder.OverrideAccessors( null, nameof(this.MethodTemplate) );
47 }
48
49 [Template]
50 private dynamic? MethodTemplate()
51 {
52 this._auditSink.Audit(
53 meta.This,
54 meta.Target.Method.Name,
55 string.Join( ", ", meta.Target.Parameters.ToValueArray() ) );
56
57 return meta.Proceed();
58 }
59}
1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Advising;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5using System;
6
7namespace Doc.ChildAspect;
8
9[Inheritable]
10public class AuditedObjectAttribute : TypeAspect
11{
12 public override void BuildAspect( IAspectBuilder<INamedType> builder )
13 {
14 base.BuildAspect( builder );
15
16 builder.Outbound
17 .SelectMany( b => b.Methods )
18 .Where( m => m is { Accessibility: Accessibility.Public } )
19 .AddAspectIfEligible<AuditedMemberAttribute>();
20
21 builder.Outbound
22 .SelectMany( b => b.Properties )
23 .Where(
24 m => m is { Accessibility: Accessibility.Public, Writeability: Writeability.All } )
25 .AddAspectIfEligible<AuditedMemberAttribute>();
26 }
27}
1namespace Doc.ChildAspect;
2
3[AuditedObject]
4public class Invoice
5{
6 public void SomeAuditedOperation() { }
7
8 [AuditedMember( false )]
9 public void SomeNonAuditedOperation() { }
10
11 public string? SomeAuditedProperty { get; set; }
12}
1namespace Doc.ChildAspect;
2
3[AuditedObject]
4public class Invoice
5{
6 public void SomeAuditedOperation()
7 {
8 _auditSink.Audit(this, "SomeAuditedOperation", string.Join(", ", new object[] { }));
9 }
10
11 [AuditedMember(false)]
12 public void SomeNonAuditedOperation() { }
13
14 private string? _someAuditedProperty;
15
16 public string? SomeAuditedProperty
17 {
18 get
19 {
20 return _someAuditedProperty;
21 }
22
23 set
24 {
25 _auditSink.Audit(this, "set_SomeAuditedProperty", string.Join(", ", new object[] { value }));
26 _someAuditedProperty = value;
27 }
28 }
29
30 private IAuditSink _auditSink;
31
32 public Invoice(IAuditSink? auditSink = default)
33 {
34 this._auditSink = auditSink ?? throw new System.ArgumentNullException(nameof(auditSink));
35 }
36}
1namespace Doc.ChildAspect;
2
3public interface IAuditSink
4{
5 void Audit( object obj, string operation, string description );
6}
Accessing the parent aspect from the child aspect
Parent aspects are enumerated in the Predecessors property, accessible by the child aspect from meta.AspectInstance or builder.AspectInstance.
Requiring an aspect without creating a new instance
Instead of calling AddAspect, you can use RequireAspect. This method is generic, and its type parameter must be set to an aspect type with a default constructor. It checks if the target declaration already contains an aspect of the required type, and if not, it adds a new aspect instance.
If you were using AddAspect and the aspect was already present, a new aspect instance would be created, a primary aspect instance would be chosen, and the other instances would be made available as secondary instances. With RequireAspect, there would be no secondary instance, but the parent aspect would be exposed as a predecessor in the Predecessors collection.