An open API service indexing awesome lists of open source software.

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#

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

[![Build and Release](https://github.com/Machibuse/Porticle.Grpc.TypeMapper/actions/workflows/release.yaml/badge.svg)](https://github.com/Machibuse/Porticle.Grpc.TypeMapper/actions/workflows/release.yaml)

## Nuget

[![NuGet Latest Version](https://img.shields.io/nuget/v/Porticle.Grpc.TypeMapper.svg)](https://www.nuget.org/packages/Porticle.Grpc.TypeMapper/) [![NuGet Downloads](https://img.shields.io/nuget/dt/Porticle.Grpc.TypeMapper.svg)](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