Skip to content

Commit 7b650a0

Browse files
committed
Added basic protobuf support
1 parent 442e754 commit 7b650a0

30 files changed

Lines changed: 1280 additions & 5 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.7.0</VersionPrefix>
5+
<VersionPrefix>8.8.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.1" />
2526
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
2627
<PackageVersion Include="Verify.Xunit" Version="29.2.0" />

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[![Thinktecture.Runtime.Extensions.EntityFrameworkCore9](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore9.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore9)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore9/)
1010
[![Thinktecture.Runtime.Extensions.Json](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.Json.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.Json)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Json/)
1111
[![Thinktecture.Runtime.Extensions.Newtonsoft.Json](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.Newtonsoft.Json.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.Newtonsoft.Json)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Newtonsoft.Json/)
12+
[![Thinktecture.Runtime.Extensions.ProtoBuf](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.ProtoBuf.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.ProtoBuf)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.ProtoBuf/)
1213
[![Thinktecture.Runtime.Extensions.MessagePack](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.MessagePack.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.MessagePack)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.MessagePack/)
1314
[![Thinktecture.Runtime.Extensions.AspNetCore](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.AspNetCore.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.AspNetCore)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.AspNetCore/)
1415

@@ -75,7 +76,7 @@ Unlike regular C# enums which are limited to numeric values and lack extensibili
7576
* Include additional fields, properties and behavior
7677
* Use polymorphism to define custom behavior for each value
7778
* Prevent creation of invalid values
78-
* Integrate seamlessly with JSON serializers, MessagePack, Entity Framework Core and ASP.NET Core
79+
* Integrate seamlessly with JSON serializers, Protobuf, MessagePack, Entity Framework Core and ASP.NET Core
7980

8081
Install: `Install-Package Thinktecture.Runtime.Extensions`
8182

@@ -97,6 +98,7 @@ Roslyn Analyzers and CodeFixes help the developers to implement the Smart Enums
9798

9899
Provides support for:
99100
* JSON (System.Text.Json and Newtonsoft)
101+
* Protobuf
100102
* Minimal Api Parameter Binding and ASP.NET Core Model Binding
101103
* Entity Framework Core
102104
* MessagePack
@@ -346,6 +348,7 @@ Key Features:
346348
* Comprehensive validation support with descriptive error messages
347349
* Framework integration:
348350
* JSON serialization (System.Text.Json and Newtonsoft.Json)
351+
* Protobuf serialization
349352
* Entity Framework Core support
350353
* ASP.NET Core Model Binding
351354
* MessagePack serialization

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>, IValueObjectConvertible<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
}

0 commit comments

Comments
 (0)