Reputation: 1739
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; }
}
}
Upvotes: 5
Views: 2862
Reputation: 33427
I would suggest you make changes to your ErrorResponseModel
class so that your dictionary accepts object type as value, like Dictionary<string, object>
. Here comes the idea, with object type the dictionary value can accept adding string OR string array.
So for your StackTrace
, you can split it into multiple lines using split by "\r\n" and get a string array of it, that I can pass to my StackTrace
value.
So let's 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 handles the 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 returns the following:
Upvotes: 4
Reputation: 125247
There's a StackTrace class which can help you to get list of StackFrame objects. Each frame, includes information like:
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; }
}
Upvotes: 3