diff --git a/Directory.Packages.props b/Directory.Packages.props index acdca03c666..beba2030348 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + diff --git a/src/Docfx.YamlSerialization/Helpers/ReflectionExtensions.cs b/src/Docfx.YamlSerialization/Helpers/ReflectionExtensions.cs index a60a1924d29..b5b3ff5d366 100644 --- a/src/Docfx.YamlSerialization/Helpers/ReflectionExtensions.cs +++ b/src/Docfx.YamlSerialization/Helpers/ReflectionExtensions.cs @@ -11,12 +11,18 @@ internal static class ReflectionExtensions /// Determines whether the specified type has a default constructor. /// /// The type. + /// Allow private constructor. /// /// true if the type has a default constructor; otherwise, false. /// - public static bool HasDefaultConstructor(this Type type) + public static bool HasDefaultConstructor(this Type type, bool allowPrivateConstructors) { - return type.IsValueType || type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null) != null; + var bindingFlags = BindingFlags.Public | BindingFlags.Instance; + if (allowPrivateConstructors) + { + bindingFlags |= BindingFlags.NonPublic; + } + return type.IsValueType || type.GetConstructor(bindingFlags, null, Type.EmptyTypes, null) != null; } public static IEnumerable GetPublicProperties(this Type type) diff --git a/src/Docfx.YamlSerialization/Helpers/TypeConverterCache.cs b/src/Docfx.YamlSerialization/Helpers/TypeConverterCache.cs new file mode 100644 index 00000000000..bbdcb2e2b83 --- /dev/null +++ b/src/Docfx.YamlSerialization/Helpers/TypeConverterCache.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using YamlDotNet.Serialization; + +namespace Docfx.YamlSerialization.Helpers; + +/// +/// A cache / map for instances. +/// +/// +/// This class is copied from following YamlDotNet implementation. +/// https://github.com/aaubry/YamlDotNet/blob/master/YamlDotNet/Serialization/Utilities/TypeConverterCache.cs +/// +internal sealed class TypeConverterCache +{ + private readonly IYamlTypeConverter[] typeConverters; + private readonly ConcurrentDictionary cache = new(); + + public TypeConverterCache(IEnumerable? typeConverters) + : this(typeConverters?.ToArray() ?? []) + { + } + + public TypeConverterCache(IYamlTypeConverter[] typeConverters) + { + this.typeConverters = typeConverters; + } + + /// + /// Returns the first that accepts the given type. + /// + /// The to lookup. + /// The that accepts this type or if no converter is found. + /// if a type converter was found; otherwise. + public bool TryGetConverterForType(Type type, [NotNullWhen(true)] out IYamlTypeConverter? typeConverter) + { + var result = cache.GetOrAdd(type, static (t, tc) => LookupTypeConverter(t, tc), typeConverters); + + typeConverter = result.TypeConverter; + return result.HasMatch; + } + + /// + /// Returns the of the given type. + /// + /// The type of the converter. + /// The of the given type. + /// If no type converter of the given type is found. + /// + /// Note that this method searches on the type of the itself. If you want to find a type converter + /// that accepts a given , use instead. + /// + public IYamlTypeConverter GetConverterByType(Type converter) + { + // Intentially avoids LINQ as this is on a hot path + foreach (var typeConverter in typeConverters) + { + if (typeConverter.GetType() == converter) + { + return typeConverter; + } + } + + throw new ArgumentException($"{nameof(IYamlTypeConverter)} of type {converter.FullName} not found", nameof(converter)); + } + + private static (bool HasMatch, IYamlTypeConverter? TypeConverter) LookupTypeConverter(Type type, IYamlTypeConverter[] typeConverters) + { + // Intentially avoids LINQ as this is on a hot path + foreach (var typeConverter in typeConverters) + { + if (typeConverter.Accepts(type)) + { + return (true, typeConverter); + } + } + + return (false, null); + } +} diff --git a/src/Docfx.YamlSerialization/NodeDeserializers/EmitArrayNodeDeserializer.cs b/src/Docfx.YamlSerialization/NodeDeserializers/EmitArrayNodeDeserializer.cs index 0dc273ac031..7f6fc724e95 100644 --- a/src/Docfx.YamlSerialization/NodeDeserializers/EmitArrayNodeDeserializer.cs +++ b/src/Docfx.YamlSerialization/NodeDeserializers/EmitArrayNodeDeserializer.cs @@ -12,12 +12,22 @@ namespace Docfx.YamlSerialization.NodeDeserializers; public class EmitArrayNodeDeserializer : INodeDeserializer { + private readonly INamingConvention _enumNamingConvention; + private readonly ITypeInspector _typeDescriptor; + private static readonly MethodInfo DeserializeHelperMethod = typeof(EmitArrayNodeDeserializer).GetMethod(nameof(DeserializeHelper))!; - private static readonly ConcurrentDictionary, object?>> _funcCache = + + private static readonly ConcurrentDictionary, INamingConvention, ITypeInspector, object?>> _funcCache = new(); - bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) + public EmitArrayNodeDeserializer(INamingConvention enumNamingConvention, ITypeInspector typeDescriptor) + { + _enumNamingConvention = enumNamingConvention; + _typeDescriptor = typeDescriptor; + } + + bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!expectedType.IsArray) { @@ -26,27 +36,44 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func(IParser reader, Type expectedType, Func nestedObjectDeserializer) + public static TItem[] DeserializeHelper( + IParser reader, + Type expectedType, + Func nestedObjectDeserializer, + INamingConvention enumNamingConvention, + ITypeInspector typeDescriptor) { var items = new List(); - EmitGenericCollectionNodeDeserializer.DeserializeHelper(reader, expectedType, nestedObjectDeserializer, items); + EmitGenericCollectionNodeDeserializer.DeserializeHelper(reader, expectedType, nestedObjectDeserializer, items, enumNamingConvention, typeDescriptor); return items.ToArray(); } - private static Func, object?> AddItem(Type expectedType) + private static Func, INamingConvention, ITypeInspector, object?> AddItem(Type expectedType) { - var dm = new DynamicMethod(string.Empty, typeof(object), [typeof(IParser), typeof(Type), typeof(Func)]); + var dm = new DynamicMethod( + string.Empty, + returnType: typeof(object), + parameterTypes: + [ + typeof(IParser), // reader + typeof(Type), // expectedType + typeof(Func), // nestedObjectDeserializer + typeof(INamingConvention), // enumNamingConvention + typeof(ITypeInspector), // typeDescriptor + ]); var il = dm.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Ldarg_S, (byte)4); il.Emit(OpCodes.Call, DeserializeHelperMethod.MakeGenericMethod(expectedType.GetElementType()!)); il.Emit(OpCodes.Ret); - return (Func, object?>)dm.CreateDelegate(typeof(Func, object?>)); + return (Func, INamingConvention, ITypeInspector, object?>)dm.CreateDelegate(typeof(Func, INamingConvention, ITypeInspector, object?>)); } } diff --git a/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericCollectionNodeDeserializer.cs b/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericCollectionNodeDeserializer.cs index 58f5dd74ff2..632ffdaf6c1 100644 --- a/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericCollectionNodeDeserializer.cs +++ b/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericCollectionNodeDeserializer.cs @@ -7,7 +7,6 @@ using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.Utilities; using EditorBrowsable = System.ComponentModel.EditorBrowsableAttribute; using EditorBrowsableState = System.ComponentModel.EditorBrowsableState; @@ -19,15 +18,21 @@ public class EmitGenericCollectionNodeDeserializer : INodeDeserializer private static readonly MethodInfo DeserializeHelperMethod = typeof(EmitGenericCollectionNodeDeserializer).GetMethod(nameof(DeserializeHelper))!; private readonly IObjectFactory _objectFactory; - private readonly Dictionary _gpCache = []; - private readonly Dictionary, object?>> _actionCache = []; + private readonly INamingConvention _enumNamingConvention; + private readonly ITypeInspector _typeDescriptor; + private readonly Dictionary _gpCache = + new(); + private readonly Dictionary, object?, INamingConvention, ITypeInspector>> _actionCache = + new(); - public EmitGenericCollectionNodeDeserializer(IObjectFactory objectFactory) + public EmitGenericCollectionNodeDeserializer(IObjectFactory objectFactory, INamingConvention enumNamingConvention, ITypeInspector typeDescriptor) { _objectFactory = objectFactory; + _enumNamingConvention = enumNamingConvention; + _typeDescriptor = typeDescriptor; } - bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!_gpCache.TryGetValue(expectedType, out var gp)) { @@ -55,25 +60,44 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func), typeof(object)]); + var dm = new DynamicMethod( + string.Empty, + returnType: typeof(void), + [ + typeof(IParser), + typeof(Type), + typeof(Func), + typeof(object), + typeof(INamingConvention), + typeof(ITypeInspector) + ]); + var il = dm.GetILGenerator(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Ldarg_0); // reader + il.Emit(OpCodes.Ldarg_1); // expectedType + il.Emit(OpCodes.Ldarg_2); // nestedObjectDeserializer + il.Emit(OpCodes.Ldarg_3); // result il.Emit(OpCodes.Castclass, typeof(ICollection<>).MakeGenericType(gp)); + il.Emit(OpCodes.Ldarg_S, (byte)4); // enumNamingConvention + il.Emit(OpCodes.Ldarg_S, (byte)5); // typeDescriptor il.Emit(OpCodes.Call, DeserializeHelperMethod.MakeGenericMethod(gp)); il.Emit(OpCodes.Ret); - action = (Action, object?>)dm.CreateDelegate(typeof(Action, object?>)); + action = (Action, object?, INamingConvention, ITypeInspector>)dm.CreateDelegate(typeof(Action, object?, INamingConvention, ITypeInspector>)); _actionCache[gp] = action; } - action(reader, expectedType, nestedObjectDeserializer, value); + action(reader, expectedType, nestedObjectDeserializer, value, _enumNamingConvention, _typeDescriptor); return true; } [EditorBrowsable(EditorBrowsableState.Never)] - public static void DeserializeHelper(IParser reader, Type expectedType, Func nestedObjectDeserializer, ICollection result) + public static void DeserializeHelper( + IParser reader, + Type expectedType, + Func nestedObjectDeserializer, + ICollection result, + INamingConvention enumNamingConvention, + ITypeInspector typeDescriptor) { reader.Consume(); while (!reader.Accept(out _)) @@ -81,13 +105,13 @@ public static void DeserializeHelper(IParser reader, Type expectedType, F var value = nestedObjectDeserializer(reader, typeof(TItem)); if (value is not IValuePromise promise) { - result.Add(TypeConverter.ChangeType(value, NullNamingConvention.Instance)); + result.Add(TypeConverter.ChangeType(value, enumNamingConvention, typeDescriptor)); } else if (result is IList list) { var index = list.Count; result.Add(default!); - promise.ValueAvailable += v => list[index] = TypeConverter.ChangeType(v, NullNamingConvention.Instance); + promise.ValueAvailable += v => list[index] = TypeConverter.ChangeType(v, enumNamingConvention, typeDescriptor); } else { diff --git a/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericDictionaryNodeDeserializer.cs b/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericDictionaryNodeDeserializer.cs index 8294a3dd44d..04fe761597c 100644 --- a/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericDictionaryNodeDeserializer.cs +++ b/src/Docfx.YamlSerialization/NodeDeserializers/EmitGenericDictionaryNodeDeserializer.cs @@ -24,7 +24,7 @@ public EmitGenericDictionaryNodeDeserializer(IObjectFactory objectFactory) _objectFactory = objectFactory; } - bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!_gpCache.TryGetValue(expectedType, out var gp)) { diff --git a/src/Docfx.YamlSerialization/NodeDeserializers/ExtensibleObjectNodeDeserializer.cs b/src/Docfx.YamlSerialization/NodeDeserializers/ExtensibleObjectNodeDeserializer.cs index 24980f3120d..c883079bcb4 100644 --- a/src/Docfx.YamlSerialization/NodeDeserializers/ExtensibleObjectNodeDeserializer.cs +++ b/src/Docfx.YamlSerialization/NodeDeserializers/ExtensibleObjectNodeDeserializer.cs @@ -4,7 +4,6 @@ using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.Utilities; namespace Docfx.YamlSerialization.NodeDeserializers; @@ -14,15 +13,19 @@ public sealed class ExtensibleObjectNodeDeserializer : INodeDeserializer private readonly IObjectFactory _objectFactory; private readonly ITypeInspector _typeDescriptor; private readonly bool _ignoreUnmatched; + private readonly bool _caseInsensitivePropertyMatching; + private readonly INamingConvention _enumNamingConvention; - public ExtensibleObjectNodeDeserializer(IObjectFactory objectFactory, ITypeInspector typeDescriptor, bool ignoreUnmatched) + public ExtensibleObjectNodeDeserializer(IObjectFactory objectFactory, ITypeInspector typeDescriptor, INamingConvention enumNamingConvention, bool ignoreUnmatched, bool caseInsensitivePropertyMatching) { _objectFactory = objectFactory; _typeDescriptor = typeDescriptor; + _enumNamingConvention = enumNamingConvention; _ignoreUnmatched = ignoreUnmatched; + _caseInsensitivePropertyMatching = caseInsensitivePropertyMatching; } - bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!reader.TryConsume(out _)) { @@ -34,7 +37,7 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func(out _)) { var propertyName = reader.Consume(); - var property = _typeDescriptor.GetProperty(expectedType, value, propertyName.Value, _ignoreUnmatched); + var property = _typeDescriptor.GetProperty(expectedType, value, propertyName.Value, _ignoreUnmatched, _caseInsensitivePropertyMatching); if (property == null) { reader.SkipThisAndNestedEvents(); @@ -44,7 +47,7 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func { - var convertedValue = TypeConverter.ChangeType(v, property.Type, NullNamingConvention.Instance); + var convertedValue = TypeConverter.ChangeType(v, property.Type, _enumNamingConvention, _typeDescriptor); property.Write(valueRef, convertedValue); }; } diff --git a/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs b/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs index 29dfdfab69a..0c0d2e2c583 100644 --- a/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs +++ b/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs @@ -3,11 +3,12 @@ using System.Collections; using System.ComponentModel; -using System.Globalization; +using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; -using Docfx.YamlSerialization.Helpers; +using System.Text; using Docfx.YamlSerialization.ObjectDescriptors; +using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using IObjectGraphVisitor = System.Object; @@ -23,154 +24,223 @@ public class FullObjectGraphTraversalStrategy : IObjectGraphTraversalStrategy { private static MethodInfo TraverseGenericDictionaryHelperMethod { get; } = typeof(FullObjectGraphTraversalStrategy).GetMethod(nameof(TraverseGenericDictionaryHelper))!; - protected YamlSerializer Serializer { get; } + private readonly int _maxRecursion; private readonly ITypeInspector _typeDescriptor; private readonly ITypeResolver _typeResolver; private readonly INamingConvention _namingConvention; - private readonly Dictionary, Action> _behaviorCache = []; - private readonly Dictionary, Action> _traverseGenericDictionaryCache = []; + private readonly IObjectFactory _objectFactory; - public FullObjectGraphTraversalStrategy(YamlSerializer serializer, ITypeInspector typeDescriptor, ITypeResolver typeResolver, int maxRecursion, INamingConvention? namingConvention) - { - if (maxRecursion <= 0) - { - throw new ArgumentOutOfRangeException(nameof(maxRecursion), maxRecursion, "maxRecursion must be greater than 1"); - } + // private readonly Dictionary, Action, ObjectSerializer>> _behaviorCache = new(); + private readonly Dictionary, Action, ObjectSerializer>> _traverseGenericDictionaryCache = new(); + + protected YamlSerializer Serializer { get; } - ArgumentNullException.ThrowIfNull(typeDescriptor); - ArgumentNullException.ThrowIfNull(typeResolver); + public FullObjectGraphTraversalStrategy(YamlSerializer serializer, ITypeInspector typeDescriptor, ITypeResolver typeResolver, int maxRecursion, INamingConvention namingConvention, IObjectFactory objectFactory) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxRecursion); Serializer = serializer; + _typeDescriptor = typeDescriptor; _typeResolver = typeResolver; + _maxRecursion = maxRecursion; - _namingConvention = namingConvention ?? NullNamingConvention.Instance; + _namingConvention = namingConvention; + _objectFactory = objectFactory; } - void IObjectGraphTraversalStrategy.Traverse(IObjectDescriptor graph, IObjectGraphVisitor visitor, TContext context) + void IObjectGraphTraversalStrategy.Traverse(IObjectDescriptor graph, IObjectGraphVisitor visitor, TContext context, ObjectSerializer serializer) { - Traverse(graph, visitor, 0, context); + Traverse(null, "", graph, visitor, context, new Stack(_maxRecursion), serializer); } - protected virtual void Traverse(IObjectDescriptor value, IObjectGraphVisitor visitor, int currentDepth, TContext context) + protected virtual void Traverse(IPropertyDescriptor? propertyDescriptor, object name, IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path, ObjectSerializer serializer) { - if (++currentDepth > _maxRecursion) + if (path.Count >= _maxRecursion) { - throw new InvalidOperationException("Too much recursion when traversing the object graph"); + var message = new StringBuilder(); + message.AppendLine("Too much recursion when traversing the object graph."); + message.AppendLine("The path to reach this recursion was:"); + + var lines = new Stack>(path.Count); + var maxNameLength = 0; + foreach (var segment in path) + { + var segmentName = segment.Name?.ToString() ?? string.Empty; + maxNameLength = Math.Max(maxNameLength, segmentName.Length); + lines.Push(new KeyValuePair(segmentName, segment.Value.Type.FullName!)); + } + + foreach (var line in lines) + { + message + .Append(" -> ") + .Append(line.Key.PadRight(maxNameLength)) + .Append(" [") + .Append(line.Value) + .AppendLine("]"); + } + + throw new MaximumRecursionLevelReachedException(message.ToString()); } - if (!visitor.Enter(value, context)) + if (!visitor.Enter(propertyDescriptor, value, context, serializer)) { return; } - var typeCode = Type.GetTypeCode(value.Type); - switch (typeCode) + path.Push(new ObjectPathSegment(name, value)); + + try { - case TypeCode.Boolean: - case TypeCode.Byte: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.SByte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - case TypeCode.Single: - case TypeCode.Double: - case TypeCode.Decimal: - case TypeCode.String: - case TypeCode.Char: - case TypeCode.DateTime: - visitor.VisitScalar(value, context); - break; - case TypeCode.DBNull: - visitor.VisitScalar(new BetterObjectDescriptor(null, typeof(object), typeof(object)), context); - break; - case TypeCode.Empty: - throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "TypeCode.{0} is not supported.", typeCode)); - default: - if (value.Value == null || value.Type == typeof(TimeSpan)) - { - visitor.VisitScalar(value, context); + var typeCode = Type.GetTypeCode(value.Type); + switch (typeCode) + { + case TypeCode.Boolean: + case TypeCode.Byte: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + case TypeCode.String: + case TypeCode.Char: + case TypeCode.DateTime: + visitor.VisitScalar(value, context, serializer); + break; + + case TypeCode.DBNull: + visitor.VisitScalar(new BetterObjectDescriptor(null, typeof(object), typeof(object)), context, serializer); + break; + + case TypeCode.Empty: + throw new NotSupportedException($"TypeCode.{typeCode} is not supported."); + + case TypeCode.Object: + default: + if (value.Value == null || value.Type == typeof(TimeSpan)) + { + visitor.VisitScalar(value, context, serializer); + break; + } + + var underlyingType = Nullable.GetUnderlyingType(value.Type); + if (underlyingType != null) + { + // This is a nullable type, recursively handle it with its underlying type. + // Note that if it contains null, the condition above already took care of it + Traverse( + propertyDescriptor, + "Value", + new BetterObjectDescriptor(value.Value, underlyingType, value.Type, value.ScalarStyle), + visitor, + context, + path, + serializer + ); + } + else + { + TraverseObject(propertyDescriptor, value, visitor, context, path, serializer); + } break; - } - - var underlyingType = Nullable.GetUnderlyingType(value.Type); - if (underlyingType != null) - { - // This is a nullable type, recursively handle it with its underlying type. - // Note that if it contains null, the condition above already took care of it - Traverse(new BetterObjectDescriptor(value.Value, underlyingType, value.Type, value.ScalarStyle), visitor, currentDepth, context); - } - else - { - TraverseObject(value, visitor, currentDepth, context); - } - break; + } + } + finally + { + path.Pop(); } } - protected virtual void TraverseObject(IObjectDescriptor value, IObjectGraphVisitor visitor, int currentDepth, TContext context) + protected virtual void TraverseObject( + IPropertyDescriptor? propertyDescriptor, + IObjectDescriptor value, + IObjectGraphVisitor visitor, + TContext context, + Stack path, + ObjectSerializer serializer) { + Debug.Assert(context != null); + var key = Tuple.Create(value.Type, typeof(TContext)); - if (!_behaviorCache.TryGetValue(key, out var action)) + + if (typeof(IDictionary).IsAssignableFrom(value.Type)) { - if (typeof(IDictionary).IsAssignableFrom(value.Type)) - { - action = TraverseDictionary; - } - else - { - var dictionaryType = ReflectionUtility.GetImplementedGenericInterface(value.Type, typeof(IDictionary<,>)); - if (dictionaryType != null) - { - action = (v, vi, d, c) => TraverseGenericDictionary(v, dictionaryType, vi, d, c); - } - else if (typeof(IEnumerable).IsAssignableFrom(value.Type)) - { - action = TraverseList; - } - else - { - action = TraverseProperties; - } - } - _behaviorCache[key] = action; + TraverseDictionary(propertyDescriptor, value, visitor, typeof(object), typeof(object), context, path, serializer); + return; + } + + if (_objectFactory.GetDictionary(value, out var adaptedDictionary, out var genericArguments)) + { + Debug.Assert(genericArguments != null); + + var objectDescriptor = new ObjectDescriptor(adaptedDictionary, value.Type, value.StaticType, value.ScalarStyle); + var dictionaryKeyType = genericArguments[0]; + var dictionaryValueType = genericArguments[1]; + + TraverseDictionary(propertyDescriptor, objectDescriptor, visitor, dictionaryKeyType, dictionaryValueType, context, path, serializer); + return; + } + + if (typeof(IEnumerable).IsAssignableFrom(value.Type)) + { + TraverseList(propertyDescriptor, value, visitor, context, path, serializer); + return; } - action(value, visitor, currentDepth, context!); + + TraverseProperties(value, visitor, context, path, serializer); } - protected virtual void TraverseDictionary(IObjectDescriptor dictionary, object visitor, int currentDepth, object context) + protected virtual void TraverseDictionary( + IPropertyDescriptor? propertyDescriptor, + IObjectDescriptor dictionary, + IObjectGraphVisitor visitor, + Type keyType, + Type valueType, + TContext context, + Stack path, + ObjectSerializer serializer) { - var v = (IObjectGraphVisitor)visitor; - var c = (TContext)context; - v.VisitMappingStart(dictionary, typeof(object), typeof(object), c); + visitor.VisitMappingStart(dictionary, keyType, valueType, context, serializer); - foreach (DictionaryEntry entry in (IDictionary)dictionary.NonNullValue()) + var isDynamic = dictionary.Type.FullName!.Equals("System.Dynamic.ExpandoObject"); + foreach (DictionaryEntry? entry in (IDictionary)dictionary.NonNullValue()) { - var key = GetObjectDescriptor(entry.Key, typeof(object)); - var value = GetObjectDescriptor(entry.Value, typeof(object)); + var entryValue = entry!.Value; + var keyValue = isDynamic ? _namingConvention.Apply(entryValue.Key.ToString()!) : entryValue.Key; + var key = GetObjectDescriptor(keyValue, keyType); + var value = GetObjectDescriptor(entryValue.Value, valueType); - if (v.EnterMapping(key, value, c)) + if (visitor.EnterMapping(key, value, context, serializer)) { - Traverse(key, v, currentDepth, c); - Traverse(value, v, currentDepth, c); + Traverse(propertyDescriptor, keyValue, key, visitor, context, path, serializer); + Traverse(propertyDescriptor, keyValue, value, visitor, context, path, serializer); } } - v.VisitMappingEnd(dictionary, c); + visitor.VisitMappingEnd(dictionary, context, serializer); } - private void TraverseGenericDictionary(IObjectDescriptor dictionary, Type dictionaryType, IObjectGraphVisitor visitor, int currentDepth, IObjectGraphVisitorContext context) + private void TraverseGenericDictionary( + IObjectDescriptor dictionary, + Type dictionaryType, + IObjectGraphVisitor visitor, + TContext context, + Stack path, + ObjectSerializer serializer) { - var v = (IObjectGraphVisitor)visitor; - var c = (TContext)context; + Debug.Assert(dictionary.Value != null); + var entryTypes = dictionaryType.GetGenericArguments(); // dictionaryType is IDictionary - v.VisitMappingStart(dictionary, entryTypes[0], entryTypes[1], c); + visitor.VisitMappingStart(dictionary, entryTypes[0], entryTypes[1], context, serializer); var key = Tuple.Create(entryTypes[0], entryTypes[1], typeof(TContext)); if (!_traverseGenericDictionaryCache.TryGetValue(key, out var action)) @@ -178,14 +248,26 @@ private void TraverseGenericDictionary(IObjectDescriptor dictionary, T action = GetTraverseGenericDictionaryHelper(entryTypes[0], entryTypes[1], typeof(TContext)); _traverseGenericDictionaryCache[key] = action; } - action(this, dictionary.Value, v, currentDepth, _namingConvention, c); + action(this, dictionary.Value, visitor, _namingConvention ?? NullNamingConvention.Instance, context!, path, serializer); - v.VisitMappingEnd(dictionary, c); + visitor.VisitMappingEnd(dictionary, context, serializer); } - private static Action GetTraverseGenericDictionaryHelper(Type tkey, Type tvalue, Type tcontext) + private static Action, ObjectSerializer> GetTraverseGenericDictionaryHelper(Type tkey, Type tvalue, Type tcontext) { - var dm = new DynamicMethod(string.Empty, typeof(void), [typeof(FullObjectGraphTraversalStrategy), typeof(object), typeof(IObjectGraphVisitor), typeof(int), typeof(INamingConvention), typeof(IObjectGraphVisitorContext)]); + var dm = new DynamicMethod( + string.Empty, + returnType: typeof(void), + parameterTypes: + [ + typeof(FullObjectGraphTraversalStrategy), + typeof(object), + typeof(IObjectGraphVisitor), + typeof(INamingConvention), + typeof(IObjectGraphVisitorContext), + typeof(Stack), + typeof(ObjectSerializer), + ]); var il = dm.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); @@ -196,77 +278,116 @@ private void TraverseGenericDictionary(IObjectDescriptor dictionary, T il.Emit(OpCodes.Ldarg_S, (byte)5); il.Emit(OpCodes.Call, TraverseGenericDictionaryHelperMethod.MakeGenericMethod(tkey, tvalue, tcontext)); il.Emit(OpCodes.Ret); - return (Action)dm.CreateDelegate(typeof(Action)); + return (Action, ObjectSerializer>)dm.CreateDelegate(typeof(Action, ObjectSerializer>)); } [EditorBrowsable(EditorBrowsableState.Never)] - public static void TraverseGenericDictionaryHelper( + private static void TraverseGenericDictionaryHelper( FullObjectGraphTraversalStrategy self, + IPropertyDescriptor propertyDescriptor, IDictionary dictionary, - IObjectGraphVisitor visitor, - int currentDepth, + IObjectGraphVisitor visitor, INamingConvention namingConvention, - IObjectGraphVisitorContext context) + TContext context, + Stack paths, + ObjectSerializer serializer) { - var v = (IObjectGraphVisitor)visitor; - var c = (TContext)context; + var isDynamic = dictionary.GetType().FullName!.Equals("System.Dynamic.ExpandoObject"); + foreach (var entry in dictionary) { - var keyString = isDynamic ? namingConvention.Apply(entry.Key!.ToString()!) : entry.Key!.ToString(); + Debug.Assert(entry.Key != null); + Debug.Assert(entry.Value != null); + + var keyString = isDynamic + ? namingConvention.Apply(entry.Key!.ToString()!)! + : entry.Key.ToString()!; var key = self.GetObjectDescriptor(keyString, typeof(TKey)); var value = self.GetObjectDescriptor(entry.Value, typeof(TValue)); - if (v.EnterMapping(key, value, c)) + if (visitor.EnterMapping(key, value, context, serializer)) { - self.Traverse(key, v, currentDepth, c); - self.Traverse(value, v, currentDepth, c); + self.Traverse(propertyDescriptor, propertyDescriptor.Name, key, visitor, context, paths, serializer); + self.Traverse(propertyDescriptor, propertyDescriptor.Name, key, visitor, context, paths, serializer); } } } - private void TraverseList(IObjectDescriptor value, IObjectGraphVisitor visitor, int currentDepth, IObjectGraphVisitorContext context) + private void TraverseList( + IPropertyDescriptor? propertyDescriptor, + IObjectDescriptor value, + IObjectGraphVisitor visitor, + TContext context, + Stack path, + ObjectSerializer serializer) { - var v = (IObjectGraphVisitor)visitor; - var c = (TContext)context; - var enumerableType = ReflectionUtility.GetImplementedGenericInterface(value.Type, typeof(IEnumerable<>)); - var itemType = enumerableType != null ? - enumerableType.GetGenericArguments()[0] - : typeof(object); + var itemType = _objectFactory.GetValueType(value.Type); - v.VisitSequenceStart(value, itemType, c); + visitor.VisitSequenceStart(value, itemType, context, serializer); + + var index = 0; foreach (var item in (IEnumerable)value.NonNullValue()) { - Traverse(GetObjectDescriptor(item, itemType), v, currentDepth, c); + Traverse(propertyDescriptor, index, GetObjectDescriptor(item, itemType), visitor, context, path, serializer); + ++index; } - v.VisitSequenceEnd(value, c); + visitor.VisitSequenceEnd(value, context, serializer); } - protected virtual void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, int currentDepth, IObjectGraphVisitorContext context) + protected virtual void TraverseProperties( + IObjectDescriptor value, + IObjectGraphVisitor visitor, + TContext context, + Stack path, + ObjectSerializer serializer) { - var v = (IObjectGraphVisitor)visitor; - var c = (TContext)context; - v.VisitMappingStart(value, typeof(string), typeof(object), c); + Debug.Assert(visitor != null); + Debug.Assert(context != null); + Debug.Assert(value.Value != null); + + if (context.GetType() != typeof(Nothing)) + { + _objectFactory.ExecuteOnSerializing(value.Value); + } + + visitor.VisitMappingStart(value, typeof(string), typeof(object), context, serializer); var source = value.NonNullValue(); - foreach (var propertyDescriptor in _typeDescriptor.GetProperties(value.Type, value.Value)) + foreach (var propertyDescriptor in _typeDescriptor.GetProperties(value.Type, source)) { var propertyValue = propertyDescriptor.Read(source); - - if (v.EnterMapping(propertyDescriptor, propertyValue, c)) + if (visitor.EnterMapping(propertyDescriptor, propertyValue, context, serializer)) { - Traverse(new BetterObjectDescriptor(propertyDescriptor.Name, typeof(string), typeof(string)), v, currentDepth, c); - Traverse(propertyValue, v, currentDepth, c); + Traverse(null, propertyDescriptor.Name, new BetterObjectDescriptor(propertyDescriptor.Name, typeof(string), typeof(string), ScalarStyle.Plain), visitor, context, path, serializer); + Traverse(propertyDescriptor, propertyDescriptor.Name, propertyValue, visitor, context, path, serializer); } } - v.VisitMappingEnd(value, c); + visitor.VisitMappingEnd(value, context, serializer); + + if (context.GetType() != typeof(Nothing)) + { + _objectFactory.ExecuteOnSerialized(value.Value); + } } private IObjectDescriptor GetObjectDescriptor(object? value, Type staticType) { return new BetterObjectDescriptor(value, _typeResolver.Resolve(staticType, value), staticType); } + + protected internal readonly struct ObjectPathSegment + { + public readonly object Name; + public readonly IObjectDescriptor Value; + + public ObjectPathSegment(object name, IObjectDescriptor value) + { + this.Name = name; + this.Value = value; + } + } } diff --git a/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs b/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs index 3e53a587526..53b0a95facc 100644 --- a/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs +++ b/src/Docfx.YamlSerialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs @@ -1,11 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using Docfx.YamlSerialization.Helpers; using YamlDotNet.Serialization; -using IObjectGraphVisitor = System.Object; -using IObjectGraphVisitorContext = System.Object; namespace Docfx.YamlSerialization.ObjectGraphTraversalStrategies; @@ -16,18 +13,33 @@ namespace Docfx.YamlSerialization.ObjectGraphTraversalStrategies; /// public class RoundtripObjectGraphTraversalStrategy : FullObjectGraphTraversalStrategy { - public RoundtripObjectGraphTraversalStrategy(YamlSerializer serializer, ITypeInspector typeDescriptor, ITypeResolver typeResolver, int maxRecursion) - : base(serializer, typeDescriptor, typeResolver, maxRecursion, null) + private readonly TypeConverterCache _converters; + private readonly Settings _settings; + + public RoundtripObjectGraphTraversalStrategy(IEnumerable converters, YamlSerializer serializer, ITypeInspector typeDescriptor, ITypeResolver typeResolver, int maxRecursion, INamingConvention namingConvention, Settings settings, IObjectFactory objectFactory) + : base(serializer, typeDescriptor, typeResolver, maxRecursion, namingConvention, objectFactory) { + _converters = new TypeConverterCache(converters); + _settings = settings; } - protected override void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, int currentDepth, IObjectGraphVisitorContext context) + ////protected override void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, int currentDepth, IObjectGraphVisitorContext context) + ////{ + //// if (!value.Type.HasDefaultConstructor() && !Serializer.Converters.Any(c => c.Accepts(value.Type))) + //// { + //// throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Type '{0}' cannot be deserialized because it does not have a default constructor or a type converter.", value.Type)); + //// } + + //// base.TraverseProperties(value, visitor, currentDepth, context); + ////} + + protected override void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path, ObjectSerializer serializer) { - if (!value.Type.HasDefaultConstructor() && !Serializer.Converters.Any(c => c.Accepts(value.Type))) + if (!value.Type.HasDefaultConstructor(_settings.AllowPrivateConstructors) && !_converters.TryGetConverterForType(value.Type, out _)) { - throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Type '{0}' cannot be deserialized because it does not have a default constructor or a type converter.", value.Type)); + throw new InvalidOperationException($"Type '{value.Type}' cannot be deserialized because it does not have a default constructor or a type converter."); } - base.TraverseProperties(value, visitor, currentDepth, context); + base.TraverseProperties(value, visitor, context, path, serializer); } } diff --git a/src/Docfx.YamlSerialization/ObjectGraphVisitors/ExclusiveObjectGraphVisitor.cs b/src/Docfx.YamlSerialization/ObjectGraphVisitors/ExclusiveObjectGraphVisitor.cs index 0afe1efc7ee..a26e4e67304 100644 --- a/src/Docfx.YamlSerialization/ObjectGraphVisitors/ExclusiveObjectGraphVisitor.cs +++ b/src/Docfx.YamlSerialization/ObjectGraphVisitors/ExclusiveObjectGraphVisitor.cs @@ -23,13 +23,13 @@ public ExclusiveObjectGraphVisitor(IObjectGraphVisitor nextVisitor) return type.IsValueType ? Activator.CreateInstance(type) : null; } - public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { var defaultValueAttribute = key.GetCustomAttribute(); object? defaultValue = defaultValueAttribute != null ? defaultValueAttribute.Value : GetDefault(key.Type); - return !Equals(value.Value, defaultValue) && base.EnterMapping(key, value, context); + return !Equals(value.Value, defaultValue) && base.EnterMapping(key, value, context, serializer); } } diff --git a/src/Docfx.YamlSerialization/TypeInspectors/EmitTypeInspector.cs b/src/Docfx.YamlSerialization/TypeInspectors/EmitTypeInspector.cs index bd893e48aed..1dbbc65e60d 100644 --- a/src/Docfx.YamlSerialization/TypeInspectors/EmitTypeInspector.cs +++ b/src/Docfx.YamlSerialization/TypeInspectors/EmitTypeInspector.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.Serialization; using Docfx.YamlSerialization.Helpers; using Docfx.YamlSerialization.ObjectDescriptors; using YamlDotNet.Core; @@ -15,6 +16,8 @@ public class EmitTypeInspector : ExtensibleTypeInspectorSkeleton { private static readonly ConcurrentDictionary _cache = new(); private static readonly ConcurrentDictionary> _propertyDescriptorCache = new(); + private static readonly ConcurrentDictionary<(Type, string), string> _enumNameCache = new(); + private static readonly ConcurrentDictionary<(Type, string), string> _enumValueCache = new(); private readonly ITypeResolver _resolver; public EmitTypeInspector(ITypeResolver resolver) @@ -62,6 +65,58 @@ where name.StartsWith(ep.Prefix, StringComparison.Ordinal) select new ExtensiblePropertyDescriptor(ep, name, _resolver)).FirstOrDefault(); } + // This code is based on ReflectionTypeInspector implementation(See: https://github.com/aaubry/YamlDotNet/blob/master/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs) + public override string GetEnumName(Type enumType, string name) + { + var key = (enumType, name); + if (_enumNameCache.TryGetValue(key, out var result)) + return result; + + // Try to gets enum name from EnumMemberAttribute and resolve enum name. + foreach (var enumMember in enumType.GetMembers()) + { + var attribute = enumMember.GetCustomAttribute(inherit: false); + if (attribute != null && attribute.Value == name) + { + name = enumMember.Name; + break; + } + } + + // Add resolved name to cache + _enumNameCache.TryAdd(key, name); + return name; + } + + public override string GetEnumValue(object enumValue) + { + var enumType = enumValue.GetType(); + var valueText = enumValue.ToString()!; + var key = (enumType, valueText); + + if (_enumValueCache.TryGetValue(key, out var result)) + return result; + + // Try to gets enum value from EnumMemberAttribute and resolve enum value. + if (enumType.GetCustomAttribute() != null) + { + var enumMember = enumType.GetMember(valueText).FirstOrDefault(); + if (enumMember != null) + { + var attribute = enumMember.GetCustomAttribute(inherit: false); + if (attribute?.Value != null) + { + valueText = attribute.Value; + } + } + } + + // Add resolved text to cache. + _enumValueCache.TryAdd(key, valueText); + + return valueText; + } + private sealed class CachingItem { private CachingItem() { } @@ -401,6 +456,12 @@ public EmitPropertyDescriptor(EmitPropertyDescriptorSkeleton skeleton, ITypeReso public Type? TypeOverride { get; set; } + public bool AllowNulls { get; set; } + + public bool Required { get; set; } + + public Type? ConverterType { get; set; } + public T GetCustomAttribute() where T : Attribute => (T)_skeleton.GetCustomAttribute(typeof(T)); public IObjectDescriptor Read(object target) @@ -461,6 +522,12 @@ public ExtensiblePropertyDescriptor( public Type? TypeOverride { get; set; } + public bool AllowNulls { get; set; } + + public bool Required { get; set; } + + public Type? ConverterType { get; set; } + public T? GetCustomAttribute() where T : Attribute => null; public IObjectDescriptor Read(object target) diff --git a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleNamingConventionTypeInspector.cs b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleNamingConventionTypeInspector.cs index aeb619b4593..f7fa3fd3eaa 100644 --- a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleNamingConventionTypeInspector.cs +++ b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleNamingConventionTypeInspector.cs @@ -25,4 +25,10 @@ from p in innerTypeDescriptor.GetProperties(type, container) public override IPropertyDescriptor? GetProperty(Type type, object? container, string name) => innerTypeDescriptor.GetProperty(type, container, name); + + public override string GetEnumName(Type enumType, string name) => + innerTypeDescriptor.GetEnumName(enumType, name); + + public override string GetEnumValue(object enumValue) => + innerTypeDescriptor.GetEnumValue(enumValue); } diff --git a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleReadableAndWritablePropertiesTypeInspector.cs b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleReadableAndWritablePropertiesTypeInspector.cs index 1407f50a52a..01ad2ab9f38 100644 --- a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleReadableAndWritablePropertiesTypeInspector.cs +++ b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleReadableAndWritablePropertiesTypeInspector.cs @@ -21,4 +21,10 @@ where p.CanWrite public override IPropertyDescriptor? GetProperty(Type type, object? container, string name) => _innerTypeDescriptor.GetProperty(type, container, name); + + public override string GetEnumName(Type enumType, string name) => + _innerTypeDescriptor.GetEnumName(enumType, name); + + public override string GetEnumValue(object enumValue) => + _innerTypeDescriptor.GetEnumValue(enumValue); } diff --git a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleTypeInspectorSkeleton.cs b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleTypeInspectorSkeleton.cs index 235855130aa..715bec3a57f 100644 --- a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleTypeInspectorSkeleton.cs +++ b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleTypeInspectorSkeleton.cs @@ -11,12 +11,11 @@ public abstract class ExtensibleTypeInspectorSkeleton : ITypeInspector, IExtensi { public abstract IEnumerable GetProperties(Type type, object? container); - public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched) + public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched, bool caseInsensitivePropertyMatching) { - var candidates = - from p in GetProperties(type, container) - where p.Name == name - select p; + var candidates = caseInsensitivePropertyMatching + ? GetProperties(type, container).Where(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + : GetProperties(type, container).Where(p => p.Name == name); using var enumerator = candidates.GetEnumerator(); if (!enumerator.MoveNext()) @@ -61,4 +60,8 @@ from p in GetProperties(type, container) } public virtual IPropertyDescriptor? GetProperty(Type type, object? container, string name) => null; + + public abstract string GetEnumName(Type enumType, string name); + + public abstract string GetEnumValue(object enumValue); } diff --git a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleYamlAttributesTypeInspector.cs b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleYamlAttributesTypeInspector.cs index 21878ccdaa2..0fb61db306f 100644 --- a/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleYamlAttributesTypeInspector.cs +++ b/src/Docfx.YamlSerialization/TypeInspectors/ExtensibleYamlAttributesTypeInspector.cs @@ -48,4 +48,10 @@ public override IEnumerable GetProperties(Type type, object public override IPropertyDescriptor? GetProperty(Type type, object? container, string name) => innerTypeDescriptor.GetProperty(type, container, name); + + public override string GetEnumName(Type enumType, string name) => + innerTypeDescriptor.GetEnumName(enumType, name); + + public override string GetEnumValue(object enumValue) => + innerTypeDescriptor.GetEnumValue(enumValue); } diff --git a/src/Docfx.YamlSerialization/YamlDeserializer.cs b/src/Docfx.YamlSerialization/YamlDeserializer.cs index 221e8b70d56..71d6fe8f571 100644 --- a/src/Docfx.YamlSerialization/YamlDeserializer.cs +++ b/src/Docfx.YamlSerialization/YamlDeserializer.cs @@ -52,20 +52,32 @@ public IEnumerable GetProperties(Type type, object? contain return TypeDescriptor.GetProperties(type, container); } - public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched) + public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched, bool caseInsensitivePropertyMatching) { - return TypeDescriptor.GetProperty(type, container, name, ignoreUnmatched); + return TypeDescriptor.GetProperty(type, container, name, ignoreUnmatched, caseInsensitivePropertyMatching); + } + + public string GetEnumName(Type enumType, string name) + { + return TypeDescriptor.GetEnumName(enumType, name); + } + public string GetEnumValue(object enumValue) + { + return TypeDescriptor.GetEnumValue(enumValue); } } public YamlDeserializer( IObjectFactory? objectFactory = null, INamingConvention? namingConvention = null, + INamingConvention? enumNamingConvention = null, bool ignoreUnmatched = false, - bool ignoreNotFoundAnchor = true) + bool ignoreNotFoundAnchor = true, + bool caseInsensitivePropertyMatching = false) { objectFactory ??= new DefaultEmitObjectFactory(); namingConvention ??= NullNamingConvention.Instance; + enumNamingConvention ??= NullNamingConvention.Instance; _typeDescriptor.TypeDescriptor = new ExtensibleYamlAttributesTypeInspector( @@ -89,24 +101,24 @@ public YamlDeserializer( [ new TypeConverterNodeDeserializer(_converters), new NullNodeDeserializer(), - new ScalarNodeDeserializer(attemptUnknownTypeDeserialization: false, _reflectionTypeConverter, YamlFormatter.Default, NullNamingConvention.Instance), - new EmitArrayNodeDeserializer(), + new ScalarNodeDeserializer(attemptUnknownTypeDeserialization: false, _reflectionTypeConverter, _typeDescriptor, YamlFormatter.Default, enumNamingConvention), + new EmitArrayNodeDeserializer(enumNamingConvention,_typeDescriptor), new EmitGenericDictionaryNodeDeserializer(objectFactory), new DictionaryNodeDeserializer(objectFactory, duplicateKeyChecking: true), - new EmitGenericCollectionNodeDeserializer(objectFactory), - new CollectionNodeDeserializer(objectFactory, NullNamingConvention.Instance), + new EmitGenericCollectionNodeDeserializer(objectFactory, enumNamingConvention,_typeDescriptor), + new CollectionNodeDeserializer(objectFactory, enumNamingConvention, _typeDescriptor), new EnumerableNodeDeserializer(), - new ExtensibleObjectNodeDeserializer(objectFactory, _typeDescriptor, ignoreUnmatched) + new ExtensibleObjectNodeDeserializer(objectFactory, _typeDescriptor, enumNamingConvention, ignoreUnmatched, caseInsensitivePropertyMatching), ]; _tagMappings = new Dictionary(PredefinedTagMappings); TypeResolvers = [ new TagNodeTypeResolver(_tagMappings), new DefaultContainersNodeTypeResolver(), - new ScalarYamlNodeTypeResolver() + new ScalarYamlNodeTypeResolver(), ]; - NodeValueDeserializer nodeValueDeserializer = new(NodeDeserializers, TypeResolvers, _reflectionTypeConverter, NullNamingConvention.Instance); + NodeValueDeserializer nodeValueDeserializer = new(NodeDeserializers, TypeResolvers, _reflectionTypeConverter, enumNamingConvention, _typeDescriptor); if (ignoreNotFoundAnchor) { _valueDeserializer = new LooseAliasValueDeserializer(nodeValueDeserializer); diff --git a/src/Docfx.YamlSerialization/YamlSerializer.cs b/src/Docfx.YamlSerialization/YamlSerializer.cs index 4c2c6170481..22bc0001268 100644 --- a/src/Docfx.YamlSerialization/YamlSerializer.cs +++ b/src/Docfx.YamlSerialization/YamlSerializer.cs @@ -11,6 +11,7 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; @@ -23,11 +24,15 @@ public class YamlSerializer private readonly SerializationOptions _options; private readonly INamingConvention _namingConvention; private readonly ITypeResolver _typeResolver; + private readonly ITypeInspector _typeDescriptor; + private readonly IObjectFactory _objectFactory; + private readonly Settings _settings; - public YamlSerializer(SerializationOptions options = SerializationOptions.None, INamingConvention? namingConvention = null) + public YamlSerializer(SerializationOptions options = SerializationOptions.None, INamingConvention? namingConvention = null, Settings? settings = null) { _options = options; _namingConvention = namingConvention ?? NullNamingConvention.Instance; + _settings = settings ?? new Settings(); Converters = new List(); foreach (IYamlTypeConverter yamlTypeConverter in YamlTypeConverters.BuiltInConverters) @@ -38,6 +43,9 @@ public YamlSerializer(SerializationOptions options = SerializationOptions.None, _typeResolver = IsOptionSet(SerializationOptions.DefaultToStaticType) ? new StaticTypeResolver() : new DynamicTypeResolver(); + + _typeDescriptor = CreateTypeInspector(); + _objectFactory = new DefaultObjectFactory(); } private bool IsOptionSet(SerializationOptions option) @@ -66,7 +74,7 @@ private void EmitDocument(IEmitter emitter, IObjectDescriptor graph) emitter.Emit(new StreamStart()); emitter.Emit(new DocumentStart()); - traversalStrategy.Traverse(graph, emittingVisitor, emitter); + traversalStrategy.Traverse(graph, emittingVisitor, emitter, GetObjectSerializer(emitter)); emitter.Emit(new DocumentEnd(true)); emitter.Emit(new StreamEnd()); @@ -76,6 +84,8 @@ private IObjectGraphVisitor CreateEmittingVisitor(IEmitter emitter, IO { IObjectGraphVisitor emittingVisitor = new EmittingObjectGraphVisitor(eventEmitter); + emittingVisitor = new CustomSerializationObjectGraphVisitor(emittingVisitor, Converters, GetObjectSerializer(emitter)); + void nestedObjectSerializer(object? v, Type? t = null) => SerializeValue(emitter, v, t); emittingVisitor = new CustomSerializationObjectGraphVisitor(emittingVisitor, Converters, nestedObjectSerializer); @@ -83,7 +93,8 @@ private IObjectGraphVisitor CreateEmittingVisitor(IEmitter emitter, IO if (!IsOptionSet(SerializationOptions.DisableAliases)) { var anchorAssigner = new AnchorAssigner(Converters); - traversalStrategy.Traverse(graph, anchorAssigner, default); + + traversalStrategy.Traverse(graph, anchorAssigner, default, GetObjectSerializer(emitter)); emittingVisitor = new AnchorAssigningObjectGraphVisitor(emittingVisitor, eventEmitter, anchorAssigner); } @@ -110,7 +121,12 @@ public void SerializeValue(IEmitter emitter, object? value, Type? type) graph ); - traversalStrategy.Traverse(graph, emittingVisitor, emitter); + traversalStrategy.Traverse(graph, emittingVisitor, emitter, GetObjectSerializer(emitter)); + } + + private ObjectSerializer GetObjectSerializer(IEmitter emitter) + { + return (object? v, Type? t = null) => SerializeValue(emitter, v, t); } private IEventEmitter CreateEventEmitter() @@ -119,7 +135,7 @@ private IEventEmitter CreateEventEmitter() if (IsOptionSet(SerializationOptions.JsonCompatible)) { - return new JsonEventEmitter(writer, YamlFormatter.Default, NullNamingConvention.Instance); + return new JsonEventEmitter(writer, YamlFormatter.Default, NullNamingConvention.Instance, _typeDescriptor); } else { @@ -130,29 +146,28 @@ private IEventEmitter CreateEventEmitter() quoteYaml1_1Strings: false, defaultScalarStyle: ScalarStyle.Any, formatter: YamlFormatter.Default, - enumNamingConvention: NullNamingConvention.Instance); + enumNamingConvention: NullNamingConvention.Instance, + typeInspector: _typeDescriptor); } } private IObjectGraphTraversalStrategy CreateTraversalStrategy() + { + return IsOptionSet(SerializationOptions.Roundtrip) + ? new RoundtripObjectGraphTraversalStrategy(Converters, this, _typeDescriptor, _typeResolver, 50, _namingConvention, _settings, _objectFactory) + : new FullObjectGraphTraversalStrategy(this, _typeDescriptor, _typeResolver, 50, _namingConvention, _objectFactory); + } + + private ITypeInspector CreateTypeInspector() { ITypeInspector typeDescriptor = new EmitTypeInspector(_typeResolver); if (IsOptionSet(SerializationOptions.Roundtrip)) - { typeDescriptor = new ReadableAndWritablePropertiesTypeInspector(typeDescriptor); - } typeDescriptor = new NamingConventionTypeInspector(typeDescriptor, _namingConvention); typeDescriptor = new YamlAttributesTypeInspector(typeDescriptor); - if (IsOptionSet(SerializationOptions.Roundtrip)) - { - return new RoundtripObjectGraphTraversalStrategy(this, typeDescriptor, _typeResolver, 50); - } - else - { - return new FullObjectGraphTraversalStrategy(this, typeDescriptor, _typeResolver, 50, _namingConvention); - } + return typeDescriptor; } }