Skip to content

Add OnSerialize callbacks to POCOs #54709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,13 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata)

if (typeMetadata.GenerateSerializationLogic)
{
serializeFuncSource = GenerateFastPathFuncForObject(typeCompilableName, serializeMethodName, typeMetadata.CanBeNull, properties);
serializeFuncSource = GenerateFastPathFuncForObject(
typeCompilableName,
serializeMethodName,
typeMetadata.CanBeNull,
typeMetadata.ImplementsIJsonOnSerialized,
typeMetadata.ImplementsIJsonOnSerializing,
properties);
serializeFuncNamedArg = $@"serializeFunc: {serializeMethodName}";
}
else
Expand Down Expand Up @@ -635,6 +641,8 @@ private string GenerateFastPathFuncForObject(
string typeInfoTypeRef,
string serializeMethodName,
bool canBeNull,
bool implementsIJsonOnSerialized,
bool implementsIJsonOnSerializing,
List<PropertyGenerationSpec>? properties)
{
JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions;
Expand All @@ -646,6 +654,12 @@ private string GenerateFastPathFuncForObject(
StringBuilder sb = new();

// Begin method definition
if (implementsIJsonOnSerializing)
{
sb.Append($@"(({IJsonOnSerializingFullName}){ValueVarName}).OnSerializing();");
sb.Append($@"{Environment.NewLine} ");
}

sb.Append($@"{WriterVarName}.WriteStartObject();");

if (properties != null)
Expand Down Expand Up @@ -733,6 +747,12 @@ private string GenerateFastPathFuncForObject(

{WriterVarName}.WriteEndObject();");

if (implementsIJsonOnSerialized)
{
sb.Append($@"{Environment.NewLine} ");
sb.Append($@"(({IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();");
};

return GenerateFastPathFuncForType(serializeMethodName, typeInfoTypeRef, sb.ToString(), canBeNull);
}

Expand Down
12 changes: 11 additions & 1 deletion src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
bool foundDesignTimeCustomConverter = false;
string? converterInstatiationLogic = null;

bool implementsIJsonOnSerialized = false;
bool implementsIJsonOnSerializing = false;

IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(type);
foreach (CustomAttributeData attributeData in attributeDataList)
{
Expand Down Expand Up @@ -577,6 +580,11 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
}

// GetInterface() is currently not implemented, so we use GetInterfaces().
IEnumerable<string> interfaces = type.GetInterfaces().Select(interfaceType => interfaceType.FullName);
implementsIJsonOnSerialized = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializedFullName) != null;
implementsIJsonOnSerializing = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializingFullName) != null;

for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
{
const BindingFlags bindingFlags =
Expand Down Expand Up @@ -627,7 +635,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeGenerationSpec(collectionValueType, generationMode) : null,
constructionStrategy,
nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeGenerationSpec(nullableUnderlyingType, generationMode) : null,
converterInstatiationLogic);
converterInstatiationLogic,
implementsIJsonOnSerialized,
implementsIJsonOnSerializing);

return typeMetadata;
}
Expand Down
6 changes: 4 additions & 2 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ namespace System.Text.Json.SourceGeneration
[Generator]
public sealed partial class JsonSourceGenerator : ISourceGenerator
{
private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration";
private const string IJsonOnSerializedFullName = "System.Text.Json.Serialization.IJsonOnSerialized";
private const string IJsonOnSerializingFullName = "System.Text.Json.Serialization.IJsonOnSerializing";

/// <summary>
/// Registers a syntax resolver to receive compilation units.
/// </summary>
Expand Down Expand Up @@ -51,8 +55,6 @@ public void Execute(GeneratorExecutionContext executionContext)
}
}

private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration";

/// <summary>
/// Helper for unit tests.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<CLSCompliant>false</CLSCompliant>
Expand Down
9 changes: 8 additions & 1 deletion src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ internal class TypeGenerationSpec

public ClassType ClassType { get; private set; }

public bool ImplementsIJsonOnSerialized { get; private set; }
public bool ImplementsIJsonOnSerializing { get; private set; }

public bool IsValueType { get; private set; }

public bool CanBeNull { get; private set; }
Expand Down Expand Up @@ -67,7 +70,9 @@ public void Initialize(
TypeGenerationSpec? collectionValueTypeMetadata,
ObjectConstructionStrategy constructionStrategy,
TypeGenerationSpec? nullableUnderlyingTypeMetadata,
string? converterInstantiationLogic)
string? converterInstantiationLogic,
bool implementsIJsonOnSerialized,
bool implementsIJsonOnSerializing)
{
GenerationMode = generationMode;
TypeRef = $"global::{typeRef}";
Expand All @@ -84,6 +89,8 @@ public void Initialize(
ConstructionStrategy = constructionStrategy;
NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata;
ConverterInstantiationLogic = converterInstantiationLogic;
ImplementsIJsonOnSerialized = implementsIJsonOnSerialized;
ImplementsIJsonOnSerializing = implementsIJsonOnSerializing;
}

private bool FastPathIsSupported()
Expand Down
16 changes: 16 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 @@ -733,6 +733,22 @@ public abstract partial class JsonValue : System.Text.Json.Nodes.JsonNode
}
namespace System.Text.Json.Serialization
{
public partial interface IJsonOnDeserialized
{
void OnDeserialized();
}
public partial interface IJsonOnDeserializing
{
void OnDeserializing();
}
public partial interface IJsonOnSerialized
{
void OnSerialized();
}
public partial interface IJsonOnSerializing
{
void OnSerializing();
}
public abstract partial class JsonAttribute : System.Attribute
{
protected JsonAttribute() { }
Expand Down
4 changes: 4 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyNameAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
<Compile Include="System\Text\Json\Serialization\IgnoreReferenceResolver.cs" />
<Compile Include="System\Text\Json\Serialization\IJsonOnDeserialized.cs" />
<Compile Include="System\Text\Json\Serialization\IJsonOnDeserializing.cs" />
<Compile Include="System\Text\Json\Serialization\IJsonOnSerialized.cs" />
<Compile Include="System\Text\Json\Serialization\IJsonOnSerializing.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerContext.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Collections.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Converters.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,

obj = jsonTypeInfo.CreateObject!()!;

if (obj is IJsonOnDeserializing onDeserializing)
{
onDeserializing.OnDeserializing();
}

// Process all properties.
while (true)
{
Expand Down Expand Up @@ -108,6 +113,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,

obj = jsonTypeInfo.CreateObject!()!;

if (obj is IJsonOnDeserializing onDeserializing)
{
onDeserializing.OnDeserializing();
}

state.Current.ReturnValue = obj;
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
}
Expand Down Expand Up @@ -216,14 +226,21 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
}
}

if (obj is IJsonOnDeserialized onDeserialized)
{
onDeserialized.OnDeserialized();
}

// Unbox
Debug.Assert(obj != null);
value = (T)obj;

// Check if we are trying to build the sorted cache.
if (state.Current.PropertyRefCache != null)
{
jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
}

value = (T)obj;

return true;
}

Expand All @@ -235,20 +252,24 @@ internal sealed override bool OnTryWrite(
{
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;

// Minimize boxing for structs by only boxing once here
object objectValue = value!;
object obj = value; // box once

if (!state.SupportContinuation)
{
writer.WriteStartObject();
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
{
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref)
{
return true;
}
}

if (obj is IJsonOnSerializing onSerializing)
{
onSerializing.OnSerializing();
}

List<KeyValuePair<string, JsonPropertyInfo?>> properties = state.Current.JsonTypeInfo.PropertyCache!.List;
for (int i = 0; i < properties.Count; i++)
{
Expand All @@ -259,7 +280,7 @@ internal sealed override bool OnTryWrite(
state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo;
state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;

bool success = jsonPropertyInfo.GetMemberAndWriteJson(objectValue, ref state, writer);
bool success = jsonPropertyInfo.GetMemberAndWriteJson(obj, ref state, writer);
// Converters only return 'false' when out of data which is not possible in fast path.
Debug.Assert(success);

Expand All @@ -275,14 +296,13 @@ internal sealed override bool OnTryWrite(
state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty;
state.Current.NumberHandling = dataExtensionProperty.NumberHandling;

bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(objectValue, ref state, writer);
bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer);
Debug.Assert(success);

state.Current.EndProperty();
}

writer.WriteEndObject();
return true;
}
else
{
Expand All @@ -291,12 +311,17 @@ internal sealed override bool OnTryWrite(
writer.WriteStartObject();
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
{
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref)
{
return true;
}
}

if (obj is IJsonOnSerializing onSerializing)
{
onSerializing.OnSerializing();
}

state.Current.ProcessedStartToken = true;
}

Expand All @@ -310,7 +335,7 @@ internal sealed override bool OnTryWrite(
state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo;
state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;

if (!jsonPropertyInfo.GetMemberAndWriteJson(objectValue!, ref state, writer))
if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer))
{
Debug.Assert(jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value ||
jsonPropertyInfo.ConverterBase.TypeToConvert == JsonTypeInfo.ObjectType);
Expand Down Expand Up @@ -342,7 +367,7 @@ internal sealed override bool OnTryWrite(
state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty;
state.Current.NumberHandling = dataExtensionProperty.NumberHandling;

if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(objectValue, ref state, writer))
if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer))
{
return false;
}
Expand All @@ -366,9 +391,16 @@ internal sealed override bool OnTryWrite(
state.Current.ProcessedEndToken = true;
writer.WriteEndObject();
}
}

return true;
if (obj is IJsonOnSerialized onSerialized)
{
onSerialized.OnSerialized();
}

value = (T)obj; // unbox

return true;
}

// AggressiveInlining since this method is only called from two locations and is on a hot path.
Expand Down Expand Up @@ -437,6 +469,11 @@ internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonRea

object obj = state.Current.JsonTypeInfo.CreateObject!()!;
state.Current.ReturnValue = obj;

if (obj is IJsonOnDeserializing onDeserializing)
{
onDeserializing.OnDeserializing();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo

ReadConstructorArguments(ref state, ref reader, options);

obj = CreateObject(ref state.Current);
obj = (T)CreateObject(ref state.Current);

if (obj is IJsonOnDeserializing onDeserializing)
{
onDeserializing.OnDeserializing();
}

if (argumentState.FoundPropertyCount > 0)
{
Expand Down Expand Up @@ -91,7 +96,12 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
return false;
}

obj = CreateObject(ref state.Current);
obj = (T)CreateObject(ref state.Current);

if (obj is IJsonOnDeserializing onDeserializing)
{
onDeserializing.OnDeserializing();
}

if (argumentState.FoundPropertyCount > 0)
{
Expand Down Expand Up @@ -128,6 +138,17 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
}
}

if (obj is IJsonOnDeserialized onDeserialized)
{
onDeserialized.OnDeserialized();
}

EndRead(ref state);

// Unbox
Debug.Assert(obj != null);
value = (T)obj;

// Check if we are trying to build the sorted cache.
if (state.Current.PropertyRefCache != null)
{
Expand All @@ -140,10 +161,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
state.Current.JsonTypeInfo.UpdateSortedParameterCache(ref state.Current);
}

EndRead(ref state);

value = (T)obj;

return true;
}

Expand Down
Loading