Playing with gRPC and .NET 6
Create a gRPC server in .NET.
Join the DZone community and get the full member experience.
Join For FreeIn this tutorial, I want to show you, step by step, how you can create a gRPC server in .NET that implements the interface defined in the "proto" file.
In the next image, I mark the items that we will focus on this tutorial.
Prerequisites
I'm using macOs and the next commands are specific for this OS:
- gRPC compiler: To install Protobuf compiler, you can execute this in a terminal:
brew install protobuf
- .NET 6 SDK: Here you can find the links to download and install the .NET 6 SDK
- Visual Studio Code or IDE of your choice
- grpcurl: A command-line tool that provides interaction with gRPC services
brew install grpcurl
- grpcui: builds on top of gRPCurl and adds an interactive web UI for gRPC, similar to tools such as Postman and Swagger UI.
brew install grpcui
Steps
In a general manner, we should follow the next steps:
- Create a project and configure it to generate the server stubs.
- Create the proto file.
- Register the proto file on your project and compile the project.
- Implement the business logic.
- Register the classes on
Program.cs
. - Set up Kestrel to enable HTTP/2 without TLS (on development environment).
- Start the server and test using a client (I will use grpcurl).
CountryGrpcServer
that implements the business logic to search, create, or get a list of countries with some basic data. The CountryGrpcServer.proto
file (described below) declares the remote procedures.
1. Create a Project and Configure It to Generate the Server Stubs
dotnet new grpc -o grpc.country.server -n CountryGrpcServer
The output is something like this:
Here:
-o
parameter is used to define the project directory name:grpc.country.server
.-n
parameter is used to define the project name:CountryGrpcServer
.
2. Create the Proto File
The proto file defines the messages (data structure) and the methods that will be exposed by the server and consume by the client. The next image is a simplified view of the proto file:
For this tutorial, I've create a CountryGrpcServer.proto
file and save it in the Protos
directory located in the project.
vim grpc.country.server/Protos/CountryGrpcServer.proto
Copy the next lines in the file:
syntax = "proto3";
/*The Proto file that has Empty message definition*/
import "google/protobuf/empty.proto";
// Defining the namespace in which the generate classes will be
option csharp_namespace = "Sumaris.Grpc.Services";
// The service name will be used by the compiler when generate the base classes
// Here I declare five procedure
service CountryService{
//Server streaming RPC
rpc getAllCountries(google.protobuf.Empty)
returns (stream Country);
// Unitary RPC
rpc listAllCountries(google.protobuf.Empty)
returns ( CountryList);
// Unitary RPC
rpc findCountryByName( FindCountryByNameRequest )
returns (FindCountryByNameResponse);
// Unitary RPC
rpc createCountry (CountryCreateRequest)
returns (CountryCreateRespopnse);
// Bidrectional streaming RPC
rpc findCountriesByNames( stream FindCountryByNameRequest)
returns (stream Country);
}
message Country{
string name=1;
string capitalCity=2;
float area=3;
}
message CountryList{repeated Country country = 1;}
message FindCountryByNameRequest{string name=1;}
message FindCountryByNameResponse{Country country=1;}
message CountryCreateRequest{ Country country=1;}
message CountryCreateRespopnse{bool created=1;}
3. Register the Proto File on Your Project and Compile the Project
To register the project, you can:
A. Edit the csproj
file and add the next lines.
<ItemGroup>
<Protobuf Include="Protos\CountryGrpcServer.proto" GrpcServices="Server">
<Link>Protos\CountryGrpcServer.proto</Link>
<Access>Public</Access>
<ProtoCompile>True</ProtoCompile>
<CompileOutputs>True</CompileOutputs>
<OutputDir>obj\Debug\net6.0\</OutputDir>
<Generator>MSBuild:Compile</Generator>
</Protobuf>
</ItemGroup>
B. Or, using the Visual Studio Code, follow the next steps:
i. Right-click on Connected Services -> Open service gallery.
ii. Select the option "Create a new service reference of an API for gRPC."
iii. Select the "add" button.
iv. Select the CountryGrpcServer.proto
file created on the Protos
directory of our project and select the class type as Server.
Now, please, open a terminal, move into the project created in step one, and compile the project.
dotnet build
The output looks something like this:
When you build the project, dotnet creates two classes on the $ProjectPath/obj/Debug/net6.0/Protos/
directory. These classes are:
Both classes are in the Sumaris.Grpc.Services
namespace. This is the value of the option csharp_namespace
declared on the proto file.
The CountryGrpcServerGrpc.cs
defines the partial class CountryService
(this is the name of the service declared on the proto file) which contains an abstract inner class called CountryServiceBase
(the name is created using the name of the service plus the word Base).
This abstract class declares the same methods defined on the proto file and will be the base class that we will inherit from to implement our business class.
4. Implement the Business Logic
Now, we will create a class that has our business logic. This class has to extend from the abstract base class Sumaris.Grpc.Services.CountryService.CountryServiceBase
.
I've create the class BusinessCountryService
on the file located in Services subdirectory with the next lines:
using System;
using Sumaris.Grpc.Services;
using Google.Protobuf.WellKnownTypes;
using static Sumaris.Grpc.Services.CountryService;
using Grpc.Core;
namespace CountryGrpcServer.Services
{
public class BusinessCountryService: CountryServiceBase
{
private static List<Country> countries;
static BusinessCountryService()
{
countries = new List<Country>();
countries.Add(new Country() { Area = 1285216.60f, CapitalCity = "Lima", Name = "Perú" });
countries.Add(new Country() { Area = 756102.4f, CapitalCity = "Santiago de Chile", Name = "Chile" });
countries.Add(new Country() { Area = 283561f, CapitalCity = "Quito", Name = "Ecuador" });
countries.Add(new Country() { Area = 1141748f, CapitalCity = "Bogotá", Name = "Colombia" });
countries.Add(new Country() { Area = 2791820f, CapitalCity = "Buenos Aires", Name = "Argentina" });
}
private readonly ILogger<GreeterService> _logger;
public BusinessCountryService(ILogger<GreeterService> logger)
{
_logger = logger;
}
/** Server streamin RPC procedure
* rpc getAllCountries(google.protobuf.Empty) returns(stream Country)
* We use the async and await
*/
public override async Task getAllCountries(Empty request,IServerStreamWriter<Country> responseStream, ServerCallContext context) {
foreach (var t in countries) { await responseStream.WriteAsync(t); }
}
/** Unitary RPC procedure
* rpc listAllCountries(google.protobuf.Empty) returns ( CountryList);
*/
public override Task<CountryList> listAllCountries( Empty request,ServerCallContext context)
{
var r = new CountryList();
r.Country.AddRange(countries);
countries.ForEach(t => r.Country.Add(t));
return Task.FromResult(r);
}
/** Unitary RPC procedure
* rpc findCountryByName( FindCountryByNameRequest ) returns (FindCountryByNameResponse);
*/
public override Task<FindCountryByNameResponse> findCountryByName(FindCountryByNameRequest request,ServerCallContext context)
{
var r = new FindCountryByNameResponse();
r.Country = countries.Find(c => c.Name.ToLower().Equals(request.Name.ToLower()));
return Task.FromResult(r);
}
/** Unitary RPC procedure
* rpc createCountry (CountryCreateRequest) returns (CountryCreateRespopnse);
*/
public override Task<CountryCreateRespopnse> createCountry(CountryCreateRequest request,ServerCallContext context)
{
countries.Add(request.Country);
var r = new CountryCreateRespopnse();
r.Created = true;
return Task.FromResult(r);
}
/** Biderectional streaming RPC
* rpc findCountriesByNames( stream FindCountryByNameRequest) returns (stream Country);
*/
public override async Task findCountriesByNames(IAsyncStreamReader<FindCountryByNameRequest> requestStream,
IServerStreamWriter<Country> responseStream,ServerCallContext context){
await foreach (var t in requestStream.ReadAllAsync())
{
var r = countries.Find(c => c.Name.ToLower().Equals(t.Name.ToLower()));
if (r is not null)
{
await responseStream.WriteAsync(r);
}
}
}
}
}
Please note that if the server has to response in a streaming manner, the method has to be async and use the await word when you write asynchronously in the output channel.
If the client send a stream request, you have to you use the await word to read asynchronously and the method have to be marked as async again.
5. Register the Classes on Program.cs
The next step is register our business class on the web application using the method MapGrpcService
of the WebApplication instance.
Please open the Programs.cs
file and copy the next:
app.MapGrpcService<BusinessCountryService>();
6. Set Up Kestrel to Enable HTTP/2 Without TLS (on Development Environment)
Kestrel doesn’t support HTTP/2 with TLS on macOS systems, and we need to turn off TLS (you don't have to do this for the production environment, however, because you should use TLS).
Please copy the next lines on the Program.cs
:
builder.WebHost.ConfigureKestrel(options =>
{
// Setup a HTTP/2 endpoint without TLS.
options.ListenLocalhost(5000, o => o.Protocols =
HttpProtocols.Http2);
});
After all, the Program.cs
file should be as follows:
using CountryGrpcServer.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
// Add services to the container.
builder.Services.AddGrpc();
builder.WebHost.ConfigureKestrel(options =>
{
// Setup a HTTP/2 endpoint without TLS.
options.ListenLocalhost(5000, o => o.Protocols =
HttpProtocols.Http2);
});
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapGrpcService<BusinessCountryService>();
app.Run();
7. Start the Server and Test Using a Client (I Will Use grpcurl)
Open a terminal, move into the project, and execute the next command:
dotnet run
Execute the next command in a terminal (Move into the project):
grpcurl -d '{"name":"Peru"}{"name":"Ecuador"}{"name":"Chile"}' \
-proto ./Protos/CountryGrpcServer.proto \
-plaintext localhost:5000 \
CountryService/findCountriesByNames
Note that I'm using a relative path to the CountryGrpcServer.proto
file.
Now, the server returns:
Conclusion
We used the Protocol buffer files to generate the server implementation in .NET and the async/await to manage streaming server or client.
Feel free to let me know if you have any questions or feedback.
In my next article, I will show you how we can create a gRPC client using .NET.
Opinions expressed by DZone contributors are their own.
Comments