How to share gRPC .proto definitions of custom types via Nuget

Joren Thijs
5 min readMar 30, 2024

--

Introduction

Like most companies, mine maintains a custom types library full of special types for our problem domain. Unfortunately when sending these types via gRPC we end up redefining them in every .proto file. And as the C# types that get generated from this are different from our internal types they have to be mapped somehow. Multiply this problem with the amount of protobuf contracts you have and this leads to a lot of duplication.

Solution

Along with our MyCompany.Internal.Types Nuget package we are also going to publish a MyCompany.Internal.Types.Grpc package containing protobuf definitions for these types.

A lot of what I am about to show you is based on Sanket Naik’s excellent Blog Post: Sharing gRPC .proto Files with Nuget Packages Made Easy

Code for this example is on Github.

Steps:

  1. Define protobuf definitions for custom types
  2. Extend generated gRPC types with implicit operators
  3. Publish Nuget package
  4. Consume Nuget package

Step 1

In our example we have a custom type library MyCompany.Internal.Types that we share via Nuget. Inside is a Money type that we want to send via gRPC.

namespace MyCompany.Internal.Types;

public class Money
{
public Money(decimal value, Currency currency)
{
Value = value;
Currency = currency;
}

public decimal Value { get; set; }

public Currency Currency { get; set; }
}

/// <summary>
/// Currencies according to the ISO 4217 standard.
/// </summary>
public enum Currency
{
/// <summary>Australian dollar</summary>
AUD = 036,

/// <summary>United States dollar</summary>
USD = 840,

/// <summary>Euro</summary>
EUR = 978,
}

Our first step is to create a MyCompany.Internal.Types.Grpc class library project. In it we will create our Protos directory. This is the directory that will be shared with other projects. In the directory I create a (optional) file structure that my protobuf imports will follow.

Protos
└── mycompany
└── internal
└── types.proto

In the types.proto file I define gRPC versions of my custom types.
Note that for the C# decimal type there is no native protobuf replacement so I used the custom DecimalValue from the Microsoft documentation.

syntax = "proto3";

option csharp_namespace = "MyCompany.Internal.Types.Grpc";

package mycompany.internal.types;

// Substitute for C# decimal type from https://learn.microsoft.com/en-us/aspnet/core/grpc/protobuf?view=aspnetcore-8.0#decimals
message DecimalValue {

// Whole units part of the amount
int64 units = 1;

// Nano units of the amount (10^-9)
// Must be same sign as units
sfixed32 nanos = 2;
}

message Money {
DecimalValue value = 1;
// The three-letter currency code defined in ISO 4217.
string currency = 2;
}

Step 2

I generated the C# types by referencing the .proto file in my csproj file.

<ItemGroup>
<Protobuf Include="Protos\mycompany\internal\types.proto" />
</ItemGroup>

Now I can hook into the generated partial classes and add implicit operators for the custom types. Note the namespace matching the csharp_namespace option I set in my .proto file.

namespace MyCompany.Internal.Types.Grpc;

public partial class DecimalValue
{
private const decimal _nanoFactor = 1_000_000_000;

public DecimalValue(long units, int nanos)
{
Units = units;
Nanos = nanos;
}

public static implicit operator decimal(DecimalValue grpcDecimal)
{
return grpcDecimal.Units + (grpcDecimal.Nanos / _nanoFactor);
}

public static implicit operator DecimalValue(decimal value)
{
var units = decimal.ToInt64(value);
var nanos = decimal.ToInt32((value - units) * _nanoFactor);
return new(units, nanos);
}
}

public partial class Money
{
public Money(DecimalValue value, string currency)
{
Value = value;
Currency = currency;
}

public static implicit operator MyCompany.Internal.Types.Money(Money grpcMoney)
{
var currency = Enum.Parse<Currency>(grpcMoney.Currency);
return new(grpcMoney.Value, currency);
}

public static implicit operator Money(MyCompany.Internal.Types.Money money)
{
var currency = ((int)money.Currency).ToString();
return new(money.Value, currency);
}
}

Step 3

I add the types.proto file to my Nuget package content by adding a Content directive to my csproj file.

<ItemGroup>
<Protobuf Include="Protos\mycompany\internal\types.proto" />
<!-- Set visible to false so the file doesn't show up in solution explorer twice -->
<Content Include="Protos\mycompany\internal\types.proto" Pack="true" Visible="false" />
</ItemGroup>

If you publish a Nuget package with dotnet pack and inspect the resulting file using nuget.info you should see your Protos directory structure under the content directory.

Step 4

To use the exported protobuf definitions install the Nuget package.
I am using a MyCompany.Api example project. Note the use of the MSBuild directive GeneratePathProperty=”true" on the gRPC package reference. This will give us the ability to refer to the package’s path.

<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />
<PackageReference Include="MyCompany.Internal.Types" Version="1.0.0" />
<PackageReference Include="MyCompany.Internal.Types.Grpc" Version="1.0.0" GeneratePathProperty="true" />
</ItemGroup>

Next we add the Protos directory from the Nuget package to the AdditionalImportDirs directive of our Protobuf directive.
For more information see the Protobuf MSBuild reference.

<ItemGroup>
<Protobuf Include="Protos\order_service.proto" GrpcServices="Server" AdditionalImportDirs="$(PkgMyCompany_Internal_Types_Grpc)\content\Protos" />
</ItemGroup>

As Sanket explained in his excellent Blog Post.

Note that, $(PkgGrpc_Shared) is by conventions where $(PkgGrpc_Shared)will be resolved to Grpc.Shared Nuget directory (the variable name starts with Pkg and is followed by the package name when . is replaced by _)

The Protos directory from our Nuget package is now the root from where we can import additional .proto files.

syntax = "proto3";

option csharp_namespace = "MyCompany.Api";

import "mycompany/internal/types.proto";

package mycompany.api;

service OrderService {
rpc GetOrderAmount (GetOrderAmountRequest) returns (AmountReply);
}

message GetOrderAmountRequest {
string debt_id = 1;
}

message AmountReply {
mycompany.internal.types.Money amount = 1;
}

Note how in our proto file we refer to the custom grpc type with the same “namespace” as the non grpc type mycompany.internal.types.Money this is by design.

Warning: The protobuf plugin of your Jetbrains IDE might complain about not being able to find this directory. To fix this you can click the error and tell the plugin to add the directory to it’s settings.

In our gRPC service implementation we can now just return our normal custom types. They will be implicitly converted without us having to redefine and remap them for every gRPC contract that uses these types.

using Grpc.Core;
using MyCompany.Internal.Types;

namespace MyCompany.Api.Services;

public class MyService() : OrderService.OrderServiceBase
{
public override async Task<AmountReply> GetOrderAmount(GetOrderAmountRequest request, ServerCallContext context)
{
await Task.Delay(1);
return new AmountReply
{
Amount = new Internal.Types.Money(5, Currency.USD)
};
}
}

Note how we are assigning a Internal.Types.Money object to a Internal.Types.Grpc.Money variable without having to do anything.

Conclusion

There you go! A nice way to share gRPC definitions of your custom types.
If you look at the example project on github you will find a example README section that you can add to your own library so consumers will know how to use it.

Hope it helps!

--

--