// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace EpicGames.Core { /// /// Methods for adding context information to exceptions /// public static class ExceptionUtils { /// /// Unique key name for adding context to exceptions thrown inside Epic apps /// const string ContextEntryName = "EpicGames.Context"; /// /// Adds a context message to a list stored on the given exception. Intended to be used in situations where supplying additional context /// for an exception is valuable, but wrapping it would remove information. /// /// The exception that was thrown /// Message to append to the context list public static void AddContext(Exception ex, string message) { List? messages = ex.Data[ContextEntryName] as List; if (messages == null) { messages = []; ex.Data[ContextEntryName] = messages; } messages.Add(message); } /// /// Adds a context message to a list stored on the given exception. Intended to be used in situations where supplying additional context /// for an exception is valuable, but wrapping it would remove information. /// /// The exception that was thrown /// Formatting string for /// Message to append to the context list public static void AddContext(Exception ex, string format, params object[] args) { AddContext(ex, String.Format(format, args)); } /// /// Enumerates the context lines from the given exception /// /// The exception to retrieve context from /// Sequence of context lines public static IEnumerable GetContext(Exception ex) { List? messages = ex.Data[ContextEntryName] as List; if (messages != null) { foreach (string message in messages) { yield return message; } } } /// /// Formats an exception for display in the log, including additional lines of context that were attached to it. /// /// The exception to format /// String containing the exception information. May be multiple lines. public static string FormatException(Exception ex) { StringBuilder errorMessage = new StringBuilder(); if (ex is AggregateException exAgg) { Exception? innerException = exAgg.InnerException; if (innerException != null) { errorMessage.Append(innerException.ToString()); foreach (string line in GetContext(innerException)) { errorMessage.AppendFormat("\n {0}", line); } } } else { errorMessage.Append(ex.ToString()); } foreach (string line in GetContext(ex)) { errorMessage.AppendFormat("\n{0}", line); } return errorMessage.Replace("\r\n", "\n").ToString(); } /// /// Formats a detailed information about where an exception occurs, including any inner exceptions /// /// The exception to format /// String containing the exception information. May be multiple lines. public static string FormatExceptionDetails(Exception ex) { List exceptionStack = []; for (Exception? currentEx = ex; currentEx != null; currentEx = currentEx.InnerException) { exceptionStack.Add(currentEx); } StringBuilder message = new StringBuilder(); for (int idx = exceptionStack.Count - 1; idx >= 0; idx--) { Exception currentEx = exceptionStack[idx]; message.AppendFormat("{0}{1}: {2}\n{3}", (idx == exceptionStack.Count - 1) ? "" : "Wrapped by ", currentEx.GetType().Name, currentEx.Message, currentEx.StackTrace); if (currentEx.Data.Count > 0) { foreach (object? key in currentEx.Data.Keys) { if (key == null) { continue; } object? value = currentEx.Data[key]; if (value == null) { continue; } string valueString; if(value is List valueList) { valueString = String.Format("({0})", String.Join(", ", valueList.Select(x => String.Format("\"{0}\"", x)))); } else { valueString = value.ToString() ?? String.Empty; } message.AppendFormat(" data: {0} = {1}", key, valueString); } } } return message.Replace("\r\n", "\n").ToString(); } } }