Skip to content

Commit

Permalink
Expose JsonSerializer.IsReadOnly and MakeReadOnly() APIs. (#74431)
Browse files Browse the repository at this point in the history
* Expose JsonSerializer.IsReadOnly and MakeReadOnly() APIs.

* Address feedback
  • Loading branch information
eiriktsarpalis authored Aug 24, 2022
1 parent 0469020 commit 4b14c92
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 49 deletions.
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
public bool IgnoreReadOnlyFields { get { throw null; } set { } }
public bool IgnoreReadOnlyProperties { get { throw null; } set { } }
public bool IncludeFields { get { throw null; } set { } }
public bool IsReadOnly { get { throw null; } }
public int MaxDepth { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } }
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
Expand All @@ -364,6 +365,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Getting a converter for a type may require reflection which depends on unreferenced code.")]
public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; }
public System.Text.Json.Serialization.Metadata.JsonTypeInfo GetTypeInfo(System.Type type) { throw null; }
public void MakeReadOnly() { }
}
public enum JsonTokenType : byte
{
Expand Down
8 changes: 4 additions & 4 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@
<data name="TrailingCommaNotAllowedBeforeObjectEnd" xml:space="preserve">
<value>The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options.</value>
</data>
<data name="SerializerOptionsImmutable" xml:space="preserve">
<data name="SerializerOptionsReadOnly" xml:space="preserve">
<value>Serializer options cannot be changed once serialization or deserialization has occurred.</value>
</data>
<data name="StreamNotWritable" xml:space="preserve">
Expand Down Expand Up @@ -578,7 +578,7 @@
<data name="NodeDuplicateKey" xml:space="preserve">
<value>An item with the same key has already been added. Key: {0}</value>
</data>
<data name="SerializerContextOptionsImmutable" xml:space="preserve">
<data name="SerializerContextOptionsReadOnly" xml:space="preserve">
<value>JsonSerializerOptions instances cannot be modified once encapsulated by a JsonSerializerContext. Such encapsulation can happen either when calling 'JsonSerializerOptions.AddContext' or when passing the options instance to a JsonSerializerContext constructor.</value>
</data>
<data name="ConverterForPropertyMustBeValid" xml:space="preserve">
Expand Down Expand Up @@ -659,8 +659,8 @@
<data name="JsonPropertyInfoBoundToDifferentParent" xml:space="preserve">
<value>JsonPropertyInfo with name '{0}' for type '{1}' is already bound to different JsonTypeInfo.</value>
</data>
<data name="JsonTypeInfoUsedButTypeInfoResolverNotSet" xml:space="preserve">
<value>JsonTypeInfo metadata references a JsonSerializerOptions instance that doesn't specify a TypeInfoResolver.</value>
<data name="JsonSerializerOptionsNoTypeInfoResolverSpecified" xml:space="preserve">
<value>JsonSerializerOptions instance must specify a TypeInfoResolver setting before being marked as read-only.</value>
</data>
<data name="JsonPolymorphismOptionsAssociatedWithDifferentJsonTypeInfo" xml:space="preserve">
<value>Parameter already associated with a different JsonTypeInfo instance.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,25 @@ public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver
/// </remarks>
public JsonSerializerOptions Options
{
get => _options ??= new JsonSerializerOptions { TypeInfoResolver = this, IsImmutable = true };
get
{
JsonSerializerOptions? options = _options;

if (options is null)
{
options = new JsonSerializerOptions { TypeInfoResolver = this };
options.MakeReadOnly();
_options = options;
}

return options;
}

internal set
{
Debug.Assert(!value.IsImmutable);
Debug.Assert(!value.IsReadOnly);
value.TypeInfoResolver = this;
value.IsImmutable = true;
value.MakeReadOnly();
_options = value;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ internal JsonTypeInfo GetTypeInfoInternal(Type type, bool ensureConfigured = tru
{
JsonTypeInfo? typeInfo = null;

if (IsImmutable)
if (IsReadOnly)
{
typeInfo = GetCachingContext()?.GetOrAddJsonTypeInfo(type);
if (ensureConfigured)
Expand Down Expand Up @@ -111,7 +111,7 @@ internal JsonTypeInfo ObjectTypeInfo
{
get
{
Debug.Assert(IsImmutable);
Debug.Assert(IsReadOnly);
return _objectTypeInfo ??= GetTypeInfoInternal(JsonTypeInfo.ObjectType);
}
}
Expand All @@ -127,7 +127,7 @@ internal void ClearCaches()

private CachingContext? GetCachingContext()
{
Debug.Assert(IsImmutable);
Debug.Assert(IsReadOnly);

return _cachingContext ??= TrackedCachingContexts.GetOrCreate(this);
}
Expand Down Expand Up @@ -178,7 +178,7 @@ internal static class TrackedCachingContexts

public static CachingContext GetOrCreate(JsonSerializerOptions options)
{
Debug.Assert(options.IsImmutable, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
Debug.Assert(options.IsReadOnly, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
Debug.Assert(options._typeInfoResolver != null);

ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> cache = s_cache;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public static JsonSerializerOptions Default
private bool _propertyNameCaseInsensitive;
private bool _writeIndented;

private volatile bool _isImmutable;
private volatile bool _isReadOnly;

/// <summary>
/// Constructs a new <see cref="JsonSerializerOptions"/> instance.
Expand Down Expand Up @@ -602,20 +602,35 @@ public ReferenceHandler? ReferenceHandler
new ReflectionEmitCachingMemberAccessor() :
new ReflectionMemberAccessor();
#elif NETFRAMEWORK
new ReflectionEmitCachingMemberAccessor();
new ReflectionEmitCachingMemberAccessor();
#else
new ReflectionMemberAccessor();
new ReflectionMemberAccessor();
#endif

internal bool IsImmutable
/// <summary>
/// Specifies whether the current instance has been locked for modification.
/// </summary>
/// <remarks>
/// A <see cref="JsonSerializerOptions"/> instance can be locked either if
/// it has been passed to one of the <see cref="JsonSerializer"/> methods,
/// has been associated with a <see cref="JsonSerializerContext"/> instance,
/// or a user explicitly called the <see cref="MakeReadOnly"/> method on the instance.
/// </remarks>
public bool IsReadOnly => _isReadOnly;

/// <summary>
/// Locks the current instance for further modification.
/// </summary>
/// <exception cref="InvalidOperationException">The instance does not specify a <see cref="TypeInfoResolver"/> setting.</exception>
/// <remarks>This method is idempotent.</remarks>
public void MakeReadOnly()
{
get => _isImmutable;
set
if (_typeInfoResolver is null)
{
Debug.Assert(value, "cannot unlock options instances");
Debug.Assert(_typeInfoResolver != null, "cannot lock without a resolver.");
_isImmutable = true;
ThrowHelper.ThrowInvalidOperationException_JsonSerializerOptionsNoTypeInfoResolverSpecified();
}

_isReadOnly = true;
}

/// <summary>
Expand All @@ -629,23 +644,13 @@ internal void InitializeForReflectionSerializer()
// the default resolver to gain access to the default converters.
DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
_typeInfoResolver ??= defaultResolver;
IsImmutable = true;
MakeReadOnly();
_isInitializedForReflectionSerializer = true;
}

internal bool IsInitializedForReflectionSerializer => _isInitializedForReflectionSerializer;
private volatile bool _isInitializedForReflectionSerializer;

internal void InitializeForMetadataGeneration()
{
if (_typeInfoResolver is null)
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet();
}

IsImmutable = true;
}

private JsonTypeInfo? GetTypeInfoNoCaching(Type type)
{
JsonTypeInfo? info = _typeInfoResolver?.GetTypeInfo(type, this);
Expand Down Expand Up @@ -709,9 +714,9 @@ internal JsonWriterOptions GetWriterOptions()

internal void VerifyMutable()
{
if (_isImmutable)
if (_isReadOnly)
{
ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(_typeInfoResolver as JsonSerializerContext);
ThrowHelper.ThrowInvalidOperationException_SerializerOptionsReadOnly(_typeInfoResolver as JsonSerializerContext);
}
}

Expand All @@ -725,7 +730,7 @@ public ConverterList(JsonSerializerOptions options, IList<JsonConverter>? source
_options = options;
}

protected override bool IsImmutable => _options.IsImmutable;
protected override bool IsImmutable => _options.IsReadOnly;
protected override void VerifyMutable() => _options.VerifyMutable();
}

Expand All @@ -736,12 +741,12 @@ private static JsonSerializerOptions GetOrCreateDefaultOptionsInstance()
var options = new JsonSerializerOptions
{
TypeInfoResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance(),
IsImmutable = true
_isReadOnly = true
};

return Interlocked.CompareExchange(ref s_defaultOptions, options, null) ?? options;
}

private string DebuggerDisplay => $"TypeInfoResolver = {TypeInfoResolver?.GetType()?.Name}, IsImmutable = {IsImmutable}";
private string DebuggerDisplay => $"TypeInfoResolver = {TypeInfoResolver?.GetType()?.Name}, IsImmutable = {IsReadOnly}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,9 @@ internal void Configure()
{
Debug.Assert(Monitor.IsEntered(_configureLock), "Configure called directly, use EnsureConfigured which locks this method");

if (!Options.IsImmutable)
if (!Options.IsReadOnly)
{
Options.InitializeForMetadataGeneration();
Options.MakeReadOnly();
}

PropertyInfoForTypeInfo.EnsureChildOf(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ public static void ThrowInvalidOperationException_ResolverTypeInfoOptionsNotComp
}

[DoesNotReturn]
public static void ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet()
public static void ThrowInvalidOperationException_JsonSerializerOptionsNoTypeInfoResolverSpecified()
{
throw new InvalidOperationException(SR.JsonTypeInfoUsedButTypeInfoResolverNotSet);
throw new InvalidOperationException(SR.JsonSerializerOptionsNoTypeInfoResolverSpecified);
}

[DoesNotReturn]
Expand Down Expand Up @@ -170,11 +170,11 @@ public static void ThrowInvalidOperationException_SerializationConverterOnAttrib
}

[DoesNotReturn]
public static void ThrowInvalidOperationException_SerializerOptionsImmutable(JsonSerializerContext? context)
public static void ThrowInvalidOperationException_SerializerOptionsReadOnly(JsonSerializerContext? context)
{
string message = context == null
? SR.SerializerOptionsImmutable
: SR.SerializerContextOptionsImmutable;
? SR.SerializerOptionsReadOnly
: SR.SerializerContextOptionsReadOnly;

throw new InvalidOperationException(message);
}
Expand Down
Loading

0 comments on commit 4b14c92

Please sign in to comment.