https://github.com/eduardsergeev/greeterservice
Example of gRPC/Protobuf code generation
https://github.com/eduardsergeev/greeterservice
code-first code-generation dotnet grpc protobuf
Last synced: 2 months ago
JSON representation
Example of gRPC/Protobuf code generation
- Host: GitHub
- URL: https://github.com/eduardsergeev/greeterservice
- Owner: EduardSergeev
- License: mit
- Created: 2023-02-21T15:59:52.000Z (over 3 years ago)
- Default Branch: master
- Last Pushed: 2024-07-08T09:45:59.000Z (almost 2 years ago)
- Last Synced: 2024-07-09T09:39:29.687Z (almost 2 years ago)
- Topics: code-first, code-generation, dotnet, grpc, protobuf
- Language: C#
- Homepage: https://www.nuget.org/packages/IndependentReserve.Grpc.Tools#readme-body-tab
- Size: 12.3 MB
- Stars: 0
- Watchers: 4
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# gRPC/Protobuf code generation example
[MSDN gRPC example](https://learn.microsoft.com/en-us/aspnet/core/grpc/) implemented using [IndependentReserve.Grpc.Tools](https://www.nuget.org/packages/IndependentReserve.Grpc.Tools/#readme-body-tab) package
[](https://github.com/EduardSergeev/GreeterService/actions?query=workflow%3Abuild+branch%3Amaster)
[](https://eduardsergeev.github.io/GreeterService/ubuntu-latest/results/SingleDto-report.html)
[](https://eduardsergeev.github.io/GreeterService/windows-latest/results/SingleDto-report.html)
## The purpose of the package
[IndependentReserve.Grpc.Tools](https://www.nuget.org/packages/IndependentReserve.Grpc.Tools) adds code-first way to implement gRPC services using [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools).
`Grpc.Tools` requires service and message contracts to be defined in [Protobuf](https://en.wikipedia.org/wiki/Protocol_Buffers) to generate C# message classes and service stubs. However since Protobuf is not native to .NET this requirement increases the complexity of the code and often requires ad-hoc solutions for data conversion between generated gRPC/Protobuf code and the rest of the system code.
`IndependentReserve.Grpc.Tools` on the other hand generates all Protobuf definition required by `Grpc.Tools` from a plain .NET ([POCO](https://en.wikipedia.org/wiki/Plain_old_CLR_object)) interface and POCO [DTO](https://en.wikipedia.org/wiki/Data_transfer_object)'s referenced by the interface methods. It also generates gRPC service and client classes which internally use generated by `Grpc.Tools` service and client code but operate with the original DTO (gRPC-agnostic) classes.
## Example structure
### Client & service code
This example uses [IndependentReserve.Grpc.Tools](https://www.nuget.org/packages/IndependentReserve.Grpc.Tools) (the _tool_) to generate gRPC code from a plain .NET interface (the _source interface_): simple [IGreeterService](./Greeter.Common/IGreeterService.cs) which is equivalent to [MSDN example](https://learn.microsoft.com/en-us/aspnet/core/grpc/) and more involved [IGreeterExtendedService.cs](./Greeter.Common/IGreeterExtendedService.cs) which instead of `string` type parameters uses a [set of DTO](./Greeter.Common/Dto) classes.
Here is how `IGreeterExtendedService` source interface is defined:
```c#
public interface IGreeterExtendedService
{
Greeting SayGreeting(Person person);
}
```
Referenced DTO definitions:
```c#
public readonly record struct Person
(
Name Name,
List OtherNames,
string[] Aliases,
Details Details
);
public readonly record struct Name
(
Title Title,
string FirstName,
string LastName,
string? MiddleName = null
);
public enum Title
{
Mr, Mrs, Miss, Ms, Sir, Dr
}
public readonly record struct Details
(
DateTime DateOfBirth,
double Height,
decimal Length,
Address[] Addresses
);
public readonly record struct Address
(
string[] Street,
string City,
string? State,
uint? Postcode,
string? Country
);
public readonly record struct Greeting
(
string Subject,
IEnumerable Lines
);
```
gRPC/Protobuf code (both service and client) is generated by the tool during the build in _target project_ [Greeter.Grpc](./Greeter.Grpc). This project contains only [Greeter.Grpc.csproj file](./Greeter.Grpc/Greeter.Grpc.csproj) which:
- [Contains](./Greeter.Grpc/Greeter.Grpc.csproj#L9) `PackageReference` to the tool's NuGet package:
```xml
```
- [Marks](./Greeter.Grpc/Greeter.Grpc.csproj#L12) dependent source project with `GenerateGrpc` attribute:
```xml
```
which forces the tool to generate gRPC code for all source interfaces found `Greeter.Common` project
Generated gRPC code is automatically included into the build pipeline. Generated code contains a set of `*.proto` and `*.cs` files but in practice developer only needs to know about two C# classes:
- `GreeterExtendedServiceGrpcService`: gRPC service class which derives from generated by `Grpc.Tools` service stub class `GreeterExtendedServiceBase` and can be directly hosted in ASP.NET app
Generated service class content:
```c#
public partial class GreeterExtendedServiceGrpcService : Greeter.Common.Grpc.GreeterExtendedService.GreeterExtendedServiceBase
{
private readonly ILogger _logger;
private readonly IGreeterExtendedService _greeterExtendedService;
public GreeterExtendedServiceGrpcService(
ILogger logger,
IGreeterExtendedService greeterExtendedService)
{
_logger = logger;
_greeterExtendedService = greeterExtendedService;
}
public override async Task SayGreeting(SayGreetingRequest request, ServerCallContext context)
{
var args = MapperTo>.MapFrom(new { Item1 = request.Person });
var result = _greeterExtendedService.SayGreeting(@person: args.Item1);
return MapperTo.MapFrom(new { Result = result });
}
}
```
- `GreeterExtendedServiceGrpcClient`: gRPC client class which implements `IGreeterExtendedService` by calling service via gRPC using generated by `Grpc.Tools` `GreeterExtendedServiceClient` client class
Generated client class content:
```c#
public partial class GreeterExtendedServiceGrpcClient : GrpcClient, IGreeterExtendedService
{
private readonly Lazy _client;
public GreeterExtendedServiceGrpcClient(IGrpcServiceConfiguration config, bool useGrpcWeb = true)
: base(config, useGrpcWeb)
{
var invoker = Channel.CreateCallInvoker();
SetupCallInvoker(ref invoker);
_client = new(() => new(invoker));
}
partial void SetupCallInvoker(ref CallInvoker invoker);
private Greeter.Common.Grpc.GreeterExtendedService.GreeterExtendedServiceClient Client => _client.Value;
public Greeter.Common.Greeting SayGreeting(Greeter.Common.Person @person)
{
var response = Client.SayGreeting(MapperTo.MapFrom(new { Person = @person }));
return MapperTo>.MapFrom(response).Result;
}
public async System.Threading.Tasks.Task SayGreetingAsync(Greeter.Common.Person @person)
{
var response = await Client.SayGreetingAsync(MapperTo.MapFrom(new { Person = @person })).ConfigureAwait(false);
return MapperTo>.MapFrom(response).Result;
}
}
```
Both classes are placed in `obj/{Configuration}/{TargetFramework}/Grpc/Partials` directory.
[Service code](./Greeter.Service) then uses `GreeterExtendedServiceGrpcService` class to [map gRPC service](./Greeter.Service/Program.cs#L22) thus exposing service implementation via gRPC:
```c#
app.MapGrpcService();
```
while [client code](./Greeter.Client) can [instantiate and execute](./Greeter.Client/Program.cs#L77-L80) `GreeterExtendedServiceGrpcClient` methods to call the service via gRPC:
```c#
var extendedClient = new Greeter.Common.Grpc.GreeterExtendedServiceGrpcClient(config, false);
WriteGreeting(extendedClient.SayGreeting(person));
WriteGreeting(await extendedClient.SayGreetingAsync(person));
```
WriteGreeting definition:
```c#
void WriteGreeting(Greeting greeting)
{
WriteLine(greeting.Subject);
foreach(var line in greeting.Lines)
{
WriteLine(line);
}
}
```
### DTO ↔ Protobuf conversion test code
The tool can also automatically generate unit tests which test `DTO → Protobuf → byte[] → Protobuf → DTO` (round-trip) conversion/serialization path.
[Greeter.Test](./Greeter.Test) project contains the example of configuration for this scenario. Entire configuration is located in [Greeter.Test.csproj](./Greeter.Test/Greeter.Test.csproj) file:
- Just like for gRPC code generation the `PackageReference` is [added](./Greeter.Test/Greeter.Test.csproj#L9) to the project:
```xml
```
- But instead of `GenerateGrpc` attribute the source project is [marked](./Greeter.Test/Greeter.Test.csproj#L16) by `GenerateGrpcTests` attribute which forces the tool to generate tests for all source interface methods:
```xml
```
Note that here we also [reference](./Greeter.Test/Greeter.Test.csproj#L17) `Greeter.Grpc` project which contains generated gRPC/Protobuf code to be tested by generated test code
## How to build and run the examples
Server:
```console
cd Greeter.Service
dotnet run
```
Client:
```console
cd Greeter.Client
dotnet run
```
Tests:
```console
cd Greeter.Test
dotnet test
```
Docker:
```console
docker build -t greeter-service -f Greeter.Service/Dockerfile .
docker run -it --rm -p 5001:443 greeter-service
```
Benchmarks:
```console
cd Greeter.Bench
dotnet run -c Release
```
## Benchmark results
Latest benchmark results can be found on [docs](../docs/docs) branch:
- [Linux](../docs/docs/ubuntu-latest/results)
- [Windows](../docs/docs/windows-latest/results)
Benchmark results example:
[Serialisation](Greeter.Bench/StringArraySerialisation.cs) of `string[]` vs `string?[]` collection (vs JSON serialisation as baseline):
## Quick start with the package
[IndependentReserve.Grpc.Tools](https://www.nuget.org/packages/IndependentReserve.Grpc.Tools) package can generate all required gRPC code from a plain .NET interface (so called _source interface_). The only requirement is that source interface must be located in a separate assembly/project which the project where gRPC code is generated (_target project_) depends on.
To add gRPC code into target project do:
1. Add a package references to [IndependentReserve.Grpc](https://www.nuget.org/packages/IndependentReserve.Grpc) and to [IndependentReserve.Grpc.Tools](https://www.nuget.org/packages/IndependentReserve.Grpc.Tools), e.g. via:
```console
dotnet add package IndependentReserve.Grpc
dotnet add package IndependentReserve.Grpc.Tools
```
Why do we need two packages:
Actually if you just manually add `PackageReference` to `IndependentReserve.Grpc.Tools` like that:
```xml
```
the reference to `IndependentReserve.Grpc` is added implicitly (transitively) so it does not have to be added explicitly.
However due to a bug in the [latest IndependentReserve.Grpc.Tools](https://www.nuget.org/packages/IndependentReserve.Grpc.Tools/4.1.215) when the package reference to it is added via `dotnet add` command a set of `<*Assets/>` attributes are also added:
```xml
runtime; build; native; contentfiles; analyzers; buildtransitive
all
```
These unnecessary `<*Assets/>` attributes break transitive dependency on `IndependentReserve.Grpc` which later result in compilation errors due to missing dependent types from `IndependentReserve.Grpc`.
1. In target project `*.csproj` file mark `ProjectReference` to dependent project which contains source interface(s) with `GenerateGrpc` attribute, e.g.:
```xml
```
How source interfaces are located:
By default the tool searches for all public interfaces which names match `Service$` regular expression (e.g. `ISomeService`) and generates all required gRPC-related code for every found interface.
To use a different pattern for interface search specify a custom regular expression (.NET flavor) via `GrpcServicePattern` attribute, e.g.:
```xml
true
I[^.]*ServiceInterface$
```
Once this is done all relevant gRPC code is generated and added to target project build pipeline. Both server and client code is generated, specifically, the following two classes are expected to be used by client or service code:
- `{service-name}GrpcService.cs`: generated gRPC service implementation
What is in gRPC service class:
[Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools)-based gRPC service implementation which depends on source interface (required parameter in constructor) which is expected to implement underlying service logic. Effectively this implementation simply exposes passed source interface implementation via gRPC interface.
- `{service-name}GrpcClient.cs`: generated gRPC client implementation
What is in gRPC client class:
This class implements source interface by calling the service via gRPC (using internal gRPC client class in turn generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools)). For each method from source interface both synchronous and asynchronous methods are generated.