How to share gRPC .proto definitions of custom types via Nuget
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:
- Define protobuf definitions for custom types
- Extend generated gRPC types with implicit operators
- Publish Nuget package
- 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 toGrpc.Shared
Nuget directory (the variable name starts withPkg
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!