Open sandboxFocusImprove this doc

Example: Enriching exceptions with parameter values

Call stacks are essential for diagnosing software issues; however, without parameter values, they can be less helpful. They lack context by only showing the sequence of method calls, which fails to reveal the data being processed during execution. Parameter values give you insight into the application's state, which is vital to pinpoint the root cause easily. Without parameter values, recreating an issue becomes time-consuming and increases the tedium of tracing through code paths since you cannot directly assess the impact of input data on the error from the call stack alone.

In this example, we will show how to include parameter values into the call stack. The idea is to add an exception handler to all non-trivial methods, to append context information to the current Exception, and rethrow the exception to the next stack frame.

Here is an example of a method that has an exception handler:

Source Code


1public static class Calculator
2{
3    public static int Fibonaci(int n)
4    {


5        if (n < 0)
6        {
7            throw new ArgumentOutOfRangeException(nameof(n));
8        }
9
10        if (n == 0)
11        {
12            return 0;
13        }
14
15        // Intentionally ommitting these lines to create an error.
16        //else if (n == 1)
17        //{
18        //    return 1
19        //}
20        else
21        {
22            return Fibonaci(n - 1) + Fibonaci(n - 2);






23        }
24    }
25}
Transformed Code
1using System;
2
3public static class Calculator
4{
5    public static int Fibonaci(int n)
6    {
7try
8        {
9            if (n < 0)
10        {
11            throw new ArgumentOutOfRangeException(nameof(n));
12        }
13
14        if (n == 0)
15        {
16            return 0;
17        }
18
19        // Intentionally ommitting these lines to create an error.
20        //else if (n == 1)
21        //{
22        //    return 1
23        //}
24        else
25        {
26            return Fibonaci(n - 1) + Fibonaci(n - 2);
27        }
28}
29        catch (Exception e)
30        {
31            e.AppendContextFrame($"Calculator.Fibonaci({n})");
32            throw;
33        }
34    }
35}

In the last-chance exception handler, you can now include the context information in the crash report:

1internal class Program
2{
3    private static void Main()
4    {
5        try
6        {
7            Calculator.Fibonaci(5);
8        }
9        catch (Exception e)
10        {
11            Console.WriteLine(e);
12            var context = e.GetContextInfo();
13
14            if (context != null)
15            {
16                Console.WriteLine("---with---");
17                Console.Write(context);
18                Console.WriteLine("----------");
19            }
20        }
21    }
22}

This program produces the following output:

System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'n')
   at Calculator.Fibonaci(Int32 n) in Calculator.cs:line 8
   at Calculator.Fibonaci(Int32 n) in Calculator.cs:line 23
   at Calculator.Fibonaci(Int32 n) in Calculator.cs:line 23
   at Calculator.Fibonaci(Int32 n) in Calculator.cs:line 23
   at Calculator.Fibonaci(Int32 n) in Calculator.cs:line 23
   at Calculator.Fibonaci(Int32 n) in Calculator.cs:line 23
   at Program.Main() in Program.cs:line 7
---with---
Calculator.Fibonaci(-1)
Calculator.Fibonaci(1)
Calculator.Fibonaci(2)
Calculator.Fibonaci(3)
Calculator.Fibonaci(4)
Calculator.Fibonaci(5)
----------

As you can see, parameter values are now included in the crash report.

Infrastructure code

Let's see how it works.

The exception handler calls the helper class EnrichExceptionHelper:

1using Metalama.Framework.Aspects;

2using System.Text;
3
4public static class EnrichExceptionHelper
5{
6    private const string _slotName = "Context";
7
8    [ExcludeAspect(typeof(EnrichExceptionAttribute))]
9    public static void AppendContextFrame(this Exception e, string frame)
10    {
11        // Get or create a StringBuilder for the exception where we will add additional context data.
12        var stringBuilder = (StringBuilder?)e.Data[_slotName];
13
14        if (stringBuilder == null)
15        {
16            stringBuilder = new StringBuilder();
17            e.Data[_slotName] = stringBuilder;
18        }
19
20        // Add current context information to the string builder.
21        stringBuilder.Append(frame);
22        stringBuilder.AppendLine();
23    }
24
25    public static string? GetContextInfo(this Exception e)
26        => ((StringBuilder?)e.Data[_slotName])?.ToString();








27}

Aspect code

The EnrichExceptionAttribute aspect is responsible for adding the exception handler to methods:

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4
5public class EnrichExceptionAttribute : OverrideMethodAspect
6{
7    public override dynamic? OverrideMethod()
8    {
9        // Compile-time code: create a formatting string containing the method name and placeholder for formatting parameters.
10        var methodSignatureBuilder = new InterpolatedStringBuilder();
11        methodSignatureBuilder.AddText(meta.Target.Type.ToString());
12        methodSignatureBuilder.AddText(".");
13        methodSignatureBuilder.AddText(meta.Target.Method.Name);
14        methodSignatureBuilder.AddText("(");
15
16        foreach (var p in meta.Target.Parameters)
17        {
18            if (p.Index > 0)
19            {
20                methodSignatureBuilder.AddText(", ");
21            }
22
23            if (p.RefKind == RefKind.Out)
24            {
25                methodSignatureBuilder.AddText($"{p.Name} = <out> ");
26            }
27            else
28            {
29                methodSignatureBuilder.AddExpression(p.Value);
30            }
31        }
32
33        methodSignatureBuilder.AddText(")");
34
35        try
36        {
37            return meta.Proceed();
38        }
39        catch (Exception e)
40        {
41            e.AppendContextFrame((string)methodSignatureBuilder.ToValue());
42
43            throw;
44        }
45    }
46}

Most of the code in this aspect builds an interpolated string, including the method name and its parameters. We have commented on this technique in detail in Logging example, step 3: Adding parameters values.

Fabric code

Adding the aspect to all methods by hand as a custom attribute would be highly cumbersome. Instead, we are using a fabric that adds the exception handler to all public methods of all public types. Note that we exclude the ToString method to avoid infinite recursion. For the same reason, we have excluded the aspect from the EnrichExceptionHelper.AppendContextFrame method.

1using Metalama.Framework.Fabrics;
2using Metalama.Framework.Code;
3
4internal class Fabric : ProjectFabric
5{
6    public override void AmendProject(IProjectAmender amender) =>
7        amender
8            .SelectTypes()
9            .Where(type => type.Accessibility == Accessibility.Public)
10            .SelectMany(type => type.Methods)
11            .Where(method =>
12                method.Accessibility == Accessibility.Public && method.Name != "ToString")
13            .AddAspectIfEligible<EnrichExceptionAttribute>();
14}
Warning

Including sensitive information (e.g., user credentials, personal data, etc.) in logs can pose a security risk. Be cautious 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