In this post, I want to introduce my very early experience (after a few hours of experimentation) of gRPC and ASP.NET Core 3.0. I’ve conducted some experiments as part of our quarterly Madgex hack day. This will be an introductory post so I don’t expect to show everything in fine detail. This is intended to give an overview of how the main pieces fit together and I’ll hopefully then dive into the details in future posts.
What is gRPC?
gRPC is a schema-first framework initially created by Google. It supports service to service communication over HTTP/2 connections. It uses the Protobuf wire transfer serialisation for lightweight, fast messaging between the services. As more and more of us build interconnected microservices it can sometimes be a pain using REST as our protocol over HTTP for such purposes. REST was designed for human-readable data transfer and often results in a fair amount of boiler-plate code on both the client and server services to implement.
gRPC allows us to define our message schema and “contract” up front using Protocol Buffers. This file is then used to automatically generate much of the client and server code on our behalf. It is an interesting technology and looks well placed to become a popular choice when working with microservices. For my hack day project, I wanted to take it for a quick spin and see if I could get a client and server working over gRPC.
NOTE: The information in this post is therefore based on early code and has the potential to change during the remaining previews and after release. If you’re reading this in the distant future you may want to look for updated blog posts from me!
Getting Started
Microsoft is actively contributing to the “gRPC for .NET” repository to support gRPC in the ASP.NET Core 3.0 timeframe. Currently, this is a work in progress, there is no formal NuGet package available and it depends on some features not yet available in the current ASP.NET Core 3.0 preview 2 release.
To support working with the newest bits, I found that I first needed to download the latest “preview 3 “daily build using the links provided on GitHub.
The sample “domain” for my hack was to develop a basic client data API which would be exposed as a gRPC server.
Creating the Protobuf Service Descriptor
gRPC uses a .proto (Protobuf) file to define the shape of your service contract. This contains the “schema” for the messages that will be sent between the exposed services.
My initial proto file looks like this:
syntax = "proto3";
package ClientPropertyEndpoint;
service ClientProperty {
rpc GetProperty (PropertyRequest) returns (PropertyReply) {}
}
message PropertyRequest {
string propertyId = 1;
}
message PropertyReply {
string clientName = 1;
string organisationName = 2;
string productName = 3;
}
For now, I’ve defined a single RPC service interface called GetProperty. This accepts a PropertyRequest and returns a PropertyReply which are defined underneath the service.
Messages are defined using scalar types by providing the type, a name and then a unique number for the field. These uniquely identify the fields as they’ll appear in the binary message format and should not change once defined.
For example, my PropertyRequest message has a single string value called “propertyId” and I’ve assigned it a value of 1.
Creating the Server
The next step is to create an ASP.NET Core 3.0 server which will use code automatically generated from the proto file to reduce the amount of code we need to add. We can stub out the functionality over the generated code, which handles the serialisation and communication.
I created a standard ASP.NET Core 3.0 API project and then updated the csproj file in line with the available example in the grpc-dotnet repo.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="..\..\Proto\*.proto" GrpcServices="Server" />
<Content Include="@(Protobuf)" LinkBase="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.6.1" />
<PackageReference Include="Grpc.Tools" Version="1.19.0-pre1" PrivateAssets="All" />
<PackageReference Include="MediatR" Version="6.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="6.0.1" />
</ItemGroup>
</Project>
Without diving too deep this references the .proto file and the Grpc.Tools library in order to support the code generation work.
The next step is create a class derived from the generate code base class which once completed looks like this:
using Grpc.Core;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using ClientPropertyEndpoint;
using MediatR;
namespace Server
{
public class PropertyService : ClientProperty.ClientPropertyBase
{
private readonly ILogger _logger;
private readonly IMediator _mediator;
public PropertyService(ILoggerFactory loggerFactory, IMediator mediator)
{
_logger = loggerFactory.CreateLogger<PropertyService>();
_mediator = mediator;
}
public override async Task<PropertyReply> GetProperty(PropertyRequest request, ServerCallContext context)
{
_logger.LogInformation($"Handling request for property Id '{request.PropertyId}'");
var property = await _mediator.Send(new ClientPropertyQuery(request.PropertyId));
if (property is null)
context.Status = new Status(StatusCode.NotFound, $"Property Id '{request.PropertyId}' was not found");
return property ?? new PropertyReply();
}
}
}
You can see the using statement in line 4 which matches the name of the package as defined in the proto file.
A static class ClientProperty has been generated which includes a sub-class called ClientPropertyBase which is what we derive from.
I can then override the base GetProperty method, which is so named because the proto file defines a service which includes that interface. It accepts a PropertyRequest type and returns a Task<PropertyReply>. Both of these have been code generated for me based on the proto file.
The only code I need to add here is the logic for fulfilling the request. In this sample, I have an in-memory store of some basic test data which I query using MediatR. It’s not too important how those bits work and you can code this however you need.
The program.cs for this API looks like this:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.Limits.MinRequestBodyDataRate = null;
options.ListenLocalhost(50051, listenOptions =>
{
// later we'll use SSL
listenOptions.Protocols = HttpProtocols.Http2;
});
})
.UseStartup<Startup>();
});
}
This using the new ASP.NET Core 3.0 generic host flow to register a web host which uses Kestrel. Kestrel is set up to listen on HTTP2 on port 50051.
The final bit I’ll show is the Startup class which looks like this:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ClientPropertyStore>();
services.AddGrpc();
services.AddMediatR(typeof(Startup).GetTypeInfo().Assembly);
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting(routes =>
{
routes.MapGrpcService<PropertyService>();
});
}
}
This calls the AddGrpc extension on the IServiceCollection, available for now due to the Grpc.AspNetCore.Server code I copied in. Later this will be built in a NuGet library.
It then uses the new ASP.NET Core 3.0 routing middleware to map a GrpcService route to the PropertyService. This will allow the server to handle the gRPC requests.
Creating the Client
To send request to the server I created a basic .NET Core console app client. The csproj file for this also had a reference to the proto file so that the client code could be generated.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="..\..\Proto\ClientProperty.proto" GrpcServices="Client" />
<Content Include="@(Protobuf)" LinkBase="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.6.1" />
<PackageReference Include="Grpc.Core" Version="1.19.0-pre1" />
<PackageReference Include="Grpc.Tools" Version="1.19.0-pre1" PrivateAssets="All" />
</ItemGroup>
</Project>
The only code needed lives in the Main method in this sample:
static async Task Main(string[] args)
{
Console.WriteLine("Starting GRPC Client...");
Console.WriteLine();
var channel = new Channel("localhost:50051", SslCredentials.Insecure);
var client = new ClientPropertyEndpoint.ClientProperty.ClientPropertyClient(channel);
try
{
var response = await client.GetPropertyAsync(new PropertyRequest { PropertyId = "ac516792-94fc-4e0c-b39c-59b2fcd58a4e" });
Console.WriteLine("Org Name: " + response.OrganisationName);
Console.WriteLine("Client Name: " + response.ClientName);
Console.WriteLine("Product Name: " + response.ProductName);
}
catch (RpcException ex)
{
if (ex.StatusCode == StatusCode.NotFound)
{
Console.WriteLine(ex.Status.Detail);
}
}
Console.WriteLine();
Console.WriteLine("Shutting down");
channel.ShutdownAsync().Wait();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
First we establish a gRPC channel to the server. For this sample I’m using insecure communication. I’ll show how SSL can be applied in a future post.
I then create a ClientPropertyClient which is a class created by the code gen process based on the proto file.
I can then call methods on the client which map to the methods as defined in the proto file. I simply create a PropertyRequest object (again, the class is from generated code) and send it via the client.
Here I write out the values from the PropertyResponse object to the console. Good enough for now!
I also handle a RpcException which is thrown if the server sends a NotFound status for example. I’ve not explored this too deeply so there may be better approaches to achieve this.
And that’s basically it. I can fire up the server and then fire the client up which will send a request over HTTP2 and gRPC. Awesome!
Summary
This has been a high-level overview of gRPC, after I’ve only spent a few hours learning how it works. I hope it serves to show one of the big benefits of the gRPC proto file which is the fact that we have to write very little boiler-plate code. There are no Controllers to define and even the models can be easily generated for both the client and server. On the client side, I don’t need to construct HTTP requests and fire them. I can simply make a call to a code generated client method, passing a simple request message.