Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Ensure source generated metadata properties are read-only.
  • Loading branch information
eiriktsarpalis committed Oct 3, 2022
commit 7e9eb391aedd85a9ea361615088bec8619aae440
32 changes: 15 additions & 17 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,8 @@ private static string GenerateFastPathFuncForType(TypeGenerationSpec typeGenSpec

return $@"

// Intentionally not a static method because we create a delegate to it. Invoking delegates to instance
// methods is almost as fast as virtual calls. Static methods need to go through a shuffle thunk.
private void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {valueTypeRef} {ValueVarName})
{{
{GetEarlyNullCheckSource(emitNullCheck)}
Expand Down Expand Up @@ -1085,16 +1087,19 @@ private static string GenerateForType(TypeGenerationSpec typeMetadata, string me
/// </summary>
public {typeInfoPropertyTypeRef} {typeFriendlyName}
{{
get => _{typeFriendlyName} ??= {typeMetadata.CreateTypeInfoMethodName}({OptionsInstanceVariableName});
get => _{typeFriendlyName} ??= {typeMetadata.CreateTypeInfoMethodName}({OptionsInstanceVariableName}, makeReadOnly: true);
}}

// Intentionally not a static method because we create a delegate to it. Invoking delegates to instance
// methods is almost as fast as virtual calls. Static methods need to go through a shuffle thunk.
private {typeInfoPropertyTypeRef} {typeMetadata.CreateTypeInfoMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName})
private {typeInfoPropertyTypeRef} {typeMetadata.CreateTypeInfoMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}, bool makeReadOnly)
{{
{typeInfoPropertyTypeRef}? {JsonTypeInfoReturnValueLocalVariableName} = null;
{WrapWithCheckForCustomConverter(metadataInitSource, typeCompilableName)}

if (makeReadOnly)
{{
{JsonMetadataServicesTypeRef}.MakeReadOnly({JsonTypeInfoReturnValueLocalVariableName});
}}

return {JsonTypeInfoReturnValueLocalVariableName};
}}
{additionalSource}";
Expand Down Expand Up @@ -1271,30 +1276,23 @@ private string GetGetTypeInfoImplementation()
// Explicit IJsonTypeInfoResolver implementation
sb.AppendLine();
sb.Append(@$"{JsonTypeInfoTypeRef}? {JsonTypeInfoResolverTypeRef}.GetTypeInfo({TypeTypeRef} type, {JsonSerializerOptionsTypeRef} {OptionsLocalVariableName})
{{
if ({OptionsInstanceVariableName} == {OptionsLocalVariableName})
{{
return this.GetTypeInfo(type);
}}
else
{{");
{{");
// TODO (https://github.com/dotnet/runtime/issues/52218): Make this Dictionary-lookup-based if root-serializable type count > 64.
foreach (TypeGenerationSpec metadata in types)
{
if (metadata.ClassType != ClassType.TypeUnsupportedBySourceGen)
{
sb.Append($@"
if (type == typeof({metadata.TypeRef}))
{{
return {metadata.CreateTypeInfoMethodName}({OptionsLocalVariableName});
}}
if (type == typeof({metadata.TypeRef}))
{{
return {metadata.CreateTypeInfoMethodName}({OptionsLocalVariableName}, makeReadOnly: false);
}}
");
}
}

sb.Append($@"
return null;
}}
return null;
}}
");

Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ public static partial class JsonMetadataServices
public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonNode> JsonNodeConverter { get { throw null; } }
public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonObject> JsonObjectConverter { get { throw null; } }
public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonValue> JsonValueConverter { get { throw null; } }
public static void MakeReadOnly(System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo) { throw null; }
public static System.Text.Json.Serialization.JsonConverter<object?> ObjectConverter { get { throw null; } }
[System.CLSCompliantAttribute(false)]
public static System.Text.Json.Serialization.JsonConverter<sbyte> SByteConverter { get { throw null; } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,20 @@ public static JsonTypeInfo<T> CreateValueInfo<T>(JsonSerializerOptions options,
JsonTypeInfo<T> info = new SourceGenJsonTypeInfo<T>(converter, options);
return info;
}

/// <summary>
/// Marks the provided <see cref="JsonTypeInfo"/> instance as locked for further modification.
/// </summary>
/// <param name="jsonTypeInfo">The metadata instance to lock for modification.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsonTypeInfo"/> is null.</exception>
public static void MakeReadOnly(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo));
}

jsonTypeInfo.IsReadOnly = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ internal static JsonPropertyInfo GetPropertyPlaceholder()

private protected void VerifyMutable()
{
if (_isConfigured)
if (_isConfigured || ParentTypeInfo?.IsReadOnly == true)
{
ThrowHelper.ThrowInvalidOperationException_PropertyInfoImmutable();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions

internal void VerifyMutable()
{
if (_isConfigured)
if (_isConfigured || IsReadOnly)
{
ThrowHelper.ThrowInvalidOperationException_TypeInfoImmutable();
}
Expand All @@ -489,6 +489,8 @@ internal void VerifyMutable()

internal bool IsConfigured => _isConfigured;

internal bool IsReadOnly { get; set; }

internal void EnsureConfigured()
{
Debug.Assert(!Monitor.IsEntered(_configureLock), "recursive locking detected.");
Expand Down Expand Up @@ -749,6 +751,8 @@ internal void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDiction
Debug.Assert(jsonPropertyInfo.MemberName != null, "MemberName can be null in custom JsonPropertyInfo instances and should never be passed in this method");
string memberName = jsonPropertyInfo.MemberName;

jsonPropertyInfo.EnsureChildOf(this);

if (jsonPropertyInfo.IsExtensionData)
{
if (ExtensionDataProperty != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ public static void VariousNestingAndVisibilityLevelsAreSupported()
Assert.NotNull(NestedPublicContext.NestedProtectedInternalClass.Default);
}

[Fact]
public static void ContextMetadataIsImmutable()
{
JsonTypeInfo<Person> typeInfo = PersonJsonContext.Default.Person;

Assert.Throws<InvalidOperationException>(() => typeInfo.CreateObject = null);
Assert.Throws<InvalidOperationException>(() => typeInfo.OnDeserializing = obj => { });
Assert.Throws<InvalidOperationException>(() => typeInfo.Properties.Clear());

JsonPropertyInfo propertyInfo = typeInfo.Properties[0];
Assert.Throws<InvalidOperationException>(() => propertyInfo.Name = "differentName");
Assert.Throws<InvalidOperationException>(() => propertyInfo.IsExtensionData = true);
Assert.Throws<InvalidOperationException>(() => propertyInfo.IsRequired = true);
Assert.Throws<InvalidOperationException>(() => propertyInfo.Order = -1);
}

[Fact]
public static void VariousGenericsAreSupported()
{
Expand Down