Added some ObjectExtensions
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>.netstandard2.1; net8.0; net4.8;</TargetFrameworks>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<Version>1.4.8</Version>
|
||||
<Version>1.4.9</Version>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<FileVersion>1.4.8</FileVersion>
|
||||
<FileVersion>1.4.9</FileVersion>
|
||||
<Authors>EonaCat (Jeroen Saey)</Authors>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Company>EonaCat (Jeroen Saey)</Company>
|
||||
@@ -25,7 +25,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EVRevisionFormat>1.4.8+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVRevisionFormat>1.4.9+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVDefault>true</EVDefault>
|
||||
<EVInfo>true</EVInfo>
|
||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace EonaCat.Logger.Extensions;
|
||||
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
public static long ToUnixTimestamp(this DateTime dateTime)
|
||||
|
||||
@@ -5,6 +5,9 @@ using System.Text;
|
||||
|
||||
namespace EonaCat.Logger.Extensions;
|
||||
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
public static string FormatExceptionToMessage(this Exception exception, string module = null, string method = null)
|
||||
|
||||
425
EonaCat.Logger/Extensions/ObjectExtensions.cs
Normal file
425
EonaCat.Logger/Extensions/ObjectExtensions.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
using EonaCat.Json;
|
||||
using EonaCat.Json.Serialization;
|
||||
using EonaCat.Logger.Managers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace EonaCat.Logger.Extensions
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||
|
||||
public enum DumpFormat
|
||||
{
|
||||
Json,
|
||||
Xml,
|
||||
Tree
|
||||
}
|
||||
|
||||
public static class ObjectExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Dumps any object to a string in JSON, XML, or detailed tree format.
|
||||
/// </summary>
|
||||
/// <param name="currentObject">Object to dump</param>
|
||||
/// <param name="format">"json" (default), "xml", or "tree"</param>
|
||||
/// <param name="detailed">For JSON: include private/internal fields. Ignored for tree format</param>
|
||||
/// <param name="maxDepth">Optional max depth for tree dump. Null = no limit</param>
|
||||
/// <param name="maxCollectionItems">Optional max items to display in collections. Null = show all</param>
|
||||
/// <returns>String representation of the object</returns>
|
||||
public static string Dump(this object currentObject, DumpFormat format = DumpFormat.Json, bool detailed = false, int? maxDepth = null, int? maxCollectionItems = null)
|
||||
{
|
||||
if (currentObject == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case DumpFormat.Xml:
|
||||
return DumpXml(currentObject);
|
||||
case DumpFormat.Tree:
|
||||
return DumpTree(currentObject, maxDepth, maxCollectionItems);
|
||||
case DumpFormat.Json:
|
||||
default:
|
||||
return DumpJson(currentObject, detailed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error dumping object: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string DumpJson(object currentObject, bool isDetailed)
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
Formatting = Formatting.Indented
|
||||
};
|
||||
|
||||
if (isDetailed)
|
||||
{
|
||||
settings.ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
IgnoreSerializableAttribute = false,
|
||||
IgnoreSerializableInterface = false
|
||||
};
|
||||
}
|
||||
|
||||
return JsonHelper.ToJson(currentObject, settings);
|
||||
}
|
||||
|
||||
private static string DumpXml(object currentObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var xmlSerializer = new XmlSerializer(currentObject.GetType());
|
||||
using (var stringWriter = new StringWriter())
|
||||
{
|
||||
xmlSerializer.Serialize(stringWriter, currentObject);
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"XML serialization failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string DumpTree(object currentObject, int? maxDepth, int? maxCollectionItems)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
var visitedHashSet = new HashSet<object>(new ReferenceEqualityComparer());
|
||||
DumpTreeInternal(currentObject, stringBuilder, 0, visitedHashSet, maxDepth, maxCollectionItems);
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static void DumpTreeInternal(object currentObject, StringBuilder stringBuilder, int indent, HashSet<object> visited, int? maxDepth, int? maxCollectionItems)
|
||||
{
|
||||
string indentation = new string(' ', indent * 2);
|
||||
|
||||
if (currentObject == null)
|
||||
{
|
||||
stringBuilder.AppendLine($"{indentation}null");
|
||||
return;
|
||||
}
|
||||
|
||||
Type type = currentObject.GetType();
|
||||
string typeName = type.FullName;
|
||||
|
||||
if (IsPrimitive(type))
|
||||
{
|
||||
stringBuilder.AppendLine($"{indentation}{currentObject} ({typeName})");
|
||||
return;
|
||||
}
|
||||
|
||||
if (visited.Contains(currentObject))
|
||||
{
|
||||
stringBuilder.AppendLine($"{indentation}<<circular reference to {typeName}>>");
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxDepth.HasValue && indent >= maxDepth.Value)
|
||||
{
|
||||
stringBuilder.AppendLine($"{indentation}<<max depth reached: {typeName}>>");
|
||||
return;
|
||||
}
|
||||
|
||||
visited.Add(currentObject);
|
||||
|
||||
if (currentObject is IEnumerable enumerable && !(currentObject is string))
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (var _ in enumerable)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (maxCollectionItems.HasValue && count > maxCollectionItems.Value)
|
||||
{
|
||||
stringBuilder.AppendLine($"{indentation}{typeName} [<<{count} items, collapsed>>]");
|
||||
return;
|
||||
}
|
||||
|
||||
stringBuilder.AppendLine($"{indentation}{typeName} [");
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
DumpTreeInternal(item, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems);
|
||||
}
|
||||
stringBuilder.AppendLine($"{indentation}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.AppendLine($"{indentation}{typeName} {{");
|
||||
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
var members = type.GetFields(flags);
|
||||
|
||||
foreach (var field in members)
|
||||
{
|
||||
object value = null;
|
||||
|
||||
try
|
||||
{
|
||||
value = field.GetValue(currentObject);
|
||||
}
|
||||
catch
|
||||
{
|
||||
value = "<<unavailable>>";
|
||||
}
|
||||
|
||||
stringBuilder.Append($"{indentation} {field.Name} = ");
|
||||
DumpTreeInternal(value, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems);
|
||||
}
|
||||
|
||||
var properties = type.GetProperties(flags);
|
||||
|
||||
foreach (var current in properties)
|
||||
{
|
||||
if (current.GetIndexParameters().Length > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
object value = null;
|
||||
try { value = current.GetValue(currentObject); } catch { value = "<<unavailable>>"; }
|
||||
stringBuilder.Append($"{indentation} {current.Name} = ");
|
||||
DumpTreeInternal(value, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems);
|
||||
}
|
||||
|
||||
stringBuilder.AppendLine($"{indentation}}}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPrimitive(Type type)
|
||||
{
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal)
|
||||
|| type == typeof(DateTime)
|
||||
|| type == typeof(DateTimeOffset)
|
||||
|| type == typeof(Guid)
|
||||
|| type == typeof(TimeSpan);
|
||||
}
|
||||
|
||||
private class ReferenceEqualityComparer : IEqualityComparer<object>
|
||||
{
|
||||
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
|
||||
public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
|
||||
public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
|
||||
{
|
||||
if (items == null || action == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
action(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Check if collection is null or empty</summary>
|
||||
public static bool IsNullOrEmpty<T>(this IEnumerable<T> items) => items == null || !items.Any();
|
||||
|
||||
/// <summary>Check if collection has items</summary>
|
||||
public static bool HasItems<T>(this IEnumerable<T> items) => !items.IsNullOrEmpty();
|
||||
|
||||
/// <summary>Safe get by index</summary>
|
||||
public static T SafeGet<T>(this IList<T> list, int index, T defaultValue = default)
|
||||
{
|
||||
if (list == null || index < 0 || index >= list.Count)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return list[index];
|
||||
}
|
||||
|
||||
/// <summary>Convert collection to delimited string</summary>
|
||||
public static string ToDelimitedString<T>(this IEnumerable<T> items, string delimiter = ", ")
|
||||
{
|
||||
return items == null ? "" : string.Join(delimiter, items);
|
||||
}
|
||||
|
||||
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);
|
||||
|
||||
public static string Truncate(this string s, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
return s.Length <= maxLength ? s : s.Substring(0, maxLength);
|
||||
}
|
||||
|
||||
public static bool ContainsIgnoreCase(this string s, string value) =>
|
||||
s?.IndexOf(value ?? "", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
public static string OrDefault(this string s, string defaultValue) =>
|
||||
string.IsNullOrEmpty(s) ? defaultValue : s;
|
||||
|
||||
public static bool IsWeekend(this DateTime date) =>
|
||||
date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday;
|
||||
|
||||
public static DateTime StartOfDay(this DateTime date) =>
|
||||
date.Date;
|
||||
|
||||
public static DateTime EndOfDay(this DateTime date) =>
|
||||
date.Date.AddDays(1).AddTicks(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Log an object only if a condition is true.
|
||||
/// </summary>
|
||||
public static async Task LogIfAsync(this LogManager logger, object currentObject, Func<object, bool> condition,
|
||||
ELogType logType = ELogType.INFO, string message = null)
|
||||
{
|
||||
if (logger == null || currentObject == null || condition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (condition(currentObject))
|
||||
{
|
||||
string output = message ?? currentObject.Dump();
|
||||
await logger.WriteAsync(output, logType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static IDisposable BeginLoggingScope(this ILogger logger, object context)
|
||||
{
|
||||
if (logger == null || context == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return logger.BeginScope(context.ToDictionary());
|
||||
}
|
||||
|
||||
public static void LogExecutionTime(this ILogger logger, Action action, string operationName)
|
||||
{
|
||||
if (logger == null || action == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
action();
|
||||
sw.Stop();
|
||||
logger.LogInformation("{Operation} executed in {ElapsedMilliseconds}ms", operationName, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Unix timestamp, expressed as the number of seconds since the Unix epoch, to a local DateTime
|
||||
/// value.
|
||||
/// </summary>
|
||||
/// <remarks>The returned DateTime is expressed in the local time zone. To obtain a UTC DateTime,
|
||||
/// use DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime instead.</remarks>
|
||||
/// <param name="timestamp">The Unix timestamp representing the number of seconds that have elapsed since 00:00:00 UTC on 1 January
|
||||
/// 1970.</param>
|
||||
/// <returns>A DateTime value that represents the local date and time equivalent of the specified Unix timestamp.</returns>
|
||||
public static DateTime FromUnixTimestamp(this long timestamp) =>
|
||||
DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified task without waiting for its completion and handles any exceptions that occur during
|
||||
/// its execution.
|
||||
/// </summary>
|
||||
/// <remarks>Use this method to start a task when you do not need to await its completion but want
|
||||
/// to ensure that exceptions are observed. This method should be used with caution, as exceptions may be
|
||||
/// handled asynchronously and may not be propagated to the calling context. Avoid using this method for tasks
|
||||
/// that must complete before continuing execution.</remarks>
|
||||
/// <param name="task">The task to execute in a fire-and-forget manner. Cannot be null.</param>
|
||||
/// <param name="onError">An optional callback that is invoked if the task throws an exception. If not provided, exceptions are
|
||||
/// written to the console.</param>
|
||||
public static async void FireAndForget(this Task task, Action<Exception> onError = null)
|
||||
{
|
||||
if (task == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try { await task; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (onError != null)
|
||||
{
|
||||
onError(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("FireAndForget Exception: " + ex.FormatExceptionToMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Check if object has property</summary>
|
||||
public static bool HasProperty(this object obj, string name) =>
|
||||
obj != null && obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) != null;
|
||||
|
||||
/// <summary>Get property value safely</summary>
|
||||
public static object GetPropertyValue(this object obj, string name)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var prop = obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
return prop?.GetValue(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dictionary containing the public and non-public instance properties and fields of the specified
|
||||
/// object.
|
||||
/// </summary>
|
||||
/// <remarks>Indexed properties are excluded from the resulting dictionary. Both public and
|
||||
/// non-public instance members are included. If multiple members share the same name, property values will
|
||||
/// overwrite field values with the same name.</remarks>
|
||||
/// <param name="obj">The object whose properties and fields are to be included in the dictionary. Can be null.</param>
|
||||
/// <returns>A dictionary with the names and values of the object's properties and fields. If the object is null, returns
|
||||
/// an empty dictionary.</returns>
|
||||
public static Dictionary<string, object> ToDictionary(this object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
var dict = new Dictionary<string, object>();
|
||||
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
foreach (var prop in obj.GetType().GetProperties(flags))
|
||||
{
|
||||
if (prop.GetIndexParameters().Length > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dict[prop.Name] = prop.GetValue(obj);
|
||||
}
|
||||
foreach (var field in obj.GetType().GetFields(flags))
|
||||
{
|
||||
dict[field.Name] = field.GetValue(obj);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,21 @@ namespace EonaCat.Logger.Managers
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task WriteAsync(object currentObject, ELogType logType = ELogType.INFO, bool? writeToConsole = null,
|
||||
string customSplunkSourceType = null,
|
||||
string grayLogFacility = null, string grayLogSource = null,
|
||||
string grayLogVersion = "1.1", bool disableSplunkSSL = false, DumpFormat dumpFormat = DumpFormat.Json, bool isDetailedDump = false, int? dumpDepth = null, int? maxCollectionItems = null)
|
||||
{
|
||||
if (currentObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await WriteAsync(currentObject.Dump(dumpFormat, isDetailedDump, dumpDepth, maxCollectionItems), logType, writeToConsole,
|
||||
customSplunkSourceType, grayLogFacility, grayLogSource,
|
||||
grayLogVersion, disableSplunkSSL);
|
||||
}
|
||||
|
||||
public async Task WriteAsync(Exception exception, string module = null, string method = null,
|
||||
bool criticalException = false,
|
||||
bool? writeToConsole = null, string customSplunkSourceType = null, string grayLogFacility = null,
|
||||
|
||||
Reference in New Issue
Block a user