Friday 3 February 2023

Looking into a stacktrace to find a given method name

When you log exceptions, it is sometimes interesting to look after a method name in the stackTrace (the 'callstack' where the application or system is running) An example usage of helper method I wrote to find the StackFrame containing the method of name to search is shown below.
 
     StackFrameExtensions.MethodStackFrameInfo methodStackFrame = stackTrace.FindStackFrameWithMethod("service", 2, nameof(OnEntry).ToLower(), nameof(LogUnauthorizedAccessDetails).ToLower());
 
This specifies that we want to look in the stack trace after a method which is containing 'service' (case insensitive), with a given minimum frame count of at least 2 from the place you retrieve the stackTrace and ignoring the method name "OnEntry" if it is found (Case insensitive). I have used this inside Postsharp aspects. It can be helpful in code where you either use AOP or some code where you expect a method call exist with a method name containing some string with a given distance (frame count) from the location in code you retrieve the stack frames. Note that you can always obtain a StackTrace by just instantiating a new StackTrace, e.g :
 
  var stackTrace = new StackTrace(); 
 
The stack trace helper extension method then looks like this:
 
 using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;

namespace DebuggingExtensionsLib
{
    public static class StackFrameExtensions
    {
        /// <summary>
        /// Retrieves the first stack frame of interest matching having a method with a name containing the <paramref name="methodNameContaining"/>. Case-insensitive search.
        /// </summary>
        /// <param name="stackTrace"></param>
        /// <param name="methodNameContaining">Pass in the method name to search for (case insensitive match)</param>
        /// <param name="minimumFrameCount">The minimum stack frame count. Defaults to 2.</param>
        /// <param name="ignoreMethodNamesContaining">Pass in one or more method names to ignore</param>
        /// <returns></returns>
        public static MethodStackFrameInfo FindStackFrameWithMethod(this StackTrace stackTrace, string methodNameContaining, int minimumFrameCount = 2, params string[] ignoreMethodNamesContaining)
        {
            try
            {
                var stackFrames = stackTrace.GetFrames();

                if (stackFrames == null || stackFrames.Length < minimumFrameCount)
                {
                    return null;
                }

                for (int i = 0; i < stackFrames.Length; i++)
                {
                    MethodBase mi = stackFrames[i].GetMethod();
                    if (mi.ReflectedType == null)
                    {
                        continue;
                    }
                    if (ignoreMethodNamesContaining != null && ignoreMethodNamesContaining.Any())
                    {
                        if (ignoreMethodNamesContaining.Contains(mi.Name, StringComparer.CurrentCultureIgnoreCase))
                        {
                            continue;
                        }
                    }
                    // Looks like the parameter value is not possible to obtain
                    string fullMethodName = $"{mi.ReflectedType.Name}.{mi.Name}";

                    if (!fullMethodName.Contains(methodNameContaining, StringComparison.CurrentCultureIgnoreCase))
                    {
                        continue;
                    }
                   
                    var parameterDictionary = mi.GetParameters().Select(mp => new
                    {
                        ParameterName = mp.Name,
                        ParameterType = mp.ParameterType.Name
                    }).ToDictionary(x => x.ParameterName, x => x.ParameterType);

                    var stackFrameInfo = new MethodStackFrameInfo
                    {
                        MethodName = fullMethodName,
                        MethodParameters = parameterDictionary
                    };
                    return stackFrameInfo;
                }

                return null;
            }
            catch (Exception err)
            {
                Debug.WriteLine(err);
                return null;
            }
        }

        public class MethodStackFrameInfo
        {
            public string MethodName { get; set; }

            public Dictionary<string, string> MethodParameters { get; set; } = new Dictionary<string, string>();

            public override string ToString()
            {
                return $"{MethodName}({string.Join(",", MethodParameters.Select(x => x.Value + " " + x.Key).ToArray())})";
            }
        }
    }
}
 
We return here an instance of MethodStackFrameInfo, where we get the method name and also the parameters given into the method with the value and the key, this means in this context the data type and the parameter name given as method parameter. It will return text like SomeClass.SomeMethod(Int32 somearg1, System.String somearg2). Hence you can log this information in a logger to understand which method was called, up the stack with a known name. This can be practical also in systems where you have some wrapping code and the code you want to inspect if was called is some stack frames up the stack.

Monday 23 January 2023

Creating a json string representing enum names and values in C#

A short blog post here demonstrating how to create a JSON string showing enum names and values for an enum in C#, simple stuff! From Linqpad 7:

void Main()
{
	var jsonEnum = EnumUtil.GenerateJsonForEnum<PasientOvertattEnum>();
	jsonEnum.Dump();	
}

public static class EnumUtil
{

	/// <summary>
	/// Generates a json string (array) for enum values
	/// Checked that it gives valid json array string here : https://jsonlint.com/
	/// </summary>
	public static string GenerateJsonForEnum<TEnum>()
	 where TEnum : struct, IConvertible
	{
		var enumItems = new List<object>();
		var sb = new StringBuilder();
		sb.AppendLine("[\n");
		bool isEnumValueFound = false;
		foreach (var enumValue in Enum.GetValues(typeof(TEnum)))
		{
			sb.AppendLine($@"	{{	""Name"": ""{enumValue}"", ""Value"": ""{(int)enumValue}""	}},");
			isEnumValueFound = true;
		}
		if (isEnumValueFound)
		{
			sb.Remove(sb.Length - 3, 1);
		}
		sb.AppendLine("\t]");
		return sb.ToString();
	}
}



This gives the following sample json string when testing:


[

  {  "Name": "Velgverdi", "Value": "0"  },
  {  "Name": "AkershusUniversitetssykehusHF", "Value": "1"  },
  {  "Name": "DiakonhjemmetSykehusAS", "Value": "2"  },
  {  "Name": "FinnmarkssykehusetHF", "Value": "3"  },
  {  "Name": "HaraldsplassDiakonaleSykehusAS", "Value": "4"  },
  {  "Name": "HelgelandssykehusetHF", "Value": "5"  },
  {  "Name": "HelseBergenHF", "Value": "6"  },
  {  "Name": "HelseFonnaHF", "Value": "7"  },
  {  "Name": "HelseFordeHF", "Value": "8"  },
  {  "Name": "HelseMoreogRomsdalHF", "Value": "9"  },
  {  "Name": "HelseNordTrondelagHF", "Value": "10"  },
  {  "Name": "HelseStavangerHF", "Value": "11"  },
  {  "Name": "LovisenbergDiakonaleSykehusAS", "Value": "12"  },
  {  "Name": "NordlandssykehusetHF", "Value": "13"  },
  {  "Name": "OsloUniversitetssykehusHF", "Value": "14"  },
  {  "Name": "SandvikaNevrosenter", "Value": "15"  },
  {  "Name": "StOlavshospitalHF", "Value": "16"  },
  {  "Name": "SykehusetiVestfoldHF", "Value": "17"  },
  {  "Name": "SykehusetInnlandetHF", "Value": "18"  },
  {  "Name": "SykehusetTelemarkHF", "Value": "19"  },
  {  "Name": "SykehusetOstfoldHF", "Value": "20"  },
  {  "Name": "SorlandetsykehusHF", "Value": "21"  },
  {  "Name": "UniversitetssykehusetNordNorgeHF", "Value": "22"  },
  {  "Name": "VestreVikenHF", "Value": "23"  },
  {  "Name": "Utenlands", "Value": "24"  },
  {  "Name": "Ukjent", "Value": "25"  }
  ]




The JSON string above has been tested and validated okay against : https://jsonlint.com/ So if you need to show the data contents of an enum into a Json, this is a simple way of doing this.

Saturday 7 January 2023

List patterns in C# 11 - And getting a compiler error on code that should work

I have tested out list patterns in .NET 7 and C#. I am using Linqpad 7 and .NET 7.0.1. List patterns are useful I guess to compared sequences and fun to test out. The '_' discard here means to ignore the number at a given position and the '..' range here is to match anything between a given set of value and one index and then a given value at a higher index with arbitrary values between. But in one of the samples, it says you can capture variables inside list patterns. I cannot make it work, I get a compiler error. I am getting a CS0165 'Use of unassigned local variable' error when I try to access the variable(s) captured. I tried checking the crashing code also inside VsCode, still getting the error, however if I debug inside Linqpad I can see the variables that are captured got values at least.

    var someOddNumbers = new int[] { 1, 3, 5, 7, 9, 11 };
    bool resultX = someOddNumbers is [1, 3, _, _, _, 11];
    resultX.Dump("The 'someOddNumbers' equals a sequence of numbers 1,3,then three arbitrary numbers, then 11?");

    bool isOdd = someOddNumbers is [1, .., 9, 11];
    isOdd.Dump("The 'someOddNumbers' equals a sequence of numbers 1, some arbitrary numbers, then ending with 9 and 11?");
    
    result = input is [var firstOddNumber,.. var lastOddNumber];

    if (result)
    {
        Console.WriteLine($"The captured variables are: {firstOddNumber} and {lastOddNumber}"); //this lines gives the CS0165 error
    }

If I comment out the if block I can run the code sample, and in the debugger I can see firstOddNumber and lastOddNumber being set to a value at runtime. But the C# 11 compiler seems to think this is illeagal code since it is using an unintialized variable. I expected to not get a compiler error and be able to also capture the variables defined in the list pattern. I cannot understand the usage of such variables if I cannot use them. I understand that these variables might not be captured if the list pattern does not match, but even when checking if a match was present, I got the compilation error. I can however run the code, just not access the variables.