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.

The Hassle-Free Code Upgrade Tool for .NET

Early-bird licenses are just $195 or license your team from $295 per seat.