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 {
8 }
9}
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 {
12 }
13}
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 {
8 }
9}
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("LOG01", Severity.Error,
12 "The type '{0}' must have a field 'ILogger _logger' or a property 'ILogger Logger'.");
13
14 private static readonly DiagnosticDefinition<( DeclarationKind, IFieldOrProperty)>
15 _loggerFieldOrIncorrectTypeError =
16 new("LOG02", Severity.Error, "The {0} '{1}' must be of type ILogger.");
17
18 public override void BuildAspect(IAspectBuilder<IMethod> builder)
19 {
20 var declaringType = builder.Target.DeclaringType;
21
22 // Finds a field named '_logger' or a property named 'Property'.
23 var loggerFieldOrProperty =
24 (IFieldOrProperty?)declaringType.AllFields.OfName("_logger").SingleOrDefault() ??
25 declaringType.AllProperties.OfName("Logger").SingleOrDefault();
26
27 // Report an error if the field or property does not exist.
28 if (loggerFieldOrProperty == null)
29 {
30 builder.Diagnostics.Report(_missingLoggerFieldError.WithArguments(declaringType));
31
32 return;
33 }
34
35 // Verify the type of the logger field or property.
36 if (!loggerFieldOrProperty.Type.Is(typeof(ILogger)))
37 {
38 builder.Diagnostics.Report(
39 _loggerFieldOrIncorrectTypeError.WithArguments((declaringType.DeclarationKind,
40 loggerFieldOrProperty)));
41
42 return;
43 }
44
45 // Override the target method with our template. Pass the logger field or property to the template.
46 builder.Advice.Override(builder.Target, nameof(this.OverrideMethod),
47 new { loggerFieldOrProperty });
48 }
49
50 public override void BuildEligibility(IEligibilityBuilder<IMethod> builder)
51 {
52 base.BuildEligibility(builder);
53
54 // Now that we reference an instance field, we cannot log static methods.
55 builder.MustNotBeStatic();
56 }
57
58 [Template]
59 private dynamic? OverrideMethod(IFieldOrProperty loggerFieldOrProperty)
60 {
61 // Define a `logger` run-time variable and assign it to the ILogger field or property,
62 // e.g. `this._logger` or `this.Logger`.
63 var logger = (ILogger)loggerFieldOrProperty.Value!;
64
65 // Determine if tracing is enabled.
66 var isTracingEnabled = logger.IsEnabled(LogLevel.Trace);
67
68 // Write entry message.
69 if (isTracingEnabled)
70 {
71 var entryMessage = BuildInterpolatedString(false);
72 entryMessage.AddText(" started.");
73 LoggerExtensions.LogTrace(logger, entryMessage.ToValue());
74 }
75
76 try
77 {
78 // Invoke the method and store the result in a variable.
79 var result = meta.Proceed();
80
81 if (isTracingEnabled)
82 {
83 // Display the success message. The message is different when the method is void.
84 var successMessage = BuildInterpolatedString(true);
85
86 if (meta.Target.Method.ReturnType.Is(typeof(void)))
87 {
88 // When the method is void, display a constant text.
89 successMessage.AddText(" succeeded.");
90 }
91 else
92 {
93 // When the method has a return value, add it to the message.
94 successMessage.AddText(" returned ");
95 successMessage.AddExpression(result);
96 successMessage.AddText(".");
97 }
98
99 LoggerExtensions.LogTrace(logger, successMessage.ToValue());
100 }
101
102 return result;
103 }
104 catch (Exception e) when (logger.IsEnabled(LogLevel.Warning))
105 {
106 // Display the failure message.
107 var failureMessage = BuildInterpolatedString(false);
108 failureMessage.AddText(" failed: ");
109 failureMessage.AddExpression(e.Message);
110 LoggerExtensions.LogWarning(logger, failureMessage.ToValue());
111
112 throw;
113 }
114 }
115
116 // Builds an InterpolatedStringBuilder with the beginning of the message.
117 private static InterpolatedStringBuilder BuildInterpolatedString(bool includeOutParameters)
118 {
119 var stringBuilder = new InterpolatedStringBuilder();
120
121 // Include the type and method name.
122 stringBuilder.AddText(
123 meta.Target.Type.ToDisplayString(CodeDisplayFormat.MinimallyQualified));
124 stringBuilder.AddText(".");
125 stringBuilder.AddText(meta.Target.Method.Name);
126 stringBuilder.AddText("(");
127 var i = 0;
128
129 // Include a placeholder for each parameter.
130 foreach (var p in meta.Target.Parameters)
131 {
132 var comma = i > 0 ? ", " : "";
133
134 if (p.RefKind == RefKind.Out && !includeOutParameters)
135 {
136 // When the parameter is 'out', we cannot read the value.
137 stringBuilder.AddText($"{comma}{p.Name} = <out> ");
138 }
139 else
140 {
141 // Otherwise, add the parameter value.
142 stringBuilder.AddText($"{comma}{p.Name} = {{");
143 stringBuilder.AddExpression(p);
144 stringBuilder.AddText("}");
145 }
146
147 i++;
148 }
149
150 stringBuilder.AddText(")");
151
152 return stringBuilder;
153 }
154}
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.