diff --git a/src/NLog/Internal/ObjectReflectionCache.cs b/src/NLog/Internal/ObjectReflectionCache.cs index 14a87749e5..5571c5d8cd 100644 --- a/src/NLog/Internal/ObjectReflectionCache.cs +++ b/src/NLog/Internal/ObjectReflectionCache.cs @@ -53,6 +53,34 @@ internal class ObjectReflectionCache public ObjectPropertyList LookupObjectProperties(object value) { + if (TryLookupExpandoObject(value, out var propertyValues)) + { + return propertyValues; + } + + var objectType = value.GetType(); + var propertyInfos = BuildObjectPropertyInfos(value, objectType); + _objectTypeCache.TryAddValue(objectType, propertyInfos); + return new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + } + + public bool TryLookupExpandoObject(object value, out ObjectPropertyList objectPropertyList) + { + if (value is IDictionary expando) + { + objectPropertyList = new ObjectPropertyList(expando); + return true; + } + +#if DYNAMIC_OBJECT + if (value is DynamicObject d) + { + var dictionary = DynamicObjectToDict(d); + objectPropertyList = new ObjectPropertyList(dictionary); + return true; + } +#endif + Type objectType = value.GetType(); if (_objectTypeCache.TryGetValue(objectType, out var propertyInfos)) { @@ -62,7 +90,8 @@ public ObjectPropertyList LookupObjectProperties(object value) propertyInfos = new ObjectPropertyInfos(propertyInfos.Properties, fastLookup); _objectTypeCache.TryAddValue(objectType, propertyInfos); } - return new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + objectPropertyList = new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + return true; } if (_objectTypeOverride.Count > 0) @@ -74,26 +103,47 @@ public ObjectPropertyList LookupObjectProperties(object value) { var objectProperties = objectReflection.Invoke(value); if (objectProperties?.Count > 0) - return new ObjectPropertyList(objectProperties); + { + objectPropertyList = new ObjectPropertyList(objectProperties); + return true; + } // object.ToString() since no properties propertyInfos = ObjectPropertyInfos.SimpleToString; - return new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + objectPropertyList = new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + return true; } } } -#if DYNAMIC_OBJECT - if (value is DynamicObject d) + if (TryExtractExpandoObject(objectType, out propertyInfos)) { - var dictionary = DynamicObjectToDict(d); - return new ObjectPropertyList(dictionary); + _objectTypeCache.TryAddValue(objectType, propertyInfos); + objectPropertyList = new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + return true; } -#endif - propertyInfos = BuildObjectPropertyInfos(value, objectType); - _objectTypeCache.TryAddValue(objectType, propertyInfos); - return new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); + objectPropertyList = default(ObjectPropertyList); + return false; + } + + private static bool TryExtractExpandoObject(Type objectType, out ObjectPropertyInfos propertyInfos) + { + foreach (var interfaceType in objectType.GetInterfaces()) + { + if (interfaceType.IsGenericType() && interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + { + if (interfaceType.GetGenericArguments()[0] == typeof(string)) + { + var dictionaryEnumerator = (IDictionaryEnumerator)Activator.CreateInstance(typeof(DictionaryEnumerator<,>).MakeGenericType(interfaceType.GetGenericArguments())); + propertyInfos = new ObjectPropertyInfos(null, new[] { new FastPropertyLookup(string.Empty, TypeCode.Object, (o, p) => dictionaryEnumerator.GetEnumerator(o)) }); + return true; + } + } + } + + propertyInfos = default(ObjectPropertyInfos); + return false; } private static ObjectPropertyInfos BuildObjectPropertyInfos(object value, Type objectType) @@ -207,6 +257,7 @@ private static FastPropertyLookup[] BuildFastLookup(PropertyInfo[] properties, b public 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; @@ -215,19 +266,7 @@ public struct PropertyValue { public readonly string Name; public readonly object Value; - public TypeCode TypeCode - { - get - { - if (_typecode == TypeCode.Object) - { - return Convert.GetTypeCode(Value); - } - - return Value == null ? TypeCode.Empty : _typecode; - } - } - + public TypeCode TypeCode => Value == null ? TypeCode.Empty : _typecode; private readonly TypeCode _typecode; public PropertyValue(string name, object value, TypeCode typeCode) @@ -252,7 +291,7 @@ public PropertyValue(object owner, FastPropertyLookup fastProperty) } } - public int Count => _fastLookup?.Length ?? _properties?.Length ?? (_object as ICollection)?.Count ?? (_object as ICollection>)?.Count ?? 0; + public bool ConvertToString => _properties?.Length == 0; internal ObjectPropertyList(object value, PropertyInfo[] properties, FastPropertyLookup[] fastLookup) { @@ -265,38 +304,85 @@ public ObjectPropertyList(IDictionary value) { _object = value; // Expando objects _properties = null; - _fastLookup = null; + _fastLookup = CreateIDictionaryEnumerator; } public bool TryGetPropertyValue(string name, out PropertyValue propertyValue) { - if (_fastLookup != null) + if (_properties != null) { - int nameHashCode = NameComparer.GetHashCode(name); - foreach (var fastProperty in _fastLookup) + if (_fastLookup != null) { - if (fastProperty.NameHashCode==nameHashCode && NameComparer.Equals(fastProperty.Name, name)) - { - propertyValue = new PropertyValue(_object, fastProperty); - return true; - } + return TryFastLookupPropertyValue(name, out propertyValue); + } + else + { + return TrySlowLookupPropertyValue(name, out propertyValue); } } - else if (_properties != null) + else if (_object is IDictionary expandoObject) { - foreach (var propInfo in _properties) + if (expandoObject.TryGetValue(name, out var objectValue)) { - if (NameComparer.Equals(propInfo.Name, name)) - { - propertyValue = new PropertyValue(_object, propInfo); - return true; - } + propertyValue = new PropertyValue(name, objectValue, TypeCode.Object); + return true; } + propertyValue = default(PropertyValue); + return false; + } + else + { + return TryListLookupPropertyValue(name, out propertyValue); } - else if (_object is IDictionary expandoObject && expandoObject.TryGetValue(name, out var objectValue)) + } + + /// + /// Scans properties for name (Skips string-compare and value-lookup until finding match) + /// + private bool TryFastLookupPropertyValue(string name, out PropertyValue propertyValue) + { + int nameHashCode = NameComparer.GetHashCode(name); + foreach (var fastProperty in _fastLookup) { - propertyValue = new PropertyValue(name, objectValue, TypeCode.Object); - return true; + if (fastProperty.NameHashCode == nameHashCode && NameComparer.Equals(fastProperty.Name, name)) + { + propertyValue = new PropertyValue(_object, fastProperty); + return true; + } + } + propertyValue = default(PropertyValue); + return false; + } + + /// + /// 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; @@ -312,7 +398,7 @@ public Enumerator GetEnumerator() if (_properties != null) return new Enumerator(_object, _properties, _fastLookup); else - return new Enumerator((_object as IDictionary).GetEnumerator()); + return new Enumerator((IEnumerator>)_fastLookup[0].ValueLookup(_object, null)); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -459,5 +545,30 @@ public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, Dy } } #endif + + private interface IDictionaryEnumerator + { + IEnumerator> GetEnumerator(object value); + } + + private static readonly IDictionary EmptyDictionary = new NLog.Internal.SortHelpers.ReadOnlySingleBucketDictionary(); + + internal sealed class DictionaryEnumerator : IDictionaryEnumerator + { + public IEnumerator> GetEnumerator(object value) + { + if (value is IDictionary dictionary) + { + return YieldEnumerator(dictionary); + } + return EmptyDictionary.GetEnumerator(); + } + + private IEnumerator> YieldEnumerator(IDictionary dictionary) + { + foreach (var item in dictionary) + yield return new KeyValuePair(item.Key.ToString(), item.Value); + } + } } } diff --git a/src/NLog/Internal/PropertiesDictionary.cs b/src/NLog/Internal/PropertiesDictionary.cs index 3b6d9a7bda..cd486266b2 100644 --- a/src/NLog/Internal/PropertiesDictionary.cs +++ b/src/NLog/Internal/PropertiesDictionary.cs @@ -466,15 +466,7 @@ protected MessageTemplateParameter CurrentParameter } if (_eventEnumeratorCreated) { - string parameterName; - try - { - parameterName = XmlHelper.XmlConvertToString(_eventEnumerator.Current.Key ?? string.Empty); - } - catch - { - parameterName = ""; - } + string parameterName = XmlHelper.XmlConvertToString(_eventEnumerator.Current.Key ?? string.Empty) ?? string.Empty; return new MessageTemplateParameter(parameterName, _eventEnumerator.Current.Value.Value, null, CaptureType.Unknown); } throw new InvalidOperationException(); diff --git a/src/NLog/Internal/StringBuilderExt.cs b/src/NLog/Internal/StringBuilderExt.cs index aa76da4cc5..c489928b0e 100644 --- a/src/NLog/Internal/StringBuilderExt.cs +++ b/src/NLog/Internal/StringBuilderExt.cs @@ -32,6 +32,7 @@ // using System; +using System.Globalization; using System.IO; using System.Text; using NLog.MessageTemplates; @@ -333,28 +334,28 @@ internal static void Append4DigitsZeroPadded(this StringBuilder builder, int num /// /// Apend a int type (byte, int) as string /// - internal static void AppendIntegerAsString(this StringBuilder sb, object value, TypeCode objTypeCode) + internal static void AppendIntegerAsString(this StringBuilder sb, IConvertible value, TypeCode objTypeCode) { switch (objTypeCode) { - case TypeCode.Byte: sb.AppendInvariant((byte)value); break; - case TypeCode.SByte: sb.AppendInvariant((sbyte)value); break; - case TypeCode.Int16: sb.AppendInvariant((short)value); break; - case TypeCode.Int32: sb.AppendInvariant((int)value); break; + case TypeCode.Byte: sb.AppendInvariant(value.ToByte(CultureInfo.InvariantCulture)); break; + case TypeCode.SByte: sb.AppendInvariant(value.ToSByte(CultureInfo.InvariantCulture)); break; + case TypeCode.Int16: sb.AppendInvariant(value.ToInt16(CultureInfo.InvariantCulture)); break; + case TypeCode.Int32: sb.AppendInvariant(value.ToInt32(CultureInfo.InvariantCulture)); break; case TypeCode.Int64: { - long int64 = (long)value; + long int64 = value.ToInt64(CultureInfo.InvariantCulture); if (int64 < int.MaxValue && int64 > int.MinValue) sb.AppendInvariant((int)int64); else sb.Append(int64); } break; - case TypeCode.UInt16: sb.AppendInvariant((ushort)value); break; - case TypeCode.UInt32: sb.AppendInvariant((uint)value); break; + case TypeCode.UInt16: sb.AppendInvariant(value.ToUInt16(CultureInfo.InvariantCulture)); break; + case TypeCode.UInt32: sb.AppendInvariant(value.ToUInt32(CultureInfo.InvariantCulture)); break; case TypeCode.UInt64: { - ulong uint64 = (ulong)value; + ulong uint64 = value.ToUInt64(CultureInfo.InvariantCulture); if (uint64 < uint.MaxValue) sb.AppendInvariant((uint)uint64); else diff --git a/src/NLog/Internal/XmlHelper.cs b/src/NLog/Internal/XmlHelper.cs index d6250f154e..7e8684bbee 100644 --- a/src/NLog/Internal/XmlHelper.cs +++ b/src/NLog/Internal/XmlHelper.cs @@ -34,10 +34,10 @@ namespace NLog.Internal { using System; + using System.Globalization; using System.Text; using System.Text.RegularExpressions; using System.Xml; - using NLog.Internal; /// /// Helper class for XML @@ -172,8 +172,7 @@ private static bool SmallAndNoEscapeNeeded(string text, bool xmlEncodeNewlines) /// Object value converted to string internal static string XmlConvertToStringSafe(object value) { - TypeCode objTypeCode = Convert.GetTypeCode(value); - return XmlConvertToString(value, objTypeCode, true); + return XmlConvertToString(value, true); } /// @@ -183,8 +182,7 @@ internal static string XmlConvertToStringSafe(object value) /// Object value converted to string internal static string XmlConvertToString(object value) { - TypeCode objTypeCode = Convert.GetTypeCode(value); - return XmlConvertToString(value, objTypeCode, false); + return XmlConvertToString(value, false); } /// @@ -271,6 +269,38 @@ StringBuilder CreateStringBuilder(int i) } } + private static string XmlConvertToString(object value, bool safeConversion) + { + try + { + IConvertible convertibleValue = value as IConvertible; + TypeCode objTypeCode = value == null ? TypeCode.Empty : (convertibleValue?.GetTypeCode() ?? TypeCode.Object); + if (objTypeCode != TypeCode.Object) + { + return XmlConvertToString(convertibleValue, objTypeCode, safeConversion); + } + + return XmlConvertToStringInvariant(value, safeConversion); + } + catch + { + return safeConversion ? "" : null; + } + } + + private static string XmlConvertToStringInvariant(object value, bool safeConversion) + { + try + { + string valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return safeConversion ? RemoveInvalidXmlChars(valueString) : valueString; + } + catch + { + return safeConversion ? "" : null; + } + } + /// /// Converts object value to invariant format (understood by JavaScript) /// @@ -278,7 +308,7 @@ StringBuilder CreateStringBuilder(int i) /// Object TypeCode /// Check and remove unusual unicode characters from the result string. /// Object value converted to string - internal static string XmlConvertToString(object value, TypeCode objTypeCode, bool safeConversion = false) + internal static string XmlConvertToString(IConvertible value, TypeCode objTypeCode, bool safeConversion = false) { if (value == null) { @@ -288,57 +318,47 @@ internal static string XmlConvertToString(object value, TypeCode objTypeCode, bo switch (objTypeCode) { case TypeCode.Boolean: - return XmlConvert.ToString((bool)value); // boolean as lowercase + return XmlConvert.ToString(value.ToBoolean(CultureInfo.InvariantCulture)); // boolean as lowercase case TypeCode.Byte: - return XmlConvert.ToString((byte)value); + return XmlConvert.ToString(value.ToByte(CultureInfo.InvariantCulture)); case TypeCode.SByte: - return XmlConvert.ToString((sbyte)value); + return XmlConvert.ToString(value.ToSByte(CultureInfo.InvariantCulture)); case TypeCode.Int16: - return XmlConvert.ToString((short)value); + return XmlConvert.ToString(value.ToInt16(CultureInfo.InvariantCulture)); case TypeCode.Int32: - return XmlConvert.ToString((int)value); + return XmlConvert.ToString(value.ToInt32(CultureInfo.InvariantCulture)); case TypeCode.Int64: - return XmlConvert.ToString((long)value); + return XmlConvert.ToString(value.ToInt64(CultureInfo.InvariantCulture)); case TypeCode.UInt16: - return XmlConvert.ToString((ushort)value); + return XmlConvert.ToString(value.ToUInt16(CultureInfo.InvariantCulture)); case TypeCode.UInt32: - return XmlConvert.ToString((uint)value); + return XmlConvert.ToString(value.ToUInt32(CultureInfo.InvariantCulture)); case TypeCode.UInt64: - return XmlConvert.ToString((ulong)value); + return XmlConvert.ToString(value.ToUInt64(CultureInfo.InvariantCulture)); case TypeCode.Single: { - float singleValue = (float)value; - if (float.IsInfinity(singleValue)) - return Convert.ToString(singleValue, System.Globalization.CultureInfo.InvariantCulture); // Infinity instead of INF - else - return XmlConvert.ToString(singleValue); // 8 digits scale + float singleValue = value.ToSingle(CultureInfo.InvariantCulture); + return float.IsInfinity(singleValue) ? + Convert.ToString(singleValue, CultureInfo.InvariantCulture) : + XmlConvert.ToString(singleValue); } case TypeCode.Double: { - double doubleValue = (double)value; - if (double.IsInfinity(doubleValue)) - return Convert.ToString(doubleValue, System.Globalization.CultureInfo.InvariantCulture); // Infinity instead of INF - else - return XmlConvert.ToString(doubleValue); // 16 digits scale + double doubleValue = value.ToDouble(CultureInfo.InvariantCulture); + return double.IsInfinity(doubleValue) ? + Convert.ToString(doubleValue, CultureInfo.InvariantCulture) : + XmlConvert.ToString(doubleValue); } case TypeCode.Decimal: - return XmlConvert.ToString((decimal)value); + return XmlConvert.ToString(value.ToDecimal(CultureInfo.InvariantCulture)); case TypeCode.DateTime: - return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Utc); + return XmlConvert.ToString(value.ToDateTime(CultureInfo.InvariantCulture), XmlDateTimeSerializationMode.Utc); case TypeCode.Char: - return XmlConvert.ToString((char)value); + return XmlConvert.ToString(value.ToChar(CultureInfo.InvariantCulture)); case TypeCode.String: - return safeConversion ? RemoveInvalidXmlChars((string)value) : (string)value; + return safeConversion ? RemoveInvalidXmlChars(value.ToString(CultureInfo.InvariantCulture)) : value.ToString(CultureInfo.InvariantCulture); default: - try - { - string valueString = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture); - return safeConversion ? RemoveInvalidXmlChars(valueString) : valueString; - } - catch - { - return null; - } + return XmlConvertToStringInvariant(value, safeConversion); } } diff --git a/src/NLog/Layouts/XmlElementBase.cs b/src/NLog/Layouts/XmlElementBase.cs index 1cc3df76a0..34cbf510d5 100644 --- a/src/NLog/Layouts/XmlElementBase.cs +++ b/src/NLog/Layouts/XmlElementBase.cs @@ -373,8 +373,9 @@ private void AppendLogEventXmlProperties(LogEventInfo logEventInfo, StringBuilde { if (string.IsNullOrEmpty(key)) continue; + object propertyValue = MappedDiagnosticsContext.GetObject(key); - AppendXmlPropertyValue(key, propertyValue, Convert.GetTypeCode(propertyValue), sb, orgLength); + AppendXmlPropertyValue(key, propertyValue, sb, orgLength); } } @@ -385,8 +386,9 @@ private void AppendLogEventXmlProperties(LogEventInfo logEventInfo, StringBuilde { if (string.IsNullOrEmpty(key)) continue; + object propertyValue = MappedDiagnosticsLogicalContext.GetObject(key); - AppendXmlPropertyValue(key, propertyValue, Convert.GetTypeCode(propertyValue), sb, orgLength); + AppendXmlPropertyValue(key, propertyValue, sb, orgLength); } } #endif @@ -416,19 +418,18 @@ private void AppendLogEventProperties(LogEventInfo logEventInfo, StringBuilder s propertyValue = Convert.ToString(prop.Value ?? string.Empty, logEventInfo.FormatProvider ?? LoggingConfiguration?.DefaultCultureInfo); - AppendXmlPropertyObjectValue(prop.Name, propertyValue, Convert.GetTypeCode(propertyValue), sb, orgLength, - default(SingleItemOptimizedHashSet), 0); + AppendXmlPropertyObjectValue(prop.Name, propertyValue, sb, orgLength, default(SingleItemOptimizedHashSet), 0); } } - private bool AppendXmlPropertyObjectValue(string propName, object propertyValue, TypeCode objTypeCode, StringBuilder sb, int orgLength, SingleItemOptimizedHashSet objectsInPath, int depth, bool ignorePropertiesElementName = false) + private bool AppendXmlPropertyObjectValue(string propName, object propertyValue, StringBuilder sb, int orgLength, SingleItemOptimizedHashSet objectsInPath, int depth, bool ignorePropertiesElementName = false) { - if (propertyValue == null) - objTypeCode = TypeCode.Empty; - + IConvertible convertibleValue = propertyValue as IConvertible; + TypeCode objTypeCode = propertyValue == null ? TypeCode.Empty : (convertibleValue?.GetTypeCode() ?? TypeCode.Object); if (objTypeCode != TypeCode.Object) { - AppendXmlPropertyValue(propName, propertyValue, objTypeCode, sb, orgLength, false, ignorePropertiesElementName); + string xmlValueString = XmlHelper.XmlConvertToString(convertibleValue, objTypeCode, true); + AppendXmlPropertyValue(propName, xmlValueString, sb, orgLength, false, ignorePropertiesElementName); } else { @@ -456,19 +457,21 @@ private bool AppendXmlPropertyObjectValue(string propName, object propertyValue, AppendXmlDictionaryObject(propName, dict, sb, orgLength, objectsInPath, nextDepth, ignorePropertiesElementName); } } - else if (propertyValue is IDictionary expando) + else if (propertyValue is System.Collections.IEnumerable collection) { - using (StartCollectionScope(ref objectsInPath, expando)) + if (_objectReflectionCache.TryLookupExpandoObject(propertyValue, out var propertyValues)) { - var propertyValues = new ObjectReflectionCache.ObjectPropertyList(expando); - AppendXmlObjectPropertyValues(propName, ref propertyValues, sb, orgLength, ref objectsInPath, nextDepth, ignorePropertiesElementName); + using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(propertyValue, ref objectsInPath, false, _referenceEqualsComparer)) + { + AppendXmlObjectPropertyValues(propName, ref propertyValues, sb, orgLength, ref objectsInPath, nextDepth, ignorePropertiesElementName); + } } - } - else if (propertyValue is System.Collections.IEnumerable collection) - { - using (StartCollectionScope(ref objectsInPath, collection)) + else { - AppendXmlCollectionObject(propName, collection, sb, orgLength, objectsInPath, nextDepth, ignorePropertiesElementName); + using (StartCollectionScope(ref objectsInPath, collection)) + { + AppendXmlCollectionObject(propName, collection, sb, orgLength, objectsInPath, nextDepth, ignorePropertiesElementName); + } } } else @@ -491,7 +494,7 @@ private static SingleItemOptimizedHashSet.SingleItemScopedInsert StartCo private void AppendXmlCollectionObject(string propName, System.Collections.IEnumerable collection, StringBuilder sb, int orgLength, SingleItemOptimizedHashSet objectsInPath, int depth, bool ignorePropertiesElementName) { - string propNameElement = AppendXmlPropertyValue(propName, null, TypeCode.Empty, sb, orgLength, true); + string propNameElement = AppendXmlPropertyValue(propName, string.Empty, sb, orgLength, true); if (!string.IsNullOrEmpty(propNameElement)) { foreach (var item in collection) @@ -500,7 +503,7 @@ private void AppendXmlCollectionObject(string propName, System.Collections.IEnum if (beforeValueLength > MaxXmlLength) break; - if (!AppendXmlPropertyObjectValue(PropertiesCollectionItemName, item, Convert.GetTypeCode(item), sb, orgLength, objectsInPath, depth, true)) + if (!AppendXmlPropertyObjectValue(PropertiesCollectionItemName, item, sb, orgLength, objectsInPath, depth, true)) { sb.Length = beforeValueLength; } @@ -511,7 +514,7 @@ private void AppendXmlCollectionObject(string propName, System.Collections.IEnum private void AppendXmlDictionaryObject(string propName, System.Collections.IDictionary dictionary, StringBuilder sb, int orgLength, SingleItemOptimizedHashSet objectsInPath, int depth, bool ignorePropertiesElementName) { - string propNameElement = AppendXmlPropertyValue(propName, null, TypeCode.Empty, sb, orgLength, true, ignorePropertiesElementName); + string propNameElement = AppendXmlPropertyValue(propName, string.Empty, sb, orgLength, true, ignorePropertiesElementName); if (!string.IsNullOrEmpty(propNameElement)) { foreach (var item in new DictionaryEntryEnumerable(dictionary)) @@ -520,7 +523,7 @@ private void AppendXmlDictionaryObject(string propName, System.Collections.IDict if (beforeValueLength > MaxXmlLength) break; - if (!AppendXmlPropertyObjectValue(item.Key?.ToString(), item.Value, Convert.GetTypeCode(item.Value), sb, orgLength, objectsInPath, depth)) + if (!AppendXmlPropertyObjectValue(item.Key?.ToString(), item.Value, sb, orgLength, objectsInPath, depth)) { sb.Length = beforeValueLength; } @@ -531,13 +534,13 @@ private void AppendXmlDictionaryObject(string propName, System.Collections.IDict private void AppendXmlObjectPropertyValues(string propName, ref ObjectReflectionCache.ObjectPropertyList propertyValues, StringBuilder sb, int orgLength, ref SingleItemOptimizedHashSet objectsInPath, int depth, bool ignorePropertiesElementName = false) { - if (propertyValues.Count == 0) + if (propertyValues.ConvertToString) { - AppendXmlPropertyValue(propName, propertyValues.ToString(), TypeCode.String, sb, orgLength, false, ignorePropertiesElementName); + AppendXmlPropertyValue(propName, propertyValues.ToString(), sb, orgLength, false, ignorePropertiesElementName); } else { - string propNameElement = AppendXmlPropertyValue(propName, null, TypeCode.Empty, sb, orgLength, true, ignorePropertiesElementName); + string propNameElement = AppendXmlPropertyValue(propName, string.Empty, sb, orgLength, true, ignorePropertiesElementName); if (!string.IsNullOrEmpty(propNameElement)) { foreach (var property in propertyValues) @@ -546,9 +549,18 @@ private void AppendXmlObjectPropertyValues(string propName, ref ObjectReflection if (beforeValueLength > MaxXmlLength) break; - if (!AppendXmlPropertyObjectValue(property.Name, property.Value, property.TypeCode, sb, orgLength, objectsInPath, depth)) + var propertyTypeCode = property.TypeCode; + if (propertyTypeCode != TypeCode.Object) { - sb.Length = beforeValueLength; + string xmlValueString = XmlHelper.XmlConvertToString((IConvertible)property.Value, propertyTypeCode, true); + AppendXmlPropertyStringValue(property.Name, xmlValueString, sb, orgLength, false, ignorePropertiesElementName); + } + else + { + if (!AppendXmlPropertyObjectValue(property.Name, property.Value, sb, orgLength, objectsInPath, depth)) + { + sb.Length = beforeValueLength; + } } } AppendClosingPropertyTag(propNameElement, sb, ignorePropertiesElementName); @@ -556,7 +568,13 @@ private void AppendXmlObjectPropertyValues(string propName, ref ObjectReflection } } - private string AppendXmlPropertyValue(string propName, object propertyValue, TypeCode objTypeCode, StringBuilder sb, int orgLength, bool ignoreValue = false, bool ignorePropertiesElementName = false) + private string AppendXmlPropertyValue(string propName, object propertyValue, StringBuilder sb, int orgLength, bool ignoreValue = false, bool ignorePropertiesElementName = false) + { + string xmlValueString = ignoreValue ? string.Empty : XmlHelper.XmlConvertToStringSafe(propertyValue); + return AppendXmlPropertyStringValue(propName, xmlValueString, sb, orgLength, ignoreValue, ignorePropertiesElementName); + } + + private string AppendXmlPropertyStringValue(string propName, string xmlValueString, StringBuilder sb, int orgLength, bool ignoreValue = false, bool ignorePropertiesElementName = false) { if (string.IsNullOrEmpty(PropertiesElementName)) return string.Empty; // Not supported @@ -598,7 +616,6 @@ private string AppendXmlPropertyValue(string propName, object propertyValue, Typ if (!ignoreValue) { - string xmlValueString = XmlHelper.XmlConvertToString(propertyValue, objTypeCode, true); if (RenderAttribute(sb, PropertiesElementValueAttribute, xmlValueString)) { sb.Append("/>"); diff --git a/src/NLog/MessageTemplates/ValueFormatter.cs b/src/NLog/MessageTemplates/ValueFormatter.cs index d665b5d4e7..bb40d19461 100644 --- a/src/NLog/MessageTemplates/ValueFormatter.cs +++ b/src/NLog/MessageTemplates/ValueFormatter.cs @@ -34,6 +34,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Text; using NLog.Config; using NLog.Internal; @@ -108,7 +109,7 @@ public bool FormatValue(object value, string format, CaptureType captureType, IF /// public bool FormatObject(object value, string format, IFormatProvider formatProvider, StringBuilder builder) { - if (SerializeSimpleObject(value, format, formatProvider, builder)) + if (SerializeSimpleObject(value, format, formatProvider, builder, false)) { return true; } @@ -119,26 +120,18 @@ public bool FormatObject(object value, string format, IFormatProvider formatProv return SerializeWithoutCyclicLoop(collection, format, formatProvider, builder, default(SingleItemOptimizedHashSet), 0); } - builder.Append(Convert.ToString(value, formatProvider)); + SerializeConvertToString(value, formatProvider, builder); return true; } /// /// Try serialising a scalar (string, int, NULL) or simple type (IFormattable) /// - /// - /// - /// - /// - /// - private bool SerializeSimpleObject(object value, string format, IFormatProvider formatProvider, StringBuilder builder) + private bool SerializeSimpleObject(object value, string format, IFormatProvider formatProvider, StringBuilder builder, bool convertToString = true) { if (value is string stringValue) { - bool includeQuotes = format != LiteralFormatSymbol; - if (includeQuotes) builder.Append('"'); - builder.Append(stringValue); - if (includeQuotes) builder.Append('"'); + SerializeStringObject(stringValue, format, builder); return true; } @@ -148,31 +141,61 @@ private bool SerializeSimpleObject(object value, string format, IFormatProvider return true; } - IFormattable formattable; - if (!string.IsNullOrEmpty(format) && (formattable = value as IFormattable) != null) + // Optimize for types that are pretty much invariant in all cultures when no format-string + if (value is IConvertible convertibleValue) { - builder.Append(formattable.ToString(format, formatProvider)); + SerializeConvertibleObject(convertibleValue, format, formatProvider, builder); return true; } - - // Optimize for types that are pretty much invariant in all cultures when no format-string - TypeCode objTypeCode = Convert.GetTypeCode(value); - switch (objTypeCode) + else { - case TypeCode.Boolean: + if (!string.IsNullOrEmpty(format) && value is IFormattable formattable) { - builder.Append((bool)value ? "true" : "false"); + builder.Append(formattable.ToString(format, formatProvider)); return true; } - case TypeCode.Char: + + if (convertToString) { - bool includeQuotes = format != LiteralFormatSymbol; - if (includeQuotes) builder.Append('"'); - builder.Append((char)value); - if (includeQuotes) builder.Append('"'); + SerializeConvertToString(value, formatProvider, builder); return true; } + return false; + } + } + + private void SerializeConvertibleObject(IConvertible value, string format, IFormatProvider formatProvider, StringBuilder builder) + { + TypeCode convertibleTypeCode = value.GetTypeCode(); + if (convertibleTypeCode == TypeCode.String) + { + SerializeStringObject(value.ToString(), format, builder); + return; + } + + if (!string.IsNullOrEmpty(format) && value is IFormattable formattable) + { + builder.Append(formattable.ToString(format, formatProvider)); + return; + } + + switch (convertibleTypeCode) + { + case TypeCode.Boolean: + { + builder.Append(value.ToBoolean(CultureInfo.InvariantCulture) ? "true" : "false"); + break; + } + case TypeCode.Char: + { + bool includeQuotes = format != LiteralFormatSymbol; + if (includeQuotes) builder.Append('"'); + builder.Append(value.ToChar(CultureInfo.InvariantCulture)); + if (includeQuotes) builder.Append('"'); + break; + } + case TypeCode.Byte: case TypeCode.SByte: case TypeCode.Int16: @@ -181,25 +204,36 @@ private bool SerializeSimpleObject(object value, string format, IFormatProvider case TypeCode.UInt16: case TypeCode.UInt32: case TypeCode.UInt64: - { - Enum enumValue; - if ((enumValue = value as Enum) != null) - { - AppendEnumAsString(builder, enumValue); - } - else { - builder.AppendIntegerAsString(value, objTypeCode); + if (value is Enum enumValue) + { + AppendEnumAsString(builder, enumValue); + } + else + { + builder.AppendIntegerAsString(value, convertibleTypeCode); + } + break; } - } - return true; case TypeCode.Object: // Guid, TimeSpan, DateTimeOffset default: // Single, Double, Decimal, etc. + SerializeConvertToString(value, formatProvider, builder); break; } + } - return false; + private static void SerializeConvertToString(object value, IFormatProvider formatProvider, StringBuilder builder) + { + builder.Append(Convert.ToString(value, formatProvider)); + } + + private static void SerializeStringObject(string stringValue, string format, StringBuilder builder) + { + bool includeQuotes = format != LiteralFormatSymbol; + if (includeQuotes) builder.Append('"'); + builder.Append(stringValue); + if (includeQuotes) builder.Append('"'); } private void AppendEnumAsString(StringBuilder sb, Enum value) @@ -262,15 +296,9 @@ private bool SerializeDictionaryObject(IDictionary dictionary, string format, IF if (separator) builder.Append(", "); - if (item.Key is string || !(item.Key is IEnumerable)) - FormatObject(item.Key, format, formatProvider, builder); - else - SerializeWithoutCyclicLoop((IEnumerable)item.Key, format, formatProvider, builder, objectsInPath, depth + 1); + SerializeCollectionItem(item.Key, format, formatProvider, builder, ref objectsInPath, depth); builder.Append("="); - if (item.Value is string || !(item.Value is IEnumerable)) - FormatObject(item.Value, format, formatProvider, builder); - else - SerializeWithoutCyclicLoop((IEnumerable)item.Value, format, formatProvider, builder, objectsInPath, depth + 1); + SerializeCollectionItem(item.Value, format, formatProvider, builder, ref objectsInPath, depth); separator = true; } return true; @@ -286,16 +314,23 @@ private bool SerializeCollectionObject(IEnumerable collection, string format, IF if (separator) builder.Append(", "); - if (item is string || !(item is IEnumerable)) - FormatObject(item, format, formatProvider, builder); - else - SerializeWithoutCyclicLoop((IEnumerable)item, format, formatProvider, builder, objectsInPath, depth + 1); + SerializeCollectionItem(item, format, formatProvider, builder, ref objectsInPath, depth); separator = true; } return true; } + private void SerializeCollectionItem(object item, string format, IFormatProvider formatProvider, StringBuilder builder, ref SingleItemOptimizedHashSet objectsInPath, int depth) + { + if (item is IConvertible convertible) + SerializeConvertibleObject(convertible, format, formatProvider, builder); + else if (item is IEnumerable enumerable) + SerializeWithoutCyclicLoop(enumerable, format, formatProvider, builder, objectsInPath, depth + 1); + else + SerializeSimpleObject(item, format, formatProvider, builder); + } + /// /// Convert a value to a string with format and append to . /// diff --git a/src/NLog/Targets/DefaultJsonSerializer.cs b/src/NLog/Targets/DefaultJsonSerializer.cs index 5b145d2696..d130be1056 100644 --- a/src/NLog/Targets/DefaultJsonSerializer.cs +++ b/src/NLog/Targets/DefaultJsonSerializer.cs @@ -113,7 +113,8 @@ public string SerializeObject(object value, JsonSerializeOptions options) } else { - TypeCode objTypeCode = Convert.GetTypeCode(value); + IConvertible convertibleValue = value as IConvertible; + TypeCode objTypeCode = convertibleValue?.GetTypeCode() ?? TypeCode.Object; if (objTypeCode != TypeCode.Object && objTypeCode != TypeCode.Char && StringHelpers.IsNullOrWhiteSpace(options.Format) && options.FormatProvider == null) { Enum enumValue; @@ -123,8 +124,8 @@ public string SerializeObject(object value, JsonSerializeOptions options) } else { - string xmlStr = XmlHelper.XmlConvertToString(value, objTypeCode); - if (SkipQuotes(value, objTypeCode)) + string xmlStr = XmlHelper.XmlConvertToString(convertibleValue, objTypeCode); + if (SkipQuotes(convertibleValue, objTypeCode)) { return xmlStr; } @@ -166,96 +167,106 @@ public bool SerializeObject(object value, StringBuilder destination) /// Object serialized succesfully (true/false). public bool SerializeObject(object value, StringBuilder destination, JsonSerializeOptions options) { - return SerializeObject(value, Convert.GetTypeCode(value), destination, options, default(SingleItemOptimizedHashSet), 0); + 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. - /// The TypeCode for the object to serialize. /// Write the resulting JSON to this destination. /// serialisation options /// The objects in path (Avoid cyclic reference loop). /// The current depth (level) of recursion. /// Object serialized succesfully (true/false). - private bool SerializeObject(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, - SingleItemOptimizedHashSet objectsInPath, int depth) + private bool SerializeObject(object value, StringBuilder destination, JsonSerializeOptions options, SingleItemOptimizedHashSet objectsInPath, int depth) { - if (objTypeCode == TypeCode.Object && objectsInPath.Contains(value)) + int originalLength = destination.Length; + + try { - return false; // detected reference loop, skip serialization - } + if (SerializeSimpleObjectValue(value, destination, options)) + { + return true; + } - if (value == null) + return SerializeObjectWithReflection(value, destination, options, ref objectsInPath, depth); + } + catch { - destination.Append("null"); + destination.Length = originalLength; + return false; } - else if (objTypeCode == TypeCode.String) + } + + private bool SerializeObjectWithReflection(object value, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet objectsInPath, int depth) + { + int originalLength = destination.Length; + if (originalLength > MaxJsonLength) { - destination.Append('"'); - AppendStringEscape(destination, value.ToString(), options.EscapeUnicode); - destination.Append('"'); + return false; } - else if (objTypeCode != TypeCode.Object) + + if (objectsInPath.Contains(value)) { - return SerializeWithTypeCode(value, objTypeCode, destination, options, ref objectsInPath, depth); + return false; } - else if (value is IDictionary dict) + + if (value is IDictionary dict) { using (StartCollectionScope(ref objectsInPath, dict)) { SerializeDictionaryObject(dict, destination, options, objectsInPath, depth); + return true; } } - else if (value is IDictionary expando) + + if (value is IEnumerable enumerable) { - // Special case for Expando-objects - using (StartCollectionScope(ref objectsInPath, expando)) + if (_objectReflectionCache.TryLookupExpandoObject(value, out var propertyValues)) { - return SerializeObjectProperties(new ObjectReflectionCache.ObjectPropertyList(expando), destination, options, objectsInPath, depth); + if (propertyValues.ConvertToString || depth >= options.MaxRecursionLimit) + { + return SerializeObjectAsString(value, destination, options); + } + else + { + using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, false, _referenceEqualsComparer)) + { + return SerializeObjectProperties(propertyValues, destination, options, objectsInPath, depth); + } + } } - } - else if (value is IEnumerable enumerable) - { + using (StartCollectionScope(ref objectsInPath, value)) { SerializeCollectionObject(enumerable, destination, options, objectsInPath, depth); + return true; } } else { - return SerializeWithTypeCode(value, objTypeCode, destination, options, ref objectsInPath, depth); + return SerializeObjectWithProperties(value, destination, options, ref objectsInPath, depth); } - return true; } - private bool SerializeWithTypeCode(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet objectsInPath, int depth) + private bool SerializeSimpleObjectValue(object value, StringBuilder destination, JsonSerializeOptions options, bool forceToString = false) { - var hasFormat = !StringHelpers.IsNullOrWhiteSpace(options.Format); - if ((options.FormatProvider != null || hasFormat) && (value is IFormattable formattable)) + var convertibleValue = value as IConvertible; + var objTypeCode = value == null ? TypeCode.Empty : (convertibleValue?.GetTypeCode() ?? TypeCode.Object); + if (objTypeCode != TypeCode.Object) { - return SerializeWithFormatProvider(value, objTypeCode, destination, options, formattable, options.Format, hasFormat); + SerializeSimpleTypeCodeValue(convertibleValue, objTypeCode, destination, options, forceToString); + return true; } - else + + if (value is DateTimeOffset) { - if (objTypeCode == TypeCode.Object) - { - if (value is DateTimeOffset) - { - QuoteValue(destination, $"{value:yyyy-MM-dd HH:mm:ss zzz}"); - return true; - } - else - { - return SerializeObjectWithProperties(value, destination, options, ref objectsInPath, depth); - } - } - else - { - return SerializeSimpleTypeCodeValue(value, objTypeCode, destination, options); - } + QuoteValue(destination, $"{value:yyyy-MM-dd HH:mm:ss zzz}"); + return true; } + + return false; // Not simple } private static SingleItemOptimizedHashSet.SingleItemScopedInsert StartCollectionScope(ref SingleItemOptimizedHashSet objectsInPath, object value) @@ -263,41 +274,23 @@ private static SingleItemOptimizedHashSet.SingleItemScopedInsert StartCo return new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, true, _referenceEqualsComparer); } - private bool SerializeWithFormatProvider(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, IFormattable formattable, string format, bool hasFormat) + private void SerializeWithFormatProvider(IFormattable formattable, bool includeQuotes, StringBuilder destination, JsonSerializeOptions options, bool hasFormat) { - int originalLength = destination.Length; - - try + if (includeQuotes) { - bool includeQuotes = !SkipQuotes(value, objTypeCode); - if (includeQuotes) - { - destination.Append('"'); - } - - if (hasFormat) - { - var formatProvider = options.FormatProvider ?? _defaultFormatProvider; - destination.AppendFormat(formatProvider, string.Concat("{0:", format, "}"), value); - } - else - { - //format provider passed without FormatProvider - var str = formattable.ToString("", options.FormatProvider); - AppendStringEscape(destination, str, options.EscapeUnicode); - } + destination.Append('"'); + } - if (includeQuotes) - { - destination.Append('"'); - } + var formatProvider = options.FormatProvider ?? (hasFormat ? _defaultFormatProvider : null); + var str = formattable.ToString(hasFormat ? options.Format : "", formatProvider); + if (includeQuotes) + AppendStringEscape(destination, str, options.EscapeUnicode); + else + destination.Append(str); - return true; - } - catch + if (includeQuotes) { - destination.Length = originalLength; - return false; + destination.Append('"'); } } @@ -327,10 +320,9 @@ private void SerializeDictionaryObject(IDictionary dictionary, StringBuilder des } var itemKey = item.Key; - var itemKeyTypeCode = Convert.GetTypeCode(itemKey); if (options.QuoteKeys) { - if (!SerializeObjectAsString(itemKey, itemKeyTypeCode, destination, options)) + if (!SerializeObjectAsString(itemKey, destination, options)) { destination.Length = originalLength; continue; @@ -338,7 +330,7 @@ private void SerializeDictionaryObject(IDictionary dictionary, StringBuilder des } else { - if (!SerializeObject(itemKey, itemKeyTypeCode, destination, options, objectsInPath, nextDepth)) + if (!SerializeObject(itemKey, destination, options, objectsInPath, nextDepth)) { destination.Length = originalLength; continue; @@ -361,8 +353,7 @@ private void SerializeDictionaryObject(IDictionary dictionary, StringBuilder des //only serialize, if key and value are serialized without error (e.g. due to reference loop) var itemValue = item.Value; - var itemValueTypeCode = Convert.GetTypeCode(itemValue); - if (!SerializeObject(itemValue, itemValueTypeCode, destination, options, objectsInPath, nextDepth)) + if (!SerializeObject(itemValue, destination, options, objectsInPath, nextDepth)) { destination.Length = originalLength; } @@ -419,7 +410,7 @@ private void SerializeCollectionObject(IEnumerable value, StringBuilder destinat destination.Append(','); } - if (!SerializeObject(val, Convert.GetTypeCode(val), destination, options, objectsInPath, nextDepth)) + if (!SerializeObject(val, destination, options, objectsInPath, nextDepth)) { destination.Length = originalLength; } @@ -433,15 +424,10 @@ private void SerializeCollectionObject(IEnumerable value, StringBuilder destinat private bool SerializeObjectWithProperties(object value, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet objectsInPath, int depth) { - int originalLength = destination.Length; - if (originalLength > MaxJsonLength) - { - return false; - } - if (depth < options.MaxRecursionLimit) { - try + var objectPropertyList = _objectReflectionCache.LookupObjectProperties(value); + if (!objectPropertyList.ConvertToString) { if (ReferenceEquals(options, instance._serializeOptions) && value is Exception) { @@ -449,66 +435,69 @@ private bool SerializeObjectWithProperties(object value, StringBuilder destinati options = instance._exceptionSerializeOptions; } - var objectPropertyList = _objectReflectionCache.LookupObjectProperties(value); using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, false, _referenceEqualsComparer)) { return SerializeObjectProperties(objectPropertyList, destination, options, objectsInPath, depth); } } - catch - { - //nothing to add, so return is OK - destination.Length = originalLength; - return false; - } - } - else - { - return SerializeObjectAsString(value, TypeCode.Object, destination, options); } + + return SerializeObjectAsString(value, destination, options); } - private bool SerializeSimpleTypeCodeValue(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, bool forceQuotes = false) + private void SerializeSimpleTypeCodeValue(IConvertible value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, bool forceToString = false) { - if (objTypeCode == TypeCode.String || objTypeCode == TypeCode.Char) + if (value == null) + { + destination.Append(forceToString ? "\"\"" : "null"); + } + else if (objTypeCode == TypeCode.String || objTypeCode == TypeCode.Char) { destination.Append('"'); AppendStringEscape(destination, value.ToString(), options.EscapeUnicode); destination.Append('"'); } - else if (IsNumericTypeCode(objTypeCode, false)) - { - if (!options.EnumAsInteger && value is Enum enumValue) - { - QuoteValue(destination, EnumAsString(enumValue)); - } - else - { - if (forceQuotes) - destination.Append('"'); - destination.AppendIntegerAsString(value, objTypeCode); - if (forceQuotes) - destination.Append('"'); - } - } else { - string str = XmlHelper.XmlConvertToString(value, objTypeCode); - if (str == null) + var hasFormat = !StringHelpers.IsNullOrWhiteSpace(options.Format); + if ((options.FormatProvider != null || hasFormat) && (value is IFormattable formattable)) { - return false; + bool includeQuotes = forceToString || objTypeCode == TypeCode.Object || !SkipQuotes(value, objTypeCode); + SerializeWithFormatProvider(formattable, includeQuotes, destination, options, hasFormat); } - - if (!forceQuotes && SkipQuotes(value, objTypeCode)) + else if (IsNumericTypeCode(objTypeCode, false)) { - destination.Append(str); + SerializeSimpleNumericValue(value, objTypeCode, destination, options, forceToString); } else { - QuoteValue(destination, str); + string str = XmlHelper.XmlConvertToString(value, objTypeCode); + if (!forceToString && str != null && SkipQuotes(value, objTypeCode)) + { + destination.Append(str); + } + else + { + QuoteValue(destination, str); + } } } - return true; + } + + private void SerializeSimpleNumericValue(IConvertible value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, bool forceToString) + { + if (!options.EnumAsInteger && value is Enum enumValue) + { + QuoteValue(destination, EnumAsString(enumValue)); + } + else + { + if (forceToString) + destination.Append('"'); + destination.AppendIntegerAsString(value, objTypeCode); + if (forceToString) + destination.Append('"'); + } } private static CultureInfo CreateFormatProvider() @@ -551,7 +540,7 @@ private string EnumAsString(Enum value) /// /// No quotes needed for this type? /// - private static bool SkipQuotes(object value, TypeCode objTypeCode) + private static bool SkipQuotes(IConvertible value, TypeCode objTypeCode) { switch (objTypeCode) { @@ -563,12 +552,12 @@ private static bool SkipQuotes(object value, TypeCode objTypeCode) case TypeCode.Decimal: return true; case TypeCode.Double: { - double dblValue = (double)value; + double dblValue = value.ToDouble(CultureInfo.InvariantCulture); return !double.IsNaN(dblValue) && !double.IsInfinity(dblValue); } case TypeCode.Single: { - float floatValue = (float)value; + float floatValue = value.ToSingle(CultureInfo.InvariantCulture); return !float.IsNaN(floatValue) && !float.IsInfinity(floatValue); } default: @@ -708,15 +697,9 @@ private static bool EscapeChar(char ch, bool escapeUnicode) return escapeUnicode && ch > 127; } - private bool SerializeObjectProperties(ObjectReflectionCache.ObjectPropertyList objectPropertyList,StringBuilder destination, JsonSerializeOptions options, + private bool SerializeObjectProperties(ObjectReflectionCache.ObjectPropertyList objectPropertyList, StringBuilder destination, JsonSerializeOptions options, SingleItemOptimizedHashSet objectsInPath, int depth) { - if (objectPropertyList.Count == 0) - { - //no props - return SerializeObjectAsString(objectPropertyList.ToString(), TypeCode.Object, destination, options); - } - destination.Append('{'); bool first = true; @@ -743,19 +726,28 @@ private static bool EscapeChar(char ch, bool escapeUnicode) } destination.Append(':'); - if (!SerializeObject(propertyValue.Value, propertyValue.TypeCode, destination, options, objectsInPath, depth + 1)) + var objTypeCode = propertyValue.TypeCode; + if (objTypeCode != TypeCode.Object) { - destination.Length = originalLength; + SerializeSimpleTypeCodeValue((IConvertible)propertyValue.Value, objTypeCode, destination, options); + first = false; } else { - first = false; + if (!SerializeObject(propertyValue.Value, destination, options, objectsInPath, depth + 1)) + { + destination.Length = originalLength; + } + else + { + first = false; + } } } } catch { - //skip this property + // skip single property destination.Length = originalLength; } } @@ -769,25 +761,34 @@ private static bool HasNameAndValue(ObjectReflectionCache.ObjectPropertyList.Pro return propertyValue.Name != null && propertyValue.Value != null; } - private bool SerializeObjectAsString(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options) + private bool SerializeObjectAsString(object value, StringBuilder destination, JsonSerializeOptions options) { + var originalLength = destination.Length; + try { - if (objTypeCode == TypeCode.Object) + if (SerializeSimpleObjectValue(value, destination, options, true)) { - var str = Convert.ToString(value, CultureInfo.InvariantCulture); - destination.Append('"'); - AppendStringEscape(destination, str, options.EscapeUnicode); - destination.Append('"'); return true; } - else + + var hasFormat = !StringHelpers.IsNullOrWhiteSpace(options.Format); + if ((options.FormatProvider != null || hasFormat) && (value is IFormattable formattable)) { - return SerializeSimpleTypeCodeValue(value, objTypeCode, destination, options, true); + SerializeWithFormatProvider(formattable, true, destination, options, hasFormat); + return true; } + + var str = Convert.ToString(value, CultureInfo.InvariantCulture); + destination.Append('"'); + AppendStringEscape(destination, str, options.EscapeUnicode); + destination.Append('"'); + return true; } catch { + // skip bad object + destination.Length = originalLength; return false; } } diff --git a/tests/NLog.UnitTests/Internal/ExpandoTestDictionary.cs b/tests/NLog.UnitTests/Internal/ExpandoTestDictionary.cs new file mode 100644 index 0000000000..1d286802f7 --- /dev/null +++ b/tests/NLog.UnitTests/Internal/ExpandoTestDictionary.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) 2004-2019 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.UnitTests.Internal +{ + using System; + using System.Collections; + using System.Collections.Generic; + + /// + /// Special Expando-Object that has custom object-value (Similar to JObject) + /// + internal class ExpandoTestDictionary : IDictionary + { + private readonly Dictionary _properties = new Dictionary(); + + public IConvertible this[string key] { get => ((IDictionary)_properties)[key]; set => ((IDictionary)_properties)[key] = value; } + + public ICollection Keys => ((IDictionary)_properties).Keys; + + public ICollection Values => ((IDictionary)_properties).Values; + + public int Count => ((IDictionary)_properties).Count; + + public bool IsReadOnly => ((IDictionary)_properties).IsReadOnly; + + public void Add(string key, IConvertible value) + { + ((IDictionary)_properties).Add(key, value); + } + + public void Add(KeyValuePair item) + { + ((IDictionary)_properties).Add(item); + } + + public void Clear() + { + ((IDictionary)_properties).Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)_properties).Contains(item); + } + + public bool ContainsKey(string key) + { + return ((IDictionary)_properties).ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)_properties).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return ((IDictionary)_properties).GetEnumerator(); + } + + public bool Remove(string key) + { + return ((IDictionary)_properties).Remove(key); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)_properties).Remove(item); + } + + public bool TryGetValue(string key, out IConvertible value) + { + return ((IDictionary)_properties).TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IDictionary)_properties).GetEnumerator(); + } + } +} diff --git a/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTestsBase.cs b/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTestsBase.cs index da19751234..d6440878fc 100644 --- a/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTestsBase.cs +++ b/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTestsBase.cs @@ -272,6 +272,16 @@ public void SerializeTrickyDict_Test() Assert.Equal("{\"key1\":13,\"key 2\":1.3}", actual); } + [Fact] + public void SerializeExpandoDict_Test() + { + IDictionary dictionary = new Internal.ExpandoTestDictionary(); + dictionary.Add("key 2", 1.3m); + dictionary.Add("level", LogLevel.Info); + var actual = SerializeObject(dictionary); + Assert.Equal("{\"key 2\":1.3, \"level\":{\"Name\":\"Info\", \"Ordinal\":2}}", actual); + } + [Fact] public void SerializeIntegerKeyDict_Test() {