Format stack trace to manually return in API response

Viewed 1047

Currently it is returning a single unintelligible chunk composed of multiple of these things glued one next to the other like this:

at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\\Repositories\\projectx\\src\\ProyectX.Services\\Service.cs:line 116\r\n at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\\Repositories\\projectx\\src\\ProyectX.Services\\Service.cs:line 116\r\n

Goal:

at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\Repositories\projectx\src\ProyectX.Services\Service.cs:line 116

at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\Repositories\projectx\src\ProyectX.Services\Service.cs:line 116

I tried with

Regex.Unescape(exception.StackTrace)

JsonSerializer.Serialize(exception.StackTrace, new JsonSerializerOptions() {WriteIndented = true });

The middleware is in Startup.cs

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseMiddleware<ErrorHandlerMiddleware>();

Middleweare:

using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using ProyectX.Domain.Exceptions;
using ProyectX.Domain.Models;

namespace ProyectX.API.Middleware
{
    public class ErrorHandlerMiddleware
    {
        private readonly RequestDelegate _next;

        public ErrorHandlerMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context, IWebHostEnvironment env)
        {
            try
            {
                await _next(context);
            }
            catch (Exception exception)
            {
                var response = context.Response;
                response.ContentType = "application/json";

                switch (exception)
                {
                    case InvalidOperationException:
                        response.StatusCode = (int)HttpStatusCode.BadRequest;
                        break;
                    default:
                        response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        break;
                }

                var details = new Dictionary<string, string>
                {
                    { "Message", exception.Message },
                };

                if (env.IsDevelopment())
                {
                    details.Add("StackTrace", exception.StackTrace);
                }

                var errorResponse = new ErrorResponseModel(exception, details);
                var result = JsonSerializer.Serialize(errorResponse);

                await response.WriteAsync(result);
            }
        }
    }
}

ErrorResponseModel

using System;
using System.Collections.Generic;

namespace ProyectX.Domain.Models
{
    public class ErrorResponseModel
    {
        public ErrorResponseModel(Exception ex, Dictionary<string, string> details)
        {
            Type = ex.GetType().Name;
            Details = details;
        }

        public string Type { get; set; }

        public IDictionary<string, string> Details { get; set; }
    }
}
2 Answers

I would suggest changing a bit in your ErrorResponseModel class so that your dictionary accept object type as value, like Dictionary<string, object>. Here comes the idea, that you can both added string OR string array.

So for your tacktrace, you can split it multiple line using split by "\r\n" and get a string array of it, that I can pass to my StackTrace value.

So lets come to your model:

public class ErrorResponseModel
{
    public ErrorResponseModel(Exception ex, Dictionary<string, object> details)
    {
        Type = ex.GetType().Name;
        Details = details;
    }

    public string Type { get; set; }

    public IDictionary<string, object> Details { get; set; }
}

Here is the part that handle exception:

var details = new Dictionary<string, object>
{
    { "Message", exception.Message} ,
};

var lines = exception.StackTrace?.Split("\r\n").Select(e => e.TrimStart());
details.Add("StackTrace", lines);

var errorResponse = new ErrorResponseModel(exception, details);

var result = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions() { WriteIndented = true });

All this return following out:

enter image description here

Exception information model using StackTrace and StackFrame classes

There's a StackTrace class which can help you to get list of StackFrame objects. Each frame, includes information like:

  • Line Number
  • Method (Class, Assembly)
  • Code File

Looking into ASP.NET Core ErrorPage and DeveloperExceptionPageMiddleware you will see the framework has created a model class including the exception information like stack frame and then in the error page has formatted the model and showed in a readable format.

This is how Exception class also generates the StackTrace: by calling StackTrace.ToString which gets the stack frames and format above information in a string.

You can also do the same and generate the string in a better format, or as a better idea, create an exception or stacktrace model and let the formatting be done later.

For example, this is the exception model (including stacktrace) that you can have:

{
  "Message":"Attempted to divide by zero.",
  "StackFrames":[
    {
      "LineNumber":13,
      "Method":"Main(String[] args)",
      "Class":"SampleConsoleApp.Program",
      "AssemblyName":"SampleConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
      "AssemblyFile":"file:///C:/SampleConsoleApp/bin/Debug/netcoreapp3.1/SampleConsoleApp.dll",
      "CodeFile":"C:\\SampleConsoleApp\\Program.cs"
    },
    {
      "LineNumber":23,
      "Method":"Divide(Int32 x, Int32 y)",
      "Class":"SampleConsoleApp.Program",
      "AssemblyName":"SampleConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
      "AssemblyFile":"file:///C:/SampleConsoleApp/bin/Debug/netcoreapp3.1/SampleConsoleApp.dll",
      "CodeFile":"C:\\SampleConsoleApp\\Program.cs"
     }
  ],
  "InnerException":null
}

Above json string is created from my exception model:

try
{
}
catch (Exception ex)
{
    var model = ExceptionModel.Create(ex);

    // You can convert it to json and store it
    // Or format it and write to log 
}

ExceptionModel including StackTrace

Here is how I've implemented exception model:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
public class ExceptionModel
{
    public string Message { get; set; }
    public IEnumerable<StackFrameModel> StackFrames { get; set; }
    public ExceptionModel InnerException { get; set; }
    public static ExceptionModel Create(Exception ex = null)
    {
        var trace = ex == null ? new StackTrace(true) : new StackTrace(ex, true);
        var model = new ExceptionModel();
        model.Message = ex?.Message;
        model.StackFrames = trace.GetFrames().Reverse()
            .Select(x => new StackFrameModel()
        {
            LineNumber = x.GetFileLineNumber(),
            Method = GetMethodSignature(x.GetMethod()),
            Class = x.GetMethod()?.DeclaringType?.FullName,
            AssemblyName = x.GetMethod()?.DeclaringType?.Assembly?.FullName,
            AssemblyFile = x.GetMethod()?.DeclaringType?.Assembly?.CodeBase,
            CodeFile = x.GetFileName(),
        });
        if (ex?.InnerException != null)
            model.InnerException = ExceptionModel.Create(ex.InnerException);
        return model;
    }
    private static string GetTypeName(Type type)
    {
        return type?.FullName?.Replace('+', '.');
    }
    private static string GetMethodSignature(MethodBase mb)
    {
        var sb = new StringBuilder();
        sb.Append(mb.Name);

        // deal with the generic portion of the method
        if (mb is MethodInfo && ((MethodInfo)mb).IsGenericMethod)
        {
            Type[] typars = ((MethodInfo)mb).GetGenericArguments();
            sb.Append("[");
            int k = 0;
            bool fFirstTyParam = true;
            while (k < typars.Length)
            {
                if (fFirstTyParam == false)
                    sb.Append(",");
                else
                    fFirstTyParam = false;

                sb.Append(typars[k].Name);
                k++;
            }
            sb.Append("]");
        }

        // arguments printing
        sb.Append("(");
        ParameterInfo[] pi = mb.GetParameters();
        bool fFirstParam = true;
        for (int j = 0; j < pi.Length; j++)
        {
            if (fFirstParam == false)
                sb.Append(", ");
            else
                fFirstParam = false;

            String typeName = "<UnknownType>";
            if (pi[j].ParameterType != null)
                typeName = pi[j].ParameterType.Name;
            sb.Append(typeName + " " + pi[j].Name);
        }
        sb.Append(")");
        return sb.ToString();
    }
}
public class StackFrameModel
{
    public int LineNumber { get; set; }
    public string Method { get; set; }
    public string Class { get; set; }
    public string AssemblyName { get; set; }
    public string AssemblyFile { get; set; }
    public string CodeFile { get; set; }
}
Related