.NET Tools Coding Concepts How-To's Tutorials

Getting Started with ASP.NET Core and gRPC

As developers, we find ourselves in a renaissance of API design philosophies. There is REST, HTTP APIs, GraphQL, SOAP, and the list continues to grow. Given all the choices, we can understand that it can be difficult to tell which one to use.

In this post, we’d like to introduce you to gRPC, one of the newer approaches .NET developers have in designing their APIs. The method is gaining popularity due to its efficiency, contract-based design, interoperability across technology stacks, and code-generation tooling.

By the end of this post, you’ll have an idea of what gRPC is, what makes it work, and how to implement an ASP.NET Core client/server demo. At which point, you’ll have the tools necessary to decide if the approach is right for you.

Let’s get started!

What is gRPC?

gRPC, which stands for google Remote Procedure Calls, is an architectural service pattern that helps developers build and consume distributed services in the same style as invoking in-process object methods. The goal of gRPC is to make distributed applications and services more manageable for both clients and server implementations through a predefined set of interfaces.

gRPC is a modern open-source, high-performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking, and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications, and browsers to backend services. — gRPC About Page

gRPC is technology stack agnostic, supporting client and server languages like Java, Go, Python, Ruby, C#, and F#. In total, there are ten client library language implementations of gRPC. The approach allows for a diverse system of solutions, utilizing each ecosystem’s best to deliver overall value. Its interoperability comes from embracing ubiquitous technologies such as HTTP/2, Protocol Buffer (Protobuf), and other formats.

Advantages to gRPC include the following:

  • Ideal for low latency, highly scalable, distributed systems.
  • Designing services and messages are separate from the implementation.
  • Layered designs allow for extensibility.
  • Support for high-level concepts like streaming, synchronous, asynchronous, cancellations, and timeouts.

gRPC is already in wide use across organizations and existing products like Netflix, Cloudflare, Google, and Docker. Many have adopted gRPC due to its ability to provide Bi-directional streaming with HTTP/2 in a consistent yet language-idiomatic way. Leveraging HTTP/2, many adopters of gRPC also get TLS based security which adds a layer of protection to a distributed system. Being on a protocol such as HTTP/2 also affords gRPC adopters the ability to add telemetry and diagnostic tools to their hosting platforms, whether on-premises or in a cloud environment.

In summary, gRPC is a way to build distributed systems quickly that focus on interoperability and performance. While third-parties can access these services, gRPC systems are built frequently for internal distributed systems.

What is Protocol Buffers

While gRPC does support multiple serialization formats, the default and most commonly used is Protocol Buffers. Protocol Buffers is an open-source serialization structure with an emphasis on efficiency. We can define messages using the protocol buffer language, which currently has two public variants of proto2 and proto3.

The language itself contains constructs for messages, scalar value types, optional and default values, enumerations, and much more. We can usually find definitions for protocol buffer in a .proto file. All JetBrains IDEs have protocol buffer support through the Protocol Buffer Editor plugin.

Let’s start with a basic message definition of SearchRequest.

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

We can see that there are three properties within our message definition of query, page_number, and result_per_page. These properties also vary in their requirement to be specified by the caller. Finally, we notice the field number assignments at the end of each line. These numbers define our data’s order during the encoding process, known as message binary format. Knowing the orderer of our data, Protobuf reduces the need for additional markers for field names, as can be found in other formats like JSON or XML. That also means that once a message format is in use, the order of fields must not change.

Protobuf allows us to define our messages, data, and services. As we’ll see in the next sections, C# tooling can take advantage of the .proto files to generate much of the boilerplate associated with building services in other API approaches.

gRPC and .NET example

The folks on the .NET team have been hard at work bringing gRPC to the .NET community with first-class support. In the rest of this blog post, we’ll create an independent gRPC demo containing both a server and a client. I recommend installing the Protobuf plugin mentioned previously.

Our first step is to create a new ASP.NET Core application using the Empty template.

ASP.NET Core empty web application template in JetBrains Rider

When we have our development environment initialized, we’ll need to add a few NuGet packages. These packages will help generate our gRPC base classes used to implement our messages and services. To quickly add all the necessary packages, paste the following ItemGroup into your .csproj file.

<ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.15.5" />
    <PackageReference Include="Grpc.AspNetCore" Version="2.35.0" />
    <PackageReference Include="Grpc.Net.Client" Version="2.35.0" />
    <PackageReference Include="Grpc.Tools" Version="2.36.1">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
</ItemGroup>

You may also install the following packages using the NuGet tool:

  • Google.Protobuf
  • Grpc.AspNetCore
  • Grpc.Net.Client
  • Grpc.Tools

Our next step is to add a new Protos folder under our project and create a new empty file named greet.proto.

Creating a proto file inside of JetBrains Rider

We’ll use the Protobuf definition language to map out our gRPC service, a request, and a response.

syntax = "proto3";

option csharp_namespace = "GrpcDemo";

// 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;
}

Protobuf also has an option keyword that allows for additional metadata definitions that our generators can use to create idiomatic code. In this case, the gRPC tools will generate our base classes within the GrpcDemo namespace.

The final step is to utilize the generators during our build process. Luckily, it is as straightforward as adding an element to our .csproj file.

<ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server,Client" />
</ItemGroup>

We should notice the GrpcServices attribute. It tells the tool which side our current application will be on, server or client. In our case, we’ll be generating both to create our demo.

The gRPC service

Let’s start with implementing our gRPC service. Looking at our previous definition, we can see a single SayHello method. In our project, we can add a new C# class. We’ll be hosting this service in ASP.NET Core, which means we’ll have access to all the features that come with ASP.NET Core, such as dependency injection and services.

In a new C# file, we create a Service class that derives from Greeter.GreeterBase. If this class is not available, be sure to build your solution first, it will be available after our first compilation.

namespace GrpcDemo
{
    public class Service : Greeter.GreeterBase
    {

    }
}

Typing override within the class, we’ll see the method definitions found in our greet.proto file.

Let’s implement the Service and its SayHello method.

using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Logging;

namespace GrpcDemo
{
    public class Service : Greeter.GreeterBase
    {
        private readonly ILogger<Service> _logger;
        public Service(ILogger<Service> logger)
        {
            _logger = logger;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = "Hello " + request.Name
            });
        }
    }
}

The gRPC client

We’ll leverage ASP.NET Core’s background service to host a client local to our gRPC service. While we are hosting both client/server within the same process, they’ll still communicate over the HTTP/2 protocol.

We’ll start by creating a new C# class called Client, and it will also derive from the BackgroundService class, allowing us to host it in ASP.NET Core’s hosted service infrastructure.

using System;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Net.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace GrpcDemo
{
    public class Client : BackgroundService
    {
        private readonly ILogger<Client> _logger;
        private readonly string _url;

        public Client(ILogger<Client> logger, IConfiguration configuration)
        {
            _logger = logger;
            _url = configuration["Kestrel:Endpoints:gRPC:Url"];
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            using var channel = GrpcChannel.ForAddress(_url);
            var client = new Greeter.GreeterClient(channel);

            while (!stoppingToken.IsCancellationRequested)
            {
                var reply = await client.SayHelloAsync(new HelloRequest
                {
                    Name = "Worker"
                });

                _logger.LogInformation("Greeting: {reply.Message} -- {DateTime.Now}");
                 await Task.Delay(1000, stoppingToken);
             }
         }
     }
 }

We can see the essential client elements happen within the ExecuteAsync method of our worker service.

  1. We create a new gRPC channel using our URL.
  2. We use the generated client definition and passing in the channel.
  3. We call our SayHelloAsync method with our HelloRequest contract.

Now that we have our server and client implemented let’s wire them up in ASP.NET Core.

ASP.NET Core setup

We have two places we’ll need to add our client and server implementations. Let’s start with the server since it’s likely where most folks will start with gRPC.

In our web project’s Startup file, we need to register gRPC with our services collection. In the ConfigureServices method, we need to invoke the AddGrpc extension method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();
}

In our Configure method, we need to register the endpoints for our Service implementation. We can register gRPC service using the MapGrpcService extension method used on the IEndpointRouteBuilder interface.

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); });

    endpoints.MapGrpcService<Service>();
});

Our service is ready to start receiving gRPC client requests. Let’s set up our client now. In our Program file, we need to add our Client as a hosted service. Let’s modify our CreateHostBuilder method to do just that.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace GrpcDemo
{
    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.UseStartup<Startup>();
                })
                .ConfigureServices(svc =>
                {
                    svc.AddHostedService<Client>();
                });
    }
}

This next step is optional depending on our development environment. The .NET gRPC implementation has some development caveats with TLS on macOS not being supported and the development ASP.NET Core certificate trust level on Linux devices. The .NET team will address these issues in future versions of .NET. Please modify the appsettings.json file to have a working demo across all OS platforms.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Endpoints" : {
      "Http" : {
        "Url" : "https://localhost:5001",
        "Protocols": "Http1AndHttp2"
      },
      "gRPC": {
        "Url": "http://localhost:5000",
        "Protocols": "Http2"
      }
    }
  }
}

We are configuring Kestrel, ASP.NET Core’s server, to listen only over HTTP for gRPC calls and to use the HTTP/2 protocol.

OK, we’re ready to run our demo! The application should start, and our background service will begin calling our gRPC service. We’ll see the results in our console output window.

JetBrains Rider console output

Congratulations! We did it. We just successfully built a client/server gRPC demo.

Endpoint Debugger

Eagle-eyed ASP.NET Core developers may have noticed that we host gRPC services within ASP.NET Core using endpoints. To confirm this is the face, we can modify our root path endpoint to return a result set of all registered endpoints. Replace the existing “Hello, World” code with the following implementation. You may need to add references to the Microsoft.AspNetCore.Routing namespace.

endpoints.MapGet("/", async context =>
{
    var endpointDataSource = context
        .RequestServices.GetRequiredService<EndpointDataSource>();

    await context.Response.WriteAsJsonAsync(new
    {
        results = endpointDataSource
            .Endpoints
            .OfType<RouteEndpoint>()
            .Where(e => e.DisplayName?.StartsWith("gRPC") == true)
            .Select(e => new
            {
                name = e.DisplayName, 
                pattern = e.RoutePattern.RawText,
                order = e.Order
            })
            .ToList()
    });
});

Rerunning our application, we’ll see all the gRPC endpoints registered with ASP.NET Core.

// 20210310105244
// https://localhost:5001/

{
  "results": [
    {
      "name": "gRPC - /Greeter/SayHello",
      "pattern": "/Greeter/SayHello",
      "order": 0
    },
    {
      "name": "gRPC - Unimplemented service",
      "pattern": "{unimplementedService}/{unimplementedMethod}",
      "order": 0
    },
    {
      "name": "gRPC - Unimplemented method for Greeter",
      "pattern": "Greeter/{unimplementedMethod}",
      "order": 0
    }
  ]

You’ll notice the path pattern for the gRPC endpoint is a simple literal string. The gRPC approach uses this technique to avoid parsing complex URL paths and query strings, bypassing the overheads with parsing a complicated URI.

To see the completed demo, you can head over to my GitHub repository, where you can fork the solution in its entirety.

Conclusion

gRPC grew out of a need to build interoperability across a distributed services system, not by reinventing the wheel but by embracing existing technologies. Its strength comes from efficient and standardized internal services, with a programming model similar to in-process object calls. gRPC and its binary serialization are ideal for large data transfer across the network. Simultaneously, the technology’s adoption of HTTP/2 allows for natural extension points and existing tooling to help understand the function of a gRPC system.

Multi-language development teams adopting a microservices approach will find benefits in standardizing their communication across service boundaries. Since we love .NET, developers will find the experience enjoyable with excellent generation tools that make building and consuming gRPC definitions straightforward and familiar. Finally, the integration with ASP.NET Core enables development teams to ease into gRPC adoption and apply it at their own pace.

We hope this post has been a fun read. Let us know if you’ve already adopted gRPC, are thinking about it, or don’t think it’s right for you and your development team.

References

image description

Discover more