Practical .NET

Implementing State in .NET Core gRPC Messages with oneof

In the real world, you've been dealing with the State pattern every time you designed a set of database tables. The Protocol Buffers specification lets you do the same thing when you define the messages you send and receive from your gRPC Web Service.

In programming one of the ways we use the word "state" is to talk about methods and data that apply only in specific scenarios. A customer, for example, should only have an amount in their credit limit if their status allows them to buy things on credit. When coding, we usually handle "state-like" problems using State, Strategy, or Role patterns. In a database, we handle them through data integrity rules that allow you to specify that, based on the value of a column in the table, another column either must be null or must have a value.

Managing state is supported, at least partially, in gRPC services through ProtoBuf's oneof definition. A oneof definition allows you to specify that only one of a group of fields will have a value (or values).

For example, let's consider returning a status message from a service's AddCustomer method. If everything goes well, the service should return the new customer's Id (in a RESTful application, we'd return the URL to be used to retrieve the new customer). If things go wrong, however, it should return an error code. Using oneof, I would define the return message so that it either contains a Message or Cust_Id (but never both), with a definition like this:

message AddCustomerResponse {
   oneof ResponseType 
   {
      string Message = 1;
      int32 Cust_Id = 2;
   }
}

The code that uses my AddResponseCustomer object, would look something like this:

AddCustomerResponse acr = new AddCustomerResponse();
try
{
   //…code to update customer…
   acr.CustId = 123;
}
catch (Exception ex)
{
   acr.Message = "oops";}
}

As you can see, either the CustId or the Message property is set depending on the state of the service (in error or not), but both are never set.

Processing oneof Messages
Of course, when a client gets my AddCustomerResponse, both the Message and CustId properties are going to be present even though only one of them will be set. How does the client determine which property to use? And how does the service ensure that both are never set? The generated gRPC code deals with that both at the server (where this message is being created) and at the client (where this message would be processed).

On the server, the generated code guarantees that only one of the two properties will be set. Setting the CustId property automatically causes the Message property to be cleared and vice versa. If you repeatedly set the properties involved in the oneof, the last property set is the one that retains its value and triggers the other property being cleared.

It may be hard to detect that because, as is the case with all the properties generated from the .proto file fields, these properties aren't nullable: "clearing" a property means having it set to its default value. As a result, if the server doesn't set either property then Message will be set to string.Empty and CustId will be 0…which is exactly the same result that you'd get if the update was successful and the CustId was set to 0.

Fortunately, the code generation process also creates an indicator property that lets you distinguish between the results. In addition to the Message and CustId fields, the AddCustomerResponse class also has a read-only property called ResponseTypeCase. That property will have one of an enumerated set of values: None (indicating that none of the fields in the oneof have been set) and a named value for each of the fields in the oneof (in my case, values named CustId and Message).

This means that typical code on the client that works with the oneof properties will look like this:

switch (acr.ResponseTypeCase)
{
   case ResponseTypeOneofCase.None:
     //…do nothing…
     break;
   case ResponseTypeOneofCase.CustId:
     //…access the new customer object…
     break;
  case ResponseTypeOneofCase.Message:
     //…report an error…
     break;
}

By the way, my oneof declaration also generates a ClearResponseType method that sets all the properties in the oneof declaration to their default value and sets ResponseTypeCase to None.

Options
But your oneof fields aren't limited to scalar data types. In fact, I suspect that most state scenarios don't pick between single data values but between sets of data values. For example, rather than returning just the customer Id when everything goes well I could return all of the customer data. This oneof declaration picks between returning a set of status information and a set of customer information:

message AddCustomerResponse {
   oneof ResponseType 
   {
      StatusResponse status = 1;
      CustomerResponse cust = 2;
   }
}
message StatusResponse 
{
  bool Was_Successful = 1;
  int32 Status_Code = 2;
  string Message = 3;
}
message CustomerResponse 
{
  int32 Cust_Id = 1;
  string First_Name = 2;
  string lastName = 3;
…
There are other options I haven't discussed here. For example, while I've assumed that it's a message that's generated on the server that's using the oneof field, you can use it in messages generated on the client. And, while I have only one oneof field in my message, you can have as many as you want. As I discussed in an earlier post, along with other gRPC message structures oneof definitions can be imported and shared among definitions.

Really, this is a terrific solution and, like any cool technology, now you'll be looking for a problem where you can use it.

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