Metalama (preview)Commented examplesLoggingStep 5.​ Using ILogger without DI
Open sandboxFocusImprove this doc

Logging example, step 5: ILogger without dependency injection

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("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 named Logger. 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 the args 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.