Skip to content

Commit 82b9006

Browse files
committed
Added basic protobuf support for complex value objects
1 parent f500f18 commit 82b9006

13 files changed

Lines changed: 971 additions & 2 deletions
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using System.Text;
2+
3+
namespace Thinktecture.CodeAnalysis.ValueObjects;
4+
5+
public sealed class ComplexValueObjectProtoBufCodeGenerator : CodeGeneratorBase
6+
{
7+
private readonly ITypeInformation _type;
8+
private readonly IReadOnlyList<InstanceMemberInfo> _assignableInstanceFieldsAndProperties;
9+
private readonly StringBuilder _sb;
10+
11+
public override string CodeGeneratorName => "Complex-ValueObject-ProtoBuf-CodeGenerator";
12+
public override string FileNameSuffix => ".ProtoBuf";
13+
14+
public ComplexValueObjectProtoBufCodeGenerator(
15+
ITypeInformation type,
16+
IReadOnlyList<InstanceMemberInfo> assignableInstanceFieldsAndProperties,
17+
StringBuilder stringBuilder)
18+
{
19+
_type = type;
20+
_assignableInstanceFieldsAndProperties = assignableInstanceFieldsAndProperties;
21+
_sb = stringBuilder;
22+
}
23+
24+
public override void Generate(CancellationToken cancellationToken)
25+
{
26+
_sb.Append(GENERATED_CODE_PREFIX).Append(@"
27+
");
28+
29+
if (_type.Namespace is not null)
30+
{
31+
_sb.Append(@"
32+
namespace ").Append(_type.Namespace).Append(@";
33+
");
34+
}
35+
36+
_sb.RenderContainingTypesStart(_type.ContainingTypes);
37+
38+
_sb.Append(@"
39+
[global::ProtoBuf.ProtoContract(Serializer = typeof(ValueObjectProtoBufSerializer))]
40+
partial ").Append(_type.IsReferenceType ? "class" : "struct").Append(" ").Append(_type.Name).Append(@"
41+
{
42+
public sealed class ValueObjectProtoBufSerializer : global::ProtoBuf.Serializers.ISerializer<").AppendTypeFullyQualified(_type).Append(@">
43+
{
44+
private const global::ProtoBuf.Serializers.SerializerFeatures WireTypeMask = (global::ProtoBuf.Serializers.SerializerFeatures) 15;
45+
46+
public global::ProtoBuf.Serializers.SerializerFeatures Features => global::ProtoBuf.Serializers.SerializerFeatures.WireTypeStartGroup | global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage;
47+
");
48+
49+
for (var i = 0; i < _assignableInstanceFieldsAndProperties.Count; i++)
50+
{
51+
var memberInfo = _assignableInstanceFieldsAndProperties[i];
52+
53+
_sb.Append(@"
54+
private global::ProtoBuf.Serializers.ISerializer<").AppendTypeFullyQualified(memberInfo).Append(">? _").Append(memberInfo.ArgumentName).Append("Serializer;");
55+
}
56+
57+
_sb.Append(@"
58+
59+
/// <inheritdoc />
60+
public ").AppendTypeFullyQualified(_type).Append(@" Read(ref global::ProtoBuf.ProtoReader.State state, ").AppendTypeFullyQualifiedNullAnnotated(_type).Append(@" value)
61+
{");
62+
63+
InitSerializers();
64+
65+
cancellationToken.ThrowIfCancellationRequested();
66+
67+
for (var i = 0; i < _assignableInstanceFieldsAndProperties.Count; i++)
68+
{
69+
var memberInfo = _assignableInstanceFieldsAndProperties[i];
70+
71+
_sb.Append(@"
72+
").AppendTypeFullyQualifiedNullAnnotated(memberInfo).Append(" ").AppendEscaped(memberInfo.ArgumentName).Append(" = default;");
73+
}
74+
75+
_sb.Append(@"
76+
77+
int fieldNumber;
78+
79+
while ((fieldNumber = state.ReadFieldHeader()) > 0)
80+
{
81+
switch (fieldNumber)
82+
{");
83+
84+
cancellationToken.ThrowIfCancellationRequested();
85+
86+
for (var i = 0; i < _assignableInstanceFieldsAndProperties.Count; i++)
87+
{
88+
var memberInfo = _assignableInstanceFieldsAndProperties[i];
89+
90+
// TODO: Use protomember attribute number
91+
_sb.Append(@"
92+
case ").Append(i + 1).Append(@":
93+
{
94+
if ((_").Append(memberInfo.ArgumentName).Append(@"Serializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
95+
{
96+
").AppendEscaped(memberInfo.ArgumentName).Append(" = state.ReadMessage(_").Append(memberInfo.ArgumentName).Append("Serializer.Features, default!, _").Append(memberInfo.ArgumentName).Append(@"Serializer);
97+
}
98+
else
99+
{
100+
").AppendEscaped(memberInfo.ArgumentName).Append(" = _").Append(memberInfo.ArgumentName).Append(@"Serializer.Read(ref state, default!);
101+
}
102+
break;
103+
}");
104+
}
105+
106+
_sb.Append(@"
107+
default:
108+
{
109+
throw new global::ProtoBuf.ProtoException($""Unexpected field number {fieldNumber} during deserialization of \""").Append(_type.Name).Append(@"\""."");
110+
}
111+
}
112+
}
113+
114+
var validationError = ").AppendTypeFullyQualified(_type).Append(".Validate(");
115+
116+
cancellationToken.ThrowIfCancellationRequested();
117+
118+
for (var i = 0; i < _assignableInstanceFieldsAndProperties.Count; i++)
119+
{
120+
var memberInfo = _assignableInstanceFieldsAndProperties[i];
121+
122+
_sb.Append(@"
123+
").AppendEscaped(memberInfo.ArgumentName).Append("!,");
124+
}
125+
126+
_sb.Append(@"
127+
out var obj);
128+
129+
if (validationError is not null)
130+
throw new global::ProtoBuf.ProtoException(validationError.ToString() ?? ""Unable to deserialize \""").Append(_type.Name).Append(@"\""."");
131+
132+
return obj!;
133+
}
134+
135+
/// <inheritdoc />
136+
public void Write(ref global::ProtoBuf.ProtoWriter.State state, ").AppendTypeFullyQualified(_type).Append(@" value)
137+
{");
138+
139+
InitSerializers();
140+
141+
cancellationToken.ThrowIfCancellationRequested();
142+
143+
for (var i = 0; i < _assignableInstanceFieldsAndProperties.Count; i++)
144+
{
145+
var memberInfo = _assignableInstanceFieldsAndProperties[i];
146+
147+
// TODO: Use protomember attribute number
148+
_sb.Append(@"
149+
state.WriteFieldHeader(").Append(i + 1).Append(", (global::ProtoBuf.WireType) (_").Append(memberInfo.ArgumentName).Append(@"Serializer.Features & WireTypeMask));
150+
151+
if ((_").Append(memberInfo.ArgumentName).Append(@"Serializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
152+
{
153+
state.WriteMessage(_").Append(memberInfo.ArgumentName).Append("Serializer.Features, value.").Append(memberInfo.Name).Append(@");
154+
}
155+
else
156+
{
157+
_").Append(memberInfo.ArgumentName).Append("Serializer.Write(ref state, value.").Append(memberInfo.Name).Append(@");
158+
}
159+
");
160+
}
161+
162+
_sb.Append(@"
163+
}
164+
}
165+
}");
166+
167+
_sb.RenderContainingTypesEnd(_type.ContainingTypes)
168+
.Append(@"
169+
");
170+
}
171+
172+
private void InitSerializers()
173+
{
174+
for (var i = 0; i < _assignableInstanceFieldsAndProperties.Count; i++)
175+
{
176+
var memberInfo = _assignableInstanceFieldsAndProperties[i];
177+
178+
_sb.Append(@"
179+
_").Append(memberInfo.ArgumentName).Append("Serializer ??= state.GetSerializer<").AppendTypeFullyQualified(memberInfo).Append(">();");
180+
}
181+
182+
_sb.Append(@"
183+
");
184+
}
185+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public bool MustGenerateCode(KeyedSerializerGeneratorState state)
2020

2121
public bool MustGenerateCode(ComplexSerializerGeneratorState state)
2222
{
23-
return false;
23+
return !state.AttributeInfo.HasProtoContractAttribute
24+
&& !state.AttributeInfo.DesiredFactories.Any(f => f.UseForSerialization.Has(SerializationFrameworks.ProtoBuf));
2425
}
2526

2627
public CodeGeneratorBase Create(KeyedSerializerGeneratorState state, StringBuilder stringBuilder)
@@ -30,7 +31,7 @@ public CodeGeneratorBase Create(KeyedSerializerGeneratorState state, StringBuild
3031

3132
public CodeGeneratorBase Create(ComplexSerializerGeneratorState state, StringBuilder stringBuilder)
3233
{
33-
throw new NotSupportedException();
34+
return new ComplexValueObjectProtoBufCodeGenerator(state.Type, state.AssignableInstanceFieldsAndProperties, stringBuilder);
3435
}
3536

3637
public bool Equals(IValueObjectSerializerCodeGeneratorFactory? other) => ReferenceEquals(this, other);

test/Thinktecture.Runtime.Extensions.ProtoBuf.Tests/ProtoBuf/Serializers/ValueObjectSerializerTests/RoundTrip.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ public void Should_round_trip_serialize_smart_enums()
2525
DoRoundTrip(TestSmartEnum_Class_StringBased.Value1);
2626
}
2727

28+
[Fact]
29+
public void Should_round_trip_complex_value_objects()
30+
{
31+
DoRoundTrip(ComplexValueObjectWithReservedIdentifiers.Create(1, 2));
32+
DoRoundTrip(ComplexValueObjectWithString.Create("Test"));
33+
DoRoundTrip(ValueObjectWithInitProperties.Create(1, 2, 4, 5));
34+
DoRoundTrip(Boundary.Create(1, 2));
35+
DoRoundTrip(TestValueObject_Complex_Class.Create("Test 1", "Test 2"));;
36+
DoRoundTrip(TestValueObject_Complex_Struct.Create("Test 1", "Test 2"));;
37+
DoRoundTrip(StructValueObjectWithoutMembers.Create());
38+
DoRoundTrip(NestedComplexValueObject.Create(
39+
ChildComplexValueObject.Create(
40+
(IntBasedStructValueObject)1,
41+
(StringBasedReferenceValueObject)"Test 1"),
42+
(IntBasedStructValueObject)2,
43+
(StringBasedReferenceValueObject)"Test 2"));
44+
}
45+
2846
private static void DoRoundTrip<T>(T value)
2947
{
3048
var memoryStream = new MemoryStream();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// <auto-generated />
2+
#nullable enable
3+
4+
namespace Thinktecture.Tests;
5+
6+
[global::ProtoBuf.ProtoContract(Serializer = typeof(ValueObjectProtoBufSerializer))]
7+
partial class TestValueObject
8+
{
9+
public sealed class ValueObjectProtoBufSerializer : global::ProtoBuf.Serializers.ISerializer<global::Thinktecture.Tests.TestValueObject>
10+
{
11+
private const global::ProtoBuf.Serializers.SerializerFeatures WireTypeMask = (global::ProtoBuf.Serializers.SerializerFeatures) 15;
12+
13+
public global::ProtoBuf.Serializers.SerializerFeatures Features => global::ProtoBuf.Serializers.SerializerFeatures.WireTypeStartGroup | global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage;
14+
15+
private global::ProtoBuf.Serializers.ISerializer<string>? _referenceFieldSerializer;
16+
private global::ProtoBuf.Serializers.ISerializer<int>? _structPropertySerializer;
17+
private global::ProtoBuf.Serializers.ISerializer<decimal?>? _nullableStructPropertySerializer;
18+
19+
/// <inheritdoc />
20+
public global::Thinktecture.Tests.TestValueObject Read(ref global::ProtoBuf.ProtoReader.State state, global::Thinktecture.Tests.TestValueObject? value)
21+
{
22+
_referenceFieldSerializer ??= state.GetSerializer<string>();
23+
_structPropertySerializer ??= state.GetSerializer<int>();
24+
_nullableStructPropertySerializer ??= state.GetSerializer<decimal?>();
25+
26+
string? @referenceField = default;
27+
int @structProperty = default;
28+
decimal? @nullableStructProperty = default;
29+
30+
int fieldNumber;
31+
32+
while ((fieldNumber = state.ReadFieldHeader()) > 0)
33+
{
34+
switch (fieldNumber)
35+
{
36+
case 1:
37+
{
38+
if ((_referenceFieldSerializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
39+
{
40+
@referenceField = state.ReadMessage(_referenceFieldSerializer.Features, default!, _referenceFieldSerializer);
41+
}
42+
else
43+
{
44+
@referenceField = _referenceFieldSerializer.Read(ref state, default!);
45+
}
46+
break;
47+
}
48+
case 2:
49+
{
50+
if ((_structPropertySerializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
51+
{
52+
@structProperty = state.ReadMessage(_structPropertySerializer.Features, default!, _structPropertySerializer);
53+
}
54+
else
55+
{
56+
@structProperty = _structPropertySerializer.Read(ref state, default!);
57+
}
58+
break;
59+
}
60+
case 3:
61+
{
62+
if ((_nullableStructPropertySerializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
63+
{
64+
@nullableStructProperty = state.ReadMessage(_nullableStructPropertySerializer.Features, default!, _nullableStructPropertySerializer);
65+
}
66+
else
67+
{
68+
@nullableStructProperty = _nullableStructPropertySerializer.Read(ref state, default!);
69+
}
70+
break;
71+
}
72+
default:
73+
{
74+
throw new global::ProtoBuf.ProtoException($"Unexpected field number {fieldNumber} during deserialization of \"TestValueObject\".");
75+
}
76+
}
77+
}
78+
79+
var validationError = global::Thinktecture.Tests.TestValueObject.Validate(
80+
@referenceField!,
81+
@structProperty!,
82+
@nullableStructProperty!,
83+
out var obj);
84+
85+
if (validationError is not null)
86+
throw new global::ProtoBuf.ProtoException(validationError.ToString() ?? "Unable to deserialize \"TestValueObject\".");
87+
88+
return obj!;
89+
}
90+
91+
/// <inheritdoc />
92+
public void Write(ref global::ProtoBuf.ProtoWriter.State state, global::Thinktecture.Tests.TestValueObject value)
93+
{
94+
_referenceFieldSerializer ??= state.GetSerializer<string>();
95+
_structPropertySerializer ??= state.GetSerializer<int>();
96+
_nullableStructPropertySerializer ??= state.GetSerializer<decimal?>();
97+
98+
state.WriteFieldHeader(1, (global::ProtoBuf.WireType) (_referenceFieldSerializer.Features & WireTypeMask));
99+
100+
if ((_referenceFieldSerializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
101+
{
102+
state.WriteMessage(_referenceFieldSerializer.Features, value.ReferenceField);
103+
}
104+
else
105+
{
106+
_referenceFieldSerializer.Write(ref state, value.ReferenceField);
107+
}
108+
109+
state.WriteFieldHeader(2, (global::ProtoBuf.WireType) (_structPropertySerializer.Features & WireTypeMask));
110+
111+
if ((_structPropertySerializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
112+
{
113+
state.WriteMessage(_structPropertySerializer.Features, value.StructProperty);
114+
}
115+
else
116+
{
117+
_structPropertySerializer.Write(ref state, value.StructProperty);
118+
}
119+
120+
state.WriteFieldHeader(3, (global::ProtoBuf.WireType) (_nullableStructPropertySerializer.Features & WireTypeMask));
121+
122+
if ((_nullableStructPropertySerializer.Features & global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage) == global::ProtoBuf.Serializers.SerializerFeatures.CategoryMessage)
123+
{
124+
state.WriteMessage(_nullableStructPropertySerializer.Features, value.NullableStructProperty);
125+
}
126+
else
127+
{
128+
_nullableStructPropertySerializer.Write(ref state, value.NullableStructProperty);
129+
}
130+
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)