From d52d18af223f447e766ff1b0c9ab0a9106e16b37 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 8 Oct 2023 14:11:33 +0300 Subject: [PATCH] Small performance improvements * change IReflectionService to return reusable fun to generate enum values via GetEnumValueConverter * fix inheritdoc typo * use string interpolation and less LINQ in JsonReferenceVisitorBase --- .../NewtonsoftJsonReflectionService.cs | 30 ++++---- .../NewtonsoftJsonSchemaGeneratorSettings.cs | 2 +- .../JsonSchemaExtensionDataAttribute.cs | 2 +- .../Generation/IReflectionService.cs | 5 +- .../Generation/JsonSchemaGenerator.cs | 12 ++-- .../Generation/ReflectionServiceBase.cs | 12 ++-- .../SystemTextJsonReflectionService.cs | 32 ++++----- src/NJsonSchema/JsonPathUtilities.cs | 2 +- .../Visitors/JsonReferenceVisitorBase.cs | 68 +++++++++---------- 9 files changed, 76 insertions(+), 89 deletions(-) diff --git a/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs b/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs index 7f2f3259c..fb18337bb 100644 --- a/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs +++ b/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs @@ -15,16 +15,14 @@ using NJsonSchema.Infrastructure; using System.Runtime.Serialization; using System.Reflection; -using NJsonSchema; using NJsonSchema.Generation; -using System.Collections.Generic; namespace NJsonSchema.NewtonsoftJson.Generation { - /// + /// public class NewtonsoftJsonReflectionService : ReflectionServiceBase { - /// + /// protected override JsonTypeDescription GetDescription(ContextualType contextualType, NewtonsoftJsonSchemaGeneratorSettings settings, Type originalType, bool isNullable, ReferenceTypeNullHandling defaultReferenceTypeNullHandling) { @@ -38,7 +36,7 @@ protected override JsonTypeDescription GetDescription(ContextualType contextualT return base.GetDescription(contextualType, settings, originalType, isNullable, defaultReferenceTypeNullHandling); } - /// + /// public override bool IsNullable(ContextualType contextualType, ReferenceTypeNullHandling defaultReferenceTypeNullHandling) { var jsonPropertyAttribute = contextualType.GetContextAttribute(); @@ -50,28 +48,27 @@ public override bool IsNullable(ContextualType contextualType, ReferenceTypeNull return base.IsNullable(contextualType, defaultReferenceTypeNullHandling); } - /// + /// public override bool IsStringEnum(ContextualType contextualType, NewtonsoftJsonSchemaGeneratorSettings settings) { var hasGlobalStringEnumConverter = settings.SerializerSettings?.Converters.OfType().Any() == true; return hasGlobalStringEnumConverter || base.IsStringEnum(contextualType, settings); } - /// - public override string ConvertEnumValue(object value, NewtonsoftJsonSchemaGeneratorSettings settings) + + /// + public override Func GetEnumValueConverter(NewtonsoftJsonSchemaGeneratorSettings settings) { - var converters = settings.SerializerSettings?.Converters.ToList() ?? new List(); + var converters = settings.SerializerSettings.Converters.ToList(); if (!converters.OfType().Any()) { converters.Add(new StringEnumConverter()); } - var json = JsonConvert.SerializeObject(value, Formatting.None, converters.ToArray()); - var enumString = JsonConvert.DeserializeObject(json)!; - return enumString; + return x => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(x, Formatting.None, converters.ToArray())); } - /// + /// public override void GenerateProperties(JsonSchema schema, ContextualType contextualType, NewtonsoftJsonSchemaGeneratorSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver) { var contextualAccessors = contextualType @@ -150,7 +147,7 @@ public override void GenerateProperties(JsonSchema schema, ContextualType contex } } - /// + /// public override string GetPropertyName(ContextualAccessorInfo accessorInfo, JsonSchemaGeneratorSettings settings) { return GetPropertyName(null, accessorInfo, (NewtonsoftJsonSchemaGeneratorSettings)settings); @@ -197,7 +194,7 @@ private void LoadPropertyOrField(JsonProperty jsonProperty, ContextualAccessorIn } } - private string GetPropertyName(JsonProperty? jsonProperty, ContextualAccessorInfo accessorInfo, NewtonsoftJsonSchemaGeneratorSettings settings) + private static string GetPropertyName(JsonProperty? jsonProperty, ContextualAccessorInfo accessorInfo, NewtonsoftJsonSchemaGeneratorSettings settings) { if (jsonProperty?.PropertyName != null) { @@ -208,8 +205,7 @@ private string GetPropertyName(JsonProperty? jsonProperty, ContextualAccessorInf { var propertyName = accessorInfo.GetName(); - var contractResolver = settings.ActualContractResolver as DefaultContractResolver; - return contractResolver != null + return settings.ActualContractResolver is DefaultContractResolver contractResolver ? contractResolver.GetResolvedPropertyName(propertyName) : propertyName; } diff --git a/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonSchemaGeneratorSettings.cs b/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonSchemaGeneratorSettings.cs index f0f42731c..dbab76a38 100644 --- a/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonSchemaGeneratorSettings.cs +++ b/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonSchemaGeneratorSettings.cs @@ -15,7 +15,7 @@ namespace NJsonSchema.NewtonsoftJson.Generation { - /// + /// public class NewtonsoftJsonSchemaGeneratorSettings : JsonSchemaGeneratorSettings { private Dictionary _cachedContracts = new Dictionary(); diff --git a/src/NJsonSchema/Annotations/JsonSchemaExtensionDataAttribute.cs b/src/NJsonSchema/Annotations/JsonSchemaExtensionDataAttribute.cs index 2dff2677a..2db8a781f 100644 --- a/src/NJsonSchema/Annotations/JsonSchemaExtensionDataAttribute.cs +++ b/src/NJsonSchema/Annotations/JsonSchemaExtensionDataAttribute.cs @@ -33,7 +33,7 @@ public JsonSchemaExtensionDataAttribute(string key, object value) /// Gets the value. public object Value { get; } - /// + /// public IReadOnlyDictionary ExtensionData => new Dictionary { { Key, Value } diff --git a/src/NJsonSchema/Generation/IReflectionService.cs b/src/NJsonSchema/Generation/IReflectionService.cs index 629028022..b6e131a07 100644 --- a/src/NJsonSchema/Generation/IReflectionService.cs +++ b/src/NJsonSchema/Generation/IReflectionService.cs @@ -15,12 +15,11 @@ namespace NJsonSchema.Generation public interface IReflectionService { /// - /// Converts an enum value to a JSON string. + /// Get converter that converts an enum value to a JSON string. /// - /// /// /// - string ConvertEnumValue(object value, JsonSchemaGeneratorSettings settings); + Func GetEnumValueConverter(JsonSchemaGeneratorSettings settings); /// /// Gets the property name for the given accessor info. diff --git a/src/NJsonSchema/Generation/JsonSchemaGenerator.cs b/src/NJsonSchema/Generation/JsonSchemaGenerator.cs index 66a265b14..ed7f7c224 100644 --- a/src/NJsonSchema/Generation/JsonSchemaGenerator.cs +++ b/src/NJsonSchema/Generation/JsonSchemaGenerator.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.Serialization; namespace NJsonSchema.Generation { @@ -700,6 +701,7 @@ protected virtual void GenerateEnum(JsonSchema schema, JsonTypeDescription typeD schema.EnumerationNames.Clear(); schema.IsFlagEnumerable = contextualType.GetInheritedAttribute() != null; + Func? enumValueConverter = null; var underlyingType = Enum.GetUnderlyingType(contextualType.Type); foreach (var enumName in Enum.GetNames(contextualType.Type)) { @@ -711,16 +713,16 @@ protected virtual void GenerateEnum(JsonSchema schema, JsonTypeDescription typeD else { // EnumMember only checked if StringEnumConverter is used - var attributes = contextualType.Type.GetRuntimeField(enumName)?.GetCustomAttributes(); - dynamic? enumMemberAttribute = attributes?.FirstAssignableToTypeNameOrDefault("System.Runtime.Serialization.EnumMemberAttribute"); - if (!string.IsNullOrEmpty(enumMemberAttribute?.Value)) + var enumMemberAttribute = contextualType.Type.GetRuntimeField(enumName)?.GetCustomAttribute(); + if (enumMemberAttribute != null && !string.IsNullOrEmpty(enumMemberAttribute.Value)) { - schema.Enumeration.Add((string)enumMemberAttribute!.Value); + schema.Enumeration.Add(enumMemberAttribute.Value); } else { + enumValueConverter ??= Settings.ReflectionService.GetEnumValueConverter(Settings); var value = Enum.Parse(contextualType.Type, enumName); - schema.Enumeration.Add(Settings.ReflectionService.ConvertEnumValue(value, Settings)); + schema.Enumeration.Add(enumValueConverter(value)); } } diff --git a/src/NJsonSchema/Generation/ReflectionServiceBase.cs b/src/NJsonSchema/Generation/ReflectionServiceBase.cs index 8f30b6709..7302b8a06 100644 --- a/src/NJsonSchema/Generation/ReflectionServiceBase.cs +++ b/src/NJsonSchema/Generation/ReflectionServiceBase.cs @@ -19,13 +19,13 @@ namespace NJsonSchema.Generation public abstract class ReflectionServiceBase : IReflectionService where TSettings : JsonSchemaGeneratorSettings { - /// - public abstract string ConvertEnumValue(object value, TSettings settings); + /// + public abstract Func GetEnumValueConverter(TSettings settings); - /// + /// public abstract void GenerateProperties(JsonSchema schema, ContextualType contextualType, TSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver); - /// + /// public abstract string GetPropertyName(ContextualAccessorInfo accessorInfo, JsonSchemaGeneratorSettings settings); /// Creates a from a . @@ -368,9 +368,9 @@ bool IReflectionService.IsStringEnum(ContextualType contextualType, JsonSchemaGe return IsStringEnum(contextualType, (TSettings)settings); } - string IReflectionService.ConvertEnumValue(object value, JsonSchemaGeneratorSettings settings) + Func IReflectionService.GetEnumValueConverter(JsonSchemaGeneratorSettings settings) { - return ConvertEnumValue(value, (TSettings)settings); + return GetEnumValueConverter((TSettings)settings); } void IReflectionService.GenerateProperties(JsonSchema schema, ContextualType contextualType, JsonSchemaGeneratorSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver) diff --git a/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs b/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs index 7a95e6616..c05573381 100644 --- a/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs +++ b/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs @@ -17,20 +17,16 @@ namespace NJsonSchema.Generation { - /// + /// public class SystemTextJsonReflectionService : ReflectionServiceBase { - /// + /// public override void GenerateProperties(JsonSchema schema, ContextualType contextualType, SystemTextJsonSchemaGeneratorSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver) { - foreach (var accessorInfo in contextualType - .Properties - .OfType() - .Concat(contextualType.Fields) - .Where(p => p.MemberInfo.DeclaringType == contextualType.Type)) + foreach (var accessorInfo in contextualType.Properties.OfType().Concat(contextualType.Fields)) { - if (accessorInfo.MemberInfo is FieldInfo fieldInfo && - (fieldInfo.IsPrivate || fieldInfo.IsStatic || !fieldInfo.IsDefined(typeof(DataMemberAttribute)))) + if (accessorInfo.MemberInfo.DeclaringType != contextualType.Type || + (accessorInfo.MemberInfo is FieldInfo fieldInfo && (fieldInfo.IsPrivate || fieldInfo.IsStatic || !fieldInfo.IsDefined(typeof(DataMemberAttribute))))) { continue; } @@ -70,7 +66,7 @@ public override void GenerateProperties(JsonSchema schema, ContextualType contex if (!ignored) { - var propertyTypeDescription = ((IReflectionService)this).GetDescription(accessorInfo.AccessorType, settings); + var propertyTypeDescription = GetDescription(accessorInfo.AccessorType, settings.DefaultReferenceTypeNullHandling, settings); var propertyName = GetPropertyName(accessorInfo, settings); var propertyAlreadyExists = schema.Properties.ContainsKey(propertyName); @@ -82,7 +78,7 @@ public override void GenerateProperties(JsonSchema schema, ContextualType contex } else { - throw new InvalidOperationException("The JSON property '" + propertyName + "' is defined multiple times on type '" + contextualType.Type.FullName + "'."); + throw new InvalidOperationException($"The JSON property '{propertyName}' is defined multiple times on type '{contextualType.Type.FullName}'."); } } @@ -104,17 +100,16 @@ public override void GenerateProperties(JsonSchema schema, ContextualType contex } } - /// + /// public override bool IsStringEnum(ContextualType contextualType, SystemTextJsonSchemaGeneratorSettings settings) { var hasGlobalStringEnumConverter = settings.SerializerOptions.Converters.OfType().Any(); return hasGlobalStringEnumConverter || base.IsStringEnum(contextualType, settings); } - /// - public override string ConvertEnumValue(object value, SystemTextJsonSchemaGeneratorSettings settings) + /// + public override Func GetEnumValueConverter(SystemTextJsonSchemaGeneratorSettings settings) { - // TODO(performance): How to improve this one here? var serializerOptions = new JsonSerializerOptions(); foreach (var converter in settings.SerializerOptions.Converters) { @@ -126,17 +121,16 @@ public override string ConvertEnumValue(object value, SystemTextJsonSchemaGenera serializerOptions.Converters.Add(new JsonStringEnumConverter()); } - var json = JsonSerializer.Serialize(value, value.GetType(), serializerOptions); - return JsonSerializer.Deserialize(json)!; + return x => JsonSerializer.Deserialize(JsonSerializer.Serialize(x, x.GetType(), serializerOptions)); } - /// + /// public override string GetPropertyName(ContextualAccessorInfo accessorInfo, JsonSchemaGeneratorSettings settings) { return GetPropertyName(accessorInfo, (SystemTextJsonSchemaGeneratorSettings)settings); } - private string GetPropertyName(ContextualAccessorInfo accessorInfo, SystemTextJsonSchemaGeneratorSettings settings) + private static string GetPropertyName(ContextualAccessorInfo accessorInfo, SystemTextJsonSchemaGeneratorSettings settings) { dynamic? jsonPropertyNameAttribute = accessorInfo.ContextAttributes .FirstAssignableToTypeNameOrDefault("System.Text.Json.Serialization.JsonPropertyNameAttribute", TypeNameStyle.FullName); diff --git a/src/NJsonSchema/JsonPathUtilities.cs b/src/NJsonSchema/JsonPathUtilities.cs index a335d4f5c..e83462399 100644 --- a/src/NJsonSchema/JsonPathUtilities.cs +++ b/src/NJsonSchema/JsonPathUtilities.cs @@ -94,7 +94,7 @@ private static bool FindJsonPaths(object obj, Dictionary search if (searchedObjects.ContainsKey(obj)) { searchedObjects[obj] = basePath; - if (searchedObjects.All(p => p.Value != null)) + if (searchedObjects.All(static p => p.Value != null)) { return true; } diff --git a/src/NJsonSchema/Visitors/JsonReferenceVisitorBase.cs b/src/NJsonSchema/Visitors/JsonReferenceVisitorBase.cs index 8eaca0277..5c5b23599 100644 --- a/src/NJsonSchema/Visitors/JsonReferenceVisitorBase.cs +++ b/src/NJsonSchema/Visitors/JsonReferenceVisitorBase.cs @@ -85,75 +85,75 @@ protected virtual void Visit(object obj, string path, string? typeNameHint, ISet if (schema.AdditionalItemsSchema != null) { - Visit(schema.AdditionalItemsSchema, path + "/additionalItems", null, checkedObjects, o => schema.AdditionalItemsSchema = (JsonSchema)o); + Visit(schema.AdditionalItemsSchema, $"{path}/additionalItems", null, checkedObjects, o => schema.AdditionalItemsSchema = (JsonSchema)o); } if (schema.AdditionalPropertiesSchema != null) { - Visit(schema.AdditionalPropertiesSchema, path + "/additionalProperties", null, checkedObjects, o => schema.AdditionalPropertiesSchema = (JsonSchema)o); + Visit(schema.AdditionalPropertiesSchema, $"{path}/additionalProperties", null, checkedObjects, o => schema.AdditionalPropertiesSchema = (JsonSchema)o); } if (schema.Item != null) { - Visit(schema.Item, path + "/items", null, checkedObjects, o => schema.Item = (JsonSchema)o); + Visit(schema.Item, $"{path}/items", null, checkedObjects, o => schema.Item = (JsonSchema)o); } var items = schema._items; for (var i = 0; i < items.Count; i++) { var index = i; - Visit(items[i], path + "/items[" + i + "]", null, checkedObjects, o => ReplaceOrDelete(items, index, (JsonSchema)o)); + Visit(items[i], $"{path}/items[{i}]", null, checkedObjects, o => ReplaceOrDelete(items, index, (JsonSchema)o)); } var allOf = schema._allOf; for (var i = 0; i < allOf.Count; i++) { var index = i; - Visit(allOf[i], path + "/allOf[" + i + "]", null, checkedObjects, o => ReplaceOrDelete(allOf, index, (JsonSchema)o)); + Visit(allOf[i], $"{path}/allOf[{i}]", null, checkedObjects, o => ReplaceOrDelete(allOf, index, (JsonSchema)o)); } var anyOf = schema._anyOf; for (var i = 0; i < anyOf.Count; i++) { var index = i; - Visit(anyOf[i], path + "/anyOf[" + i + "]", null, checkedObjects, o => ReplaceOrDelete(anyOf, index, (JsonSchema)o)); + Visit(anyOf[i], $"{path}/anyOf[{i}]", null, checkedObjects, o => ReplaceOrDelete(anyOf, index, (JsonSchema)o)); } var oneOf = schema._oneOf; for (var i = 0; i < oneOf.Count; i++) { var index = i; - Visit(oneOf[i], path + "/oneOf[" + i + "]", null, checkedObjects, o => ReplaceOrDelete(oneOf, index, (JsonSchema)o)); + Visit(oneOf[i], $"{path}/oneOf[{i}]", null, checkedObjects, o => ReplaceOrDelete(oneOf, index, (JsonSchema)o)); } if (schema.Not != null) { - Visit(schema.Not, path + "/not", null, checkedObjects, o => schema.Not = (JsonSchema)o); + Visit(schema.Not, $"{path}/not", null, checkedObjects, o => schema.Not = (JsonSchema)o); } if (schema.DictionaryKey != null) { - Visit(schema.DictionaryKey, path + "/x-dictionaryKey", null, checkedObjects, o => schema.DictionaryKey = (JsonSchema)o); + Visit(schema.DictionaryKey, $"{path}/x-dictionaryKey", null, checkedObjects, o => schema.DictionaryKey = (JsonSchema)o); } if (schema.DiscriminatorRaw != null) { - Visit(schema.DiscriminatorRaw, path + "/discriminator", null, checkedObjects, o => schema.DiscriminatorRaw = o); + Visit(schema.DiscriminatorRaw, $"{path}/discriminator", null, checkedObjects, o => schema.DiscriminatorRaw = o); } foreach (var p in schema.Properties.ToArray()) { - Visit(p.Value, path + "/properties/" + p.Key, p.Key, checkedObjects, o => schema.Properties[p.Key] = (JsonSchemaProperty)o); + Visit(p.Value, $"{path}/properties/{p.Key}", p.Key, checkedObjects, o => schema.Properties[p.Key] = (JsonSchemaProperty)o); } foreach (var p in schema.PatternProperties.ToArray()) { - Visit(p.Value, path + "/patternProperties/" + p.Key, null, checkedObjects, o => schema.PatternProperties[p.Key] = (JsonSchemaProperty)o); + Visit(p.Value, $"{path}/patternProperties/{p.Key}", null, checkedObjects, o => schema.PatternProperties[p.Key] = (JsonSchemaProperty)o); } foreach (var p in schema.Definitions.ToArray()) { - Visit(p.Value, path + "/definitions/" + p.Key, p.Key, checkedObjects, o => + Visit(p.Value, $"{path}/definitions/{p.Key}", p.Key, checkedObjects, o => { if (o != null) { @@ -167,25 +167,20 @@ protected virtual void Visit(object obj, string path, string? typeNameHint, ISet } } - if (!(obj is string) && !(obj is JToken) && obj.GetType() != typeof(JsonSchema)) // Reflection fallback + if (obj is not string && obj is not JToken && obj.GetType() != typeof(JsonSchema)) // Reflection fallback { if (_contractResolver.ResolveContract(obj.GetType()) is JsonObjectContract contract) { - foreach (var property in contract.Properties.Where(p => + foreach (var property in contract.Properties) { - var isJsonSchemaProperty = - obj is JsonSchema && - p.UnderlyingName != null && - JsonSchema.JsonSchemaPropertiesCache.Contains(p.UnderlyingName); - - return !isJsonSchemaProperty && !p.Ignored && - p.ShouldSerialize?.Invoke(obj) != false; - })) - { - var value = property.ValueProvider?.GetValue(obj); - if (value != null) + bool isJsonSchemaProperty = obj is JsonSchema && JsonSchema.JsonSchemaPropertiesCache.Contains(property.UnderlyingName!); + if (!isJsonSchemaProperty && !property.Ignored && property.ShouldSerialize?.Invoke(obj) != false) { - Visit(value, path + "/" + property.PropertyName, property.PropertyName!, checkedObjects, o => property.ValueProvider?.SetValue(obj, o)); + var value = property.ValueProvider?.GetValue(obj); + if (value != null) + { + Visit(value, $"{path}/{property.PropertyName}", property.PropertyName, checkedObjects, o => property.ValueProvider?.SetValue(obj, o)); + } } } } @@ -196,7 +191,7 @@ obj is JsonSchema && var value = dictionary[key]; if (value != null) { - Visit(value, path + "/" + key, key.ToString(), checkedObjects, o => + Visit(value, $"{path}/{key}", key.ToString(), checkedObjects, o => { if (o != null) { @@ -214,14 +209,15 @@ obj is JsonSchema && var contextualType = obj.GetType().ToContextualType(); if (contextualType.GetInheritedAttributes().Any()) { - foreach (var property in contextualType.Type.GetContextualProperties() - .Where(p => p.MemberInfo.DeclaringType == contextualType.Type && - !p.GetContextAttributes().Any())) + foreach (var property in contextualType.Type.GetContextualProperties()) { - var value = property.GetValue(obj); - if (value != null) + if (property.MemberInfo.DeclaringType == contextualType.Type && !property.GetContextAttributes().Any()) { - Visit(value, path + "/" + property.Name, property.Name, checkedObjects, o => property.SetValue(obj, o)); + var value = property.GetValue(obj); + if (value != null) + { + Visit(value, $"{path}/{property.Name}", property.Name, checkedObjects, o => property.SetValue(obj, o)); + } } } } @@ -232,7 +228,7 @@ obj is JsonSchema && for (var i = 0; i < items.Length; i++) { var index = i; - Visit(items[i], path + "[" + i + "]", null, checkedObjects, o => ReplaceOrDelete(list, index, o)); + Visit(items[i], $"{path}[{i}]", null, checkedObjects, o => ReplaceOrDelete(list, index, o)); } } else if (obj is IEnumerable enumerable) @@ -240,7 +236,7 @@ obj is JsonSchema && var items = enumerable.OfType().ToArray(); for (var i = 0; i < items.Length; i++) { - Visit(items[i], path + "[" + i + "]", null, checkedObjects, o => throw new NotSupportedException("Cannot replace enumerable item.")); + Visit(items[i], $"{path}[{i}]", null, checkedObjects, o => throw new NotSupportedException("Cannot replace enumerable item.")); } } }