Implement JsonTypeInfoResolver.WithAddedModifier (#88255)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 5 Jul 2023 12:55:21 +0000 (13:55 +0100)
committerGitHub <noreply@github.com>
Wed, 5 Jul 2023 12:55:21 +0000 (13:55 +0100)
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverWithAddedModifiers.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapper.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonPropertyInfo.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonTypeInfoResolverTests.cs

index d1fe0ac..7a60938 100644 (file)
@@ -1315,6 +1315,7 @@ namespace System.Text.Json.Serialization.Metadata
     public static partial class JsonTypeInfoResolver
     {
         public static System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver Combine(params System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver?[] resolvers) { throw null; }
+        public static System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver WithAddedModifier(this System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver resolver, System.Action<System.Text.Json.Serialization.Metadata.JsonTypeInfo> modifier) { throw null; }
     }
     public sealed partial class JsonTypeInfo<T> : System.Text.Json.Serialization.Metadata.JsonTypeInfo
     {
index e1cbf82..13887ad 100644 (file)
@@ -142,6 +142,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
     <Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoResolver.cs" />
     <Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoKind.cs" />
     <Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoResolverChain.cs" />
+    <Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoResolverWithAddedModifiers.cs" />
     <Compile Include="System\Text\Json\Serialization\PolymorphicSerializationState.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriterCache.cs" />
     <Compile Include="System\Text\Json\Serialization\ReferenceEqualsWrapper.cs" />
index 9380884..7a5f2f8 100644 (file)
@@ -899,7 +899,7 @@ namespace System.Text.Json
 
                 TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault
                     ? DefaultJsonTypeInfoResolver.RootDefaultInstance()
-                    : new JsonTypeInfoResolverChain(),
+                    : JsonTypeInfoResolver.Empty,
 
                 _isReadOnly = true,
             };
index 3ff45ab..0b25768 100644 (file)
@@ -121,7 +121,7 @@ namespace System.Text.Json.Serialization.Metadata
 
         [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
         [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
-        internal static JsonConverter? GetCustomConverterForMember(Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options)
+        private static JsonConverter? GetCustomConverterForMember(Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options)
         {
             Debug.Assert(memberInfo is FieldInfo or PropertyInfo);
             Debug.Assert(typeToConvert != null);
index d890b9b..59c259f 100644 (file)
@@ -27,7 +27,7 @@ namespace System.Text.Json.Serialization.Metadata
         {
             if (resolvers is null)
             {
-                throw new ArgumentNullException(nameof(resolvers));
+                ThrowHelper.ThrowArgumentNullException(nameof(resolvers));
             }
 
             var resolverChain = new JsonTypeInfoResolverChain();
@@ -40,6 +40,37 @@ namespace System.Text.Json.Serialization.Metadata
         }
 
         /// <summary>
+        /// Creates a resolver applies modifications to the metadata generated by the source <paramref name="resolver"/>.
+        /// </summary>
+        /// <param name="resolver">The source resolver generating <see cref="JsonTypeInfo"/> metadata.</param>
+        /// <param name="modifier">The delegate modifying non-null <see cref="JsonTypeInfo"/> results.</param>
+        /// <returns>A new <see cref="IJsonTypeInfoResolver"/> instance applying the modifications.</returns>
+        /// <remarks>
+        /// This method is closely related to <see cref="DefaultJsonTypeInfoResolver.Modifiers"/> property
+        /// extended to arbitrary <see cref="IJsonTypeInfoResolver"/> instances.
+        /// </remarks>
+        public static IJsonTypeInfoResolver WithAddedModifier(this IJsonTypeInfoResolver resolver, Action<JsonTypeInfo> modifier)
+        {
+            if (resolver is null)
+            {
+                ThrowHelper.ThrowArgumentNullException(nameof(resolver));
+            }
+            if (modifier is null)
+            {
+                ThrowHelper.ThrowArgumentNullException(nameof(modifier));
+            }
+
+            return resolver is JsonTypeInfoResolverWithAddedModifiers resolverWithModifiers
+                ? resolverWithModifiers.WithAddedModifier(modifier)
+                : new JsonTypeInfoResolverWithAddedModifiers(resolver, new[] { modifier });
+        }
+
+        /// <summary>
+        /// Gets a resolver that returns null <see cref="JsonTypeInfo"/> for every type.
+        /// </summary>
+        internal static IJsonTypeInfoResolver Empty { get; } = new JsonTypeInfoResolverChain();
+
+        /// <summary>
         /// Indicates whether the metadata generated by the current resolver
         /// are compatible with the run time specified <see cref="JsonSerializerOptions"/>.
         /// </summary>
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverWithAddedModifiers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverWithAddedModifiers.cs
new file mode 100644 (file)
index 0000000..2f84b35
--- /dev/null
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+
+namespace System.Text.Json.Serialization.Metadata
+{
+    internal sealed class JsonTypeInfoResolverWithAddedModifiers : IJsonTypeInfoResolver
+    {
+        private readonly IJsonTypeInfoResolver _source;
+        private readonly Action<JsonTypeInfo>[] _modifiers;
+
+        public JsonTypeInfoResolverWithAddedModifiers(IJsonTypeInfoResolver source, Action<JsonTypeInfo>[] modifiers)
+        {
+            Debug.Assert(modifiers.Length > 0);
+            _source = source;
+            _modifiers = modifiers;
+        }
+
+        public JsonTypeInfoResolverWithAddedModifiers WithAddedModifier(Action<JsonTypeInfo> modifier)
+        {
+            var newModifiers = new Action<JsonTypeInfo>[_modifiers.Length + 1];
+            _modifiers.CopyTo(newModifiers, 0);
+            newModifiers[_modifiers.Length] = modifier;
+
+            return new JsonTypeInfoResolverWithAddedModifiers(_source, newModifiers);
+        }
+
+        public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+        {
+            JsonTypeInfo? typeInfo = _source.GetTypeInfo(type, options);
+
+            if (typeInfo != null)
+            {
+                foreach (Action<JsonTypeInfo> modifier in _modifiers)
+                {
+                    modifier(typeInfo);
+                }
+            }
+
+            return typeInfo;
+        }
+    }
+}
index 259e363..5b269f8 100644 (file)
@@ -59,7 +59,7 @@ namespace System.Text.Json.Serialization.Tests
             JsonSerializerOptions defaultOptions = DefaultOptions;
             return new JsonSerializerOptions(defaultOptions)
             {
-                TypeInfoResolver = defaultOptions.TypeInfoResolver.WithModifier(modifier)
+                TypeInfoResolver = defaultOptions.TypeInfoResolver.WithAddedModifier(modifier)
             };
         }
 
@@ -79,7 +79,7 @@ namespace System.Text.Json.Serialization.Tests
 
             if (modifier != null && options.TypeInfoResolver != null)
             {
-                options.TypeInfoResolver = DefaultOptions.TypeInfoResolver.WithModifier(modifier);
+                options.TypeInfoResolver = DefaultOptions.TypeInfoResolver.WithAddedModifier(modifier);
             }
 
             if (customConverters != null)
@@ -100,34 +100,4 @@ namespace System.Text.Json.Serialization.Tests
             return options;
         }
     }
-
-    public static class JsonTypeInfoResolverExtensions
-    {
-        public static IJsonTypeInfoResolver WithModifier(this IJsonTypeInfoResolver resolver, Action<JsonTypeInfo> modifier)
-            => new JsonTypeInfoResolverWithModifier(resolver, modifier);
-
-        private class JsonTypeInfoResolverWithModifier : IJsonTypeInfoResolver
-        {
-            private readonly IJsonTypeInfoResolver _source;
-            private readonly Action<JsonTypeInfo> _modifier;
-
-            public JsonTypeInfoResolverWithModifier(IJsonTypeInfoResolver source, Action<JsonTypeInfo> modifier)
-            {
-                _source = source;
-                _modifier = modifier;
-            }
-
-            public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
-            {
-                JsonTypeInfo? typeInfo = _source.GetTypeInfo(type, options);
-
-                if (typeInfo != null)
-                {
-                    _modifier(typeInfo);
-                }
-
-                return typeInfo;
-            }
-        }
-    }
 }
index 83f64ad..7aa6e62 100644 (file)
@@ -9,79 +9,30 @@ namespace System.Text.Json.Serialization.Tests
 {
     public class DefaultJsonPropertyInfoTests_DefaultJsonTypeInfoResolver : DefaultJsonPropertyInfoTests
     {
-        protected override IJsonTypeInfoResolver CreateResolverWithModifiers(params Action<JsonTypeInfo>[] modifiers)
-        {
-            var resolver = new DefaultJsonTypeInfoResolver();
-
-            foreach (var modifier in modifiers)
-            {
-                resolver.Modifiers.Add(modifier);
-            }
-
-            return resolver;
-        }
+        protected override IJsonTypeInfoResolver Resolver { get; } = new DefaultJsonTypeInfoResolver();
     }
 
     public class DefaultJsonPropertyInfoTests_SerializerContextNoWrapping : DefaultJsonPropertyInfoTests
     {
-        protected override bool ModifiersNotSupported => true;
-
-        protected override IJsonTypeInfoResolver CreateResolverWithModifiers(params Action<JsonTypeInfo>[] modifiers)
-        {
-            if (modifiers.Length != 0)
-            {
-                Assert.Fail($"Testing non wrapped JsonSerializerContext but modifier is provided. Make sure to check {nameof(ModifiersNotSupported)}.");
-            }
-
-            return Context.Default;
-        }
+        protected override IJsonTypeInfoResolver Resolver { get; } = Context.Default;
     }
 
     public class DefaultJsonPropertyInfoTests_SerializerContextWrapped : DefaultJsonPropertyInfoTests
     {
-        protected override IJsonTypeInfoResolver CreateResolverWithModifiers(params Action<JsonTypeInfo>[] modifiers)
-            => new ContextWithModifiers(Context.Default, modifiers);
-
-        private class ContextWithModifiers : IJsonTypeInfoResolver
-        {
-            private IJsonTypeInfoResolver _context;
-            private Action<JsonTypeInfo>[] _modifiers;
-
-            public ContextWithModifiers(JsonSerializerContext context, Action<JsonTypeInfo>[] modifiers)
-            {
-                _context = context;
-                _modifiers = modifiers;
-            }
-
-            public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
-            {
-                JsonTypeInfo? typeInfo = _context.GetTypeInfo(type, options);
-
-                if (typeInfo != null)
-                {
-                    foreach (var modifier in _modifiers)
-                    {
-                        modifier(typeInfo);
-                    }
-                }
-
-                return typeInfo;
-            }
-        }
+        protected override IJsonTypeInfoResolver Resolver { get; } = Context.Default;
     }
 
     public abstract partial class DefaultJsonPropertyInfoTests
     {
-        protected virtual bool ModifiersNotSupported => false;
-        protected abstract IJsonTypeInfoResolver CreateResolverWithModifiers(params Action<JsonTypeInfo>[] modifiers);
+        protected abstract IJsonTypeInfoResolver Resolver { get; }
 
-        private JsonSerializerOptions CreateOptionsWithModifiers(params Action<JsonTypeInfo>[] modifiers)
-            => new JsonSerializerOptions()
+        private JsonSerializerOptions CreateOptionsWithModifier(Action<JsonTypeInfo> modifier)
+            => new JsonSerializerOptions
             {
-                TypeInfoResolver = CreateResolverWithModifiers(modifiers)
+                TypeInfoResolver = Resolver.WithAddedModifier(modifier)
             };
 
-        private JsonSerializerOptions CreateOptions() => CreateOptionsWithModifiers();
+        private JsonSerializerOptions CreateOptions() => new JsonSerializerOptions { TypeInfoResolver = Resolver };
 
         [Fact]
         public void RequiredAttributesGetDetectedAndFailDeserializationWhenValuesNotPresent()
@@ -136,10 +87,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public void RequiredMemberCanBeModifiedToNonRequired()
         {
-            if (ModifiersNotSupported)
-                return;
-
-            JsonSerializerOptions options = CreateOptionsWithModifiers(ti =>
+            JsonSerializerOptions options = CreateOptionsWithModifier(ti =>
             {
                 if (ti.Type == typeof(ClassWithRequiredCustomAttributes))
                 {
@@ -184,10 +132,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public void NonRequiredMemberCanBeModifiedToRequired()
         {
-            if (ModifiersNotSupported)
-                return;
-
-            JsonSerializerOptions options = CreateOptionsWithModifiers(ti =>
+            JsonSerializerOptions options = CreateOptionsWithModifier(ti =>
             {
                 if (ti.Type == typeof(ClassWithRequiredCustomAttributes))
                 {
@@ -241,10 +186,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public void RequiredExtensionDataPropertyCanBeFixedToNotBeRequiredWithResolver()
         {
-            if (ModifiersNotSupported)
-                return;
-
-            JsonSerializerOptions options = CreateOptionsWithModifiers(ti =>
+            JsonSerializerOptions options = CreateOptionsWithModifier(ti =>
             {
                 if (ti.Type == typeof(ClassWithRequiredCustomAttributeAndDataExtensionProperty))
                 {
@@ -275,10 +217,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public void RequiredExtensionDataPropertyCanBeFixedToNotBeExtensionDataWithResolver()
         {
-            if (ModifiersNotSupported)
-                return;
-
-            JsonSerializerOptions options = CreateOptionsWithModifiers(ti =>
+            JsonSerializerOptions options = CreateOptionsWithModifier(ti =>
             {
                 if (ti.Type == typeof(ClassWithRequiredCustomAttributeAndDataExtensionProperty))
                 {
@@ -319,10 +258,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public void RequiredReadOnlyPropertyCanBeFixedToNotBeRequiredWithResolver()
         {
-            if (ModifiersNotSupported)
-                return;
-
-            JsonSerializerOptions options = CreateOptionsWithModifiers(ti =>
+            JsonSerializerOptions options = CreateOptionsWithModifier(ti =>
             {
                 if (ti.Type == typeof(ClassWithRequiredCustomAttributeAndReadOnlyProperty))
                 {
@@ -354,10 +290,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public void RequiredReadOnlyPropertyCanBeFixedToBeWritableWithResolver()
         {
-            if (ModifiersNotSupported)
-                return;
-
-            JsonSerializerOptions options = CreateOptionsWithModifiers(ti =>
+            JsonSerializerOptions options = CreateOptionsWithModifier(ti =>
             {
                 if (ti.Type == typeof(ClassWithRequiredCustomAttributeAndReadOnlyProperty))
                 {
index 8f578a9..9136555 100644 (file)
@@ -1,12 +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;
 using System.Collections.Generic;
-using System.Reflection;
-using System.Text;
 using System.Text.Json.Serialization.Metadata;
-using System.Threading.Tasks;
 using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
@@ -160,6 +156,92 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
+        public static void WithAddedModifier_CallsModifierOnResolvedMetadata()
+        {
+            int modifierInvocationCount = 0;
+            JsonSerializerOptions options = new();
+            TestResolver resolver = new(JsonTypeInfo.CreateJsonTypeInfo);
+
+            IJsonTypeInfoResolver resolverWithModifier = resolver.WithAddedModifier(_ => modifierInvocationCount++);
+
+            Assert.NotNull(resolverWithModifier.GetTypeInfo(typeof(int), options));
+            Assert.Equal(1, modifierInvocationCount);
+
+            Assert.NotNull(resolverWithModifier.GetTypeInfo(typeof(string), options));
+            Assert.Equal(2, modifierInvocationCount);
+
+            Assert.NotNull(resolverWithModifier.GetTypeInfo(typeof(int), options));
+            Assert.Equal(3, modifierInvocationCount);
+        }
+
+        [Fact]
+        public static void WithAddedModifier_DoesNotCallModifierOnUnResolvedMetadata()
+        {
+            int modifierInvocationCount = 0;
+            JsonSerializerOptions options = new();
+            TestResolver resolver = new((_,_) => null);
+
+            IJsonTypeInfoResolver resolverWithModifier = resolver.WithAddedModifier(_ => modifierInvocationCount++);
+
+            Assert.Null(resolverWithModifier.GetTypeInfo(typeof(int), options));
+            Assert.Equal(0, modifierInvocationCount);
+
+            Assert.Null(resolverWithModifier.GetTypeInfo(typeof(string), options));
+            Assert.Equal(0, modifierInvocationCount);
+        }
+
+        [Fact]
+        public static void WithAddedModifier_CanChainMultipleModifiers()
+        {
+            int modifier1InvocationCount = 0;
+            int modifier2InvocationCount = 0;
+            JsonSerializerOptions options = new();
+            TestResolver resolver = new(JsonTypeInfo.CreateJsonTypeInfo);
+
+            IJsonTypeInfoResolver resolverWithModifier = resolver
+                .WithAddedModifier(_ => modifier1InvocationCount++)
+                .WithAddedModifier(_ => Assert.Equal(modifier1InvocationCount, ++modifier2InvocationCount)); // Validates order of modifier evaluation.
+
+            Assert.NotNull(resolverWithModifier.GetTypeInfo(typeof(int), options));
+            Assert.Equal(1, modifier1InvocationCount);
+            Assert.Equal(1, modifier2InvocationCount);
+        }
+
+        [Fact]
+        public static void WithAddedModifier_ChainingDoesNotMutateIntermediateResolvers()
+        {
+            int modifier1InvocationCount = 0;
+            int modifier2InvocationCount = 0;
+            JsonSerializerOptions options = new();
+            TestResolver resolver = new(JsonTypeInfo.CreateJsonTypeInfo);
+
+            IJsonTypeInfoResolver resolverWithModifier = resolver
+                .WithAddedModifier(_ => modifier1InvocationCount++);
+
+            IJsonTypeInfoResolver resolverWithChainedModifier = resolverWithModifier
+                .WithAddedModifier(_ => Assert.Equal(modifier1InvocationCount, ++modifier2InvocationCount)); // Validates order of modifier evaluation.
+
+            Assert.NotSame(resolverWithModifier, resolverWithChainedModifier);
+
+            Assert.NotNull(resolverWithChainedModifier.GetTypeInfo(typeof(int), options));
+            Assert.Equal(1, modifier1InvocationCount);
+            Assert.Equal(1, modifier2InvocationCount);
+
+            Assert.NotNull(resolverWithModifier.GetTypeInfo(typeof(int), options));
+            Assert.Equal(2, modifier1InvocationCount);
+            Assert.Equal(1, modifier2InvocationCount);
+        }
+
+        [Fact]
+        public static void WithAddedModifier_ThrowsOnNullArguments()
+        {
+            TestResolver resolver = new(JsonTypeInfo.CreateJsonTypeInfo);
+
+            Assert.Throws<ArgumentNullException>(() => ((IJsonTypeInfoResolver)null!).WithAddedModifier(_ => { }));
+            Assert.Throws<ArgumentNullException>(() => resolver.WithAddedModifier(null));
+        }
+
+        [Fact]
         public static void NullResolver_ReturnsObjectMetadata()
         {
             var options = new JsonSerializerOptions();