1
0
Fork 0
mirror of https://github.com/VSadov/Satori.git synced 2025-06-09 09:34:49 +09:00

Update JsonSerializerOptions.AddContext to have combine semantics. (#80698)

* Update JsonSerializerOptions.AddContext to have combine semantics.

* Remove unused property setter.

* Remove unused error message.

* Update XML documentation.
This commit is contained in:
Eirik Tsarpalis 2023-01-17 14:53:07 +00:00 committed by GitHub
parent e9b9489f41
commit 9a6686be5d
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 133 additions and 54 deletions

View file

@ -1268,7 +1268,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 Combine(params System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver?[] resolvers) { throw null; }
}
public abstract partial class JsonTypeInfo<T> : System.Text.Json.Serialization.Metadata.JsonTypeInfo
{

View file

@ -659,9 +659,6 @@
<data name="CreateObjectConverterNotCompatible" xml:space="preserve">
<value>The converter for type '{0}' does not support setting 'CreateObject' delegates.</value>
</data>
<data name="CombineOneOfResolversIsNull" xml:space="preserve">
<value>One of the provided resolvers is null.</value>
</data>
<data name="JsonPropertyInfoBoundToDifferentParent" xml:space="preserve">
<value>JsonPropertyInfo with name '{0}' for type '{1}' is already bound to different JsonTypeInfo.</value>
</data>

View file

@ -35,14 +35,6 @@ namespace System.Text.Json.Serialization
return options;
}
internal set
{
Debug.Assert(!value.IsReadOnly);
value.TypeInfoResolver = this;
value.MakeReadOnly();
_options = value;
}
}
/// <summary>
@ -94,7 +86,9 @@ namespace System.Text.Json.Serialization
if (options != null)
{
options.VerifyMutable();
Options = options;
options.TypeInfoResolver = this;
options.MakeReadOnly();
_options = options;
}
}

View file

@ -161,18 +161,22 @@ namespace System.Text.Json
}
/// <summary>
/// Binds current <see cref="JsonSerializerOptions"/> instance with a new instance of the specified <see cref="Serialization.JsonSerializerContext"/> type.
/// Appends a <see cref="Serialization.JsonSerializerContext"/> to the metadata resolution of the current <see cref="JsonSerializerOptions"/> instance.
/// </summary>
/// <typeparam name="TContext">The generic definition of the specified context type.</typeparam>
/// <remarks>
/// When serializing and deserializing types using the options
/// instance, metadata for the types will be fetched from the context instance.
///
/// The methods supports adding multiple contexts per options instance.
/// Metadata will be resolved in the order of configuration, similar to
/// how <see cref="JsonTypeInfoResolver.Combine(IJsonTypeInfoResolver?[])"/> resolves metadata.
/// </remarks>
public void AddContext<TContext>() where TContext : JsonSerializerContext, new()
{
VerifyMutable();
TContext context = new();
context.Options = this;
TypeInfoResolver = JsonTypeInfoResolver.Combine(TypeInfoResolver, context);
}
/// <summary>

View file

@ -1,6 +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.Collections.Generic;
namespace System.Text.Json.Serialization.Metadata
{
/// <summary>
@ -13,7 +15,7 @@ namespace System.Text.Json.Serialization.Metadata
/// </summary>
/// <param name="resolvers">Sequence of contract resolvers to be queried for metadata.</param>
/// <returns>A <see cref="IJsonTypeInfoResolver"/> combining results from <paramref name="resolvers"/>.</returns>
/// <exception cref="ArgumentException"><paramref name="resolvers"/> or any of its elements is null.</exception>
/// <exception cref="ArgumentException"><paramref name="resolvers"/> is null.</exception>
/// <remarks>
/// The combined resolver will query each of <paramref name="resolvers"/> in the specified order,
/// returning the first result that is non-null. If all <paramref name="resolvers"/> return null,
@ -23,32 +25,42 @@ namespace System.Text.Json.Serialization.Metadata
/// which typically define contract metadata for small subsets of types.
/// It can also be used to fall back to <see cref="DefaultJsonTypeInfoResolver"/> wherever necessary.
/// </remarks>
public static IJsonTypeInfoResolver Combine(params IJsonTypeInfoResolver[] resolvers)
public static IJsonTypeInfoResolver Combine(params IJsonTypeInfoResolver?[] resolvers)
{
if (resolvers == null)
if (resolvers is null)
{
throw new ArgumentNullException(nameof(resolvers));
}
var flattenedResolvers = new List<IJsonTypeInfoResolver>();
foreach (IJsonTypeInfoResolver? resolver in resolvers)
{
if (resolver == null)
if (resolver is null)
{
throw new ArgumentNullException(nameof(resolvers), SR.CombineOneOfResolversIsNull);
continue;
}
else if (resolver is CombiningJsonTypeInfoResolver nested)
{
flattenedResolvers.AddRange(nested._resolvers);
}
else
{
flattenedResolvers.Add(resolver);
}
}
return new CombiningJsonTypeInfoResolver(resolvers);
return flattenedResolvers.Count == 1
? flattenedResolvers[0]
: new CombiningJsonTypeInfoResolver(flattenedResolvers.ToArray());
}
private sealed class CombiningJsonTypeInfoResolver : IJsonTypeInfoResolver
{
private readonly IJsonTypeInfoResolver[] _resolvers;
internal readonly IJsonTypeInfoResolver[] _resolvers;
public CombiningJsonTypeInfoResolver(IJsonTypeInfoResolver[] resolvers)
{
_resolvers = resolvers.AsSpan().ToArray();
}
=> _resolvers = resolvers;
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
{

View file

@ -3,9 +3,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
@ -14,18 +16,39 @@ namespace System.Text.Json.Serialization.Tests
public static partial class JsonTypeInfoResolverTests
{
[Fact]
public static void GetTypeInfoNullArguments()
public static void CombineNullArgument()
{
IJsonTypeInfoResolver[] resolvers = null;
Assert.Throws<ArgumentNullException>(() => JsonTypeInfoResolver.Combine(resolvers));
}
[Fact]
public static void Combine_ShouldFlattenResolvers()
{
DefaultJsonTypeInfoResolver nonNullResolver1 = new();
DefaultJsonTypeInfoResolver nonNullResolver2 = new();
Assert.Throws<ArgumentNullException>(() => JsonTypeInfoResolver.Combine(null));
Assert.Throws<ArgumentNullException>(() => JsonTypeInfoResolver.Combine(null, null));
Assert.Throws<ArgumentNullException>(() => JsonTypeInfoResolver.Combine(nonNullResolver1, null));
Assert.Throws<ArgumentNullException>(() => JsonTypeInfoResolver.Combine(nonNullResolver1, nonNullResolver2, null));
Assert.Throws<ArgumentNullException>(() => JsonTypeInfoResolver.Combine(nonNullResolver1, null, nonNullResolver2));
DefaultJsonTypeInfoResolver nonNullResolver3 = new();
ValidateCombinations(Array.Empty<IJsonTypeInfoResolver>(), JsonTypeInfoResolver.Combine());
ValidateCombinations(Array.Empty<IJsonTypeInfoResolver>(), JsonTypeInfoResolver.Combine(new IJsonTypeInfoResolver[] { null }));
ValidateCombinations(Array.Empty<IJsonTypeInfoResolver>(), JsonTypeInfoResolver.Combine(null, null));
ValidateCombinations(new[] { nonNullResolver1 }, JsonTypeInfoResolver.Combine(nonNullResolver1, null));
ValidateCombinations(new[] { nonNullResolver1, nonNullResolver2 }, JsonTypeInfoResolver.Combine(nonNullResolver1, nonNullResolver2, null));
ValidateCombinations(new[] { nonNullResolver1, nonNullResolver2 }, JsonTypeInfoResolver.Combine(nonNullResolver1, null, nonNullResolver2));
ValidateCombinations(new[] { nonNullResolver1, nonNullResolver2, nonNullResolver3 }, JsonTypeInfoResolver.Combine(JsonTypeInfoResolver.Combine(JsonTypeInfoResolver.Combine(nonNullResolver1), nonNullResolver2), nonNullResolver3));
ValidateCombinations(new[] { nonNullResolver1, nonNullResolver2, nonNullResolver3 }, JsonTypeInfoResolver.Combine(JsonTypeInfoResolver.Combine(nonNullResolver1, null, nonNullResolver2), nonNullResolver3));
static void ValidateCombinations(IJsonTypeInfoResolver[] expectedResolvers, IJsonTypeInfoResolver combinedResolver)
{
if (expectedResolvers.Length == 1)
{
Assert.Same(expectedResolvers[0], combinedResolver);
}
else
{
Assert.Equal(expectedResolvers, GetCombinedResolvers(combinedResolver));
}
}
}
[Fact]
@ -126,5 +149,24 @@ namespace System.Text.Json.Serialization.Tests
Assert.Null(combined.GetTypeInfo(typeof(StringBuilder), options));
Assert.Equal(4, resolverId);
}
private static IJsonTypeInfoResolver[] GetCombinedResolvers(IJsonTypeInfoResolver resolver)
{
(Type combinedResolverType, FieldInfo underlyingResolverField) = s_combinedResolverMembers.Value;
Assert.IsType(combinedResolverType, resolver);
return (IJsonTypeInfoResolver[])underlyingResolverField.GetValue(resolver);
}
private static Lazy<(Type, FieldInfo)> s_combinedResolverMembers = new Lazy<(Type, FieldInfo)>
(
static () =>
{
Type? combinedResolverType = typeof(JsonTypeInfoResolver).GetNestedType("CombiningJsonTypeInfoResolver", BindingFlags.NonPublic);
Assert.NotNull(combinedResolverType);
FieldInfo underlyingResolverField = combinedResolverType.GetField("_resolvers", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(underlyingResolverField);
return (combinedResolverType, underlyingResolverField);
}
);
}
}

View file

@ -29,10 +29,30 @@ namespace System.Text.Json.Serialization.Tests
{
JsonSerializerOptions options = new();
options.AddContext<MyJsonContext>();
Assert.IsType<MyJsonContext>(options.TypeInfoResolver);
}
// Options can be binded only once.
CauseInvalidOperationException(() => options.AddContext<MyJsonContext>());
CauseInvalidOperationException(() => options.AddContext<MyJsonContextThatSetsOptionsInParameterlessCtor>());
[Fact]
public void AddContext_SupportsMultipleContexts()
{
JsonSerializerOptions options = new();
options.AddContext<SingleTypeContext<int>>();
options.AddContext<SingleTypeContext<string>>();
Assert.NotNull(options.GetTypeInfo(typeof(int)));
Assert.NotNull(options.GetTypeInfo(typeof(string)));
Assert.Throws<NotSupportedException>(() => options.GetTypeInfo(typeof(bool)));
}
[Fact]
public void AddContext_AppendsToExistingResolver()
{
JsonSerializerOptions options = new();
options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
options.AddContext<MyJsonContext>(); // this context always throws
// should always consult the default resolver, never falling back to the throwing resolver.
options.GetTypeInfo(typeof(int));
}
private static void CauseInvalidOperationException(Action action)
@ -48,16 +68,15 @@ namespace System.Text.Json.Serialization.Tests
{
// Context binds with options when instantiated with parameterless ctor.
MyJsonContextThatSetsOptionsInParameterlessCtor context = new();
FieldInfo optionsField = typeof(JsonSerializerContext).GetField("_options", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(optionsField);
Assert.NotNull((JsonSerializerOptions)optionsField.GetValue(context));
Assert.NotNull(context.Options);
Assert.Same(context, context.Options.TypeInfoResolver);
// Those options are overwritten when context is binded via options.AddContext<TContext>();
JsonSerializerOptions options = new();
Assert.Null(options.TypeInfoResolver);
options.AddContext<MyJsonContextThatSetsOptionsInParameterlessCtor>(); // No error.
FieldInfo resolverField = typeof(JsonSerializerOptions).GetField("_typeInfoResolver", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(resolverField);
Assert.Same(options, ((JsonSerializerContext)resolverField.GetValue(options)).Options);
Assert.NotNull(options.TypeInfoResolver);
Assert.NotSame(options, ((JsonSerializerContext)options.TypeInfoResolver).Options);
}
[Fact]
@ -66,25 +85,26 @@ namespace System.Text.Json.Serialization.Tests
// Bind the options.
JsonSerializerOptions options = new();
options.AddContext<MyJsonContext>();
Assert.False(options.IsReadOnly);
// Attempt to bind the instance again.
Assert.Throws<InvalidOperationException>(() => new MyJsonContext(options));
// Pass the options to a context constructor
_ = new MyJsonContext(options);
Assert.True(options.IsReadOnly);
}
[Fact]
public void OptionsImmutableAfterBinding()
public void OptionsMutableAfterBinding()
{
// Bind via AddContext
JsonSerializerOptions options = new();
options.PropertyNameCaseInsensitive = true;
options.AddContext<MyJsonContext>();
CauseInvalidOperationException(() => options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
Assert.False(options.IsReadOnly);
// Bind via context ctor
options = new JsonSerializerOptions();
MyJsonContext context = new MyJsonContext(options);
Assert.Same(options, context.Options);
CauseInvalidOperationException(() => options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
Assert.True(options.IsReadOnly);
}
[Fact]
@ -130,5 +150,13 @@ namespace System.Text.Json.Serialization.Tests
protected override JsonSerializerOptions? GeneratedSerializerOptions => null;
public override JsonTypeInfo? GetTypeInfo(Type type) => JsonTypeInfo.CreateJsonTypeInfo(type, Options);
}
private class SingleTypeContext<T> : JsonSerializerContext, IJsonTypeInfoResolver
{
public SingleTypeContext() : base(null) { }
protected override JsonSerializerOptions? GeneratedSerializerOptions => null;
public override JsonTypeInfo? GetTypeInfo(Type type) => GetTypeInfo(type, Options);
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) => type == typeof(T) ? JsonTypeInfo.CreateJsonTypeInfo(type, options) : null;
}
}
}

View file

@ -172,16 +172,16 @@ namespace System.Text.Json.Serialization.Tests
}
[Fact]
public static void TypeInfoResolverCannotBeSetAfterAddingContext()
public static void TypeInfoResolverCanBeSetAfterAddingContext()
{
var options = new JsonSerializerOptions();
Assert.False(options.IsReadOnly);
options.AddContext<JsonContext>();
Assert.True(options.IsReadOnly);
Assert.False(options.IsReadOnly);
Assert.IsType<JsonContext>(options.TypeInfoResolver);
Assert.Throws<InvalidOperationException>(() => options.TypeInfoResolver = new DefaultJsonTypeInfoResolver());
options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
Assert.IsType<DefaultJsonTypeInfoResolver>(options.TypeInfoResolver);
}
[Fact]
@ -194,19 +194,21 @@ namespace System.Text.Json.Serialization.Tests
}
[Fact]
public static void WhenAddingContextTypeInfoResolverAsContextOptionsAreSameAsOptions()
public static void WhenAddingContextTypeInfoResolverAsContextOptionsAreNotSameAsOptions()
{
var options = new JsonSerializerOptions();
options.AddContext<JsonContext>();
Assert.Same(options, (options.TypeInfoResolver as JsonContext).Options);
Assert.NotSame(options, (options.TypeInfoResolver as JsonContext).Options);
}
[Fact]
public static void WhenAddingContext_SettingResolverToNullThrowsInvalidOperationException()
public static void WhenAddingContext_CanSetResolverToNull()
{
var options = new JsonSerializerOptions();
options.AddContext<JsonContext>();
Assert.Throws<InvalidOperationException>(() => options.TypeInfoResolver = null);
options.TypeInfoResolver = null;
Assert.Null(options.TypeInfoResolver);
}
[Fact]