Let's take a step back. In the previous example, we used the heavy magic of
the [IntroduceDependency] custom
attribute. In this new aspect, the aspect will require an existing ILogger
field to exist and will report errors if
the target type does not meet expectations.
In the following code snippet, you can see that the aspect reports an error when the field or property is missing.
1namespace Metalama.Samples.Log4.Tests.MissingFieldOrProperty;
2
3internal class Foo
4{
5 [Log]
Error LOG01: The type 'Foo' must have a field 'ILogger _logger' or a property 'ILogger Logger'.
6 public void Bar() { }
7}
The aspect also reports an error when the field or property is not of the expected type.
1#pragma warning disable CS0169, CS8618, IDE0044, IDE0051
2
3namespace Metalama.Samples.Log4.Tests.FieldOrWrongType;
4
5internal class Foo
6{
7 private TextWriter _logger;
8
9 [Log]
Error LOG02: The type 'Foo._logger' must be of type ILogger.
10 public void Bar() { }
11}
Finally, the aspect must report an error when applied to a static method.
1namespace Metalama.Samples.Log4.Tests.StaticMethod;
2
3internal class Foo
4{
Error LAMA0037: The aspect 'Log' cannot be applied to the method 'Foo.StaticBar()' because 'Foo.StaticBar()' must not be static.
5 [Log]
6 public static void StaticBar() { }
7}
Implementation
How can we implement these new requirements? Let's look at the new implementation of the aspect.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using Metalama.Framework.Diagnostics;
5using Metalama.Framework.Eligibility;
6using Microsoft.Extensions.Logging;
7
8public class LogAttribute : MethodAspect
9{
10 private static readonly DiagnosticDefinition<INamedType> _missingLoggerFieldError =
11 new(
12 "LOG01",
13 Severity.Error,
14 "The type '{0}' must have a field 'ILogger _logger' or a property 'ILogger Logger'." );
15
16 private static readonly DiagnosticDefinition<( DeclarationKind, IFieldOrProperty)>
17 _loggerFieldOrIncorrectTypeError =
18 new( "LOG02", Severity.Error, "The {0} '{1}' must be of type ILogger." );
19
20 public override void BuildAspect( IAspectBuilder<IMethod> builder )
21 {
22 var declaringType = builder.Target.DeclaringType;
23
24 // Finds a field named '_logger' or a property named 'Property'.
25 var loggerFieldOrProperty =
26 (IFieldOrProperty?) declaringType.AllFields.OfName( "_logger" ).SingleOrDefault() ??
27 declaringType.AllProperties.OfName( "Logger" ).SingleOrDefault();
28
29 // Report an error if the field or property does not exist.
30 if ( loggerFieldOrProperty == null )
31 {
32 builder.Diagnostics.Report( _missingLoggerFieldError.WithArguments( declaringType ) );
33
34 return;
35 }
36
37 // Verify the type of the logger field or property.
38 if ( !loggerFieldOrProperty.Type.IsConvertibleTo( typeof(ILogger) ) )
39 {
40 builder.Diagnostics.Report(
41 _loggerFieldOrIncorrectTypeError.WithArguments(
42 (declaringType.DeclarationKind,
43 loggerFieldOrProperty) ) );
44
45 return;
46 }
47
48 // Override the target method with our template. Pass the logger field or property to the template.
49 builder.Advice.Override(
50 builder.Target,
51 nameof(this.OverrideMethod),
52 new { loggerFieldOrProperty } );
53 }
54
55 public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
56 {
57 base.BuildEligibility( builder );
58
59 // Now that we reference an instance field, we cannot log static methods.
60 builder.MustNotBeStatic();
61 }
62
63 [Template]
64 private dynamic? OverrideMethod( IFieldOrProperty loggerFieldOrProperty )
65 {
66 // Define a `logger` run-time variable and assign it to the ILogger field or property,
67 // e.g. `this._logger` or `this.Logger`.
68 var logger = (ILogger) loggerFieldOrProperty.Value!;
69
70 // Determine if tracing is enabled.
71 var isTracingEnabled = logger.IsEnabled( LogLevel.Trace );
72
73 // Write entry message.
74 if ( isTracingEnabled )
75 {
76 var entryMessage = BuildInterpolatedString( false );
77 entryMessage.AddText( " started." );
78 LoggerExtensions.LogTrace( logger, entryMessage.ToValue() );
79 }
80
81 try
82 {
83 // Invoke the method and store the result in a variable.
84 var result = meta.Proceed();
85
86 if ( isTracingEnabled )
87 {
88 // Display the success message. The message is different when the method is void.
89 var successMessage = BuildInterpolatedString( true );
90
91 if ( meta.Target.Method.ReturnType.Equals( typeof(void) ) )
92 {
93 // When the method is void, display a constant text.
94 successMessage.AddText( " succeeded." );
95 }
96 else
97 {
98 // When the method has a return value, add it to the message.
99 successMessage.AddText( " returned " );
100 successMessage.AddExpression( result );
101 successMessage.AddText( "." );
102 }
103
104 LoggerExtensions.LogTrace( logger, successMessage.ToValue() );
105 }
106
107 return result;
108 }
109 catch ( Exception e ) when ( logger.IsEnabled( LogLevel.Warning ) )
110 {
111 // Display the failure message.
112 var failureMessage = BuildInterpolatedString( false );
113 failureMessage.AddText( " failed: " );
114 failureMessage.AddExpression( e.Message );
115 LoggerExtensions.LogWarning( logger, failureMessage.ToValue() );
116
117 throw;
118 }
119 }
120
121 // Builds an InterpolatedStringBuilder with the beginning of the message.
122 private static InterpolatedStringBuilder BuildInterpolatedString( bool includeOutParameters )
123 {
124 var stringBuilder = new InterpolatedStringBuilder();
125
126 // Include the type and method name.
127 stringBuilder.AddText( meta.Target.Type.ToDisplayString( CodeDisplayFormat.MinimallyQualified ) );
128 stringBuilder.AddText( "." );
129 stringBuilder.AddText( meta.Target.Method.Name );
130 stringBuilder.AddText( "(" );
131 var i = 0;
132
133 // Include a placeholder for each parameter.
134 foreach ( var p in meta.Target.Parameters )
135 {
136 var comma = i > 0 ? ", " : "";
137
138 if ( p.RefKind == RefKind.Out && !includeOutParameters )
139 {
140 // When the parameter is 'out', we cannot read the value.
141 stringBuilder.AddText( $"{comma}{p.Name} = <out> " );
142 }
143 else
144 {
145 // Otherwise, add the parameter value.
146 stringBuilder.AddText( $"{comma}{p.Name} = {{" );
147 stringBuilder.AddExpression( p );
148 stringBuilder.AddText( "}" );
149 }
150
151 i++;
152 }
153
154 stringBuilder.AddText( ")" );
155
156 return stringBuilder;
157 }
158}
We added some logic to handle the ILogger
field or property.
LogAttribute class
First, the LogAttribute
class is now derived from MethodAspect instead
of OverrideMethodAspect. Our motivation for this change is that we need
the OverrideMethod
method to have a parameter not available in
the OverrideMethodAspect method.
BuildAspect method
The BuildAspect
is the entry point of the aspect. This method does the following things:
First, it looks for a field named
_logger
or a property namedLogger
. Note that it uses the AllFields and AllProperties collections, which include the members inherited from the base types. Therefore, it will also find the field or property defined in base types.The
BuildAspect
method reports an error if no field or property is found. Note that the error is defined as a static field of the class. To read more about reporting errors, see Reporting and suppressing diagnostics.Then, the
BuildAspect
method verifies the type of the_logger
field or property and reports an error if the type is incorrect.Finally, the
BuildAspect
method overrides the target method by calling builder.Override and by passing the name of the template method that will override the target method. It passes the IFieldOrProperty by using an anonymous type as theargs
parameter, where the property names of the anonymous type must match the parameter names of the template method.
To learn more about imperative advising of methods, see Overriding methods. To learn more about template parameters, see Template parameters and type parameters.
OverrideMethod method
The OverrideMethod
looks familiar, except for a few differences. The field or property that references the ILogger
is given by the loggerFieldOrProperty
parameter, which is set from the BuildAspect
method. This parameter is
of IFieldOrProperty type, which is a compile-time type representing metadata. Now, you
need to access this field or property at runtime. You do this using the Value
property, which returns an object that, for the code of your template, is of dynamic
type. When Metalama sees
this dynamic
object, it replaces it with the syntax representing the field or property, i.e., this._logger
or this.Logger
. So, the line var logger = (ILogger) loggerFieldOrProperty.Value!;
generates code that stores
the ILogger
field or property into a local variable that can be used in the runtime part of the template.
BuildEligibility
The last requirement to implement is to report an error when the aspect is applied to a static method.
We achieve this using the eligibility feature. Instead of manually reporting an error
using builder.Diagnostics.Report
, the benefit of using this feature is that, with eligibility, the IDE will not even
propose the aspect in a refactoring menu for a target that is not eligible. This is less confusing for the user of the
aspect.