Open sandboxFocusImprove this doc

Logging example, step 4: Using ILogger

In the preceding steps, we employed the Console.WriteLine method to write the trace message. We will now write messages to ILogger from the Microsoft.Extensions.Logging namespace, and use dependency injection to obtain the ILogger.

Utilizing dependency injection to obtain an ILogger instance, rather than writing directly to Console.WriteLine, provides increased flexibility, maintainability, and testability. This approach enables seamless swapping of logging implementations, promotes a clean separation of concerns, simplifies configuration management, and allows effective unit testing by substituting real loggers with mock objects during testing.

Let's examine how the new aspect transforms code.

Source Code



1internal class Calculator
2{
3    [Log]
4    public double Add(double a, double b) => a + b;



























5}
Transformed Code
1using System;
2using Microsoft.Extensions.Logging;
3
4internal class Calculator
5{
6    [Log]
7    public double Add(double a, double b) { var isTracingEnabled = _logger.IsEnabled(LogLevel.Trace);
8        if (isTracingEnabled)
9        {
10            _logger.LogTrace($"Calculator.Add(a = {{{a}}}, b = {{{b}}}) started.");
11        }
12
13        try
14        {
15            double result;
16            result = a + b;if (isTracingEnabled)
17            {
18                _logger.LogTrace($"Calculator.Add(a = {{{a}}}, b = {{{b}}}) returned {result}.");
19            }
20
21            return (double)result;
22        }
23        catch (Exception e) when (_logger.IsEnabled(LogLevel.Warning))
24        {
25            _logger.LogWarning($"Calculator.Add(a = {{{a}}}, b = {{{b}}}) failed: {e.Message}");
26            throw;
27        }
28    }
29private ILogger _logger;
30
31    public Calculator(ILogger<Calculator> logger = default(global::Microsoft.Extensions.Logging.ILogger<global::Calculator>))
32    {
33        this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
34    }
35}

As shown above, calls to Console.WriteLine have been replaced by calls to _logger.TraceInformation. There is a new ILogger field, and its value is obtained from the constructor.

Implementation

Let's now look at the aspect code.

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
5using Microsoft.Extensions.Logging;
6
7#pragma warning disable CS8618
8
9public class LogAttribute : OverrideMethodAspect
10{
    Warning CS0649: Field 'LogAttribute._logger' is never assigned to, and will always have its default value null

11    [IntroduceDependency] private readonly ILogger _logger;
12
13    public override dynamic? OverrideMethod()
14    {
15        // Determine if tracing is enabled.
16        var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace);
17
18        // Write entry message.
19        if (isTracingEnabled)
20        {
21            var entryMessage = BuildInterpolatedString(false);
22            entryMessage.AddText(" started.");
23            this._logger.LogTrace((string)entryMessage.ToValue());
24        }
25
26        try
27        {
28            // Invoke the method and store the result in a variable.
29            var result = meta.Proceed();
30
31            if (isTracingEnabled)
32            {
33                // Display the success message. The message is different when the method is void.
34                var successMessage = BuildInterpolatedString(true);
35
36                if (meta.Target.Method.ReturnType.Is(typeof(void)))
37                {
38                    // When the method is void, display a constant text.
39                    successMessage.AddText(" succeeded.");
40                }
41                else
42                {
43                    // When the method has a return value, add it to the message.
44                    successMessage.AddText(" returned ");
45                    successMessage.AddExpression(result);
46                    successMessage.AddText(".");
47                }
48
49                this._logger.LogTrace((string)successMessage.ToValue());
50            }
51
52            return result;
53        }
54        catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning))
55        {
56            // Display the failure message.
57            var failureMessage = BuildInterpolatedString(false);
58            failureMessage.AddText(" failed: ");
59            failureMessage.AddExpression(e.Message);
60            this._logger.LogWarning((string)failureMessage.ToValue());
61
62            throw;
63        }
64    }
65
66    // Builds an InterpolatedStringBuilder with the beginning of the message.
67    private static InterpolatedStringBuilder BuildInterpolatedString(bool includeOutParameters)
68    {
69        var stringBuilder = new InterpolatedStringBuilder();
70
71        // Include the type and method name.
72        stringBuilder.AddText(
73            meta.Target.Type.ToDisplayString(CodeDisplayFormat.MinimallyQualified));
74        stringBuilder.AddText(".");
75        stringBuilder.AddText(meta.Target.Method.Name);
76        stringBuilder.AddText("(");
77        var i = 0;
78
79        // Include a placeholder for each parameter.
80        foreach (var p in meta.Target.Parameters)
81        {
82            var comma = i > 0 ? ", " : "";
83
84            if (p.RefKind == RefKind.Out && !includeOutParameters)
85            {
86                // When the parameter is 'out', we cannot read the value.
87                stringBuilder.AddText($"{comma}{p.Name} = <out> ");
88            }
89            else
90            {
91                // Otherwise, add the parameter value.
92                stringBuilder.AddText($"{comma}{p.Name} = {{");
93                stringBuilder.AddExpression(p);
94                stringBuilder.AddText("}");
95            }
96
97            i++;
98        }
99
100        stringBuilder.AddText(")");
101
102        return stringBuilder;
103    }
104}

We added some logic to handle the ILogger field or property.

The main point of interest in this example is the _logger field. The [IntroduceDependency] custom attribute specifies that the field must be introduced into the target type unless it already exists, and its value should be pulled from the constructor. This custom attribute is implemented in the Metalama.Extensions.DependencyInjection package. It supports several dependency injection frameworks. To learn more about it, see Injecting dependencies into aspects.

There are few changes in the OverrideMethod method. Instead of Console.WriteLine, we use the ILogger.LogTrace method. Note that we need to cast the InterpolatedStringBuilder.ToValue() expression into a string, which may seem redundant because InterpolatedStringBuilder.ToValue() returns a dynamic. The reason is that LogTrace is an extension method, and extension methods cannot have dynamic arguments. Therefore, we must cast the dynamic value into a string, which helps the C# compiler find the correct extension method.