// // Copyright (c) 2004-2024 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen // // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // * Neither the name of Jaroslaw Kowalski nor the names of its // contributors may be used to endorse or promote products derived from this // software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. // namespace NLog.Internal { using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using NLog.Common; using NLog.Config; /// /// Converts object into a List of property-names and -values using reflection /// internal sealed class ObjectReflectionCache : IObjectTypeTransformer { private MruCache ObjectTypeCache => _objectTypeCache ?? System.Threading.Interlocked.CompareExchange(ref _objectTypeCache, new MruCache(10000), null) ?? _objectTypeCache; private MruCache? _objectTypeCache; private MruCache ObjectTypeFieldCache => _objectTypeFieldCache ?? System.Threading.Interlocked.CompareExchange(ref _objectTypeFieldCache, new MruCache(10000), null) ?? _objectTypeFieldCache; private MruCache? _objectTypeFieldCache; private readonly IServiceProvider _serviceProvider; private IObjectTypeTransformer ObjectTypeTransformation => _objectTypeTransformation ?? (_objectTypeTransformation = _serviceProvider?.GetService() ?? this); private IObjectTypeTransformer? _objectTypeTransformation; public ObjectReflectionCache(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } object? IObjectTypeTransformer.TryTransformObject(object? obj) { return null; } public ObjectPropertyList LookupObjectProperties(object value) => LookupObjectProperties(value, false); /// /// Lookup object members, optionally including public fields besides public properties. /// public ObjectPropertyList LookupObjectProperties(object value, bool includeFields) { if (TryLookupExpandoObject(value, includeFields, 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 typeCache = includeFields ? ObjectTypeFieldCache : ObjectTypeCache; if (!typeCache.TryGetValue(objectType, out var propertyInfos)) { propertyInfos = CheckForObjectProperties(value) ? BuildObjectPropertyInfos(value, includeFields) : ObjectPropertyInfos.SimpleToString; typeCache.TryAddValue(objectType, propertyInfos); } return new ObjectPropertyList(value, propertyInfos.Properties, propertyInfos.FastLookup); } /// /// Try get value from , using , and set into /// public bool TryGetObjectProperty(object? value, string[]? objectPath, out object? foundValue) { foundValue = null; if (objectPath is null) { return false; } for (int i = 0; i < objectPath.Length; ++i) { if (value is null) { return false; } var propertyList = LookupObjectProperties(value); if (propertyList.TryGetPropertyValue(objectPath[i], out var propertyValue)) { value = propertyValue.Value; } else { foundValue = null; return false; //Wrong, but done } } foundValue = value; return true; } public bool TryLookupExpandoObject(object value, out ObjectPropertyList objectPropertyList) => TryLookupExpandoObject(value, false, out objectPropertyList); private bool TryLookupExpandoObject(object value, bool includeFields, out ObjectPropertyList objectPropertyList) { if (value is IDictionary expando) { objectPropertyList = new ObjectPropertyList(expando); return true; } #if !NET35 && !NET40 && NETFRAMEWORK if (value is System.Dynamic.DynamicObject d) { var dictionary = DynamicObjectToDict(d); objectPropertyList = new ObjectPropertyList(dictionary); return true; } #endif Type objectType = value.GetType(); if (!includeFields && 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) { var dictionaryInfos = new ObjectPropertyInfos(ArrayHelper.Empty(), new[] { new FastPropertyLookup(string.Empty, TypeCode.Object, (o, p) => dictionaryEnumerator.GetEnumerator(o)) }); ObjectTypeCache.TryAddValue(objectType, dictionaryInfos); objectPropertyList = new ObjectPropertyList(value, dictionaryInfos.Properties, dictionaryInfos.FastLookup); return true; } objectPropertyList = default(ObjectPropertyList); return false; } [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2072")] private static ObjectPropertyInfos BuildObjectPropertyInfos(object value, bool includeFields) { var objectType = value.GetType(); var properties = GetPublicProperties(objectType); 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) { // Nothing to show as properties, so fall back to the public fields when asked to. var fields = includeFields ? GetPublicFields(objectType) : ArrayHelper.Empty(); if (fields.Length > 0) { // Fields are only reachable through the fast-lookup, so it cannot be built lazily return new ObjectPropertyInfos(ArrayHelper.Empty(), BuildFieldLookup(fields)); } return ObjectPropertyInfos.SimpleToString; } else { return new ObjectPropertyInfos(properties, null); } } private static bool CheckForObjectProperties(object objectValue) { if (ConvertSimpleToString(objectValue)) return false; if (objectValue is Uri) return false; if (objectValue is Delegate) return false; // Skip serializing all types in the application if (objectValue is MemberInfo) return false; // Skip serializing all types in the application if (objectValue is Assembly) return false; // Skip serializing all types in the application if (objectValue is Module) return false; // Skip serializing all types in the application if (objectValue is System.IO.Stream) return false; // Skip serializing properties that often throws exceptions return true; } private static bool ConvertSimpleToString(object objectValue) { if (objectValue is IFormattable) return true; if (objectValue is IConvertible convertible) return convertible.GetTypeCode() != TypeCode.Object; return false; } private static PropertyInfo[] GetPublicProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) { PropertyInfo[]? properties = null; try { properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); } catch (Exception ex) { InternalLogger.Warn(ex, "Failed to get object properties for type: {0}", type); } // Skip Index-Item-Properties (Ex. public string this[int Index]) if (properties != null) { foreach (var prop in properties) { if (!prop.IsValidPublicProperty()) { properties = properties.Where(p => p.IsValidPublicProperty()).ToArray(); break; } } } return properties ?? ArrayHelper.Empty(); } private static FieldInfo[] GetPublicFields([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type type) { try { return type.GetFields(BindingFlags.Instance | BindingFlags.Public); } catch (Exception ex) { InternalLogger.Warn(ex, "Failed to get object fields for type: {0}", type); return ArrayHelper.Empty(); } } private static FastPropertyLookup[] BuildFieldLookup(FieldInfo[] fields) { FastPropertyLookup[] fastLookup = new FastPropertyLookup[fields.Length]; int count = 0; for (int i = 0; i < fields.Length; ++i) { var field = fields[i]; if (IsHiddenByDerivedField(field, fields)) continue; // A derived type redeclared the name with "new", so the member is written once TypeCode typeCode = Type.GetTypeCode(field.FieldType); fastLookup[count++] = new FastPropertyLookup(field.Name, typeCode, (o, p) => field.GetValue(o)); } if (count != fastLookup.Length) Array.Resize(ref fastLookup, count); return fastLookup; } private static bool IsHiddenByDerivedField(FieldInfo field, FieldInfo[] fields) { foreach (var other in fields) { if (!ReferenceEquals(other, field) && other.Name == field.Name && other.DeclaringType != null && field.DeclaringType != null && other.DeclaringType.IsSubclassOf(field.DeclaringType)) return true; } return false; } private static FastPropertyLookup[] BuildFastLookup(PropertyInfo[] properties, bool includeType) { int fastAccessIndex = includeType ? 1 : 0; FastPropertyLookup[] fastLookup = new FastPropertyLookup[properties.Length + fastAccessIndex]; if (includeType) { fastLookup[0] = new FastPropertyLookup("Type", TypeCode.String, (o, p) => o.GetType().ToString()); } foreach (var prop in properties) { var getterMethod = prop.GetGetMethod(); var propertyType = getterMethod?.ReturnType; if (getterMethod is null || propertyType is null) { // Should never happen due to IsValidPublicProperty check fastLookup[fastAccessIndex++] = new FastPropertyLookup(prop.Name, TypeCode.String, (o, p) => null); } else { ReflectionHelpers.LateBoundMethod valueLookup = ReflectionHelpers.CreateLateBoundMethod(getterMethod); TypeCode typeCode = Type.GetTypeCode(propertyType); // Skip cyclic-reference checks when not TypeCode.Object fastLookup[fastAccessIndex++] = new FastPropertyLookup(prop.Name, typeCode, valueLookup); } } return fastLookup; } internal #if !NETFRAMEWORK readonly #endif struct ObjectPropertyList : IEnumerable { internal static readonly StringComparer NameComparer = StringComparer.Ordinal; private static readonly FastPropertyLookup[] CreateIDictionaryEnumerator = new[] { new FastPropertyLookup(string.Empty, TypeCode.Object, (o, p) => ((IDictionary)o).GetEnumerator()) }; private readonly object _object; private readonly PropertyInfo[] _properties; private readonly FastPropertyLookup[]? _fastLookup; public #if !NETFRAMEWORK readonly #endif struct PropertyValue { public readonly string Name; public readonly object? Value; public TypeCode TypeCode => Value is null ? TypeCode.Empty : _typecode; private readonly TypeCode _typecode; public bool HasNameAndValue => Name != null && Value != null; public PropertyValue(string name, object value, TypeCode typeCode) { Name = name; Value = value; _typecode = typeCode; } public PropertyValue(object owner, PropertyInfo propertyInfo) { Name = propertyInfo.Name; _typecode = TypeCode.Object; try { Value = propertyInfo.GetValue(owner, null); } catch { Value = null; } } public PropertyValue(object owner, FastPropertyLookup fastProperty) { Name = fastProperty.Name; _typecode = fastProperty.TypeCode; try { Value = fastProperty.ValueLookup(owner, ArrayHelper.Empty()); } catch { Value = null; } } public override string ToString() => $"{Name}={Value}"; } public bool IsSimpleValue => _properties.Length == 0 && (_fastLookup is null || _fastLookup.Length == 0); public object ObjectValue => _object; internal ObjectPropertyList(object value, PropertyInfo[] properties, FastPropertyLookup[]? fastLookup) { _object = value; _properties = properties; _fastLookup = fastLookup; } public ObjectPropertyList(IDictionary value) { _object = value; // Expando objects _properties = ArrayHelper.Empty(); _fastLookup = CreateIDictionaryEnumerator; } // The expando enumerator is stored as a single nameless entry, which no real member can be private static bool IsExpandoLookup(FastPropertyLookup[] fastLookup) => fastLookup.Length == 1 && fastLookup[0].Name.Length == 0; public bool TryGetPropertyValue(string name, out PropertyValue propertyValue) { if (_properties.Length == 0 && (_fastLookup is null || _fastLookup.Length == 0 || IsExpandoLookup(_fastLookup))) { if (_object is IDictionary expandoObject) { if (expandoObject.TryGetValue(name, out var objectValue)) { propertyValue = new PropertyValue(name, objectValue, TypeCode.Object); return true; } propertyValue = default(PropertyValue); return false; } return TryListLookupPropertyValue(name, out propertyValue); } if (_fastLookup != null) { // Scans properties for name (Skips string-compare and value-lookup until finding match) int nameHashCode = NameComparer.GetHashCode(name); foreach (var fastProperty in _fastLookup) { if (fastProperty.NameHashCode == nameHashCode && NameComparer.Equals(fastProperty.Name, name)) { propertyValue = new PropertyValue(_object, fastProperty); return true; } } propertyValue = default(PropertyValue); return false; } return TrySlowLookupPropertyValue(name, out propertyValue); } /// /// Scans properties for name (Skips property value lookup until finding match) /// private bool TrySlowLookupPropertyValue(string name, out PropertyValue propertyValue) { foreach (var propInfo in _properties) { if (NameComparer.Equals(propInfo.Name, name)) { propertyValue = new PropertyValue(_object, propInfo); return true; } } propertyValue = default(PropertyValue); return false; } /// /// Scans properties for name /// private bool TryListLookupPropertyValue(string name, out PropertyValue propertyValue) { foreach (var item in this) { if (NameComparer.Equals(item.Name, name)) { propertyValue = item; return true; } } propertyValue = default(PropertyValue); return false; } public override string ToString() { return _object?.ToString() ?? "null"; } public Enumerator GetEnumerator() { if (_properties.Length > 0 || _fastLookup is null || _fastLookup.Length == 0 || !IsExpandoLookup(_fastLookup)) return new Enumerator(_object, _properties, _fastLookup); var expandoObject = _fastLookup[0].ValueLookup(_object, ArrayHelper.Empty()); return expandoObject is null ? default(Enumerator) : new Enumerator((IEnumerator>)expandoObject); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public struct Enumerator : IEnumerator { private readonly object _owner; private readonly PropertyInfo[]? _properties; private readonly FastPropertyLookup[]? _fastLookup; private readonly IEnumerator>? _enumerator; private int _index; internal Enumerator(object owner, PropertyInfo[] properties, FastPropertyLookup[]? fastLookup) { _owner = owner; _properties = properties; _fastLookup = fastLookup; _index = -1; _enumerator = null; } internal Enumerator(IEnumerator> enumerator) { _owner = enumerator; _properties = null; _fastLookup = null; _index = 0; _enumerator = enumerator; } public PropertyValue Current { get { try { if (_fastLookup != null) return new PropertyValue(_owner, _fastLookup[_index]); else if (_properties != null) return new PropertyValue(_owner, _properties[_index]); else if (_enumerator != null) return new PropertyValue(_enumerator.Current.Key, _enumerator.Current.Value, TypeCode.Object); else return default(PropertyValue); } catch (Exception ex) { InternalLogger.Debug(ex, "Failed to get property value for object: {0}", _owner); return default(PropertyValue); } } } object IEnumerator.Current => Current; public void Dispose() { _enumerator?.Dispose(); } public bool MoveNext() { if (_properties != null) return ++_index < (_fastLookup?.Length ?? _properties.Length); else return _enumerator?.MoveNext() == true; } public void Reset() { if (_properties != null) _index = -1; else _enumerator?.Reset(); } } } internal #if !NETFRAMEWORK readonly #endif struct FastPropertyLookup { public readonly string Name; public readonly ReflectionHelpers.LateBoundMethod ValueLookup; public readonly TypeCode TypeCode; public readonly int NameHashCode; public FastPropertyLookup(string name, TypeCode typeCode, ReflectionHelpers.LateBoundMethod valueLookup) { Name = name; ValueLookup = valueLookup; TypeCode = typeCode; NameHashCode = ObjectPropertyList.NameComparer.GetHashCode(name); } } private #if !NETFRAMEWORK readonly #endif struct ObjectPropertyInfos : IEquatable { public readonly PropertyInfo[] Properties; public readonly FastPropertyLookup[]? FastLookup; public static readonly ObjectPropertyInfos SimpleToString = new ObjectPropertyInfos(ArrayHelper.Empty(), ArrayHelper.Empty()); public ObjectPropertyInfos(PropertyInfo[] properties, FastPropertyLookup[]? fastLookup) { Properties = properties; FastLookup = fastLookup; } public bool HasFastLookup => FastLookup != null; public bool Equals(ObjectPropertyInfos other) { return ReferenceEquals(Properties, other.Properties) && FastLookup?.Length == other.FastLookup?.Length; } } #if !NET35 && !NET40 && NETFRAMEWORK private static Dictionary DynamicObjectToDict(System.Dynamic.DynamicObject d) { var newVal = new Dictionary(); foreach (var propName in d.GetDynamicMemberNames()) { if (d.TryGetMember(new GetBinderAdapter(propName), out var result)) { newVal[propName] = result; } } return newVal; } /// /// Binder for retrieving value of /// private sealed class GetBinderAdapter : System.Dynamic.GetMemberBinder { internal GetBinderAdapter(string name) : base(name, false) { } /// public override System.Dynamic.DynamicMetaObject FallbackGetMember(System.Dynamic.DynamicMetaObject target, System.Dynamic.DynamicMetaObject errorSuggestion) { return target; } } #endif private static IDictionaryEnumerator? TryGetDictionaryEnumerator(object value) { if (!(value is IEnumerable) || value is string) return null; if (value is IDictionary) return new DictionaryEnumerator(); if (value is IDictionary) return new DictionaryEnumerator(); #if !NET35 && !NET40 if (value is IReadOnlyDictionary) return new DictionaryEnumerator(); if (value is IReadOnlyDictionary) return new DictionaryEnumerator(); #endif if (value is IDictionary && value.GetType().IsGenericType) { if (value.GetType().GetGenericArguments()[0] == typeof(string)) return new DictionaryEnumerator(DictionaryEnumerator.BuildDictionaryEnumerator); else return null; } return TryBuildDictionaryEnumerator(value); } [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2075")] private static IDictionaryEnumerator? TryBuildDictionaryEnumerator(object value) { foreach (var interfaceType in value.GetType().GetInterfaces()) { if (interfaceType.IsGenericType && (interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>) #if !NET35 && !NET40 || interfaceType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>) #endif ) && interfaceType.GetGenericArguments()[0] == typeof(string)) { #if NETFRAMEWORK return (IDictionaryEnumerator)Activator.CreateInstance(typeof(DictionaryEnumerator<,>).MakeGenericType(interfaceType.GetGenericArguments())); #else // AOT does not support creating new types at runtime foreach (var subInterfaceType in value.GetType().GetInterfaces()) { if (subInterfaceType.IsGenericType && subInterfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>) && subInterfaceType.GetGenericArguments()[0].IsGenericType && subInterfaceType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(KeyValuePair<,>) && subInterfaceType.GetGenericArguments()[0].GetGenericArguments()[0] == typeof(string)) { var enumerator = DictionaryEnumerator.BuildEnumerator(subInterfaceType, subInterfaceType.GetGenericArguments()[0]); if (enumerator != null) return new DictionaryEnumerator(enumerator); } } #endif } } return null; } private interface IDictionaryEnumerator { IEnumerator> GetEnumerator(object value); } private sealed class DictionaryEnumerator : IDictionaryEnumerator { private readonly Func>> _enumerateCollection; public DictionaryEnumerator(Func>> enumerateCollection) { _enumerateCollection = enumerateCollection; } IEnumerator> IDictionaryEnumerator.GetEnumerator(object value) { return _enumerateCollection(value); } public static IEnumerator> BuildDictionaryEnumerator(object collection) { if (collection is IDictionary dictionary) { if (dictionary.Count == 0) return EmptyDictionaryEnumerator.Default; return YieldEnumerator(dictionary); } return EmptyDictionaryEnumerator.Default; } private static IEnumerator> YieldEnumerator(IDictionary dictionary) { foreach (var item in new DictionaryEntryEnumerable(dictionary)) yield return new KeyValuePair(item.Key?.ToString() ?? string.Empty, item.Value); } #if !NETFRAMEWORK [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2075")] [UnconditionalSuppressMessage("Trimming - Allow reflection of message args", "IL2070")] public static Func>>? BuildEnumerator(Type dictionaryType, Type dictionaryItemType) { var enumeratorMethod = dictionaryType.GetMethod(nameof(IEnumerable.GetEnumerator)); var getKeyMethod = dictionaryItemType.GetProperty(nameof(KeyValuePair.Key))?.GetGetMethod(); var getValueMethod = dictionaryItemType.GetProperty(nameof(KeyValuePair.Value))?.GetGetMethod(); if (enumeratorMethod != null && getKeyMethod != null && getValueMethod != null) { var enumeratorGetter = ReflectionHelpers.CreateLateBoundMethod(enumeratorMethod); var keyGetter = ReflectionHelpers.CreateLateBoundMethod(getKeyMethod); var valueGetter = ReflectionHelpers.CreateLateBoundMethod(getValueMethod); return (collection) => { if (collection is IEnumerable && dictionaryType.IsInstanceOfType(collection)) { var enumerator = (IEnumerator?)enumeratorGetter.Invoke(collection, ArrayHelper.Empty()); if (enumerator is null || !enumerator.MoveNext()) { (enumerator as IDisposable)?.Dispose(); return EmptyDictionaryEnumerator.Default; } return EnumerateItems(enumerator, keyGetter, valueGetter); } return EmptyDictionaryEnumerator.Default; }; } return null; } private static IEnumerator> EnumerateItems(IEnumerator enumerator, ReflectionHelpers.LateBoundMethod getKeyProperty, ReflectionHelpers.LateBoundMethod getValueProperty) { try { do { var item = enumerator.Current; var keyString = getKeyProperty.Invoke(item, ArrayHelper.Empty()); var valueObject = getValueProperty.Invoke(item, ArrayHelper.Empty()); yield return new KeyValuePair(keyString?.ToString() ?? string.Empty, valueObject); } while (enumerator.MoveNext()); } finally { (enumerator as IDisposable)?.Dispose(); } } #endif } private sealed class DictionaryEnumerator : IDictionaryEnumerator { public IEnumerator> GetEnumerator(object value) { if (value is IDictionary dictionary) { if (dictionary.Count > 0) return YieldEnumerator(dictionary); } #if !NET35 && !NET40 else if (value is IReadOnlyDictionary readonlyDictionary) { if (readonlyDictionary.Count > 0) return YieldEnumerator(readonlyDictionary); } #endif return EmptyDictionaryEnumerator.Default; } private static IEnumerator> YieldEnumerator(IDictionary dictionary) { foreach (var item in dictionary) yield return new KeyValuePair(item.Key?.ToString() ?? string.Empty, item.Value); } #if !NET35 && !NET40 private static IEnumerator> YieldEnumerator(IReadOnlyDictionary dictionary) { foreach (var item in dictionary) yield return new KeyValuePair(item.Key?.ToString() ?? string.Empty, item.Value); } #endif } private sealed class EmptyDictionaryEnumerator : IEnumerator> { public static readonly EmptyDictionaryEnumerator Default = new EmptyDictionaryEnumerator(); KeyValuePair IEnumerator>.Current => default(KeyValuePair); object IEnumerator.Current => default(KeyValuePair); bool IEnumerator.MoveNext() => false; void IDisposable.Dispose() { // Nothing here on purpose } void IEnumerator.Reset() { // Nothing here on purpose } } } }