gRPC with protobuf-net in .NET 6

2022, Jul 21

A code-first/contract-first approach with gRPC is available with protobuf-net in .NET 6. When starting a gRPC template in Visual Studio 2022, proto files are provisioned by default, but if you have already C# models/contracts defined, you can make use of protobuf-net.

Protobuf

protobuf-net is an implementation on top of protobuf, a protocol buffer that provides a language-neutral, platform-neutral, extensible mechanism for serializing structured data that generates native language bindings.

For more details on gRPC and Protobuf, check the article gRPC in .NET 6

protobuf-net.Grpc

protobuf-net.Grpc is a NuGet package that combines protobuf-net with Grpc.Net, providing:

  • support for the managed Kestrel (server) and HttpClient (client) HTTP/2 bindings on .NET Core 3.1 and above
  • code-first or contract-first approach
  • with code-first support, use any .NET language (C#, VB, F#)
  • it works with the standard (unmanaged) Grpc.Core implementation if you are limited to .NET Framework (.NET Framework 4.6.1 or .NET Standard 2.0).

For more details check the protobuf-net website

Solution

The following solution was created to exemplify the implementation with protobuf-net:

protobuf-net

Check this sample in the PlayGoKids repository.

Contracts

On the article gRPC in .NET 6 I used the proto files user.proto, address.proto. In this example, I have converted them into C# models/contracts and services for comparison purposes.

File: User.cs

using System.Runtime.Serialization;

namespace GrpcDomain.Models
{
    [DataContract]
    public class User
    {
        [DataMember(Order = 1)]
        public int Id { get; private set; }

        [DataMember(Order = 2)]
        public string FirstName { get; private set; }

        [DataMember(Order = 3)]
        public string LastName { get; private set; }

        [DataMember(Order = 4)]
        public Address Address { get; set; }

        public User() { }

        public User(int id, string firstName, string lastName, Address address)
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            Address = address;
        }
    }
}

File: Address.cs

using System.Runtime.Serialization;

namespace GrpcDomain.Models
{
    [DataContract]
    public class Address
    {
        [DataMember(Order = 1)]
        public string Number { get; private set; }

        [DataMember(Order = 2)]
        public string Street { get; private set; }

        [DataMember(Order = 3)]
        public string Suburb { get; private set; }

        [DataMember(Order = 4)]
        public string City { get; private set; }

        [DataMember(Order = 5)]
        public string Country { get; private set; }

        public Address() { }

        public Address(string number, string street, string suburb, string city, string country)
        {
            Number = number;
            Street = street;
            Suburb = suburb;
            City = city;
            Country = country;
        }
    }
}

Note the C# attributes:

  • DataContract: Used to serialize the class.
  • DataMember: Used to serialize the property. Used by specifying message attribute Order.

These are called data contracts, a formal agreement between a service and a client that abstractly describes the data to be exchanged, widely used by Windows Communication Foundation (WCF).

New proto attributes are also available:

File: HelloRequest.cs

using ProtoBuf;

namespace GrpcDomain.Requests
{
    [ProtoContract]
    public class HelloRequest
    {
        [ProtoMember(1)]
        public string Name { get; set; }
    }
}

Note the C# attributes:

  • ProtoContract: Used to serialize the class.
  • ProtoMember: Used to serialize the property. Used by specifying message attribute Order (implicitly).

Services

The services can be created using ServiceContract and OperationContract attributes, or the new Service and Operation attributes part of protobuf-net.Grpc.

File: IUserService.cs

using System.ServiceModel;
using GrpcDomain.Requests;
using GrpcDomain.Responses;

namespace GrpcDomain.Interfaces
{
    [ServiceContract]
    public interface IUserService
    {
        [OperationContract]
        Task<UserResponse> GetUserAsync(UserRequest request);
    }
}

File: IGreeterService.cs

using GrpcDomain.Requests;
using GrpcDomain.Responses;
using ProtoBuf.Grpc.Configuration;

namespace GrpcDomain.Interfaces
{
    [Service]
    public interface IGreeterService
    {
        [Operation]
        Task<HelloResponse> GetGreetingAsync(HelloRequest request);
    }
}

Nuget packages

To make this possible the following NuGet packages were used:

  • Grpc.Net.Client (used on Client project)
  • protobuf-net (used on Domain project)
  • protobuf-net.Grpc (used on Client and Domain project)
  • protobuf-net.Grpc.AspNetCore (used on Service project)

Running the service

We are running both the Client and Server through the command line.

Server:

using System.Net;
using GrpcService.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using ProtoBuf.Grpc.Server;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Any, 5000, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
    });
});

builder.Services.AddCodeFirstGrpc(config =>
{
    config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal;
});

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<UserService>();
app.MapGrpcService<GreeterService>();

app.Run();

Note that HttpProtocols.Http2 must be specified with Kestrel and AddCodeFirstGrpc needs to be attached to the ServiceCollection.

Client:

using System.Text.Json;
using Grpc.Net.Client;
using GrpcDomain.Interfaces;
using GrpcDomain.Requests;
using ProtoBuf.Grpc.Client;

var channel = GrpcChannel.ForAddress("http://localhost:5000");

var client1 = channel.CreateGrpcService<IGreeterService>();
var greeting = await client1.GetGreetingAsync(new HelloRequest() { Name = "Bill" });

Console.WriteLine(greeting.Message);

var client2 = channel.CreateGrpcService<IUserService>();
var userDetails = await client2.GetUserAsync(new UserRequest() { Id = 1 });

Console.WriteLine(JsonSerializer.Serialize(userDetails));
Console.WriteLine("Press any key to exit...");
Console.ReadKey();

From here run the server and launch the client. Bob is your uncle. :)