Skip to content

Commit f500f18

Browse files
committed
Added protobuf support for keyed value objects and smart enums.
1 parent 586a538 commit f500f18

18 files changed

Lines changed: 303 additions & 4 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
5-
<VersionPrefix>8.6.1</VersionPrefix>
5+
<VersionPrefix>8.7.0</VersionPrefix>
66
<Authors>Pawel Gerr</Authors>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
2222
<PackageVersion Include="NSubstitute" Version="5.3.0" />
2323
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17" />
24+
<PackageVersion Include="protobuf-net" Version="3.2.46" />
2425
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
2526
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
2627
<PackageVersion Include="Verify.Xunit" Version="28.14.1" />

Thinktecture.Runtime.Extensions.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thinktecture.Runtime.Extens
8888
EndProject
8989
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thinktecture.Runtime.Extensions.EntityFrameworkCore9.Tests", "test\Thinktecture.Runtime.Extensions.EntityFrameworkCore9.Tests\Thinktecture.Runtime.Extensions.EntityFrameworkCore9.Tests.csproj", "{03AA88D5-6E6F-4D19-BC90-311ACC7A0BF6}"
9090
EndProject
91+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thinktecture.Runtime.Extensions.ProtoBuf", "src\Thinktecture.Runtime.Extensions.ProtoBuf\Thinktecture.Runtime.Extensions.ProtoBuf.csproj", "{6050B0B3-8181-4322-88F5-A19B7C01061A}"
92+
EndProject
93+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thinktecture.Runtime.Extensions.ProtoBuf.Tests", "test\Thinktecture.Runtime.Extensions.ProtoBuf.Tests\Thinktecture.Runtime.Extensions.ProtoBuf.Tests.csproj", "{01554478-3168-4E7E-A179-CBFC51B10908}"
94+
EndProject
9195
Global
9296
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9397
Debug|Any CPU = Debug|Any CPU
@@ -202,6 +206,14 @@ Global
202206
{03AA88D5-6E6F-4D19-BC90-311ACC7A0BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
203207
{03AA88D5-6E6F-4D19-BC90-311ACC7A0BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
204208
{03AA88D5-6E6F-4D19-BC90-311ACC7A0BF6}.Release|Any CPU.Build.0 = Release|Any CPU
209+
{6050B0B3-8181-4322-88F5-A19B7C01061A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
210+
{6050B0B3-8181-4322-88F5-A19B7C01061A}.Debug|Any CPU.Build.0 = Debug|Any CPU
211+
{6050B0B3-8181-4322-88F5-A19B7C01061A}.Release|Any CPU.ActiveCfg = Release|Any CPU
212+
{6050B0B3-8181-4322-88F5-A19B7C01061A}.Release|Any CPU.Build.0 = Release|Any CPU
213+
{01554478-3168-4E7E-A179-CBFC51B10908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
214+
{01554478-3168-4E7E-A179-CBFC51B10908}.Debug|Any CPU.Build.0 = Debug|Any CPU
215+
{01554478-3168-4E7E-A179-CBFC51B10908}.Release|Any CPU.ActiveCfg = Release|Any CPU
216+
{01554478-3168-4E7E-A179-CBFC51B10908}.Release|Any CPU.Build.0 = Release|Any CPU
205217
EndGlobalSection
206218
GlobalSection(SolutionProperties) = preSolution
207219
HideSolutionNode = FALSE
@@ -234,6 +246,8 @@ Global
234246
{6638BCDF-A0A1-4742-B611-A67946CFB803} = {AE711F89-41F2-4519-B20D-BA1FAB0EB364}
235247
{08F267E9-A291-4613-BB41-75F164E2415D} = {8F117684-7943-4DCE-8861-F2B854924837}
236248
{03AA88D5-6E6F-4D19-BC90-311ACC7A0BF6} = {AE711F89-41F2-4519-B20D-BA1FAB0EB364}
249+
{6050B0B3-8181-4322-88F5-A19B7C01061A} = {8F117684-7943-4DCE-8861-F2B854924837}
250+
{01554478-3168-4E7E-A179-CBFC51B10908} = {AE711F89-41F2-4519-B20D-BA1FAB0EB364}
237251
EndGlobalSection
238252
GlobalSection(ExtensibilityGlobals) = postSolution
239253
SolutionGuid = {1C34F508-A60B-4C0E-AFA0-0F4CFFB23603}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using ProtoBuf;
2+
using ProtoBuf.Meta;
3+
using ProtoBuf.Serializers;
4+
5+
namespace Thinktecture.ProtoBuf.Serializers;
6+
7+
/// <summary>
8+
/// ProtoBuf serializer for keyed value objects and smart enums.
9+
/// </summary>
10+
/// <typeparam name="T">The type of the value object or smart enum.</typeparam>
11+
/// <typeparam name="TKey">The type of the key.</typeparam>
12+
/// <typeparam name="TValidationError">The type of the validation error.</typeparam>
13+
public class ValueObjectSerializer<T, TKey, TValidationError> : ISerializer<T>
14+
where T : IValueObjectFactory<T, TKey, TValidationError>, IValueObjectConvertable<TKey>
15+
where TKey : notnull
16+
where TValidationError : class, IValidationError<TValidationError>
17+
{
18+
private static readonly bool _mayReturnInvalidObjects = typeof(IValidatableEnum).IsAssignableFrom(typeof(T));
19+
20+
private readonly ISerializer<TKey> _keySerializer = KeySerializerCache<TKey>.Instance.Serializer;
21+
22+
/// <inheritdoc />
23+
public SerializerFeatures Features => _keySerializer.Features;
24+
25+
/// <inheritdoc />
26+
public T Read(ref ProtoReader.State state, T value)
27+
{
28+
var key = _keySerializer.Read(ref state, default!);
29+
30+
if (key is null)
31+
return default!;
32+
33+
var validationError = T.Validate(key, null, out var obj);
34+
35+
if (validationError is not null && !_mayReturnInvalidObjects)
36+
throw new ProtoException(validationError.ToString() ?? "Protobuf deserialization failed.");
37+
38+
return obj!;
39+
}
40+
41+
/// <inheritdoc />
42+
public void Write(ref ProtoWriter.State state, T value)
43+
{
44+
if (value is null)
45+
throw new ArgumentNullException(nameof(value));
46+
47+
var key = value.ToValue();
48+
49+
_keySerializer.Write(ref state, key);
50+
}
51+
}
52+
53+
file sealed class KeySerializerCache<T>
54+
{
55+
public static readonly KeySerializerCache<T> Instance = new();
56+
57+
public ISerializer<T> Serializer { get; }
58+
59+
private KeySerializerCache()
60+
{
61+
using var state = ProtoReader.State.Create(Stream.Null, RuntimeTypeModel.Default);
62+
63+
Serializer = state.GetSerializer<T>();
64+
}
65+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<Description>Adds protobuf support to components from Thinktecture.Runtime.Extensions.</Description>
5+
<PackageTags>smart-enum;value-object;discriminated-union;protobuf</PackageTags>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\Thinktecture.Runtime.Extensions\Thinktecture.Runtime.Extensions.csproj" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="protobuf-net" />
14+
</ItemGroup>
15+
16+
</Project>

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AttributeInfo.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Thinktecture.CodeAnalysis;
66
public bool HasJsonConverterAttribute { get; }
77
public bool HasNewtonsoftJsonConverterAttribute { get; }
88
public bool HasMessagePackFormatterAttribute { get; }
9+
public bool HasProtoContractAttribute { get; }
910
public ImmutableArray<DesiredFactory> DesiredFactories { get; }
1011
public ValidationErrorState ValidationError { get; }
1112
public string? KeyMemberComparerAccessor { get; }
@@ -16,6 +17,7 @@ private AttributeInfo(
1617
bool hasJsonConverterAttribute,
1718
bool hasNewtonsoftJsonConverterAttribute,
1819
bool hasMessagePackFormatterAttribute,
20+
bool hasProtoContractAttribute,
1921
ImmutableArray<DesiredFactory> desiredFactories,
2022
ValidationErrorState validationError,
2123
string? keyMemberComparerAccessor,
@@ -25,6 +27,7 @@ private AttributeInfo(
2527
HasJsonConverterAttribute = hasJsonConverterAttribute;
2628
HasNewtonsoftJsonConverterAttribute = hasNewtonsoftJsonConverterAttribute;
2729
HasMessagePackFormatterAttribute = hasMessagePackFormatterAttribute;
30+
HasProtoContractAttribute = hasProtoContractAttribute;
2831
DesiredFactories = desiredFactories;
2932
ValidationError = validationError;
3033
KeyMemberComparerAccessor = keyMemberComparerAccessor;
@@ -37,6 +40,7 @@ private AttributeInfo(
3740
var hasJsonConverterAttribute = false;
3841
var hasNewtonsoftJsonConverterAttribute = false;
3942
var hasMessagePackFormatterAttribute = false;
43+
var hasProtoContractAttribute = false;
4044
var validationError = ValidationErrorState.Default;
4145
var valueObjectFactories = ImmutableArray<DesiredFactory>.Empty;
4246
string? keyMemberComparerAccessor = null;
@@ -64,6 +68,10 @@ private AttributeInfo(
6468
{
6569
hasMessagePackFormatterAttribute = true;
6670
}
71+
else if (attribute.AttributeClass.IsProtoContractAttribute())
72+
{
73+
hasProtoContractAttribute = true;
74+
}
6775
else if (attribute.AttributeClass.IsValueObjectFactoryAttribute())
6876
{
6977
var useForSerialization = attribute.FindUseForSerialization();
@@ -102,6 +110,7 @@ private AttributeInfo(
102110
hasJsonConverterAttribute,
103111
hasNewtonsoftJsonConverterAttribute,
104112
hasMessagePackFormatterAttribute,
113+
hasProtoContractAttribute,
105114
valueObjectFactories,
106115
validationError,
107116
keyMemberComparerAccessor,
@@ -120,6 +129,7 @@ public bool Equals(AttributeInfo other)
120129
&& HasJsonConverterAttribute == other.HasJsonConverterAttribute
121130
&& HasNewtonsoftJsonConverterAttribute == other.HasNewtonsoftJsonConverterAttribute
122131
&& HasMessagePackFormatterAttribute == other.HasMessagePackFormatterAttribute
132+
&& HasProtoContractAttribute == other.HasProtoContractAttribute
123133
&& DesiredFactories.SequenceEqual(other.DesiredFactories)
124134
&& ValidationError.Equals(other.ValidationError)
125135
&& KeyMemberComparerAccessor == other.KeyMemberComparerAccessor
@@ -134,6 +144,7 @@ public override int GetHashCode()
134144
hashCode = (hashCode * 397) ^ HasJsonConverterAttribute.GetHashCode();
135145
hashCode = (hashCode * 397) ^ HasNewtonsoftJsonConverterAttribute.GetHashCode();
136146
hashCode = (hashCode * 397) ^ HasMessagePackFormatterAttribute.GetHashCode();
147+
hashCode = (hashCode * 397) ^ HasProtoContractAttribute.GetHashCode();
137148
hashCode = (hashCode * 397) ^ DesiredFactories.ComputeHashCode();
138149
hashCode = (hashCode * 397) ^ ValidationError.GetHashCode();
139150
hashCode = (hashCode * 397) ^ (KeyMemberComparerAccessor?.GetHashCode() ?? 0);

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static class Modules
2020
public const string THINKTECTURE_RUNTIME_EXTENSIONS_JSON = "Thinktecture.Runtime.Extensions.Json.dll";
2121
public const string THINKTECTURE_RUNTIME_EXTENSIONS_NEWTONSOFT_JSON = "Thinktecture.Runtime.Extensions.Newtonsoft.Json.dll";
2222
public const string THINKTECTURE_RUNTIME_EXTENSIONS_MESSAGEPACK = "Thinktecture.Runtime.Extensions.MessagePack.dll";
23+
public const string THINKTECTURE_RUNTIME_EXTENSIONS_PROTOBUF = "Thinktecture.Runtime.Extensions.ProtoBuf.dll";
2324
}
2425

2526
public static class ComparerAccessor
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Text;
2+
3+
namespace Thinktecture.CodeAnalysis;
4+
5+
public sealed class KeyedProtoBufCodeGenerator : CodeGeneratorBase
6+
{
7+
private readonly KeyedSerializerGeneratorState _state;
8+
private readonly StringBuilder _sb;
9+
10+
public override string CodeGeneratorName => "Keyed-ProtoBuf-CodeGenerator";
11+
public override string FileNameSuffix => ".ProtoBuf";
12+
13+
public KeyedProtoBufCodeGenerator(KeyedSerializerGeneratorState state, StringBuilder stringBuilder)
14+
{
15+
_state = state;
16+
_sb = stringBuilder;
17+
}
18+
19+
public override void Generate(CancellationToken cancellationToken)
20+
{
21+
var customFactory = _state.AttributeInfo
22+
.DesiredFactories
23+
.FirstOrDefault(f => f.UseForSerialization.Has(SerializationFrameworks.ProtoBuf));
24+
var keyType = customFactory?.TypeFullyQualified ?? _state.KeyMember?.TypeFullyQualified;
25+
26+
_sb.Append(GENERATED_CODE_PREFIX).Append(@"
27+
");
28+
29+
if (_state.Type.Namespace is not null)
30+
{
31+
_sb.Append(@"
32+
namespace ").Append(_state.Type.Namespace).Append(@";
33+
");
34+
}
35+
36+
_sb.RenderContainingTypesStart(_state.Type.ContainingTypes);
37+
38+
_sb.Append(@"
39+
[global::ProtoBuf.ProtoContract(Serializer = typeof(global::Thinktecture.ProtoBuf.Serializers.ValueObjectSerializer").Append("<").AppendTypeFullyQualified(_state.Type).Append(", ").Append(keyType).Append(", ").AppendTypeFullyQualified(_state.AttributeInfo.ValidationError).Append(@">))]
40+
partial ").Append(_state.Type.IsReferenceType ? "class" : "struct").Append(" ").Append(_state.Type.Name).Append(@"
41+
{
42+
}");
43+
44+
_sb.RenderContainingTypesEnd(_state.Type.ContainingTypes)
45+
.Append(@"
46+
");
47+
}
48+
}

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SerializationFrameworks.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public enum SerializationFrameworks
88
NewtonsoftJson = 1 << 1,
99
Json = SystemTextJson | NewtonsoftJson,
1010
MessagePack = 1 << 2,
11-
All = Json | MessagePack
11+
ProtoBuf = 1 << 3,
12+
All = Json | MessagePack | ProtoBuf
1213
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Text;
2+
3+
namespace Thinktecture.CodeAnalysis.SmartEnums;
4+
5+
public sealed class ProtoBufSmartEnumCodeGeneratorFactory : IKeyedSerializerCodeGeneratorFactory
6+
{
7+
public static readonly IKeyedSerializerCodeGeneratorFactory Instance = new ProtoBufSmartEnumCodeGeneratorFactory();
8+
9+
public string CodeGeneratorName => "ProtoBuf-SmartEnum-CodeGenerator";
10+
11+
private ProtoBufSmartEnumCodeGeneratorFactory()
12+
{
13+
}
14+
15+
public bool MustGenerateCode(KeyedSerializerGeneratorState state)
16+
{
17+
return !state.AttributeInfo.HasProtoContractAttribute
18+
&& (state.KeyMember is not null || state.AttributeInfo.DesiredFactories.Any(f => f.UseForSerialization.Has(SerializationFrameworks.ProtoBuf)));
19+
}
20+
21+
public CodeGeneratorBase Create(KeyedSerializerGeneratorState state, StringBuilder stringBuilder)
22+
{
23+
return new KeyedProtoBufCodeGenerator(state, stringBuilder);
24+
}
25+
26+
public bool Equals(IKeyedSerializerCodeGeneratorFactory other)
27+
{
28+
return ReferenceEquals(this, other);
29+
}
30+
31+
public bool Equals(ICodeGeneratorFactory<KeyedSerializerGeneratorState> other)
32+
{
33+
return ReferenceEquals(this, other);
34+
}
35+
}

0 commit comments

Comments
 (0)