Don't Do It All Yourself: Exploiting gRPC Well Known Types in .NET Core
If you're creating business services that send dates and decimal data then you may be concerned that gRPC services don't support the relevant data types. Don't Panic! There are solutions. Here's how to use them.
In an earlier column, I described how to define the message formats that clients can use to communicate with your gRPC services. I followed that up with another column that showed how you can re-use and import definitions. I mention those columns because in both of them I whined about how the Protocol Buffer (ProtoBuf) specification doesn't include some data types that I take for granted: date and decimal, to be precise (out of the box, ProtoBuf expects you to use the imprecise float type for data with decimals).
As I said in the second column on importing/re-using data types, there are ways to deal with these missing data types and this column will walk you through all three. In addition, I'll show the solution to another problem: How do you define a service that doesn't return a result (well, sort of).
Define Your Own Type
One solution is to define your own data type in a .proto file and import it into the .proto file where you want to use your new definition. Your definition for a decimal type might consist of just two integers: one that defines the number and the other that specifies how much of that number is to the right of the decimal.
You don't have to stop there: You can continue on to define a partial class containing helpful code to simplify manipulating data in messages that use your definition. If you're careful about naming that partial class, .NET Core will take care of merging your partial class' code into the code generated by the gRPC tools. Here's an article on how to define your own types.
I think this is a great solution…provided you're working only with internal clients and you have a homogenous, .NET Core-only shop. External clients (e.g. business partners) won't know about your new type and will have to badger you to find out how to use your type. And, of course, anyone building an application who isn't using .NET Core (and I understand that such people exist) won't be able to use your C# code.
Well Known Types
The other solution is to take advantage of Google's predefined well known types. Instead of defining a new type, all you have to do is import the type you need. The benefit of using the well known types is interoperability: It's likely that business partners creating clients that use your services will know about and be able to use these types. If they don't, the documentation exists on the Internet. More critically, the gRPC tools will generate the helpful code on any platform to work with those types (though here, of course, I'm only going to show how to use the types in C#).
For example, to store a date, you can import the timestamp .proto file from the well known types repository. Here's what I need to import two well known types, timestamp and empty in my .proto file:
I can now use timestamp in my SalesOrder message, referencing it by its ProtoBuf namespace google.protobuf:
int32 SoId = 1;
google.protobuf.Timestamp DeliveryDate = 2;
Because this is a well known type, the gRPC tools generate helpful code for working with Timestamp fields. I can, for example, set my DeliveryDate field using static methods built into the TimeStamp class. In the following example, I'm using the class' FromDateTime static method to convert April 4th, 2020 UTC into a Timestamp (Timestamps will only work with UTC coordinated times):
so.DeliveryDate = Timestamp.FromDateTime(new DateTime(2020,4,3,0,0,0,DateTimeKind.Utc));
To convert a Timestamp back into a date, I'd use the Timestamp object's ToDateTime method:
DateTime dt = so.DeliveryDate.ToDateTime();
The Empty type has a different purpose: Rather than using it in a message, I use it to define methods in my service. When defining a gRPC method in a .proto file, your method doesn't have to accept any parameters…but it must have some return type. There's no equivalent to the void keyword in ProtoBuf but the Empty type lets you define a service that doesn't return a value. The code in a .proto file to flag that a service doesn't return anything looks like this:
rpc DeleteCustomer (CustomerIdRequest) returns (google.protobuf.Empty);
This type exists primarily for documentation purposes, however: Using the Empty type says in a relatively unambiguous way that your method doesn't return a value (though unhandled errors should still, of course, be propagated back to the client). It doesn't mean, though, that you can use void when defining your service. In fact, your method declaration will look something like this:
public override Task DeleteCustomer(CustomerIdRequest request, ServerCallContext context)
…code to delete a customer…
return Task.FromResult(new Empty());
Lesser Known Types
But…there is, at yet, no decimal type in the well known types. Which leads to the third alternative: Copy into your project a definition from Google that isn't part of the well known types but is part of Google API. The Google API has both a money.proto definition and a date.proto definition (in case you don't like the Timestamp type).
Because Money isn't a well known type, you can't import it into your .proto file unless you have your own, local copy. You need to both add the money.proto file to your project and reference it in your .csproj file (as I described in that column on importing definitions). With those two steps done, you can use the Money type in your .proto file.
Because the Money type is in the ProtoBuf google.type namespace, I would add a Money field to my SalesOrder message like this:
int32 SoId = 1;
google.type.Money TotalValue = 2;
The code to work with this type isn't particularly intuitive because the gRPC tools don't generate any helpful code for these "lesser known types," at least for C#. According to the documentation, for example, to set the TotalValue field to $1.75 in US dollars, I'd use this code:
SalesOrder so = new SalesOrder();
so.TotalValue = new Google.Type.Money();
so.TotalValue.CurrencyCode = "USD";
so.TotalValue.Units = 1;
so.TotalValue.Nanos = 500000000;
Retrieving the value would require code like this:
decimal res = so.TotalValue.Units + ((decimal)so.TotalValue.Nanos) / 1000000000;
So there are your three options. I can see how you might find none of them particularly attractive. But that's often what it comes down to in this business, isn't it? "Do you want your arm cut off or chopped off?" Hey! It's not my fault.
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.