Skip to content

Commit 4f73321

Browse files
committed
Serializers are using Argument<T> for structs disallowing default values to recognize missing values.
1 parent 860c547 commit 4f73321

File tree

16 files changed

+641
-39
lines changed

16 files changed

+641
-39
lines changed

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ private static void AnalyzeFieldDisallowingDefaultValues(SyntaxNodeAnalysisConte
101101
if (context.Node is not FieldDeclarationSyntax fieldDeclarationSyntax
102102
|| (fieldDeclarationSyntax.Declaration.Variables.Count == 1 && fieldDeclarationSyntax.Declaration.Variables[0].Initializer is not null) // public MyStruct Member = ...;
103103
|| context.ContainingSymbol is not IFieldSymbol fieldSymbol
104+
|| fieldSymbol.IsReadOnly
104105
|| fieldSymbol.IsStatic
105106
|| fieldSymbol.DeclaredAccessibility < fieldSymbol.ContainingType.DeclaredAccessibility) // required members must not be less visible than the containing type
106107
return;

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,16 @@ public JsonConverter(global::System.Text.Json.JsonSerializerOptions options)
102102
{
103103
var memberInfo = _assignableInstanceFieldsAndProperties[i];
104104

105-
_sb.Append(@"
105+
if (!memberInfo.IsReferenceTypeOrNullableStruct && memberInfo.DisallowsDefaultValue)
106+
{
107+
_sb.Append(@"
108+
global::Thinktecture.Argument<").AppendTypeFullyQualified(memberInfo).Append("> ").AppendEscaped(memberInfo.ArgumentName).Append(" = default;");
109+
}
110+
else
111+
{
112+
_sb.Append(@"
106113
").AppendTypeFullyQualifiedNullAnnotated(memberInfo).Append(" ").AppendEscaped(memberInfo.ArgumentName).Append(" = default;");
114+
}
107115
}
108116

109117
_sb.Append(@"
@@ -163,11 +171,11 @@ public JsonConverter(global::System.Text.Json.JsonSerializerOptions options)
163171
{
164172
var memberInfo = _assignableInstanceFieldsAndProperties[i];
165173

166-
if (memberInfo.DisallowsDefaultValue)
174+
if (!memberInfo.IsReferenceTypeOrNullableStruct && memberInfo.DisallowsDefaultValue)
167175
{
168176
_sb.Append(@"
169177
170-
if (").AppendEscaped(memberInfo.ArgumentName).Append(" == default(").AppendTypeFullyQualified(memberInfo).Append(@"))
178+
if (!").AppendEscaped(memberInfo.ArgumentName).Append(@".IsSet)
171179
throw new global::System.Text.Json.JsonException($""Cannot deserialize type \""").AppendTypeMinimallyQualified(_type).Append("\\\" because the member \\\"").Append(memberInfo.Name).Append("\\\" of type \\\"").AppendTypeFullyQualified(memberInfo).Append("\\\" is missing and does not allow default values.\");");
172180
}
173181
}
@@ -183,7 +191,7 @@ public JsonConverter(global::System.Text.Json.JsonSerializerOptions options)
183191
var memberInfo = _assignableInstanceFieldsAndProperties[i];
184192

185193
_sb.Append(@"
186-
").AppendEscaped(memberInfo.ArgumentName).Append("!,");
194+
").AppendEscaped(memberInfo.ArgumentName).Append(memberInfo is { IsReferenceTypeOrNullableStruct: false, DisallowsDefaultValue: true } ? ".Value," : "!,");
187195
}
188196

189197
_sb.Append(@"

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,16 @@ public sealed class ValueObjectMessagePackFormatter : global::MessagePack.Format
9494
{
9595
var memberInfo = _assignableInstanceFieldsAndProperties[i];
9696

97-
_sb.Append(@"
98-
var ").AppendEscaped(memberInfo.ArgumentName).Append(" = default(").AppendTypeFullyQualified(memberInfo).Append(")!;");
97+
if (!memberInfo.IsReferenceTypeOrNullableStruct && memberInfo.DisallowsDefaultValue)
98+
{
99+
_sb.Append(@"
100+
global::Thinktecture.Argument<").AppendTypeFullyQualified(memberInfo).Append("> ").AppendEscaped(memberInfo.ArgumentName).Append(" = default;");
101+
}
102+
else
103+
{
104+
_sb.Append(@"
105+
").AppendTypeFullyQualifiedNullAnnotated(memberInfo).Append(" ").AppendEscaped(memberInfo.ArgumentName).Append(" = default;");
106+
}
99107
}
100108

101109
_sb.Append(@"
@@ -141,11 +149,11 @@ public sealed class ValueObjectMessagePackFormatter : global::MessagePack.Format
141149
{
142150
var memberInfo = _assignableInstanceFieldsAndProperties[i];
143151

144-
if (memberInfo.DisallowsDefaultValue)
152+
if (!memberInfo.IsReferenceTypeOrNullableStruct && memberInfo.DisallowsDefaultValue)
145153
{
146154
_sb.Append(@"
147155
148-
if (").AppendEscaped(memberInfo.ArgumentName).Append(" == default(").AppendTypeFullyQualified(memberInfo).Append(@"))
156+
if (!").AppendEscaped(memberInfo.ArgumentName).Append(@".IsSet)
149157
throw new global::MessagePack.MessagePackSerializationException($""Cannot deserialize type \""").AppendTypeMinimallyQualified(_type).Append("\\\" because the member \\\"").Append(memberInfo.Name).Append("\\\" of type \\\"").AppendTypeFullyQualified(memberInfo).Append(@"\"" is missing and does not allow default values."");");
150158
}
151159
}
@@ -161,7 +169,7 @@ public sealed class ValueObjectMessagePackFormatter : global::MessagePack.Format
161169
var memberInfo = _assignableInstanceFieldsAndProperties[i];
162170

163171
_sb.Append(@"
164-
").AppendEscaped(memberInfo.ArgumentName).Append(",");
172+
").AppendEscaped(memberInfo.ArgumentName).Append(memberInfo is { IsReferenceTypeOrNullableStruct: false, DisallowsDefaultValue: true } ? ".Value," : "!,");
165173
}
166174

167175
_sb.Append(@"

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,16 @@ public override bool CanConvert(global::System.Type objectType)
103103
{
104104
var memberInfo = _assignableInstanceFieldsAndProperties[i];
105105

106-
_sb.Append(@"
106+
if (!memberInfo.IsReferenceTypeOrNullableStruct && memberInfo.DisallowsDefaultValue)
107+
{
108+
_sb.Append(@"
109+
global::Thinktecture.Argument<").AppendTypeFullyQualified(memberInfo).Append("> ").AppendEscaped(memberInfo.ArgumentName).Append(" = default;");
110+
}
111+
else
112+
{
113+
_sb.Append(@"
107114
").AppendTypeFullyQualifiedNullAnnotated(memberInfo).Append(" ").AppendEscaped(memberInfo.ArgumentName).Append(" = default;");
115+
}
108116
}
109117

110118
_sb.Append(@"
@@ -189,11 +197,11 @@ public override bool CanConvert(global::System.Type objectType)
189197
{
190198
var memberInfo = _assignableInstanceFieldsAndProperties[i];
191199

192-
if (memberInfo.DisallowsDefaultValue)
200+
if (!memberInfo.IsReferenceTypeOrNullableStruct && memberInfo.DisallowsDefaultValue)
193201
{
194202
_sb.Append(@"
195203
196-
if (").AppendEscaped(memberInfo.ArgumentName).Append(" == default(").AppendTypeFullyQualified(memberInfo).Append(@"))
204+
if (!").AppendEscaped(memberInfo.ArgumentName).Append(@".IsSet)
197205
{
198206
var (lineNumber, linePosition) = GetLineInfo(reader);
199207
@@ -218,7 +226,7 @@ public override bool CanConvert(global::System.Type objectType)
218226
var memberInfo = _assignableInstanceFieldsAndProperties[i];
219227

220228
_sb.Append(@"
221-
").AppendEscaped(memberInfo.ArgumentName).Append("!,");
229+
").AppendEscaped(memberInfo.ArgumentName).Append(memberInfo is { IsReferenceTypeOrNullableStruct: false, DisallowsDefaultValue: true } ? ".Value," : "!,");
222230
}
223231

224232
_sb.Append(@"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// <auto-generated />
2+
#nullable enable
3+
4+
namespace Thinktecture.Tests;
5+
6+
[global::System.Text.Json.Serialization.JsonConverterAttribute(typeof(JsonConverterFactory))]
7+
partial class TestValueObject
8+
{
9+
public sealed class JsonConverter : global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.TestValueObject>
10+
{
11+
private readonly global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.ClassDisallowingDefaultValues> _nonNullableReferenceTypeConverter;
12+
private readonly string _nonNullableReferenceTypePropertyName;
13+
private readonly global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.ClassDisallowingDefaultValues?> _nullableReferenceTypeConverter;
14+
private readonly string _nullableReferenceTypePropertyName;
15+
private readonly global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.StructDisallowingDefaultValues> _nonNullableStructConverter;
16+
private readonly string _nonNullableStructPropertyName;
17+
private readonly global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.StructDisallowingDefaultValues?> _nullableStructConverter;
18+
private readonly string _nullableStructPropertyName;
19+
20+
public JsonConverter(global::System.Text.Json.JsonSerializerOptions options)
21+
{
22+
if(options is null)
23+
throw new global::System.ArgumentNullException(nameof(options));
24+
25+
var namingPolicy = options.PropertyNamingPolicy;
26+
27+
this._nonNullableReferenceTypeConverter = (global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.ClassDisallowingDefaultValues>)global::Thinktecture.Internal.JsonSerializerOptionsExtensions.GetCustomMemberConverter(options, typeof(global::Thinktecture.Tests.ClassDisallowingDefaultValues));
28+
this._nonNullableReferenceTypePropertyName = namingPolicy?.ConvertName("_nonNullableReferenceType") ?? "_nonNullableReferenceType";
29+
this._nullableReferenceTypeConverter = (global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.ClassDisallowingDefaultValues?>)global::Thinktecture.Internal.JsonSerializerOptionsExtensions.GetCustomMemberConverter(options, typeof(global::Thinktecture.Tests.ClassDisallowingDefaultValues));
30+
this._nullableReferenceTypePropertyName = namingPolicy?.ConvertName("_nullableReferenceType") ?? "_nullableReferenceType";
31+
this._nonNullableStructConverter = (global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.StructDisallowingDefaultValues>)global::Thinktecture.Internal.JsonSerializerOptionsExtensions.GetCustomMemberConverter(options, typeof(global::Thinktecture.Tests.StructDisallowingDefaultValues));
32+
this._nonNullableStructPropertyName = namingPolicy?.ConvertName("_nonNullableStruct") ?? "_nonNullableStruct";
33+
this._nullableStructConverter = (global::System.Text.Json.Serialization.JsonConverter<global::Thinktecture.Tests.StructDisallowingDefaultValues?>)global::Thinktecture.Internal.JsonSerializerOptionsExtensions.GetCustomMemberConverter(options, typeof(global::Thinktecture.Tests.StructDisallowingDefaultValues?));
34+
this._nullableStructPropertyName = namingPolicy?.ConvertName("_nullableStruct") ?? "_nullableStruct";
35+
}
36+
37+
/// <inheritdoc />
38+
public override global::Thinktecture.Tests.TestValueObject? Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options)
39+
{
40+
if (reader.TokenType == global::System.Text.Json.JsonTokenType.Null)
41+
return default;
42+
43+
if (reader.TokenType != global::System.Text.Json.JsonTokenType.StartObject)
44+
throw new global::System.Text.Json.JsonException($"Unexpected token \"{reader.TokenType}\" when trying to deserialize \"TestValueObject\". Expected token: \"{(global::System.Text.Json.JsonTokenType.StartObject)}\".");
45+
46+
global::Thinktecture.Tests.ClassDisallowingDefaultValues? @nonNullableReferenceType = default;
47+
global::Thinktecture.Tests.ClassDisallowingDefaultValues? @nullableReferenceType = default;
48+
global::Thinktecture.Argument<global::Thinktecture.Tests.StructDisallowingDefaultValues> @nonNullableStruct = default;
49+
global::Thinktecture.Tests.StructDisallowingDefaultValues? @nullableStruct = default;
50+
51+
var comparer = options.PropertyNameCaseInsensitive ? global::System.StringComparer.OrdinalIgnoreCase : global::System.StringComparer.Ordinal;
52+
53+
while (reader.Read())
54+
{
55+
if (reader.TokenType == global::System.Text.Json.JsonTokenType.EndObject)
56+
break;
57+
58+
if (reader.TokenType != global::System.Text.Json.JsonTokenType.PropertyName)
59+
throw new global::System.Text.Json.JsonException($"Unexpected token \"{reader.TokenType}\" when trying to deserialize \"TestValueObject\". Expected token: \"{(global::System.Text.Json.JsonTokenType.PropertyName)}\".");
60+
61+
var propName = reader.GetString();
62+
63+
if(!reader.Read())
64+
throw new global::System.Text.Json.JsonException($"Unexpected end of the JSON message when trying the read the value of \"{propName}\" during deserialization of \"TestValueObject\".");
65+
66+
if (comparer.Equals(propName, this._nonNullableReferenceTypePropertyName))
67+
{
68+
@nonNullableReferenceType = this._nonNullableReferenceTypeConverter.Read(ref reader, typeof(global::Thinktecture.Tests.ClassDisallowingDefaultValues), options);
69+
}
70+
else if (comparer.Equals(propName, this._nullableReferenceTypePropertyName))
71+
{
72+
@nullableReferenceType = this._nullableReferenceTypeConverter.Read(ref reader, typeof(global::Thinktecture.Tests.ClassDisallowingDefaultValues), options);
73+
}
74+
else if (comparer.Equals(propName, this._nonNullableStructPropertyName))
75+
{
76+
@nonNullableStruct = this._nonNullableStructConverter.Read(ref reader, typeof(global::Thinktecture.Tests.StructDisallowingDefaultValues), options);
77+
}
78+
else if (comparer.Equals(propName, this._nullableStructPropertyName))
79+
{
80+
@nullableStruct = this._nullableStructConverter.Read(ref reader, typeof(global::Thinktecture.Tests.StructDisallowingDefaultValues?), options);
81+
}
82+
else
83+
{
84+
throw new global::System.Text.Json.JsonException($"Unknown member \"{propName}\" encountered when trying to deserialize \"TestValueObject\".");
85+
}
86+
}
87+
88+
if (!@nonNullableStruct.IsSet)
89+
throw new global::System.Text.Json.JsonException($"Cannot deserialize type \"TestValueObject\" because the member \"_nonNullableStruct\" of type \"global::Thinktecture.Tests.StructDisallowingDefaultValues\" is missing and does not allow default values.");
90+
91+
var validationError = global::Thinktecture.Tests.TestValueObject.Validate(
92+
@nonNullableReferenceType!,
93+
@nullableReferenceType!,
94+
@nonNullableStruct.Value,
95+
@nullableStruct!,
96+
out var obj);
97+
98+
if (validationError is not null)
99+
throw new global::System.Text.Json.JsonException(validationError.ToString() ?? "Unable to deserialize \"TestValueObject\".");
100+
101+
return obj;
102+
}
103+
104+
/// <inheritdoc />
105+
public override void Write(global::System.Text.Json.Utf8JsonWriter writer, global::Thinktecture.Tests.TestValueObject value, global::System.Text.Json.JsonSerializerOptions options)
106+
{
107+
writer.WriteStartObject();
108+
109+
var ignoreNullValues = options.DefaultIgnoreCondition is global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull or global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault;
110+
var ignoreDefaultValues = options.DefaultIgnoreCondition == global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault;
111+
112+
var @nonNullableReferenceTypePropertyValue = value._nonNullableReferenceType;
113+
114+
if(!ignoreNullValues || @nonNullableReferenceTypePropertyValue is not null)
115+
{
116+
writer.WritePropertyName(this._nonNullableReferenceTypePropertyName);
117+
this._nonNullableReferenceTypeConverter.Write(writer, @nonNullableReferenceTypePropertyValue, options);
118+
}
119+
var @nullableReferenceTypePropertyValue = value._nullableReferenceType;
120+
121+
if(!ignoreNullValues || @nullableReferenceTypePropertyValue is not null)
122+
{
123+
writer.WritePropertyName(this._nullableReferenceTypePropertyName);
124+
this._nullableReferenceTypeConverter.Write(writer, @nullableReferenceTypePropertyValue, options);
125+
}
126+
var @nonNullableStructPropertyValue = value._nonNullableStruct;
127+
128+
if(!ignoreDefaultValues || !@nonNullableStructPropertyValue.Equals(default(global::Thinktecture.Tests.StructDisallowingDefaultValues)))
129+
{
130+
writer.WritePropertyName(this._nonNullableStructPropertyName);
131+
this._nonNullableStructConverter.Write(writer, @nonNullableStructPropertyValue, options);
132+
}
133+
var @nullableStructPropertyValue = value._nullableStruct;
134+
135+
if(!ignoreNullValues || @nullableStructPropertyValue is not null)
136+
{
137+
writer.WritePropertyName(this._nullableStructPropertyName);
138+
this._nullableStructConverter.Write(writer, @nullableStructPropertyValue, options);
139+
}
140+
writer.WriteEndObject();
141+
}
142+
}
143+
144+
public class JsonConverterFactory : global::System.Text.Json.Serialization.JsonConverterFactory
145+
{
146+
/// <inheritdoc />
147+
public override bool CanConvert(global::System.Type typeToConvert)
148+
{
149+
return typeof(global::Thinktecture.Tests.TestValueObject).IsAssignableFrom(typeToConvert);
150+
}
151+
152+
/// <inheritdoc />
153+
public override global::System.Text.Json.Serialization.JsonConverter CreateConverter(global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options)
154+
{
155+
if (typeToConvert is null)
156+
throw new global::System.ArgumentNullException(nameof(typeToConvert));
157+
if (options is null)
158+
throw new global::System.ArgumentNullException(nameof(options));
159+
160+
return new JsonConverter(options);
161+
}
162+
}
163+
}

test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/JsonValueObjectSourceGeneratorTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,4 +363,36 @@ public partial class ComplexValueObjectWithNonNullProperty
363363

364364
await VerifyAsync(output);
365365
}
366+
367+
[Fact]
368+
public async Task Should_generate_JsonConverter_for_complex_with_properties_disallowing_default_values()
369+
{
370+
var source = """
371+
372+
using System;
373+
using Thinktecture;
374+
375+
#nullable disable
376+
377+
namespace Thinktecture.Tests;
378+
379+
public class ClassDisallowingDefaultValues : IDisallowDefaultValue;
380+
public struct StructDisallowingDefaultValues : IDisallowDefaultValue;
381+
382+
[ComplexValueObject]
383+
public partial class TestValueObject
384+
{
385+
public readonly ClassDisallowingDefaultValues _nonNullableReferenceType;
386+
public readonly ClassDisallowingDefaultValues? _nullableReferenceType;
387+
public readonly StructDisallowingDefaultValues _nonNullableStruct;
388+
public readonly StructDisallowingDefaultValues? _nullableStruct;
389+
}
390+
391+
""";
392+
var output = GetGeneratedOutput<ValueObjectSourceGenerator>(source,
393+
".Json",
394+
typeof(ComplexValueObjectAttribute).Assembly, typeof(Thinktecture.Text.Json.Serialization.ThinktectureJsonConverter<,,>).Assembly, typeof(System.Text.Json.JsonDocument).Assembly);
395+
396+
await VerifyAsync(output);
397+
}
366398
}

test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/MessagePackValueObjectSourceGeneratorTests.Should_generate_Formatter_for_complex_when_enabled_via_SerializationFrameworks.verified.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ partial class ComplexValueObjectWithNonNullProperty
1919
var count = reader.ReadArrayHeader();
2020
global::MessagePack.IFormatterResolver resolver = options.Resolver;
2121

22-
var @property = default(int)!;
22+
int @property = default;
2323

2424
try
2525
{
@@ -39,7 +39,7 @@ partial class ComplexValueObjectWithNonNullProperty
3939
}
4040

4141
var validationError = global::Thinktecture.Tests.ComplexValueObjectWithNonNullProperty.Validate(
42-
@property,
42+
@property!,
4343
out var obj);
4444

4545
if (validationError is not null)

0 commit comments

Comments
 (0)