Hey Guys!
The last days in Poland are cold & cloudy. This weather is a great motivator to reflect on… Communication protocols :-)!
This article presents….
- gRPC CodeFirst approach,
- The performance benchmark for gRPC vs REST communication in .Net core 3.1,
- How many does cost to open gRPC channel & why is worth to scope it like HttpClient,
- Note: All tests were started on my local PC, so all network traffic was occured in localhost & self signed ssl certs.
gRPC CodeFirst approach.
Firstly, I want to show You a little different way of creating gRPC web services than Microsoft official docs. Maybe a long time ago You heard something about gRPC, something about the requirement to have a .proto contract files for generating base, service classes. Maybe, when You look at this, You were scared (I did). What if I tell You, there is a different, unofficial way of creating gRPC services? Does this different approach have this same performance, no .proto files & shorter way of implementation based on C# models, interfaces & attributes? This is protobuf-net.Grpc library created by Marc Gravell. From now, You can forget about ContractFirst and focus on creating contracts by .net coding! (btw. it’s also possible to create .proto files from it :-))!
[ServiceContract] public interface IUserGrpcService { ValueTask<UserDto> ActivateAsync(ActivateUserCommand command, CallContext context = default); ValueTask<CreatedUserDto> CreateUserAsync(CreateUserCommand command, CallContext context = default); ValueTask<AuditedUserDto> LoginAsync(LoginCommand command, CallContext context = default); ValueTask<ResetPasswordDto> ResetPasswordAsync(ResetPasswordCommand command, CallContext context = default); ValueTask<UserDto> SetNewPasswordAsync(SetNewPasswordCommand command, CallContext context = default); } [ProtoContract] public class UserDto { [ProtoMember(1)] public long Id { get; set; } [ProtoMember(2, DataFormat = DataFormat.Default)] public DateTime CreatedAt { get; set; } [ProtoMember(3)] public string Email { get; set; } } // Server public class UsersGrpcService : IUserGrpcService { private readonly IMediator _mediator; public UsersGrpcService(IMediator mediator) { _mediator = mediator; } public async ValueTask<UserDto> ActivateAsync(ActivateUserCommand command, CallContext context = default) => await _mediator.Send(command, context.CancellationToken); public async ValueTask<CreatedUserDto> CreateUserAsync(CreateUserCommand command, CallContext context = default) => await _mediator.Send(command, context.CancellationToken); public async ValueTask<AuditedUserDto> LoginAsync(LoginCommand command, CallContext context = default) => await _mediator.Send(command, context.CancellationToken); public async ValueTask<ResetPasswordDto> ResetPasswordAsync(ResetPasswordCommand command, CallContext context = default) => await _mediator.Send(command, context.CancellationToken); public async ValueTask<UserDto> SetNewPasswordAsync(SetNewPasswordCommand command, CallContext context = default) => await _mediator.Send(command, context.CancellationToken); } //Startup public void ConfigureServices(IServiceCollection services) { services.AddCodeFirstGrpc(config => { config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal; config.Interceptors.Add<ExceptionInterceptor>(); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseEndpoints(endpoints => { endpoints.MapGrpcService<UsersGrpcService>(); }); // ... } //Client - create of channel and service should be done by DI. //Opening of channel & create service costs. //Remember about it. using var grpc = _grpcFactory.CreateChannel( _settings.Users.Host, _settings.Users.Port); var service = grpc.CreateService<IUserGrpcService>(); //Client - cosume method. var dto = await _service.CreateUserAsync(command); return dto;
Benchmark – environment
BenchmarkDotNet=v0.12.1, OS=macOS Catalina 10.15.3 (19D76) [Darwin 19.3.0]
Intel Core i7-8850H CPU 2.60GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.301
[Host] : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT [AttachedDebugger]
DefaultJob : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
Benchmark – how web communication should be implemented for the best performance?
Method | Count | Mean | Error | StdDev |
---|---|---|---|---|
GrpcWithChannelAndService | 1 | 11,309.2 μs | 290.53 μs | 838.25 μs |
GrpcWithChannel | 1 | 10,860.3 μs | 294.60 μs | 854.69 μs |
GrpcFromZero | 1 | 22,423.3 μs | 1,316.08 μs | 3,880.51 μs |
RestFromZero | 1 | 30,700.7 μs | 1,962.80 μs | 5,101.57 μs |
RestWithClient | 1 | 659.0 μs | 24.50 μs | 71.08 μs |
Method | Count | Mean | Error | StdDev | GrpcWithChannelAndService | 10 | 107,965.4 μs | 2,136.78 μs | 2,098.60 μs |
---|---|---|---|---|
GrpcWithChannel | 10 | 112,519.0 μs | 3,375.63 μs | 9,900.13 μs |
GrpcFromZero | 10 | 215,862.6 μs | 18,452.26 μs | 54,406.91 μs |
RestFromZero | 10 | 326,166.2 μs | 18,679.43 μs | 55,076.72 μs |
RestWithClient | 10 | 7,089.6 μs | 378.08 μs | 1,102.88 μs |
Method | Count | Mean | Error | StdDev |
---|---|---|---|---|
GrpcWithChannelAndService | 20 | 216,042.5 μs | 6,804.51 μs | 20,063.25 μs |
GrpcWithChannel | 20 | 203,643.7 μs | 5,668.72 μs | 16,081.22 μs |
GrpcFromZero | 20 | 463,342.3 μs | 22,823.91 μs | 66,216.31 μs |
RestFromZero | 20 | 652,889.9 μs | 20,515.53 μs | 58,862.91 μs |
RestWithClient | 20 | 17,238.0 μs | 1,385.95 μs | 4,064.74 μs |