Practical .NET

.NET Core: Writing Really Obvious Code with Enumerated Values in gRPC Web Services

Peter's pretty fanatical about replacing documentation/comments with readable code. So he's very excited about using enums when defining gRPC services. Very. Excited. But there are some best practices and "things to be aware of" when using this feature.

I'm a big fan of what I call Really Obvious Code (ROC - "ROC rocks!"). I've even written a column on how ROC is so much better than comments that you shouldn't be writing comments: Your time is better spent on creating ROC.

One of the most useful tools in creating ROC are enumerated values which allow you to replace "magic numbers" in your code with meaningful names. It's the difference between
cr.Status = 3;

and this:

cr.Status = CustomerCreditStatus.Unlimited;

gRPC services support using enumerated values (enums) when creating the .proto file that drives your gPRC service and the clients that access it (for more on how that works, see the column I wrote on creating gRPC services and clients). Since the definitions of the messages that you send to and receive from a gRPC service are converted into C# classes, defining enums in your .proto file gives you the same ROCing benefits that defining enums in your code does.

Defining Enumerated Values
In fact, if you've used enums in another programming language then the implementation in ProtoBuf will look familiar to you. There are some differences, however, that you need to be aware of.

For example, in a ProtoBuf every value i in the enumeration must be explicitly assigned a position and, because ProtoBuf depends on the concept of a default value, your enum must have a value for position 0. In addition, for backwards compatibility with earlier versions of ProtoBuf, the value in position 0 must be the first one listed in the enum -- though why you wouldn't make the 0 position value the first one is also a mystery to me.

This code, for example, creates an enumeration called creditStatus (and I have a point to make with the inconsistent naming standards I've used here):

enum creditStatus {
    UNKNOWNSTATUS = 0;
    CashOnly = 1;
    _CreditLimit = 2;
    unlimited = 3;
}

Elsewhere in the .proto file, a Customer message that uses this enum might look like this:

message Customer {
   int32 id = 1;
   creditStatus status = 2;

If the Customer status field isn't set to an explicit value then it will default to UNKNOWNSTATUS because that's the value in the 0 position value in the creditStatus enum.

By the way, .NET Core applies a form of kebab-casing to the names in your enumeration: All letters in the names are lowercased, except for the first letter and any letter following an underscore -- those are uppercased. The only punctuation mark permitted in a name is an underscore and it's discarded during the conversion to C#.

As a result, in C# my enum's named values become:

  • Unknownstatus
  • Cashonly
  • Creditlimit
  • Unlimited

If you prefer Pascal-cased names in your code then you'll need to deploy underscores strategically. To get CreditLimit as the name of your enumerated value, you'll need to name the field using an underscore before "limit" in your .proto file (e.g. Credit_Limit, CREDIT_LIMIT, or credit_limit would do the trick).

One last note on the default value: A client can't tell the difference between a property that's been set to the default value for your enum and a property that hasn't been set at all. A best practice, therefore, would be to make the default value for your enum (the one in position 0) to be the "no value available" option and never use it. That way a client can tell when the property hasn't been set (it will have the default value) and when you have set it (the property will have any other value).

Creating Really Obvious Code
Unlike the fields in a message, an enum can have two values in the same position, provided you set the enum's option allow_alias to true (ProtoBuf is pretty anal retentive about this option: You'll get a compile time error if you set this option and don't have two values with the same position).

This enum takes advantage of that feature to provide alternative names for the values in position 1 and 3 that might help better document code using this enum (I'm back to talking about ROC, again):

enum creditStatus {
  option allow_alias = true;
    Unknown_Status = 0;
    Cash_Only = 1;
    Dead_Beat = 1;
    Credit_Limit = 2;
    Unlimited = 3;
    Valued_Customer = 3;
}

Do be aware that the ability to apply multiple names means just two things. First, that the code that sets the value of my Customer's status property can use either Unlimited or Valued_Customer to set the property to position 3 in the enum. Second, that either of these tests against that status property would return true:

if (cr.Status == creditStatus.Unlimited)
if (cr.Status == creditStatus.ValuedCustomer)

However, if you read (or print) the status property, it will return whichever of the two values appears first in the enum's definition in the .proto file used to generate the code. So, in my example, because Unlimited appears before Valued_Customer, I'll get Unlimited if I display the value of the status property, even if Valued_Customer was used to set it.

Managing Change
In a previous column on ProtoBuf, I discussed some best practices in managing change in message formats and pointed out that the ProtoBuf specification is relatively forgiving about changes. Enums are even more forgiving than message formats.

For example, a server might be using a .proto file that includes my creditStatus enum. However, a client might have an older version of that file that doesn't include the Unlimited/ValuedCustomer entry. If the server uses that Unlimited when returning a result, that won't generate an error in the client that doesn't know about that value. However, what the status field will return is going to be dependent on the language you're using. For the record, C# returns the position number of the value the property was set to (in this case, the number 3).

And now you're ready to write Really Obvious Code when creating gRPC services. I'm so proud.

About the Author

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/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube