Open sandboxFocusImprove this doc

Logging example, step 3: Adding parameters values

Up until now, our logging aspect writes messages that include constant text and compile-time expressions. Let's now introduce the values of parameters and the method return value, which are known at run time.

It's important to include parameter values in traces because they offer valuable context to help developers comprehend the application's state during execution. With this contextual information, you can diagnose and debug problems more easily, decreasing the time spent recreating issues and tracing through code paths, resulting in a more stable and reliable application.

The code with the transformation from the new aspect can be seen below:

Source Code


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













5
6    [Log]
7    public static double Divide( double a, double b ) => a / b;













8
9    [Log]
10    public static void IntegerDivide( int a, int b, out int quotient, out int remainder )
11    {



12        quotient = a / b;
13        remainder = a % b;









14    }
15}
Transformed Code
1using System;
2
3internal static class Calculator
4{
5    [Log]
6    public static double Add( double a, double b ) { Console.WriteLine($"Calculator.Add(a = {{{a}}}, b = {{{b}}}) started.");
7        try
8        {
9            double result;
10            result = a + b;
11            Console.WriteLine($"Calculator.Add(a = {{{a}}}, b = {{{b}}}) returned {result}.");
12            return (double)result;
13        }
14        catch (Exception e)
15        {
16            Console.WriteLine($"Calculator.Add(a = {{{a}}}, b = {{{b}}}) failed: {e.Message}");
17            throw;
18        }
19    }
20
21    [Log]
22    public static double Divide( double a, double b ) { Console.WriteLine($"Calculator.Divide(a = {{{a}}}, b = {{{b}}}) started.");
23        try
24        {
25            double result;
26            result = a / b;
27            Console.WriteLine($"Calculator.Divide(a = {{{a}}}, b = {{{b}}}) returned {result}.");
28            return (double)result;
29        }
30        catch (Exception e)
31        {
32            Console.WriteLine($"Calculator.Divide(a = {{{a}}}, b = {{{b}}}) failed: {e.Message}");
33            throw;
34        }
35    }
36
37    [Log]
38    public static void IntegerDivide( int a, int b, out int quotient, out int remainder )
39    {
40Console.WriteLine($"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = <out> , remainder = <out> ) started.");
41        try
42        {
43            quotient = a / b;
44        remainder = a % b;
45object result = null;
46            Console.WriteLine($"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = {{{quotient}}}, remainder = {{{remainder}}}) succeeded.");
47            return;
48        }
49        catch (Exception e)
50        {
51            Console.WriteLine($"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = <out> , remainder = <out> ) failed: {e.Message}");
52            throw;
53        }
54    }
55}
Warning

Adding sensitive information such as user credentials, personal data, etc., to logs can pose a security risk. Exercise caution when adding parameter values to logs and avoid exposing sensitive data. To remove sensitive information from the logs, see Logging example, step 7: Removing sensitive data

Implementation

The aspect is as follows:

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4
5public class LogAttribute : OverrideMethodAspect
6{
7    public override dynamic? OverrideMethod()
8    {
9        // Write entry message.
10        var entryMessage = BuildInterpolatedString( false );
11        entryMessage.AddText( " started." );
12        Console.WriteLine( entryMessage.ToValue() );
13
14        try
15        {
16            // Invoke the method and store the result in a variable.
17            var result = meta.Proceed();
18
19            // Display the success message. The message is different when the method is void.
20            var successMessage = BuildInterpolatedString( true );
21
22            if ( meta.Target.Method.ReturnType.Equals( typeof(void) ) )
23            {
24                // When the method is void, display a constant text.
25                successMessage.AddText( " succeeded." );
26            }
27            else
28            {
29                // When the method has a return value, add it to the message.
30                successMessage.AddText( " returned " );
31                successMessage.AddExpression( result );
32                successMessage.AddText( "." );
33            }
34
35            Console.WriteLine( successMessage.ToValue() );
36
37            return result;
38        }
39        catch ( Exception e )
40        {
41            // Display the failure message.
42            var failureMessage = BuildInterpolatedString( false );
43            failureMessage.AddText( " failed: " );
44            failureMessage.AddExpression( e.Message );
45            Console.WriteLine( failureMessage.ToValue() );
46
47            throw;
48        }
49    }
50
51    // Builds an InterpolatedStringBuilder with the beginning of the message.
52    private static InterpolatedStringBuilder BuildInterpolatedString( bool includeOutParameters )
53    {
54        var stringBuilder = new InterpolatedStringBuilder();
55
56        // Include the type and method name.
57        stringBuilder.AddText( meta.Target.Type.ToDisplayString( CodeDisplayFormat.MinimallyQualified ) );
58        stringBuilder.AddText( "." );
59        stringBuilder.AddText( meta.Target.Method.Name );
60        stringBuilder.AddText( "(" );
61        var i = 0;
62
63        // Include a placeholder for each parameter.
64        foreach ( var p in meta.Target.Parameters )
65        {
66            var comma = i > 0 ? ", " : "";
67
68            if ( p.RefKind == RefKind.Out && !includeOutParameters )
69            {
70                // When the parameter is 'out', we cannot read the value.
71                stringBuilder.AddText( $"{comma}{p.Name} = <out> " );
72            }
73            else
74            {
75                // Otherwise, add the parameter value.
76                stringBuilder.AddText( $"{comma}{p.Name} = {{" );
77                stringBuilder.AddExpression( p );
78                stringBuilder.AddText( "}" );
79            }
80
81            i++;
82        }
83
84        stringBuilder.AddText( ")" );
85
86        return stringBuilder;
87    }
88}

As you can see, the aspect's code is much more complex.

The most straightforward approach to generate an interpolated string from an aspect is to use the InterpolatedStringBuilder class and to add literal parts, known at compile time and constant at run time, and run-time expressions, unknown at compile time.

The BuildInterpolatedString method of the aspect class is responsible for constructing the InterpolatedStringBuilder. Please note that BuildInterpolatedString is not a template method. It is a method that executes entirely at compile time. It has an includeOutParameters parameter that determines if the values of the out parameters are available when the interpolated string is in use.

  • Firstly, BuildInterpolatedString appends the name of the current type and method using the AddText method. Then, it iterates through the collection of parameters of the current method. This collection is available on the expression meta.Target.Parameters.

  • In the foreach loop, BuildInterpolatedString method checks if the parameter is out. If includeOutParameters parameter is false, the method appends a constant text. However, if the parameter can be read, the method adds an expression using the AddExpression method.

Below is the OverrideMethod method. As with previous examples, this method is a template containing both run-time and compile-time code.

  • Firstly, OverrideMethod calls BuildInterpolatedString to get the InterpolatedStringBuilder. Note that the interpolated string created by BuildInterpolatedString only includes the name and parameters of the method. We still want to append more information to the strings, such as the text started, succeeded, returned or failed.

  • Then, we write the entry message. The aspect calls the ToValue method to get the interpolated string from the InterpolatedStringBuilder and passes it to Console.WriteLine. Note that the ToValue method does not really return an interpolated string but returns a run-time object of dynamic type that, when used in a run-time context, expands into the interpolated string.

  • Finally, we write the success message. We want to write a different message when the method is void than when it returns a value. We implement this choice with the if in the method. As the if condition meta.Target.Method.ReturnType.Is(typeof(void)) is a compile-time expression, Metalama interprets the entire if at compile time. Notice the specific background color of the compile-time.