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.
1internal class Calculator
2{
3 [Log]
4 public double Add(double a, double b) => a + b;
5}
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.