Skip to content

Commit b814a97

Browse files
authored
Add OnSerialize callbacks to POCOs (#54709)
1 parent 03eb03e commit b814a97

21 files changed

+698
-25
lines changed

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,13 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata)
500500

501501
if (typeMetadata.GenerateSerializationLogic)
502502
{
503-
serializeFuncSource = GenerateFastPathFuncForObject(typeCompilableName, serializeMethodName, typeMetadata.CanBeNull, properties);
503+
serializeFuncSource = GenerateFastPathFuncForObject(
504+
typeCompilableName,
505+
serializeMethodName,
506+
typeMetadata.CanBeNull,
507+
typeMetadata.ImplementsIJsonOnSerialized,
508+
typeMetadata.ImplementsIJsonOnSerializing,
509+
properties);
504510
serializeFuncNamedArg = $@"serializeFunc: {serializeMethodName}";
505511
}
506512
else
@@ -635,6 +641,8 @@ private string GenerateFastPathFuncForObject(
635641
string typeInfoTypeRef,
636642
string serializeMethodName,
637643
bool canBeNull,
644+
bool implementsIJsonOnSerialized,
645+
bool implementsIJsonOnSerializing,
638646
List<PropertyGenerationSpec>? properties)
639647
{
640648
JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions;
@@ -646,6 +654,12 @@ private string GenerateFastPathFuncForObject(
646654
StringBuilder sb = new();
647655

648656
// Begin method definition
657+
if (implementsIJsonOnSerializing)
658+
{
659+
sb.Append($@"(({IJsonOnSerializingFullName}){ValueVarName}).OnSerializing();");
660+
sb.Append($@"{Environment.NewLine} ");
661+
}
662+
649663
sb.Append($@"{WriterVarName}.WriteStartObject();");
650664

651665
if (properties != null)
@@ -733,6 +747,12 @@ private string GenerateFastPathFuncForObject(
733747
734748
{WriterVarName}.WriteEndObject();");
735749

750+
if (implementsIJsonOnSerialized)
751+
{
752+
sb.Append($@"{Environment.NewLine} ");
753+
sb.Append($@"(({IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();");
754+
};
755+
736756
return GenerateFastPathFuncForType(serializeMethodName, typeInfoTypeRef, sb.ToString(), canBeNull);
737757
}
738758

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
494494
bool foundDesignTimeCustomConverter = false;
495495
string? converterInstatiationLogic = null;
496496

497+
bool implementsIJsonOnSerialized = false;
498+
bool implementsIJsonOnSerializing = false;
499+
497500
IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(type);
498501
foreach (CustomAttributeData attributeData in attributeDataList)
499502
{
@@ -577,6 +580,11 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
577580
constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
578581
}
579582

583+
// GetInterface() is currently not implemented, so we use GetInterfaces().
584+
IEnumerable<string> interfaces = type.GetInterfaces().Select(interfaceType => interfaceType.FullName);
585+
implementsIJsonOnSerialized = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializedFullName) != null;
586+
implementsIJsonOnSerializing = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializingFullName) != null;
587+
580588
for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
581589
{
582590
const BindingFlags bindingFlags =
@@ -627,7 +635,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
627635
collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeGenerationSpec(collectionValueType, generationMode) : null,
628636
constructionStrategy,
629637
nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeGenerationSpec(nullableUnderlyingType, generationMode) : null,
630-
converterInstatiationLogic);
638+
converterInstatiationLogic,
639+
implementsIJsonOnSerialized,
640+
implementsIJsonOnSerializing);
631641

632642
return typeMetadata;
633643
}

src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ namespace System.Text.Json.SourceGeneration
1818
[Generator]
1919
public sealed partial class JsonSourceGenerator : ISourceGenerator
2020
{
21+
private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration";
22+
private const string IJsonOnSerializedFullName = "System.Text.Json.Serialization.IJsonOnSerialized";
23+
private const string IJsonOnSerializingFullName = "System.Text.Json.Serialization.IJsonOnSerializing";
24+
2125
/// <summary>
2226
/// Registers a syntax resolver to receive compilation units.
2327
/// </summary>
@@ -51,8 +55,6 @@ public void Execute(GeneratorExecutionContext executionContext)
5155
}
5256
}
5357

54-
private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration";
55-
5658
/// <summary>
5759
/// Helper for unit tests.
5860
/// </summary>

src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>netstandard2.0</TargetFrameworks>
44
<CLSCompliant>false</CLSCompliant>

src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ internal class TypeGenerationSpec
3333

3434
public ClassType ClassType { get; private set; }
3535

36+
public bool ImplementsIJsonOnSerialized { get; private set; }
37+
public bool ImplementsIJsonOnSerializing { get; private set; }
38+
3639
public bool IsValueType { get; private set; }
3740

3841
public bool CanBeNull { get; private set; }
@@ -67,7 +70,9 @@ public void Initialize(
6770
TypeGenerationSpec? collectionValueTypeMetadata,
6871
ObjectConstructionStrategy constructionStrategy,
6972
TypeGenerationSpec? nullableUnderlyingTypeMetadata,
70-
string? converterInstantiationLogic)
73+
string? converterInstantiationLogic,
74+
bool implementsIJsonOnSerialized,
75+
bool implementsIJsonOnSerializing)
7176
{
7277
GenerationMode = generationMode;
7378
TypeRef = $"global::{typeRef}";
@@ -84,6 +89,8 @@ public void Initialize(
8489
ConstructionStrategy = constructionStrategy;
8590
NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata;
8691
ConverterInstantiationLogic = converterInstantiationLogic;
92+
ImplementsIJsonOnSerialized = implementsIJsonOnSerialized;
93+
ImplementsIJsonOnSerializing = implementsIJsonOnSerializing;
8794
}
8895

8996
private bool FastPathIsSupported()

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,22 @@ public abstract partial class JsonValue : System.Text.Json.Nodes.JsonNode
733733
}
734734
namespace System.Text.Json.Serialization
735735
{
736+
public partial interface IJsonOnDeserialized
737+
{
738+
void OnDeserialized();
739+
}
740+
public partial interface IJsonOnDeserializing
741+
{
742+
void OnDeserializing();
743+
}
744+
public partial interface IJsonOnSerialized
745+
{
746+
void OnSerialized();
747+
}
748+
public partial interface IJsonOnSerializing
749+
{
750+
void OnSerializing();
751+
}
736752
public abstract partial class JsonAttribute : System.Attribute
737753
{
738754
protected JsonAttribute() { }

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@
9292
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyNameAttribute.cs" />
9393
<Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
9494
<Compile Include="System\Text\Json\Serialization\IgnoreReferenceResolver.cs" />
95+
<Compile Include="System\Text\Json\Serialization\IJsonOnDeserialized.cs" />
96+
<Compile Include="System\Text\Json\Serialization\IJsonOnDeserializing.cs" />
97+
<Compile Include="System\Text\Json\Serialization\IJsonOnSerialized.cs" />
98+
<Compile Include="System\Text\Json\Serialization\IJsonOnSerializing.cs" />
9599
<Compile Include="System\Text\Json\Serialization\JsonSerializerContext.cs" />
96100
<Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Collections.cs" />
97101
<Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Converters.cs" />

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
3636

3737
obj = jsonTypeInfo.CreateObject!()!;
3838

39+
if (obj is IJsonOnDeserializing onDeserializing)
40+
{
41+
onDeserializing.OnDeserializing();
42+
}
43+
3944
// Process all properties.
4045
while (true)
4146
{
@@ -108,6 +113,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
108113

109114
obj = jsonTypeInfo.CreateObject!()!;
110115

116+
if (obj is IJsonOnDeserializing onDeserializing)
117+
{
118+
onDeserializing.OnDeserializing();
119+
}
120+
111121
state.Current.ReturnValue = obj;
112122
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
113123
}
@@ -216,14 +226,21 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
216226
}
217227
}
218228

229+
if (obj is IJsonOnDeserialized onDeserialized)
230+
{
231+
onDeserialized.OnDeserialized();
232+
}
233+
234+
// Unbox
235+
Debug.Assert(obj != null);
236+
value = (T)obj;
237+
219238
// Check if we are trying to build the sorted cache.
220239
if (state.Current.PropertyRefCache != null)
221240
{
222241
jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
223242
}
224243

225-
value = (T)obj;
226-
227244
return true;
228245
}
229246

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

238-
// Minimize boxing for structs by only boxing once here
239-
object objectValue = value!;
255+
object obj = value; // box once
240256

241257
if (!state.SupportContinuation)
242258
{
243259
writer.WriteStartObject();
244260
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
245261
{
246-
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
262+
if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref)
247263
{
248264
return true;
249265
}
250266
}
251267

268+
if (obj is IJsonOnSerializing onSerializing)
269+
{
270+
onSerializing.OnSerializing();
271+
}
272+
252273
List<KeyValuePair<string, JsonPropertyInfo?>> properties = state.Current.JsonTypeInfo.PropertyCache!.List;
253274
for (int i = 0; i < properties.Count; i++)
254275
{
@@ -259,7 +280,7 @@ internal sealed override bool OnTryWrite(
259280
state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo;
260281
state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;
261282

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

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

278-
bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(objectValue, ref state, writer);
299+
bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer);
279300
Debug.Assert(success);
280301

281302
state.Current.EndProperty();
282303
}
283304

284305
writer.WriteEndObject();
285-
return true;
286306
}
287307
else
288308
{
@@ -291,12 +311,17 @@ internal sealed override bool OnTryWrite(
291311
writer.WriteStartObject();
292312
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
293313
{
294-
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
314+
if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref)
295315
{
296316
return true;
297317
}
298318
}
299319

320+
if (obj is IJsonOnSerializing onSerializing)
321+
{
322+
onSerializing.OnSerializing();
323+
}
324+
300325
state.Current.ProcessedStartToken = true;
301326
}
302327

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

313-
if (!jsonPropertyInfo.GetMemberAndWriteJson(objectValue!, ref state, writer))
338+
if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer))
314339
{
315340
Debug.Assert(jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value ||
316341
jsonPropertyInfo.ConverterBase.TypeToConvert == JsonTypeInfo.ObjectType);
@@ -342,7 +367,7 @@ internal sealed override bool OnTryWrite(
342367
state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty;
343368
state.Current.NumberHandling = dataExtensionProperty.NumberHandling;
344369

345-
if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(objectValue, ref state, writer))
370+
if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer))
346371
{
347372
return false;
348373
}
@@ -366,9 +391,16 @@ internal sealed override bool OnTryWrite(
366391
state.Current.ProcessedEndToken = true;
367392
writer.WriteEndObject();
368393
}
394+
}
369395

370-
return true;
396+
if (obj is IJsonOnSerialized onSerialized)
397+
{
398+
onSerialized.OnSerialized();
371399
}
400+
401+
value = (T)obj; // unbox
402+
403+
return true;
372404
}
373405

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

438470
object obj = state.Current.JsonTypeInfo.CreateObject!()!;
439471
state.Current.ReturnValue = obj;
472+
473+
if (obj is IJsonOnDeserializing onDeserializing)
474+
{
475+
onDeserializing.OnDeserializing();
476+
}
440477
}
441478
}
442479
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
3232

3333
ReadConstructorArguments(ref state, ref reader, options);
3434

35-
obj = CreateObject(ref state.Current);
35+
obj = (T)CreateObject(ref state.Current);
36+
37+
if (obj is IJsonOnDeserializing onDeserializing)
38+
{
39+
onDeserializing.OnDeserializing();
40+
}
3641

3742
if (argumentState.FoundPropertyCount > 0)
3843
{
@@ -91,7 +96,12 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
9196
return false;
9297
}
9398

94-
obj = CreateObject(ref state.Current);
99+
obj = (T)CreateObject(ref state.Current);
100+
101+
if (obj is IJsonOnDeserializing onDeserializing)
102+
{
103+
onDeserializing.OnDeserializing();
104+
}
95105

96106
if (argumentState.FoundPropertyCount > 0)
97107
{
@@ -128,6 +138,17 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
128138
}
129139
}
130140

141+
if (obj is IJsonOnDeserialized onDeserialized)
142+
{
143+
onDeserialized.OnDeserialized();
144+
}
145+
146+
EndRead(ref state);
147+
148+
// Unbox
149+
Debug.Assert(obj != null);
150+
value = (T)obj;
151+
131152
// Check if we are trying to build the sorted cache.
132153
if (state.Current.PropertyRefCache != null)
133154
{
@@ -140,10 +161,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
140161
state.Current.JsonTypeInfo.UpdateSortedParameterCache(ref state.Current);
141162
}
142163

143-
EndRead(ref state);
144-
145-
value = (T)obj;
146-
147164
return true;
148165
}
149166

0 commit comments

Comments
 (0)