Upstream NLog/NLog state of the touched files before cc54b6d1e1
This commit is contained in:
1 parent
44df43fabd
commit
b78676e244
4 files changed
+1817
No files matched your search
@@ -0,0 +1,816 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2004-2024 Jaroslaw Kowalski <jaak@jkowalski.net>, 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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts object into a List of property-names and -values using reflection
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ObjectReflectionCache : IObjectTypeTransformer
|
||||||
|
{
|
||||||
|
private MruCache<Type, ObjectPropertyInfos> ObjectTypeCache => _objectTypeCache ?? System.Threading.Interlocked.CompareExchange(ref _objectTypeCache, new MruCache<Type, ObjectPropertyInfos>(10000), null) ?? _objectTypeCache;
|
||||||
|
private MruCache<Type, ObjectPropertyInfos>? _objectTypeCache;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private IObjectTypeTransformer ObjectTypeTransformation => _objectTypeTransformation ?? (_objectTypeTransformation = _serviceProvider?.GetService<IObjectTypeTransformer>() ?? 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try get value from <paramref name="value"/>, using <paramref name="objectPath"/>, and set into <paramref name="foundValue"/>
|
||||||
|
/// </summary>
|
||||||
|
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<string, object> 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<PropertyInfo>(), 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<PropertyInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ObjectPropertyList.PropertyValue>
|
||||||
|
{
|
||||||
|
internal static readonly StringComparer NameComparer = StringComparer.Ordinal;
|
||||||
|
private static readonly FastPropertyLookup[] CreateIDictionaryEnumerator = new[] { new FastPropertyLookup(string.Empty, TypeCode.Object, (o, p) => ((IDictionary<string, object>)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<object>());
|
||||||
|
}
|
||||||
|
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<string, object> value)
|
||||||
|
{
|
||||||
|
_object = value; // Expando objects
|
||||||
|
_properties = ArrayHelper.Empty<PropertyInfo>();
|
||||||
|
_fastLookup = CreateIDictionaryEnumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetPropertyValue(string name, out PropertyValue propertyValue)
|
||||||
|
{
|
||||||
|
if (_properties.Length == 0)
|
||||||
|
{
|
||||||
|
if (_object is IDictionary<string, object> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans properties for name (Skips property value lookup until finding match)
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans properties for name
|
||||||
|
/// </summary>
|
||||||
|
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<object>());
|
||||||
|
return expandoObject is null ? default(Enumerator) : new Enumerator((IEnumerator<KeyValuePair<string, object>>)expandoObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator<PropertyValue> IEnumerable<PropertyValue>.GetEnumerator() => GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
public struct Enumerator : IEnumerator<PropertyValue>
|
||||||
|
{
|
||||||
|
private readonly object _owner;
|
||||||
|
private readonly PropertyInfo[]? _properties;
|
||||||
|
private readonly FastPropertyLookup[]? _fastLookup;
|
||||||
|
private readonly IEnumerator<KeyValuePair<string, object>>? _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<KeyValuePair<string, object>> 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<ObjectPropertyInfos>
|
||||||
|
{
|
||||||
|
public readonly PropertyInfo[] Properties;
|
||||||
|
public readonly FastPropertyLookup[]? FastLookup;
|
||||||
|
|
||||||
|
public static readonly ObjectPropertyInfos SimpleToString = new ObjectPropertyInfos(ArrayHelper.Empty<PropertyInfo>(), ArrayHelper.Empty<FastPropertyLookup>());
|
||||||
|
|
||||||
|
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<string, object> DynamicObjectToDict(System.Dynamic.DynamicObject d)
|
||||||
|
{
|
||||||
|
var newVal = new Dictionary<string, object>();
|
||||||
|
foreach (var propName in d.GetDynamicMemberNames())
|
||||||
|
{
|
||||||
|
if (d.TryGetMember(new GetBinderAdapter(propName), out var result))
|
||||||
|
{
|
||||||
|
newVal[propName] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binder for retrieving value of <see cref="System.Dynamic.DynamicObject"/>
|
||||||
|
/// </summary>
|
||||||
|
private sealed class GetBinderAdapter : System.Dynamic.GetMemberBinder
|
||||||
|
{
|
||||||
|
internal GetBinderAdapter(string name)
|
||||||
|
: base(name, false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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<string, object>)
|
||||||
|
return new DictionaryEnumerator<string, object>();
|
||||||
|
if (value is IDictionary<string, string>)
|
||||||
|
return new DictionaryEnumerator<string, string>();
|
||||||
|
#if !NET35 && !NET40
|
||||||
|
if (value is IReadOnlyDictionary<string, object>)
|
||||||
|
return new DictionaryEnumerator<string, object>();
|
||||||
|
if (value is IReadOnlyDictionary<string, string>)
|
||||||
|
return new DictionaryEnumerator<string, string>();
|
||||||
|
#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<KeyValuePair<string, object?>> GetEnumerator(object value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DictionaryEnumerator : IDictionaryEnumerator
|
||||||
|
{
|
||||||
|
private readonly Func<object, IEnumerator<KeyValuePair<string, object?>>> _enumerateCollection;
|
||||||
|
|
||||||
|
public DictionaryEnumerator(Func<object, IEnumerator<KeyValuePair<string, object?>>> enumerateCollection)
|
||||||
|
{
|
||||||
|
_enumerateCollection = enumerateCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator<KeyValuePair<string, object?>> IDictionaryEnumerator.GetEnumerator(object value)
|
||||||
|
{
|
||||||
|
return _enumerateCollection(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerator<KeyValuePair<string, object?>> BuildDictionaryEnumerator(object collection)
|
||||||
|
{
|
||||||
|
if (collection is IDictionary dictionary)
|
||||||
|
{
|
||||||
|
if (dictionary.Count == 0)
|
||||||
|
return EmptyDictionaryEnumerator.Default;
|
||||||
|
return YieldEnumerator(dictionary);
|
||||||
|
}
|
||||||
|
return EmptyDictionaryEnumerator.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator<KeyValuePair<string, object?>> YieldEnumerator(IDictionary dictionary)
|
||||||
|
{
|
||||||
|
foreach (var item in new DictionaryEntryEnumerable(dictionary))
|
||||||
|
yield return new KeyValuePair<string, object?>(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<object, IEnumerator<KeyValuePair<string, object?>>>? BuildEnumerator(Type dictionaryType, Type dictionaryItemType)
|
||||||
|
{
|
||||||
|
var enumeratorMethod = dictionaryType.GetMethod(nameof(IEnumerable.GetEnumerator));
|
||||||
|
var getKeyMethod = dictionaryItemType.GetProperty(nameof(KeyValuePair<string, object>.Key))?.GetGetMethod();
|
||||||
|
var getValueMethod = dictionaryItemType.GetProperty(nameof(KeyValuePair<string, object>.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<object>());
|
||||||
|
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<KeyValuePair<string, object?>> EnumerateItems(IEnumerator enumerator, ReflectionHelpers.LateBoundMethod getKeyProperty, ReflectionHelpers.LateBoundMethod getValueProperty)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var item = enumerator.Current;
|
||||||
|
var keyString = getKeyProperty.Invoke(item, ArrayHelper.Empty<object>());
|
||||||
|
var valueObject = getValueProperty.Invoke(item, ArrayHelper.Empty<object>());
|
||||||
|
yield return new KeyValuePair<string, object?>(keyString?.ToString() ?? string.Empty, valueObject);
|
||||||
|
} while (enumerator.MoveNext());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
(enumerator as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DictionaryEnumerator<TKey, TValue> : IDictionaryEnumerator
|
||||||
|
{
|
||||||
|
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator(object value)
|
||||||
|
{
|
||||||
|
if (value is IDictionary<TKey, TValue> dictionary)
|
||||||
|
{
|
||||||
|
if (dictionary.Count > 0)
|
||||||
|
return YieldEnumerator(dictionary);
|
||||||
|
}
|
||||||
|
#if !NET35 && !NET40
|
||||||
|
else if (value is IReadOnlyDictionary<TKey, TValue> readonlyDictionary)
|
||||||
|
{
|
||||||
|
if (readonlyDictionary.Count > 0)
|
||||||
|
return YieldEnumerator(readonlyDictionary);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return EmptyDictionaryEnumerator.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator<KeyValuePair<string, object?>> YieldEnumerator(IDictionary<TKey, TValue> dictionary)
|
||||||
|
{
|
||||||
|
foreach (var item in dictionary)
|
||||||
|
yield return new KeyValuePair<string, object?>(item.Key?.ToString() ?? string.Empty, item.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !NET35 && !NET40
|
||||||
|
private static IEnumerator<KeyValuePair<string, object?>> YieldEnumerator(IReadOnlyDictionary<TKey, TValue> dictionary)
|
||||||
|
{
|
||||||
|
foreach (var item in dictionary)
|
||||||
|
yield return new KeyValuePair<string, object?>(item.Key?.ToString() ?? string.Empty, item.Value);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EmptyDictionaryEnumerator : IEnumerator<KeyValuePair<string, object?>>
|
||||||
|
{
|
||||||
|
public static readonly EmptyDictionaryEnumerator Default = new EmptyDictionaryEnumerator();
|
||||||
|
|
||||||
|
KeyValuePair<string, object?> IEnumerator<KeyValuePair<string, object?>>.Current => default(KeyValuePair<string, object?>);
|
||||||
|
|
||||||
|
object IEnumerator.Current => default(KeyValuePair<string, object?>);
|
||||||
|
|
||||||
|
bool IEnumerator.MoveNext() => false;
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
// Nothing here on purpose
|
||||||
|
}
|
||||||
|
|
||||||
|
void IEnumerator.Reset()
|
||||||
|
{
|
||||||
|
// Nothing here on purpose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,758 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2004-2024 Jaroslaw Kowalski <jaak@jkowalski.net>, 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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default class for serialization of values to JSON format.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultJsonSerializer : IJsonConverter
|
||||||
|
{
|
||||||
|
private readonly ObjectReflectionCache _objectReflectionCache;
|
||||||
|
private readonly MruCache<Enum, string> _enumCache = new MruCache<Enum, string>(4000);
|
||||||
|
|
||||||
|
private const int MaxJsonLength = 512 * 1024;
|
||||||
|
|
||||||
|
private static JsonSerializeOptions DefaultSerializerOptions = new JsonSerializeOptions();
|
||||||
|
private static JsonSerializeOptions DefaultExceptionSerializerOptions = new JsonSerializeOptions() { SanitizeDictionaryKeys = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton instance of the serializer.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Instead use ResolveService<IJsonConverter>() 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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Private. Use <see cref="Instance"/>
|
||||||
|
/// </summary>
|
||||||
|
internal DefaultJsonSerializer(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_objectReflectionCache = new ObjectReflectionCache(serviceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a serialization of an object into JSON format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The object to serialize to JSON.</param>
|
||||||
|
/// <returns>Serialized value.</returns>
|
||||||
|
public string SerializeObject(object value)
|
||||||
|
{
|
||||||
|
return SerializeObject(value, DefaultSerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a serialization of an object into JSON format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The object to serialize to JSON.</param>
|
||||||
|
/// <param name="options">serialization options</param>
|
||||||
|
/// <returns>Serialized value.</returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialization of the object in JSON format to the destination StringBuilder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The object to serialize to JSON.</param>
|
||||||
|
/// <param name="destination">Write the resulting JSON to this destination.</param>
|
||||||
|
/// <returns>Object serialized successfully (true/false).</returns>
|
||||||
|
public bool SerializeObject(object? value, StringBuilder destination)
|
||||||
|
{
|
||||||
|
return SerializeObject(value, destination, DefaultSerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialization of the object in JSON format to the destination StringBuilder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The object to serialize to JSON.</param>
|
||||||
|
/// <param name="destination">Write the resulting JSON to this destination.</param>
|
||||||
|
/// <param name="options">serialization options</param>
|
||||||
|
/// <returns>Object serialized successfully (true/false).</returns>
|
||||||
|
public bool SerializeObject(object? value, StringBuilder destination, JsonSerializeOptions options)
|
||||||
|
{
|
||||||
|
return SerializeObject(value, destination, options, default(SingleItemOptimizedHashSet<object>), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialization of the object in JSON format to the destination StringBuilder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The object to serialize to JSON.</param>
|
||||||
|
/// <param name="destination">Write the resulting JSON to this destination.</param>
|
||||||
|
/// <param name="options">serialization options</param>
|
||||||
|
/// <param name="objectsInPath">The objects in path (Avoid cyclic reference loop).</param>
|
||||||
|
/// <param name="depth">The current depth (level) of recursion.</param>
|
||||||
|
/// <returns>Object serialized successfully (true/false).</returns>
|
||||||
|
private bool SerializeObject(object? value, StringBuilder destination, JsonSerializeOptions options, SingleItemOptimizedHashSet<object> 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<object> 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<object>.SingleItemScopedInsert StartCollectionScope(ref SingleItemOptimizedHashSet<object> objectsInPath, object value)
|
||||||
|
{
|
||||||
|
return new SingleItemOptimizedHashSet<object>.SingleItemScopedInsert(value, ref objectsInPath, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SerializeDictionaryObject(IDictionary dictionary, StringBuilder destination, JsonSerializeOptions options, SingleItemOptimizedHashSet<object> 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<object> 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<object> 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<object>.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No quotes needed for this type?
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the object <see cref="TypeCode" /> if it is numeric
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="objTypeCode">TypeCode for the object</param>
|
||||||
|
/// <param name="includeDecimals">Accept fractional types as numeric type.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks input string if it needs JSON escaping, and makes necessary conversion
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destination">Destination Builder</param>
|
||||||
|
/// <param name="text">Input string</param>
|
||||||
|
/// <param name="options">all options</param>
|
||||||
|
/// <returns>JSON escaped string</returns>
|
||||||
|
private static void AppendStringEscape(StringBuilder destination, string text, JsonSerializeOptions options)
|
||||||
|
{
|
||||||
|
AppendStringEscape(destination, text, options.EscapeUnicode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks input string if it needs JSON escaping, and makes necessary conversion
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destination">Destination Builder</param>
|
||||||
|
/// <param name="text">Input string</param>
|
||||||
|
/// <param name="escapeUnicode">Should non-ASCII characters be encoded</param>
|
||||||
|
///
|
||||||
|
/// <returns>JSON escaped string</returns>
|
||||||
|
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<object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2004-2024 Jaroslaw Kowalski <jaak@jkowalski.net>, 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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options for JSON serialization
|
||||||
|
/// </summary>
|
||||||
|
public class JsonSerializeOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add quotes around object keys?
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="true"/></remarks>
|
||||||
|
[Obsolete("Marked obsolete with NLog 5.0.")]
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
public bool QuoteKeys { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format provider for value
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="null"/></remarks>
|
||||||
|
[Obsolete("Marked obsolete with NLog 5.0. Should always be InvariantCulture.")]
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
public IFormatProvider? FormatProvider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format string for value
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="null"/></remarks>
|
||||||
|
[Obsolete("Marked obsolete with NLog 5.0. Should always be InvariantCulture.")]
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
public string? Format { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should non-ascii characters be encoded.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="false"/></remarks>
|
||||||
|
public bool EscapeUnicode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should forward slashes be escaped? If <see langword="true"/>, / will be converted to \/
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="false"/></remarks>
|
||||||
|
[Obsolete("Marked obsolete with NLog 5.5. Should never escape forward slash")]
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
public bool EscapeForwardSlash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize enum as integer value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="false"/></remarks>
|
||||||
|
public bool EnumAsInteger { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the option to suppress the extra spaces in the output json.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="true"/></remarks>
|
||||||
|
public bool SuppressSpaces { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should dictionary keys be sanitized. All characters must either be letters, numbers or underscore character (_).
|
||||||
|
///
|
||||||
|
/// Any other characters will be converted to underscore character (_)
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="false"/></remarks>
|
||||||
|
public bool SanitizeDictionaryKeys { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How far down the rabbit hole should the Json Serializer go with object-reflection before stopping
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Default: <see langword="10"/></remarks>
|
||||||
|
public int MaxRecursionLimit { get; set; } = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2004-2024 Jaroslaw Kowalski <jaak@jkowalski.net>, 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test via <see cref="IJsonConverter" /> path
|
||||||
|
/// </summary>
|
||||||
|
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<System.IO.MemoryStream>(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<IExcludedInterface>(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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user