https://github.com/machibuse/porticle.grpc.typemapper
User nullable String and Guid properties with GRPC in c#
https://github.com/machibuse/porticle.grpc.typemapper
csharp grpc guid nullable-reference-types protobuf
Last synced: about 1 month ago
JSON representation
User nullable String and Guid properties with GRPC in c#
- Host: GitHub
- URL: https://github.com/machibuse/porticle.grpc.typemapper
- Owner: Machibuse
- License: mit
- Created: 2025-07-06T12:14:12.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2026-03-27T17:52:02.000Z (2 months ago)
- Last Synced: 2026-04-19T06:46:03.610Z (about 1 month ago)
- Topics: csharp, grpc, guid, nullable-reference-types, protobuf
- Language: C#
- Homepage:
- Size: 86.9 KB
- Stars: 1
- Watchers: 0
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Porticle.Grpc.TypeMapper
A Roslyn-based post-processor for protoc-generated files that adds automatic mappings for `Guid`, `Guid?`, `decimal`, `decimal?`, `string?`, nullable enums and nullable reference
types. By simply adding this
package and adding
comments to
your `.proto` file.
## Build State
[](https://github.com/Machibuse/Porticle.Grpc.TypeMapper/actions/workflows/release.yaml)
## Nuget
[](https://www.nuget.org/packages/Porticle.Grpc.TypeMapper/) [](https://www.nuget.org/packages/Porticle.Grpc.TypeMapper/)
## Overview
This library adds automatic conversion for:
- Protobuf string to C# Guid
- Protobuf google.Protobuf.StringValue to C# Guid?
- Protobuf string to C# decimal
- Protobuf google.Protobuf.StringValue to C# decimal?
- Protobuf google.Protobuf.StringValue to C# string?
- Protobuf optional enum to C# nullable enum
- Full nullable reference type support per message via `[NullableReferenceTypes]`
This Library adds a Roslyn Postprocessing zu the c# files generated by the protoc compiler.
Enabling seamless integration of Guid, Guid?, decimal, decimal?, string?, nullable Enums and `NRT Support` in your gRPC services without manual conversion.
Code.
## TL/DR
- Add `// [GrpcGuid]` as comment to a string or StringValue proto field to get Guid/Guid? in generated c# code
- Add `// [Decimal]` as comment to a string or StringValue proto field to get decimal/decimal? in generated c# code
- Add `// [NullableString]` as comment to a string or StringValue proto field to get string? in generated c# code
- Add `// [NullableEnum]` as comment to an optional enum proto field to get MyEnum? in generated c# code
- Add `// [NullableReferenceTypes]` as comment above a message to wrap all reference type properties with `#nullable enable/disable` and make nullable ones `string?` / `TypeName?`
- Add PorticleGrpcTypeMapper_WrapAllNonNullableStrings as Property to your Project to wrap all not nullable proto stings in #nullable enable/disable
- Add PorticleGrpcTypeMapper_WrapAllNullableStrings as Property to your Project to wrap all proto StringValue fields in #nullable enable/disable and change `string` to `string?`
## Installation
### Install the package via NuGet:
```powershell
dotnet add package Porticle.Grpc.TypeMapper
```
After installing the Package, this Post build step ist dynamically added to your build.
```msbuild
<_FilesToPostProcess Include="$(MSBuildProjectDirectory)\%(Protobuf_Compile.OutputDir)\%(Protobuf_Compile.Filename)"/>
```
Don't wonder ist you cant se it in your csproj file. It is dynamically added when your build is processed.
## Usage
There are several things you can do in your .proto files:
- Add `// [GrpcGuid]` as comment to a string field - Converts the corresponding c# string property to Guid
- Add `// [GrpcGuid]` as comment to a StringValue field - Converts the corresponding c# string property to Guid?
- Add `// [Decimal]` as comment to a string field - Converts the corresponding c# string property to decimal
- Add `// [Decimal]` as comment to a StringValue field - Converts the corresponding c# string property to decimal?
- Add `// [NullableString]` as comment to a StringValue field - Converts the corresponding c# string property to string?
- Add `// [NullableEnum]` as comment to a optional enum field - Converts the corresponding optional proto enum to a C# nullable Enum
- Add `// [NullableReferenceTypes]` as comment above a message definition - Enables full nullable reference type support for the entire message (see below)
First an Example of a default .proto file
### Without TypeMapper
```protobuf
syntax = "proto3";
import "google/protobuf/wrappers.proto";
enum TestEnum {
FOO = 0;
BAR = 1;
}
message User {
// Guid of the user object
string id = 1;
// Optional parent UserId
google.protobuf.StringValue optional_parent_user_id = 2;
// Optional description
google.protobuf.StringValue description = 3;
// List of roles
repeated string role_ids = 4;
// Price of the item
string price = 6;
// Simple Enum
optional TestEnum foo_bar = 5;
}
```
Will result in protoc generated code like this, everything is a string or a simple enum
```csharp
/// Guid of the user object
public string Id {
get { return id_; }
set { id_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); }
}
/// Optional Guid of the parent UserId
public string OptionalParentUserId {
get { return optionalParentUserId_; }
set { optionalParentUserId_ = value; }
}
/// Optional description string
public string Description {
get { return description_; }
set { description_ = value; }
}
/// List of roles
public pbc::RepeatedField RoleIds {
get { return roleIds_; }
}
/// Price of the item
public string Price {
get { return price_; }
set { price_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); }
}
public global::Porticle.Grpc.UnitTests.TestEnum FooBar {
get {
if ((_hasBits0 & 1) != 0)
return fooBar_;
else
return FooBarDefaultValue;
}
set {
_hasBits0 |= 1;
fooBar_ = value;
}
}
```
### Now a sample with TypeMapper enabled
```protobuf
syntax = "proto3";
import "google/protobuf/wrappers.proto";
enum TestEnum {
FOO = 0;
BAR = 1;
}
message User {
// [GrpcGuid] Guid of the user object
string id = 1;
// [GrpcGuid] Optional Guid of the parent UserId
google.protobuf.StringValue optional_parent_user_id = 2;
// [NullableString] Optional description string
google.protobuf.StringValue description = 3;
// [GrpcGuid] List of roles
repeated string role_ids = 4;
// [Decimal] Price of the item
string price = 6;
// [NullableEnum] Simple Enum
optional TestEnum foo_bar = 5;
}
```
Will result in generated code like this, using string?, Guid, Guid?, decimal and a nullable Enum
```csharp
/// [GrpcGuid] Guid of the user object
public global::System.Guid Id {
get
{
// Parse the internal string as Guid and return it
return global::System.Guid.Parse(id_);
}
set
{
// Set the internal string fron the given guid
id_ = (value).ToString("D");
}
}
/// [GrpcGuid] Optional Guid of the parent UserId
public global::System.Guid? OptionalParentUserId {
get {
// return null wehn corresponding string is null
if(optionalParentUserId_==null) return default;
// return a Guid instead of the string
return global::System.Guid.Parse(optionalParentUserId_);
}
set
{
// sets the internal string from the given guid
optionalParentUserId_ = (value)?.ToString("D");
}
}
// enable nullable for this property and return string? instead of string
#nullable enable
/// [NullableString] Optional description string
public string? Description {
get { return description_; }
set { description_ = value; }
}
#nullable disable
/// [GrpcGuid] List of roles
public IList RoleIds {
get
{
// returns a wrapper that converts a list of strings to a list of guids
return new RepeatedFieldGuidWrapper(roleIds_);
}
}
/// [Decimal] Price of the item
public decimal Price {
get
{
// Parse the internal string as decimal using InvariantCulture
return decimal.Parse(price_, System.Globalization.CultureInfo.InvariantCulture);
}
set
{
// Set the internal string from the given decimal
price_ = (value).ToString(System.Globalization.CultureInfo.InvariantCulture);
}
}
public global::Porticle.Grpc.UnitTests.TestEnum? FooBar {
get {
if ((_hasBits0 & 1) != 0)
{
return fooBar_;
}
else
{
// return null instead of default value when Has-Flag is false
return null;
}
}
set {
if(value==null) {
// Set hasflag to false when null is assigend
_hasBits0 &=~1;
fooBar_ = FooBarDefaultValue;
}
else
{
_hasBits0 |= 1;
fooBar_ = value.Value;
}
}
}
```
## `[NullableReferenceTypes]` - Per-Message Nullable Reference Types
Add `// [NullableReferenceTypes]` as a comment above a `message` definition to automatically apply nullable reference type annotations to **all** properties in that message:
1. **Non-nullable strings** (`string` with `ProtoPreconditions.CheckNotNull`) are wrapped in `#nullable enable/disable` so the compiler knows they must not be null
2. **Nullable strings** (`google.protobuf.StringValue`) are changed to `string?` and wrapped in `#nullable enable/disable`
3. **Message references** (sub-messages) are changed to `TypeName?` and wrapped in `#nullable enable/disable`
Scalar types (`int`, `double`, `bool`, etc.), enums, repeated fields, and maps are **not** affected.
### Example
```protobuf
syntax = "proto3";
import "google/protobuf/wrappers.proto";
// [NullableReferenceTypes]
message User {
string name = 1;
google.protobuf.StringValue description = 2;
Address address = 3;
int32 age = 4;
}
message Address {
string street = 1;
}
```
Will result in generated code like this:
```csharp
#nullable enable
public string Name {
get { return name_; }
set { name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); }
}
#nullable disable
#nullable enable
public string? Description {
get { return description_; }
set { description_ = value; }
}
#nullable disable
#nullable enable
public global::MyNamespace.Address? Address {
get { return address_; }
set { address_ = value; }
}
#nullable disable
// int is a value type - not affected
public int Age {
get { return age_; }
set { age_ = value; }
}
```
## Support for 'Nullable Reference Types' (Project-wide Settings)
There are actually 2 Settings for Nullable Reference Types you can set in your Project file, or better in Directory.Build.props
```XML
true
true
```
### PorticleGrpcTypeMapper_WrapAllNonNullableStrings
This means that all Strings like this
```protobuf
syntax = "proto3";
message Foo {
string normal_string = 1;
}
```
Will be wrapped in '#nullable enable/disable' like this
```csharp
#nullable enable
public string NormalString {
get { return normalString_; }
set { id_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); }
}
#nullable disable
```
### PorticleGrpcTypeMapper_WrapAllNullableStringValues
This means that all Strings like this
```protobuf
syntax = "proto3";
import "google/protobuf/wrappers.proto";
message Foo {
google.protobuf.StringValue nullable_string = 1;
}
```
Will be wrapped in '#nullable enable/disable' like this and string is changed to string?
```csharp
#nullable enable
public string? OptionalParentUserId {
get { return optionalParentUserId_; }
set { optionalParentUserId_ = value; }
}
#nullable disable
```
## What is currently not Possible?
- Mapping `repeated google.protobuf.StringValue` to `List` or `List` because grpc internally uses `RepeatedField` instead of `RepeatedField`.
This may be a bug in protoc compiler, because it is also not possible to add `null` to `repeated google.protobuf.StringValue` because there is a not null check in the Add
function in `RepeatedField`
- This Tool actually is not tested with protoc / Grpc.Tools is compiled with GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE flag
- All Messages implements ICustomDiagnisticMessage to prevent GRPC ToDiagnbosticString because ist crashes when Guids are converted to Strings