From b78676e244810d7fdf02bd2ed61925b0d48fdbfd Mon Sep 17 00:00:00 2001 From: upstream Date: Wed, 2 Sep 2026 01:50:51 +0200 Subject: [PATCH] Upstream NLog/NLog state of the touched files before cc54b6d1e1 --- src/NLog/Internal/ObjectReflectionCache.cs | 816 ++++++++++++++++++ src/NLog/Targets/DefaultJsonSerializer.cs | 758 ++++++++++++++++ src/NLog/Targets/JsonSerializeOptions.cs | 108 +++ .../DefaultJsonSerializerClassTests.cs | 135 +++ 4 files changed, 1817 insertions(+) create mode 100644 src/NLog/Internal/ObjectReflectionCache.cs create mode 100644 src/NLog/Targets/DefaultJsonSerializer.cs create mode 100644 src/NLog/Targets/JsonSerializeOptions.cs create mode 100644 tests/NLog.UnitTests/Targets/DefaultJsonSerializerClassTests.cs diff --git a/src/NLog/Internal/ObjectReflectionCache.cs b/src/NLog/Internal/ObjectReflectionCache.cs new file mode 100644 index 0000000..c7e9df6 --- /dev/null +++ b/src/NLog/Internal/ObjectReflectionCache.cs @@ -0,0 +1,816 @@ +// +// Copyright (c) 2004-2024 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of Jaroslaw Kowalski nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// + +namespace NLog.Internal +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection; + using NLog.Common; + using NLog.Config; + + /// + /// Converts object into a List of property-names and -values using reflection + /// + internal sealed class ObjectReflectionCache : IObjectTypeTransformer + { + private MruCache ObjectTypeCache => _objectTypeCache ?? System.Threading.Interlocked.CompareExchange(ref _objectTypeCache, new MruCache(10000), null) ?? _objectTypeCache; + private MruCache? _objectTypeCache; + private readonly IServiceProvider _serviceProvider; + private IObjectTypeTransformer ObjectTypeTransformation => _objectTypeTransformation ?? (_objectTypeTransformation = _serviceProvider?.GetService() ?? this); + private IObjectTypeTransformer? _objectTypeTransformation; + + public ObjectReflectionCache(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + object? IObjectTypeTransformer.TryTransformObject(object? obj) + { + return null; + } + + public ObjectPropertyList LookupObjectProperties(object value) + { + if (TryLookupExpandoObject(value, out var propertyValues)) + { + return propertyValues; + } + + if (!ReferenceEquals(ObjectTypeTransformation, this)) + { + var result = ObjectTypeTransformation.TryTransformObject(value); + if (result != null) + { + if (ConvertSimpleToString(result)) + { + return new ObjectPropertyList(result, ObjectPropertyInfos.SimpleToString.Properties, ObjectPropertyInfos.SimpleToString.FastLookup); + } + + if (TryLookupExpandoObject(result, out propertyValues)) + { + return propertyValues; + } + + value = result; + } + } + + var objectType = value.GetType(); + var propertyInfos = CheckForObjectProperties(value) ? BuildObjectPropertyInfos(value) : ObjectPropertyInfos.SimpleToString; + ObjectTypeCache.TryAddValue(objectType, propertyInfos); + return new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + } + + /// + /// Try get value from , using , and set into + /// + public bool TryGetObjectProperty(object? value, string[]? objectPath, out object? foundValue) + { + foundValue = null; + + if (objectPath is null) + { + return false; + } + + for (int i = 0; i < objectPath.Length; ++i) + { + if (value is null) + { + return false; + } + + var propertyList = LookupObjectProperties(value); + if (propertyList.TryGetPropertyValue(objectPath[i], out var propertyValue)) + { + value = propertyValue.Value; + } + else + { + foundValue = null; + return false; //Wrong, but done + } + } + + foundValue = value; + return true; + } + + public bool TryLookupExpandoObject(object value, out ObjectPropertyList objectPropertyList) + { + if (value is IDictionary expando) + { + objectPropertyList = new ObjectPropertyList(expando); + return true; + } + +#if !NET35 && !NET40 && NETFRAMEWORK + if (value is System.Dynamic.DynamicObject d) + { + var dictionary = DynamicObjectToDict(d); + objectPropertyList = new ObjectPropertyList(dictionary); + return true; + } +#endif + + Type objectType = value.GetType(); + if (ObjectTypeCache.TryGetValue(objectType, out var propertyInfos)) + { + if (!propertyInfos.HasFastLookup) + { + var fastLookup = BuildFastLookup(propertyInfos.Properties, false); + propertyInfos = new ObjectPropertyInfos(propertyInfos.Properties, fastLookup); + ObjectTypeCache.TryAddValue(objectType, propertyInfos); + } + objectPropertyList = new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + return true; + } + + var dictionaryEnumerator = TryGetDictionaryEnumerator(value); + if (dictionaryEnumerator != null) + { + propertyInfos = new ObjectPropertyInfos(ArrayHelper.Empty(), new[] { new FastPropertyLookup(string.Empty, TypeCode.Object, (o, p) => dictionaryEnumerator.GetEnumerator(o)) }); + ObjectTypeCache.TryAddValue(objectType, propertyInfos); + objectPropertyList = new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + return true; + } + + objectPropertyList = default(ObjectPropertyList); + return false; + } + + [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2072")] + private static ObjectPropertyInfos BuildObjectPropertyInfos(object value) + { + var properties = GetPublicProperties(value.GetType()); + if (value is Exception) + { + // Special handling of Exception (Include Exception-Type as artificial first property) + var fastLookup = BuildFastLookup(properties, true); + return new ObjectPropertyInfos(properties, fastLookup); + } + else if (properties.Length == 0) + { + return ObjectPropertyInfos.SimpleToString; + } + else + { + return new ObjectPropertyInfos(properties, null); + } + } + + private static bool CheckForObjectProperties(object objectValue) + { + if (ConvertSimpleToString(objectValue)) + return false; + + if (objectValue is Uri) + return false; + + if (objectValue is Delegate) + return false; // Skip serializing all types in the application + + if (objectValue is MemberInfo) + return false; // Skip serializing all types in the application + + if (objectValue is Assembly) + return false; // Skip serializing all types in the application + + if (objectValue is Module) + return false; // Skip serializing all types in the application + + if (objectValue is System.IO.Stream) + return false; // Skip serializing properties that often throws exceptions + + return true; + } + + private static bool ConvertSimpleToString(object objectValue) + { + if (objectValue is IFormattable) + return true; + + if (objectValue is IConvertible convertible) + return convertible.GetTypeCode() != TypeCode.Object; + + return false; + } + + private static PropertyInfo[] GetPublicProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) + { + PropertyInfo[]? properties = null; + + try + { + properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + } + catch (Exception ex) + { + InternalLogger.Warn(ex, "Failed to get object properties for type: {0}", type); + } + + // Skip Index-Item-Properties (Ex. public string this[int Index]) + if (properties != null) + { + foreach (var prop in properties) + { + if (!prop.IsValidPublicProperty()) + { + properties = properties.Where(p => p.IsValidPublicProperty()).ToArray(); + break; + } + } + } + + return properties ?? ArrayHelper.Empty(); + } + + private static FastPropertyLookup[] BuildFastLookup(PropertyInfo[] properties, bool includeType) + { + int fastAccessIndex = includeType ? 1 : 0; + FastPropertyLookup[] fastLookup = new FastPropertyLookup[properties.Length + fastAccessIndex]; + if (includeType) + { + fastLookup[0] = new FastPropertyLookup("Type", TypeCode.String, (o, p) => o.GetType().ToString()); + } + + foreach (var prop in properties) + { + var getterMethod = prop.GetGetMethod(); + var propertyType = getterMethod?.ReturnType; + if (getterMethod is null || propertyType is null) + { + // Should never happen due to IsValidPublicProperty check + fastLookup[fastAccessIndex++] = new FastPropertyLookup(prop.Name, TypeCode.String, (o, p) => null); + } + else + { + ReflectionHelpers.LateBoundMethod valueLookup = ReflectionHelpers.CreateLateBoundMethod(getterMethod); + TypeCode typeCode = Type.GetTypeCode(propertyType); // Skip cyclic-reference checks when not TypeCode.Object + fastLookup[fastAccessIndex++] = new FastPropertyLookup(prop.Name, typeCode, valueLookup); + } + } + return fastLookup; + } + + internal +#if !NETFRAMEWORK + readonly +#endif + struct ObjectPropertyList : IEnumerable + { + internal static readonly StringComparer NameComparer = StringComparer.Ordinal; + private static readonly FastPropertyLookup[] CreateIDictionaryEnumerator = new[] { new FastPropertyLookup(string.Empty, TypeCode.Object, (o, p) => ((IDictionary)o).GetEnumerator()) }; + private readonly object _object; + private readonly PropertyInfo[] _properties; + private readonly FastPropertyLookup[]? _fastLookup; + + public +#if !NETFRAMEWORK + readonly +#endif + struct PropertyValue + { + public readonly string Name; + public readonly object? Value; + public TypeCode TypeCode => Value is null ? TypeCode.Empty : _typecode; + private readonly TypeCode _typecode; + public bool HasNameAndValue => Name != null && Value != null; + public PropertyValue(string name, object value, TypeCode typeCode) + { + Name = name; + Value = value; + _typecode = typeCode; + } + + public PropertyValue(object owner, PropertyInfo propertyInfo) + { + Name = propertyInfo.Name; + _typecode = TypeCode.Object; + try + { + Value = propertyInfo.GetValue(owner, null); + } + catch + { + Value = null; + } + } + + public PropertyValue(object owner, FastPropertyLookup fastProperty) + { + Name = fastProperty.Name; + _typecode = fastProperty.TypeCode; + try + { + Value = fastProperty.ValueLookup(owner, ArrayHelper.Empty()); + } + catch + { + Value = null; + } + } + + public override string ToString() => $"{Name}={Value}"; + } + + public bool IsSimpleValue => _properties.Length == 0 && (_fastLookup is null || _fastLookup.Length == 0); + + public object ObjectValue => _object; + + internal ObjectPropertyList(object value, PropertyInfo[] properties, FastPropertyLookup[]? fastLookup) + { + _object = value; + _properties = properties; + _fastLookup = fastLookup; + } + + public ObjectPropertyList(IDictionary value) + { + _object = value; // Expando objects + _properties = ArrayHelper.Empty(); + _fastLookup = CreateIDictionaryEnumerator; + } + + public bool TryGetPropertyValue(string name, out PropertyValue propertyValue) + { + if (_properties.Length == 0) + { + if (_object is IDictionary expandoObject) + { + if (expandoObject.TryGetValue(name, out var objectValue)) + { + propertyValue = new PropertyValue(name, objectValue, TypeCode.Object); + return true; + } + propertyValue = default(PropertyValue); + return false; + } + + return TryListLookupPropertyValue(name, out propertyValue); + } + + if (_fastLookup != null) + { + // Scans properties for name (Skips string-compare and value-lookup until finding match) + int nameHashCode = NameComparer.GetHashCode(name); + foreach (var fastProperty in _fastLookup) + { + if (fastProperty.NameHashCode == nameHashCode && NameComparer.Equals(fastProperty.Name, name)) + { + propertyValue = new PropertyValue(_object, fastProperty); + return true; + } + } + propertyValue = default(PropertyValue); + return false; + } + + return TrySlowLookupPropertyValue(name, out propertyValue); + } + + /// + /// Scans properties for name (Skips property value lookup until finding match) + /// + private bool TrySlowLookupPropertyValue(string name, out PropertyValue propertyValue) + { + foreach (var propInfo in _properties) + { + if (NameComparer.Equals(propInfo.Name, name)) + { + propertyValue = new PropertyValue(_object, propInfo); + return true; + } + } + propertyValue = default(PropertyValue); + return false; + } + + /// + /// Scans properties for name + /// + private bool TryListLookupPropertyValue(string name, out PropertyValue propertyValue) + { + foreach (var item in this) + { + if (NameComparer.Equals(item.Name, name)) + { + propertyValue = item; + return true; + } + } + propertyValue = default(PropertyValue); + return false; + } + + public override string ToString() + { + return _object?.ToString() ?? "null"; + } + + public Enumerator GetEnumerator() + { + if (_properties.Length > 0 || _fastLookup is null || _fastLookup.Length == 0) + return new Enumerator(_object, _properties, _fastLookup); + + var expandoObject = _fastLookup[0].ValueLookup(_object, ArrayHelper.Empty()); + return expandoObject is null ? default(Enumerator) : new Enumerator((IEnumerator>)expandoObject); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private readonly object _owner; + private readonly PropertyInfo[]? _properties; + private readonly FastPropertyLookup[]? _fastLookup; + private readonly IEnumerator>? _enumerator; + private int _index; + + internal Enumerator(object owner, PropertyInfo[] properties, FastPropertyLookup[]? fastLookup) + { + _owner = owner; + _properties = properties; + _fastLookup = fastLookup; + _index = -1; + _enumerator = null; + } + + internal Enumerator(IEnumerator> enumerator) + { + _owner = enumerator; + _properties = null; + _fastLookup = null; + _index = 0; + _enumerator = enumerator; + } + + public PropertyValue Current + { + get + { + try + { + if (_fastLookup != null) + return new PropertyValue(_owner, _fastLookup[_index]); + else if (_properties != null) + return new PropertyValue(_owner, _properties[_index]); + else if (_enumerator != null) + return new PropertyValue(_enumerator.Current.Key, _enumerator.Current.Value, TypeCode.Object); + else + return default(PropertyValue); + } + catch (Exception ex) + { + InternalLogger.Debug(ex, "Failed to get property value for object: {0}", _owner); + return default(PropertyValue); + } + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + _enumerator?.Dispose(); + } + + public bool MoveNext() + { + if (_properties != null) + return ++_index < (_fastLookup?.Length ?? _properties.Length); + else + return _enumerator?.MoveNext() == true; + } + + public void Reset() + { + if (_properties != null) + _index = -1; + else + _enumerator?.Reset(); + } + } + } + + internal +#if !NETFRAMEWORK + readonly +#endif + struct FastPropertyLookup + { + public readonly string Name; + public readonly ReflectionHelpers.LateBoundMethod ValueLookup; + public readonly TypeCode TypeCode; + public readonly int NameHashCode; + + public FastPropertyLookup(string name, TypeCode typeCode, ReflectionHelpers.LateBoundMethod valueLookup) + { + Name = name; + ValueLookup = valueLookup; + TypeCode = typeCode; + NameHashCode = ObjectPropertyList.NameComparer.GetHashCode(name); + } + } + + private +#if !NETFRAMEWORK + readonly +#endif + struct ObjectPropertyInfos : IEquatable + { + public readonly PropertyInfo[] Properties; + public readonly FastPropertyLookup[]? FastLookup; + + public static readonly ObjectPropertyInfos SimpleToString = new ObjectPropertyInfos(ArrayHelper.Empty(), ArrayHelper.Empty()); + + public ObjectPropertyInfos(PropertyInfo[] properties, FastPropertyLookup[]? fastLookup) + { + Properties = properties; + FastLookup = fastLookup; + } + + public bool HasFastLookup => FastLookup != null; + + public bool Equals(ObjectPropertyInfos other) + { + return ReferenceEquals(Properties, other.Properties) && FastLookup?.Length == other.FastLookup?.Length; + } + } + +#if !NET35 && !NET40 && NETFRAMEWORK + private static Dictionary DynamicObjectToDict(System.Dynamic.DynamicObject d) + { + var newVal = new Dictionary(); + foreach (var propName in d.GetDynamicMemberNames()) + { + if (d.TryGetMember(new GetBinderAdapter(propName), out var result)) + { + newVal[propName] = result; + } + } + + return newVal; + } + + /// + /// Binder for retrieving value of + /// + private sealed class GetBinderAdapter : System.Dynamic.GetMemberBinder + { + internal GetBinderAdapter(string name) + : base(name, false) + { + } + + /// + public override System.Dynamic.DynamicMetaObject FallbackGetMember(System.Dynamic.DynamicMetaObject target, System.Dynamic.DynamicMetaObject errorSuggestion) + { + return target; + } + } +#endif + + private static IDictionaryEnumerator? TryGetDictionaryEnumerator(object value) + { + if (!(value is IEnumerable) || value is string) + return null; + + if (value is IDictionary) + return new DictionaryEnumerator(); + if (value is IDictionary) + return new DictionaryEnumerator(); +#if !NET35 && !NET40 + if (value is IReadOnlyDictionary) + return new DictionaryEnumerator(); + if (value is IReadOnlyDictionary) + return new DictionaryEnumerator(); +#endif + + if (value is IDictionary && value.GetType().IsGenericType) + { + if (value.GetType().GetGenericArguments()[0] == typeof(string)) + return new DictionaryEnumerator(DictionaryEnumerator.BuildDictionaryEnumerator); + else + return null; + } + + return TryBuildDictionaryEnumerator(value); + } + + [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2075")] + private static IDictionaryEnumerator? TryBuildDictionaryEnumerator(object value) + { + foreach (var interfaceType in value.GetType().GetInterfaces()) + { + if (interfaceType.IsGenericType + && (interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>) +#if !NET35 && !NET40 + || interfaceType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>) +#endif + ) + && interfaceType.GetGenericArguments()[0] == typeof(string)) + { +#if NETFRAMEWORK + return (IDictionaryEnumerator)Activator.CreateInstance(typeof(DictionaryEnumerator<,>).MakeGenericType(interfaceType.GetGenericArguments())); +#else + // AOT does not support creating new types at runtime + foreach (var subInterfaceType in value.GetType().GetInterfaces()) + { + if (subInterfaceType.IsGenericType + && subInterfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + && subInterfaceType.GetGenericArguments()[0].IsGenericType + && subInterfaceType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(KeyValuePair<,>) + && subInterfaceType.GetGenericArguments()[0].GetGenericArguments()[0] == typeof(string)) + { + var enumerator = DictionaryEnumerator.BuildEnumerator(subInterfaceType, subInterfaceType.GetGenericArguments()[0]); + if (enumerator != null) + return new DictionaryEnumerator(enumerator); + } + } +#endif + } + } + + return null; + } + + private interface IDictionaryEnumerator + { + IEnumerator> GetEnumerator(object value); + } + + private sealed class DictionaryEnumerator : IDictionaryEnumerator + { + private readonly Func>> _enumerateCollection; + + public DictionaryEnumerator(Func>> enumerateCollection) + { + _enumerateCollection = enumerateCollection; + } + + IEnumerator> IDictionaryEnumerator.GetEnumerator(object value) + { + return _enumerateCollection(value); + } + + public static IEnumerator> BuildDictionaryEnumerator(object collection) + { + if (collection is IDictionary dictionary) + { + if (dictionary.Count == 0) + return EmptyDictionaryEnumerator.Default; + return YieldEnumerator(dictionary); + } + return EmptyDictionaryEnumerator.Default; + } + + private static IEnumerator> YieldEnumerator(IDictionary dictionary) + { + foreach (var item in new DictionaryEntryEnumerable(dictionary)) + yield return new KeyValuePair(item.Key?.ToString() ?? string.Empty, item.Value); + } + +#if !NETFRAMEWORK + [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2075")] + [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2070")] + public static Func>>? BuildEnumerator(Type dictionaryType, Type dictionaryItemType) + { + var enumeratorMethod = dictionaryType.GetMethod(nameof(IEnumerable.GetEnumerator)); + var getKeyMethod = dictionaryItemType.GetProperty(nameof(KeyValuePair.Key))?.GetGetMethod(); + var getValueMethod = dictionaryItemType.GetProperty(nameof(KeyValuePair.Value))?.GetGetMethod(); + if (enumeratorMethod != null && getKeyMethod != null && getValueMethod != null) + { + var enumeratorGetter = ReflectionHelpers.CreateLateBoundMethod(enumeratorMethod); + var keyGetter = ReflectionHelpers.CreateLateBoundMethod(getKeyMethod); + var valueGetter = ReflectionHelpers.CreateLateBoundMethod(getValueMethod); + return (collection) => + { + if (collection is IEnumerable && dictionaryType.IsInstanceOfType(collection)) + { + var enumerator = (IEnumerator?)enumeratorGetter.Invoke(collection, ArrayHelper.Empty()); + if (enumerator is null || !enumerator.MoveNext()) + { + (enumerator as IDisposable)?.Dispose(); + return EmptyDictionaryEnumerator.Default; + } + return EnumerateItems(enumerator, keyGetter, valueGetter); + } + return EmptyDictionaryEnumerator.Default; + }; + } + + return null; + } + + private static IEnumerator> EnumerateItems(IEnumerator enumerator, ReflectionHelpers.LateBoundMethod getKeyProperty, ReflectionHelpers.LateBoundMethod getValueProperty) + { + try + { + do + { + var item = enumerator.Current; + var keyString = getKeyProperty.Invoke(item, ArrayHelper.Empty()); + var valueObject = getValueProperty.Invoke(item, ArrayHelper.Empty()); + yield return new KeyValuePair(keyString?.ToString() ?? string.Empty, valueObject); + } while (enumerator.MoveNext()); + } + finally + { + (enumerator as IDisposable)?.Dispose(); + } + } +#endif + } + + private sealed class DictionaryEnumerator : IDictionaryEnumerator + { + public IEnumerator> GetEnumerator(object value) + { + if (value is IDictionary dictionary) + { + if (dictionary.Count > 0) + return YieldEnumerator(dictionary); + } +#if !NET35 && !NET40 + else if (value is IReadOnlyDictionary readonlyDictionary) + { + if (readonlyDictionary.Count > 0) + return YieldEnumerator(readonlyDictionary); + } +#endif + return EmptyDictionaryEnumerator.Default; + } + + private static IEnumerator> YieldEnumerator(IDictionary dictionary) + { + foreach (var item in dictionary) + yield return new KeyValuePair(item.Key?.ToString() ?? string.Empty, item.Value); + } + +#if !NET35 && !NET40 + private static IEnumerator> YieldEnumerator(IReadOnlyDictionary dictionary) + { + foreach (var item in dictionary) + yield return new KeyValuePair(item.Key?.ToString() ?? string.Empty, item.Value); + } +#endif + } + + private sealed class EmptyDictionaryEnumerator : IEnumerator> + { + public static readonly EmptyDictionaryEnumerator Default = new EmptyDictionaryEnumerator(); + + KeyValuePair IEnumerator>.Current => default(KeyValuePair); + + object IEnumerator.Current => default(KeyValuePair); + + bool IEnumerator.MoveNext() => false; + + void IDisposable.Dispose() + { + // Nothing here on purpose + } + + void IEnumerator.Reset() + { + // Nothing here on purpose + } + } + } +} diff --git a/src/NLog/Targets/DefaultJsonSerializer.cs b/src/NLog/Targets/DefaultJsonSerializer.cs new file mode 100644 index 0000000..a31e523 --- /dev/null +++ b/src/NLog/Targets/DefaultJsonSerializer.cs @@ -0,0 +1,758 @@ +// +// Copyright (c) 2004-2024 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of Jaroslaw Kowalski nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// + +namespace NLog.Targets +{ + using System; + using System.Collections; + using System.ComponentModel; + using System.Globalization; + using System.Text; + using NLog.Internal; + + /// + /// Default class for serialization of values to JSON format. + /// + public class DefaultJsonSerializer : IJsonConverter + { + private readonly ObjectReflectionCache _objectReflectionCache; + private readonly MruCache _enumCache = new MruCache(4000); + + private const int MaxJsonLength = 512 * 1024; + + private static JsonSerializeOptions DefaultSerializerOptions = new JsonSerializeOptions(); + private static JsonSerializeOptions DefaultExceptionSerializerOptions = new JsonSerializeOptions() { SanitizeDictionaryKeys = true }; + + /// + /// Singleton instance of the serializer. + /// + [Obsolete("Instead use ResolveService() in Layout / Target. Marked obsolete on NLog 5.0")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static DefaultJsonSerializer Instance => _instance ?? (_instance = new DefaultJsonSerializer(LogManager.LogFactory.ServiceRepository)); + private static DefaultJsonSerializer? _instance; + + /// + /// Private. Use + /// + internal DefaultJsonSerializer(IServiceProvider serviceProvider) + { + _objectReflectionCache = new ObjectReflectionCache(serviceProvider); + } + + /// + /// Returns a serialization of an object into JSON format. + /// + /// The object to serialize to JSON. + /// Serialized value. + public string SerializeObject(object value) + { + return SerializeObject(value, DefaultSerializerOptions); + } + + /// + /// Returns a serialization of an object into JSON format. + /// + /// The object to serialize to JSON. + /// serialization options + /// Serialized value. + public string SerializeObject(object value, JsonSerializeOptions options) + { + if (value is null) + { + return "null"; + } + else if (value is string str) + { + var escapeUnicode = options.EscapeUnicode; + foreach (var chr in str) + { + if (RequiresJsonEscape(chr, escapeUnicode)) + { + StringBuilder sb = new StringBuilder(str.Length + 4); + sb.Append('"'); + AppendStringEscape(sb, str, options); + sb.Append('"'); + return sb.ToString(); + } + } + return QuoteValue(str); + } + else + { + if (value is IConvertible convertibleValue) + { + TypeCode objTypeCode = convertibleValue.GetTypeCode(); + if (objTypeCode != TypeCode.Object) + { + if (!options.EnumAsInteger && IsNumericTypeCode(objTypeCode, false) && convertibleValue is Enum enumValue) + { + return QuoteValue(EnumAsString(enumValue)); + } + else + { + string xmlStr = XmlHelper.XmlConvertToString(convertibleValue, objTypeCode); + if (SkipQuotes(convertibleValue, objTypeCode)) + { + return xmlStr; + } + else + { + return QuoteValue(xmlStr); + } + } + } + } + + StringBuilder sb = new StringBuilder(); + if (!SerializeObject(value, sb, options)) + { + return string.Empty; + } + return sb.ToString(); + } + } + + /// + /// Serialization of the object in JSON format to the destination StringBuilder + /// + /// The object to serialize to JSON. + /// Write the resulting JSON to this destination. + /// Object serialized successfully (true/false). + public bool SerializeObject(object? value, StringBuilder destination) + { + return SerializeObject(value, destination, DefaultSerializerOptions); + } + + /// + /// Serialization of the object in JSON format to the destination StringBuilder + /// + /// The object to serialize to JSON. + /// Write the resulting JSON to this destination. + /// serialization options + /// Object serialized successfully (true/false). + public bool SerializeObject(object? value, StringBuilder destination, JsonSerializeOptions options) + { + return SerializeObject(value, destination, options, default(SingleItemOptimizedHashSet), 0); + } + + /// + /// Serialization of the object in JSON format to the destination StringBuilder + /// + /// The object to serialize to JSON. + /// Write the resulting JSON to this destination. + /// serialization options + /// The objects in path (Avoid cyclic reference loop). + /// The current depth (level) of recursion. + /// Object serialized successfully (true/false). + private bool SerializeObject(object? value, StringBuilder destination, JsonSerializeOptions options, SingleItemOptimizedHashSet objectsInPath, int depth) + { + int originalLength = destination.Length; + + try + { + if (SerializeSimpleObjectValue(value, destination, options)) + { + return true; + } + + return SerializeObjectWithReflection(value, destination, options, ref objectsInPath, depth); + } + catch + { + destination.Length = originalLength; + return false; + } + } + + private bool SerializeObjectWithReflection(object? value, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet objectsInPath, int depth) + { + int originalLength = destination.Length; + if (originalLength > MaxJsonLength) + { + return false; + } + + if (value is null || objectsInPath.Contains(value)) + { + return false; + } + + if (value is IDictionary dict) + { + using (StartCollectionScope(ref objectsInPath, dict)) + { + SerializeDictionaryObject(dict, destination, options, objectsInPath, depth); + return true; + } + } + + if (value is IEnumerable collection) + { + if (_objectReflectionCache.TryLookupExpandoObject(value, out var objectPropertyList)) + { + return SerializeObjectPropertyList(value, ref objectPropertyList, destination, options, ref objectsInPath, depth); + } + else + { + using (StartCollectionScope(ref objectsInPath, value)) + { + SerializeCollectionObject(collection, destination, options, objectsInPath, depth); + return true; + } + } + } + else + { + var objectPropertyList = _objectReflectionCache.LookupObjectProperties(value); + return SerializeObjectPropertyList(value, ref objectPropertyList, destination, options, ref objectsInPath, depth); + } + } + + private bool SerializeSimpleObjectValue(object? value, StringBuilder destination, JsonSerializeOptions options, bool forceToString = false) + { + if (value is IConvertible convertibleValue) + { + var objTypeCode = convertibleValue.GetTypeCode(); + if (objTypeCode != TypeCode.Object) + { + SerializeSimpleTypeCodeValue(convertibleValue, objTypeCode, destination, options, forceToString); + return true; + } + } + + if (value is IFormattable formattable) + { + if (value is DateTimeOffset dateTimeOffset) + { + QuoteValue(destination, dateTimeOffset.ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)); + return true; + } + + destination.Append('"'); +#if NETFRAMEWORK + var str = formattable.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty; + AppendStringEscape(destination, str, options); +#else + int startPos = destination.Length; + destination.AppendFormat(CultureInfo.InvariantCulture, "{0}", formattable); // Support ISpanFormattable + PerformJsonEscapeWhenNeeded(destination, startPos, options.EscapeUnicode); +#endif + destination.Append('"'); + return true; + } + + if (value is null) + { + SerializeSimpleTypeCodeValue(null, TypeCode.Empty, destination, options, forceToString); + return true; + } + + return false; // Not simple + } + + private static SingleItemOptimizedHashSet.SingleItemScopedInsert StartCollectionScope(ref SingleItemOptimizedHashSet objectsInPath, object value) + { + return new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, true); + } + + private void SerializeDictionaryObject(IDictionary dictionary, StringBuilder destination, JsonSerializeOptions options, SingleItemOptimizedHashSet objectsInPath, int depth) + { + bool first = true; + + int nextDepth = objectsInPath.Count <= 1 ? depth : (depth + 1); + if (nextDepth > options.MaxRecursionLimit) + { + destination.Append("{}"); + return; + } + + destination.Append('{'); + foreach (var item in new DictionaryEntryEnumerable(dictionary)) + { + var originalLength = destination.Length; + if (originalLength > MaxJsonLength) + { + break; + } + + if (!first) + { + destination.Append(','); + } + + var itemKey = item.Key; + + if (!SerializeObjectAsString(itemKey, destination, options)) + { + destination.Length = originalLength; + continue; + } + + if (options.SanitizeDictionaryKeys) + { + int keyEndIndex = destination.Length - 1; + int keyStartIndex = originalLength + (first ? 0 : 1) + 1; + if (!SanitizeDictionaryKey(destination, keyStartIndex, keyEndIndex - keyStartIndex)) + { + destination.Length = originalLength; // Empty keys are not allowed + continue; + } + } + + destination.Append(':'); + + //only serialize, if key and value are serialized without error (e.g. due to reference loop) + var itemValue = item.Value; + if (!SerializeObject(itemValue, destination, options, objectsInPath, nextDepth)) + { + destination.Length = originalLength; + } + else + { + first = false; + } + } + destination.Append('}'); + } + + private static bool SanitizeDictionaryKey(StringBuilder destination, int keyStartIndex, int keyLength) + { + if (keyLength == 0) + { + return false; // Empty keys are not allowed + } + + int keyEndIndex = keyStartIndex + keyLength; + for (int i = keyStartIndex; i < keyEndIndex; ++i) + { + char keyChar = destination[i]; + if (keyChar == '_' || char.IsLetterOrDigit(keyChar)) + continue; + + destination[i] = '_'; + } + return true; + } + + private void SerializeCollectionObject(IEnumerable value, StringBuilder destination, JsonSerializeOptions options, SingleItemOptimizedHashSet objectsInPath, int depth) + { + bool first = true; + + int nextDepth = objectsInPath.Count <= 1 ? depth : (depth + 1); // Allow serialization of list-items + if (nextDepth > options.MaxRecursionLimit) + { + destination.Append("[]"); + return; + } + + int originalLength; + destination.Append('['); + foreach (var val in value) + { + originalLength = destination.Length; + if (originalLength > MaxJsonLength) + { + break; + } + + if (!first) + { + destination.Append(','); + } + + if (!SerializeObject(val, destination, options, objectsInPath, nextDepth)) + { + destination.Length = originalLength; + } + else + { + first = false; + } + } + destination.Append(']'); + } + + private bool SerializeObjectPropertyList(object value, ref ObjectReflectionCache.ObjectPropertyList objectPropertyList, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet objectsInPath, int depth) + { + if (objectPropertyList.IsSimpleValue) + { + value = objectPropertyList.ObjectValue; + if (SerializeSimpleObjectValue(value, destination, options)) + { + return true; + } + } + else if (depth < options.MaxRecursionLimit) + { + if (ReferenceEquals(options, DefaultSerializerOptions) && value is Exception) + { + // Exceptions are seldom under control, and can include random Data-Dictionary-keys, so we sanitize by default + options = DefaultExceptionSerializerOptions; + } + + using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, false)) + { + return SerializeObjectProperties(objectPropertyList, destination, options, objectsInPath, depth); + } + } + + return SerializeObjectAsString(value, destination, options); + } + + private void SerializeSimpleTypeCodeValue(IConvertible? value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, bool forceToString = false) + { + if (objTypeCode == TypeCode.Empty || value is null) + { + destination.Append(forceToString ? "\"\"" : "null"); + } + else if (objTypeCode == TypeCode.String || objTypeCode == TypeCode.Char) + { + destination.Append('"'); + AppendStringEscape(destination, value.ToString() ?? string.Empty, options); + destination.Append('"'); + } + else + { + SerializeSimpleTypeCodeValueNoEscape(value, objTypeCode, destination, options, forceToString); + } + } + + private void SerializeSimpleTypeCodeValueNoEscape(IConvertible value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, bool forceToString) + { + if (IsNumericTypeCode(objTypeCode, false)) + { + if (!options.EnumAsInteger && value is Enum enumValue) + { + QuoteValue(destination, EnumAsString(enumValue)); + } + else + { + SerializeNumericValue(value, objTypeCode, destination, forceToString); + } + } + else if (objTypeCode == TypeCode.DateTime) + { + destination.Append('"'); + destination.AppendXmlDateTimeUtcRoundTrip(value.ToDateTime(CultureInfo.InvariantCulture)); + destination.Append('"'); + } + else if (IsNumericTypeCode(objTypeCode, true) && SkipQuotes(value, objTypeCode)) + { + SerializeNumericValue(value, objTypeCode, destination, forceToString); + } + else + { + string str = XmlHelper.XmlConvertToString(value, objTypeCode); + if (!forceToString && !string.IsNullOrEmpty(str) && SkipQuotes(value, objTypeCode)) + { + destination.Append(str); + } + else + { + QuoteValue(destination, str); + } + } + } + + private static void SerializeNumericValue(IConvertible value, TypeCode objTypeCode, StringBuilder destination, bool forceToString) + { + if (forceToString) + destination.Append('"'); + destination.AppendNumericInvariant(value, objTypeCode); + if (forceToString) + destination.Append('"'); + } + + private static string QuoteValue(string value) + { + return string.Concat("\"", value, "\""); + } + + private static void QuoteValue(StringBuilder destination, string value) + { + destination.Append('"'); + destination.Append(value); + destination.Append('"'); + } + + private string EnumAsString(Enum value) + { + if (!_enumCache.TryGetValue(value, out var textValue)) + { + textValue = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; + _enumCache.TryAddValue(value, textValue); + } + return textValue ?? string.Empty; + } + + /// + /// No quotes needed for this type? + /// + private static bool SkipQuotes(IConvertible value, TypeCode objTypeCode) + { + switch (objTypeCode) + { + case TypeCode.String: return false; + case TypeCode.Char: return false; + case TypeCode.DateTime: return false; + case TypeCode.Empty: return true; + case TypeCode.Boolean: return true; + case TypeCode.Decimal: return true; + case TypeCode.Double: + { + double dblValue = value.ToDouble(CultureInfo.InvariantCulture); + return !double.IsNaN(dblValue) && !double.IsInfinity(dblValue); + } + case TypeCode.Single: + { + float floatValue = value.ToSingle(CultureInfo.InvariantCulture); + return !float.IsNaN(floatValue) && !float.IsInfinity(floatValue); + } + default: + return IsNumericTypeCode(objTypeCode, false); + } + } + + /// + /// Checks the object if it is numeric + /// + /// TypeCode for the object + /// Accept fractional types as numeric type. + /// + private static bool IsNumericTypeCode(TypeCode objTypeCode, bool includeDecimals) + { + switch (objTypeCode) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + return true; + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + return includeDecimals; + } + return false; + } + + /// + /// Checks input string if it needs JSON escaping, and makes necessary conversion + /// + /// Destination Builder + /// Input string + /// all options + /// JSON escaped string + private static void AppendStringEscape(StringBuilder destination, string text, JsonSerializeOptions options) + { + AppendStringEscape(destination, text, options.EscapeUnicode); + } + + /// + /// Checks input string if it needs JSON escaping, and makes necessary conversion + /// + /// Destination Builder + /// Input string + /// Should non-ASCII characters be encoded + /// + /// JSON escaped string + internal static void AppendStringEscape(StringBuilder destination, string text, bool escapeUnicode) + { + int i = 0; + foreach (var chr in text) + { + if (RequiresJsonEscape(chr, escapeUnicode)) + { + AppendStringEscape(destination, text, escapeUnicode, i); + return; + } + + ++i; + } + + destination.Append(text); + } + + private static void AppendStringEscape(StringBuilder destination, string text, bool escapeUnicode, int startIndex) + { + destination.Append(text, 0, startIndex); + + for (int i = startIndex; i < text.Length; ++i) + { + char ch = text[i]; + if (!RequiresJsonEscape(ch, escapeUnicode)) + { + destination.Append(ch); + continue; + } + + switch (ch) + { + case '"': + destination.Append("\\\""); + break; + + case '\\': + destination.Append("\\\\"); + break; + + case '\b': + destination.Append("\\b"); + break; + + case '\r': + destination.Append("\\r"); + break; + + case '\n': + destination.Append("\\n"); + break; + + case '\f': + destination.Append("\\f"); + break; + + case '\t': + destination.Append("\\t"); + break; + + default: + destination.AppendFormat(CultureInfo.InvariantCulture, "\\u{0:x4}", (int)ch); + break; + } + } + } + + internal static void PerformJsonEscapeWhenNeeded(StringBuilder builder, int startPos, bool escapeUnicode) + { + var builderLength = builder.Length; + for (int i = startPos; i < builderLength; ++i) + { + if (RequiresJsonEscape(builder[i], escapeUnicode)) + { + var str = builder.ToString(i, builderLength - i); + builder.Length = i; + AppendStringEscape(builder, str, escapeUnicode, 0); + break; + } + } + } + + private static bool RequiresJsonEscape(char ch, bool escapeUnicode) + { + if (ch < 32) + return true; + if (ch > 127) + return escapeUnicode; + return ch == '"' || ch == '\\'; + } + + private bool SerializeObjectProperties(ObjectReflectionCache.ObjectPropertyList objectPropertyList, StringBuilder destination, JsonSerializeOptions options, + SingleItemOptimizedHashSet objectsInPath, int depth) + { + destination.Append('{'); + + string? jsonPropertyDelimeter = null; + + foreach (var propertyValue in objectPropertyList) + { + var originalLength = destination.Length; + + try + { + if (!propertyValue.HasNameAndValue) + continue; + + destination.Append(jsonPropertyDelimeter); + + QuoteValue(destination, propertyValue.Name); + destination.Append(':'); + + var objTypeCode = propertyValue.TypeCode; + if (objTypeCode != TypeCode.Object) + { + SerializeSimpleTypeCodeValue((IConvertible?)propertyValue.Value, objTypeCode, destination, options); + } + else + { + if (!SerializeObject(propertyValue.Value, destination, options, objectsInPath, depth + 1)) + { + destination.Length = originalLength; + continue; + } + } + + if (jsonPropertyDelimeter is null) + jsonPropertyDelimeter = options.SuppressSpaces ? "," : ", "; + } + catch + { + // skip single property + destination.Length = originalLength; + } + } + + destination.Append('}'); + return true; + } + + private bool SerializeObjectAsString(object value, StringBuilder destination, JsonSerializeOptions options) + { + var originalLength = destination.Length; + + try + { + if (SerializeSimpleObjectValue(value, destination, options, true)) + { + return true; + } + + var str = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; + destination.Append('"'); + AppendStringEscape(destination, str, options); + destination.Append('"'); + return true; + } + catch + { + // skip bad object + destination.Length = originalLength; + return false; + } + } + } +} diff --git a/src/NLog/Targets/JsonSerializeOptions.cs b/src/NLog/Targets/JsonSerializeOptions.cs new file mode 100644 index 0000000..bff64fe --- /dev/null +++ b/src/NLog/Targets/JsonSerializeOptions.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) 2004-2024 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of Jaroslaw Kowalski nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// + +namespace NLog.Targets +{ + using System; + using System.ComponentModel; + + /// + /// Options for JSON serialization + /// + public class JsonSerializeOptions + { + /// + /// Add quotes around object keys? + /// + /// Default: + [Obsolete("Marked obsolete with NLog 5.0.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public bool QuoteKeys { get; set; } = true; + + /// + /// Format provider for value + /// + /// Default: + [Obsolete("Marked obsolete with NLog 5.0. Should always be InvariantCulture.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IFormatProvider? FormatProvider { get; set; } + + /// + /// Format string for value + /// + /// Default: + [Obsolete("Marked obsolete with NLog 5.0. Should always be InvariantCulture.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public string? Format { get; set; } + + /// + /// Should non-ascii characters be encoded. + /// + /// Default: + public bool EscapeUnicode { get; set; } + + /// + /// Should forward slashes be escaped? If , / will be converted to \/ + /// + /// Default: + [Obsolete("Marked obsolete with NLog 5.5. Should never escape forward slash")] + [EditorBrowsable(EditorBrowsableState.Never)] + public bool EscapeForwardSlash { get; set; } + + /// + /// Serialize enum as integer value. + /// + /// Default: + public bool EnumAsInteger { get; set; } + + /// + /// Gets or sets the option to suppress the extra spaces in the output json. + /// + /// Default: + public bool SuppressSpaces { get; set; } = true; + + /// + /// Should dictionary keys be sanitized. All characters must either be letters, numbers or underscore character (_). + /// + /// Any other characters will be converted to underscore character (_) + /// + /// Default: + public bool SanitizeDictionaryKeys { get; set; } + + /// + /// How far down the rabbit hole should the Json Serializer go with object-reflection before stopping + /// + /// Default: + public int MaxRecursionLimit { get; set; } = 10; + } +} diff --git a/tests/NLog.UnitTests/Targets/DefaultJsonSerializerClassTests.cs b/tests/NLog.UnitTests/Targets/DefaultJsonSerializerClassTests.cs new file mode 100644 index 0000000..ffb7b15 --- /dev/null +++ b/tests/NLog.UnitTests/Targets/DefaultJsonSerializerClassTests.cs @@ -0,0 +1,135 @@ +// +// Copyright (c) 2004-2024 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of Jaroslaw Kowalski nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// + +using System.Text; +using NLog.Targets; +using Xunit; + +namespace NLog.UnitTests.Targets +{ + /// + /// Test via path + /// + public class DefaultJsonSerializerClassTests : NLogTestBase + { + private static readonly object _testSyncObject = new object(); + + private interface IExcludedInterface + { + } + + private sealed class ExcludedClass : IExcludedInterface + { + public string ExcludedString { get; set; } + public override string ToString() + { + return "Skipped"; + } + } + + private sealed class IncludedClass + { + public string IncludedString { get; set; } + } + + private sealed class ContainerClass + { + public string S { get; set; } + public ExcludedClass Excluded { get; set; } + public IncludedClass Included { get; set; } + } + + private static ContainerClass BuildSampleObject() + { + var testObject = new ContainerClass + { + S = "sample", + Excluded = new ExcludedClass { ExcludedString = "shouldn't be serialized" }, + Included = new IncludedClass { IncludedString = "serialized" } + }; + return testObject; + } + + [Fact] + public void SimpleValue_RegistersSerializeAsToString_ConvertsValue() + { + var logFactory = new LogFactory(); + logFactory.Setup().SetupSerialization(s => s.RegisterObjectTransformation(o => o.Capacity)); + + var testObject = new System.IO.MemoryStream(42); + + var sb = new StringBuilder(); + var options = new JsonSerializeOptions(); + var jsonSerializer = new DefaultJsonSerializer(logFactory.ServiceRepository); + jsonSerializer.SerializeObject(testObject, sb, options); + + Assert.Equal($"{testObject.Capacity}", sb.ToString()); + } + + [Fact] + public void IExcludedInterfaceSerializer_RegistersSerializeAsToString_InvokesToString() + { + var testObject = BuildSampleObject(); + + var sb = new StringBuilder(); + var options = new JsonSerializeOptions(); + + var logFactory = new LogFactory(); + logFactory.Setup().SetupSerialization(s => s.RegisterObjectTransformation(o => o.ToString())); + + var jsonSerializer = new DefaultJsonSerializer(logFactory.ServiceRepository); + jsonSerializer.SerializeObject(testObject, sb, options); + const string expectedValue = + @"{""S"":""sample"",""Excluded"":""Skipped"",""Included"":{""IncludedString"":""serialized""}}"; + Assert.Equal(expectedValue, sb.ToString()); + } + + [Fact] + public void ExcludedClassSerializer_RegistersSerializeAsToString_InvokesToString() + { + var testObject = BuildSampleObject(); + + var sb = new StringBuilder(); + var options = new JsonSerializeOptions(); + + var logFactory = new LogFactory(); + logFactory.Setup().SetupSerialization(s => s.RegisterObjectTransformation(typeof(ExcludedClass), o => o.ToString())); + + var jsonSerializer = new DefaultJsonSerializer(logFactory.ServiceRepository); + jsonSerializer.SerializeObject(testObject, sb, options); + const string expectedValue = + @"{""S"":""sample"",""Excluded"":""Skipped"",""Included"":{""IncludedString"":""serialized""}}"; + Assert.Equal(expectedValue, sb.ToString()); + } + } +}