C# Decimals in gRPC
Protocol Buffers (Protobuf) provides two native data types for real numbers: float
and double
.
You might be fine with this but equally, and particularly if your data includes currency values,
you might be absolutely horrified. Floating point values are not ideal for handling money,
because they are not precise. This has something to do with the way the value is stored in memory,
but rather than going into that, it’s easier to explain with a few lines of code.
class Program
{
static void Main()
{
float price = 4.99f;
float fiveCupsPrice = price * 5;
Console.WriteLine($"Five cups of coffee costs {fiveCupsPrice}");
// Output: Five cups of coffee costs 24.949999
}
}
That’s right. According to the IEEE Standard for Floating Point Arithmetic (IEEE-754), five cups of coffee at $4.99 a cup will cost $24.949999.
The decimal
type in C# and F# (or Decimal
in VB.NET) does not implement the IEEE-754 standard, and is guaranteed
to be precise. It exists to handle money values: that’s why the decimal
numeric literal suffix is m
. But there is
no built-in type in Protobuf that maps to decimal
.
There is an open issue on GitHub asking for a Decimal
to be added to the Well Known Types package,
and it will probably happen at some point, but it’s not there right now. Google obviously appreciate the importance
of a precise type for money values, because their own Google APIs Protobuf definitions include
a Money type. And you can copy that type
right now to add decimal
support to your own gRPC services.
Implementing a Decimal type in Protobuf
You’re likely to want to re-use this type, so it makes sense to declare it in a shared file and import it into your
application proto
files. If your source application’s models have any decimal
properties,
Visual Recode automatically generates this type for you, placing it in a CustomTypes
folder in Protos
.
// Adapted from https://github.com/googleapis/googleapis/blob/master/google/type/money.proto
syntax = "proto3";
package customTypes;
option csharp_namespace = "YourOrganization.Protos.CustomTypes"
// Name "DecimalValue" prevents conflict with C# Decimal type
message DecimalValue {
// The whole units of the amount.
int64 units = 1;
// Number of nano (10^-9) units of the amount.
// The value must be between -999,999,999 and +999,999,999 inclusive.
// If `units` is positive, `nanos` must be positive or zero.
// If `units` is zero, `nanos` can be positive, zero, or negative.
// If `units` is negative, `nanos` must be negative or zero.
// For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000.
int32 nanos = 2;
}
In this message type, the decimal
value is represented as two integer values. This is not how C# actually represents
decimals internally, but Protobuf is designed to be consumed from all kinds of languages and this message is easy
for all of them to understand. Just make sure the .proto
file that you distribute to consumers is well-commented.
NOTE: for interoperability with other platforms, the package
name must be consistent, but the C# namespace
can differ from project to project. This .proto
file may be duplicated in every project that needs it, as long as the
package
name and message
declaration are the same.
Extending the DecimalValue type in .NET
The Protobuf compiler generates partial
classes so it’s easy to extend them in your code.
In the case of the generated DecimalValue
type, you can add implicit conversion operators to and from the .NET
decimal
type, or explicit methods to handle the conversion, or both.
using System;
namespace YourOrganization.Protos.CustomTypes
{
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 decimalValue) => decimalValue.ToDecimal();
public static implicit operator DecimalValue(decimal value) => FromDecimal(value);
public decimal ToDecimal()
{
return decimalValue.Units + decimalValue.Nanos / NanoFactor;
}
public static DecimalValue FromDecimal(decimal value)
{
var units = decimal.ToInt64(value);
var nanos = decimal.ToInt32((value - units) * NanoFactor);
return new DecimalValue(units, nanos);
}
}
}
When you migrate a WCF application to gRPC using Visual Recode, it will automatically add this extension code for you
alongside the DecimalValue.proto
file in the Protos/CustomTypes
folder.
For more information on Protobuf check out the gRPC for WCF Developers e-book that I wrote for Microsoft’s .NET Application Architecture Guidance site.
For more information on how Visual ReCode helps move WCF applications onto gRPC for .NET Core and .NET 5, you can follow the link below.