gRPC with protobuf-net in .NET 6
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
:
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 andAddCodeFirstGrpc
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. :)