Getting started with gRPC in UWP and ASP.NET Core

In this article we describe how to build a UWP app that uses the gRPC protocol -the new WCF- to communicate with a remote host application running on ASP.NET Core 3.0. We’ll discuss:

  • defining the service model,
  • generating client and server code,
  • executing simple RPC calls,
  • executing server-side streaming RPC calls,
  • executing client-side streaming RPC calls, and 
  • executing bidirectional streaming calls.

We built a small sample app that demonstrates the different types of calls. It looks like this:

ClientScreenshot

WHY ?

The announcements regarding .NET Core 3.0 and its successor .NET 5 by Scott Hunter and Richard Lander at the //BUILD 2019 conference confirmed that the classic .NET Framework is being mothballed and the server side of Windows Communication Foundation (WCF) with it. Cross-platform .NET Core is the future, and it will not support WCF because WCF is too tightly coupled to the Windows family of operating systems.

As it goes with every breaking change, the news is causing confusion and anger in some parts of the developer community, while some try saving the furniture by porting the technology to the new platform, and others prepare to move and already started comparing the pros and cons of alternative technologies. We proudly belong to the last category and are checking the alternatives – not only for the basic WCF functionality but also for its advanced features like NetTCP bindings, transactions, stateful sessions, etcetera. If you share our interest in these topics, we strongly recommend to keep an eye on the unWCF blog -great name, great content- by Mark Rendle

WHAT ?

Microsoft is pushing forward Web API and gRPC as the alternatives for WCF in ASP.NET Core. Check this link for a high level comparison between these two. In this article we’ll only focus on gRPC, an open source platform independent HTTP/2-based RPC framework with a solid Google pedigree.

Many RPC systems -including WCF- are based on the idea of defining a service, specifying the methods that can be called remotely, together with their parameters and return types. In gRPC all of these are defined using protocol buffers, a language neutral, platform neutral, human readable but binary serializable way of describing structured data, typically in *.proto files. Here’s an example of a such ‘protobuf’ message:

message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}

Check this tutorial to find out how to generate the corresponding C# classes from this, and to do parsing and serialization of instances.

The message keyword is used to describe the data structures that can be used as parameters and return types of service calls. The service keyword is used to describe … services, with an rpc entry for each method. Here’s the full protocol buffer definition of a canonical remote ‘Hello World’ service:

syntax = "proto3";

package Greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

The Grpc.Tools NuGet package contains the protoc compiler and some helpers and integration tools around it, to generate C# (or C++) code from the .proto files on the server and client side. Let’s make our hands dirty and try it out in UWP and ASP.NET Core. Here’s how client and server look like at runtime:
Screenshots

HOW ?

Before we build a client, we need a server somewhere. Make sure to have the prerequisites : .NET Core SDK 3.0 Preview and a Visual Studio 2019 with ASP.NET and web development workload. Last but not least, it should be configured for previews.

ActivatePreview

If all the prerequisites are in place, then the whole gRPC server side setup comes out-of-the-box: you just need to create a new project from the appropriate template. We followed this tutorial to create the server project:

WebProject

The project template as well as the required NuGet packages are evolving rapidly, so make sure to have a complete list of up-to-date packages:

ServerNuGets

For the server part all references, code generation configurations, and the startup code are in the project template.

For the client you have to do everything manually, since there is no ‘add web reference’ for gRPC (yet). Here are the NuGet packages that we used in the UWP client:

ClientNuGets

If you decide generate the client code from the *.proto file(s) in a separate project, then the Grpc.Tools package is not required.

Replacing the HelloWorld service

For the sample app we needed a small and representative service definition. So we imagined a fascinating technology where a Modern UI client console communicates with a server to teleport a living creature (a life form) or a group of living creatures (a party) from a location to the server (beam up) and/or from the server to a location (beam down). The matching hardware for our sample app would look like this, with the ASP.NET Core transporter room server in the back, and the UWP client console in front:

TransporterRoom

Setting up the code generation

When working with gRPC-specific .proto files, you’ll need the Grpc.Tools NuGet package. It hosts the compilers and tooling that transform the .proto files into C# or C++. With its 12MB it is relatively big. Fortunately you only need it for code generation so you don’t need to deploy it as part of your app. In its default configuration, the protobuf compiler generates code

  • for server side, client side, or both,
  • when you change something in the .proto files and when you compile the project,
  • in a folder in the /obj region, and
  • without adding the generated files to the Visual Studio project.

Here’s were the code is generated on the server side:

GeneratedServerCode

On the client side, we wanted to customize the process. Using this extremely helpful documentation on configuring gRPC in .NET, we tried to find a Grpc.Tools configuration within the UWP XAML-C# compilation pipeline that keeps the generated client files up to date and part of the project. We didn’t find one, but that’s not a big deal: we’re pretty sure that this will be fixed. Besides, you will most probably keep the .proto files and their C# counterparts in a separate project anyway.

As an example, here’s a configuration that works for the (extra) console app in the sample solution – it’s inside the .proj file:

<ItemGroup>
  <Protobuf Include="..\XamlBrewer.Uwp.Grpc.Server\Protos\startrek.proto" 
            OutputDir="Generated" 
            Link="Protos\startrek.proto" 
            GrpcServices="Client" 
            CompileOutputs="false" />
</ItemGroup>

It generates only the client side code, and stores it in the Generated folder which is part of the project. The UWP Client project then links to these generated C# files. The console app’s .proto files are also linked: to the server project. So when the service contract changes, the UWP client is immediately updated.

Defining the service

For the Transporter Room data structures we used simple messages with only string fields. The ‘Person’ definition in the beginning of this article shows that you can add a lot more complexity if needed. Here are the declarations of the LifeForm and Location messages:

// It's life, Jim, but not as we know it.
message LifeForm {
  string species = 1;
  string name = 2;
  string rank = 3;
}

// A place in space.
message Location {
  string description = 1;
}

The protoc compiler transforms this declaration into C# classes that allow you to create, compare, clone, serialize, deserialize, and parse instances:

MessageClassDiagram

For messages (data structures) the generated code is the same on the server and the client side. For services that’s obviously not the case. Here’s the declaration of the transporter room service:

// Transporter Room API
service Transporter {
  // Beam up a single life form from a location.
  rpc BeamUp(Location) returns (LifeForm) {}

  // Beam up a party of life forms to a location.
  rpc BeamUpParty(Location) returns (stream LifeForm) {}

  // Beam down a single life form, and return the location.
  rpc BeamDown(LifeForm) returns (Location) {}

  // Beam up a party of life forms, and return the location.
  rpc BeamDownParty(stream LifeForm) returns (Location) {} 

  // Replace a beamed down party of life forms by another.
  rpc ReplaceParty(stream LifeForm) returns (stream LifeForm) {}

  // For the sake of API completeness: lock the beam to a location.
  rpc Lock(Location) returns (Location) {}
}

We’ll dive into the methods later, for now just observe the very straightforward syntax. Here’s what the protoc compiler generates for this on the server side. It’s an abstract base class that defines the public methods that you need to implement in your concrete service:

ServerClassDiagram

The ASP.NET Core template comes with the necessary helpers to startup this service and expose its methods.

On the client side the protoc compiler generates a class that resembles to what Visual Studio does with SOAP service references. It’s a client that inherits from ClientBase<T>. You can hook it to a service entry point to call its methods:

ClientClassDiagram

To instantiate a client, you need to open a Channel to the service endpoint, and pass that to its constructor:

private Channel _channel;
private TransporterClient _client;

private async void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    _channel = new Channel("localhost:50051", ChannelCredentials.Insecure);
    _client = new TransporterClient(_channel);
}

Creating a channel is an expensive operation compared to invoking a remote call so in general you should reuse a single channel for as many calls as possible.

Implementing the contracts

We created two helper classes to easily generate random instances of LifeForm and Location to be used as parameter or return value. Here’s part of the Location helper:

public static class Locations
{
    private static Random _rnd = new Random(DateTime.Now.Millisecond);

    public static string WhereEver()
    {
        return _Locations[_rnd.Next(_Locations.Count)];
    }

    private static List<string> _Locations = new List<string>
    {
        "starship USS Enterprise",
        "starship USS Voyager",
        "starship USS Discovery",
        "starbase Deep Space 9",
        "USS Shenzhou",
        "where no man has gone before",
        "planet Vulcan",
        "Starfleet Academy",
        // ...
    };
}

These classes are hosted in a .NET Standard Class Library project that is referenced by client and server projects.

A simple RPC call

In a simple RPC the client sends a request to the server and waits for a response to come back – pretty much a regular function call. To beam up a LifeForm we need to provide its Location:

rpc BeamUp(Location) returns (LifeForm) {}

On the server side we need to override the corresponding abstract method. It takes a Location and an optional ServerCallContext to transport things like authentication context, headers and time-out, and it returns a Task<LifeForm>:

public override Task<LifeForm> BeamUp(Location request, ServerCallContext context)
{
    var whoEver = Data.LifeForms.WhoEver();
    var result = new LifeForm
    {
        Species = whoEver.Item1,
        Name = whoEver.Item2,
        Rank = whoEver.Item3
    };

    return Task.FromResult(result);
}

Here’s how the client creates a location, calls the method, and gets the result:

var location = new Location
{
    Description = Data.Locations.WhereEver()
};

var lifeForm = _client.BeamUp(location);

Pretty straightforward, no?

For the non-streaming RPC calls the compiler also generates an asynchronous version. Here’s an example of a client call that’s using the optional deadline parameter:

var location = await _client.BeamDownAsync(
	lifeForm, 
	deadline: DateTime.UtcNow.AddSeconds(5));

Server-side streaming

In a server-side streaming RPC the client sends a request to the server and gets a stream of messages back. The client reads from the returned stream until there are no more messages. You specify a server-side streaming method by placing the stream keyword before the response type:

rpc BeamUpParty(Location) returns (stream LifeForm) {}

The method on the server side gets the Location instance together with an IServerStreamWriter, and its main job is to write the results (some LifeForms) to that stream:

public override async Task BeamUpParty(
    Location request, 
    IServerStreamWriter<LifeForm> responseStream, 
    ServerCallContext context)
{
    var rnd = _rnd.Next(2, 5);
    for (int i = 0; i < rnd; i++)
    {
        var whoEver = Data.LifeForms.WhoEver();
        var result = new LifeForm
        {
            Species = whoEver.Item1,
            Name = whoEver.Item2,
            Rank = whoEver.Item3
        };

        await responseStream.WriteAsync(result);
    }
}

The method on the client receives an AsyncServerStreamingCall that allows it to iterate over and get the returned LifeForm instances:

var location = new Location
{
    Description = Data.Locations.WhereEver()
};

using (var lifeForms = _client.BeamUpParty(location))
{
    while (await lifeForms.ResponseStream.MoveNext())
    {
        var lifeForm = lifeForms.ResponseStream.Current;
        WriteLog($"- Beamed up {lifeForm.Rank} {lifeForm.Name} ({lifeForm.Species}).");
    }
}

Client-side streaming

In a client-side streaming RPC the client writes a sequence of messages and sends them to the server. Once the client has finished writing the messages, it waits for the server to read them all and return its response. You specify a client-side streaming method by placing the stream keyword before the request type:

rpc BeamDownParty(stream LifeForm) returns (Location) {}

The method on the server can iterate through the provided LifeForm instances by means of an IAsyncStreamReader before returning the Location:

public override async Task<Location> BeamDownParty(
	IAsyncStreamReader<LifeForm> requestStream, 
	ServerCallContext context)
{
    while (await requestStream.MoveNext())
    {
        // ...
    }

    return new Location
    {
        Description = Data.Locations.WhereEver()
    };
}

On the client side, we call the service method stub to receive an AsyncClientStreamingCall with the stream to populate. We indicate completion with a call to CompleteAsync() and then fetch the result with ResponseAsync():

using (var call = _client.BeamDownParty())
{
    foreach (var lifeForm in lifeForms)
    {
        await call.RequestStream.WriteAsync(lifeForm);
    }

    await call.RequestStream.CompleteAsync();

    var location = await call.ResponseAsync;
    WriteLog($"- Party beamed down to {location.Description}.");
}

Bidirectional streaming

In a bidirectional streaming RPC both sides send a sequence of messages using a read-write stream. The two streams operate independently, so client and server can read and write in whatever order they like. The order of messages in each stream is preserved. You specify this type of method by placing the stream keyword before both the request and the response:

rpc ReplaceParty(stream LifeForm) returns (stream LifeForm) {}

It should not come as a surprise that the bidirectional streaming API combines the patterns for client and server side streaming. The two streams operate independently, so client and server can read and write in whatever order that suits your use case. Of course the call itself needs to be initiated by the client, but that doesn’t imply that the client needs to start streaming first. Here’s our client implementation: for each LifeForm that gets beamed up (or ‘streamed down’ if you want), it writes one to the stream to get beamed down:

public async override Task ReplaceParty(
	IAsyncStreamReader<LifeForm> requestStream, 
	IServerStreamWriter<LifeForm> responseStream, 
	ServerCallContext context)
{
    while (await requestStream.MoveNext())
    {
        // var beamedUp = requestStream.Current;
        var beamDown = Data.LifeForms.WhoEver();
        await responseStream.WriteAsync(new LifeForm
        {
            Species = beamDown.Item1,
            Name = beamDown.Item2,
            Rank = beamDown.Item3
        });
    }
}

The server part first writes the party members to beam up to the request stream, and then starts to asynchronously processing the response stream:

// Creating a party.
var rnd = _rnd.Next(2, 5);
var lifeForms = new List<LifeForm>();
for (int i = 0; i < rnd; i++)
{
    var whoEver = Data.LifeForms.WhoEver();
    var lifeForm = new LifeForm
    {
        Species = whoEver.Item1,
        Name = whoEver.Item2,
        Rank = whoEver.Item3
    };

    lifeForms.Add(lifeForm);
}

WriteLog($"Replacing a party.");
using (var call = _client.ReplaceParty())
{
    var responseReaderTask = Task.Run(async () =>
    {
        while (await call.ResponseStream.MoveNext())
        {
            var beamedDown = call.ResponseStream.Current;
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                WriteLog($"- Beamed down {beamedDown.Rank} {beamedDown.Name} ({beamedDown.Species}).");
            });
        }
    });

    foreach (var request in lifeForms)
    {
        await call.RequestStream.WriteAsync(request);
        WriteLog($"- Beamed up {request.Rank} {request.Name} ({request.Species}).");
    };

    await call.RequestStream.CompleteAsync();

    await responseReaderTask;
}

The following screenshot shows how the different actions within a ‘Replace Party’ operation happen in an non-determined sequence:

BidirectionalStreaming

WHEN ?

Half of the used NuGet packages that are required for this solution are still in preview (even .NET Core itself). As long as your UWP app runs on Core CLR (i.e. in Debug mode) everything seems to already work nicely. For Release mode you’ll have to wait for a couple of weeks until the .NET Native Compiler is updated to deal with gRPC. That’s a nice perspective and should not keep you from getting started with using gRPC in your UWP apps.

WHERE ?

The sample app lives here on GitHub.

Enjoy!

Leave a comment