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:
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}
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 ) => ((StringBuilder?) e.Data[_slotName])?.ToString();
26}
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(
12 method =>
13 method.Accessibility == Accessibility.Public && method.Name != "ToString" )
14 .AddAspectIfEligible<EnrichExceptionAttribute>();
15}
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