A little more gRPC

In my last two posts I looked at using .proto files to define the IDL which protoc.exe along with gRPC generated our preferred language’s data files and method calls – in our case this meant generating C# files.

Now let’s look at how we can extend our code a little further, for example to include meat data/headers for sending tokens or other state between the client and server.

What was generated for us?

When we generated our code using the gRPC plugin, we started with the IDL method written as

service MusicService {
   rpc Query(NotesRequest) returns (NotesResponse) {
   }

however several overloads for this method were generated (I’ve cleaned them up to remove superfluous namespaces etc. and to make them easier to read in a blog post

public virtual NotesResponse Query(
   NotesRequest request, 
   Metadata headers = null, 
   DateTime? deadline = null, 
   CancellationToken cancellationToken = 
      default(CancellationToken))
{
   return Query(request, 
      new CallOptions(
         headers, 
         deadline, 
         cancellationToken));
}

public virtual NotesResponse Query(
   NotesRequest request, 
   CallOptions options)
{
   return CallInvoker.BlockingUnaryCall(
       __Method_Query, 
       null, 
       options, 
       request);
}

public virtual AsyncUnaryCall<NotesResponse> QueryAsync(
   NotesRequest request, 
   Metadata headers = null, 
   DateTime? deadline = null, 
   CancellationToken cancellationToken = 
       default(CancellationToken))
{
   return QueryAsync(
       request, 
       new CallOptions(
           headers, 
           deadline, 
           cancellationToken));
}

public virtual AsyncUnaryCall<NotesResponse> QueryAsync(
   NotesRequest request, 
   CallOptions options)
{
   return CallInvoker.AsyncUnaryCall(
      __Method_Query, 
      null, 
      options, 
      request);
}

We’ve got several overloads, include async version and there’s support for passing metadata headers, a deadline (similar to a timeout), as well as a cancellation token.

Metadata

We might use the Metadata argument to pass in SSO tokens or other relevant information that does not form part of the actual message, here’s an example of using the Query method in such a way

// client code
var response = client.Query(request, new Metadata
{
   new Metadata.Entry("SSO", token)
});

// server 
var token = 
   context?.RequestHeaders?.FirstOrDefault(e => e.Key == "sso");

Note: Beware, for some reason, the key has been turned to lower case.

In the server method we implemented in the last post you’ll notice that along with the request there’s the ServerCallContext type which contains all the other parameters that might be sent via the client, i.e. headers, cancellation token etc.

The server can return meta data using the ServerContext’s ResponseTrailers. However the client must use the Async versions of the client methods to receive these extra bits of data.

Here’s an example of returning something via ResponseTrailers, from the server

context.ResponseTrailers.Add(new Metadata.Entry("SSO", "Success"));

and the client would change to use the QueryAsync overload and possibly look like this

var response = client.QueryAsync(request, new Metadata
{
   new Metadata.Entry("SSO", "abcdefg")
});

foreach (var note in response.ResponseAsync.Result.Notes)
{
   Console.WriteLine(note);
}

var rt = response
   .GetTrailers()
   .FirstOrDefault(e => e.Key == "sso")
   .Value;

CallOptions

Ultimately the methods that take Metadata and other arguments end up wrapping those arguments in a CallOptions struct. However CallOptions also supports CallCredentials as well as a couple of other types (WriteOptions and ContextPropagationToken which I will not be looking at in this post).