ServiceModel.Grpc
enables applications to communicate with gRPC services using a code-first approach (no .proto files), helps to get around limitations of gRPC protocol like "only reference types", "exact one input", "no nulls", "no value-types". Provides exception handling. Helps to migrate existing WCF solution to gRPC with minimum effort.
The library supports lightweight runtime proxy generation via Reflection.Emit and C# source code generation.
The solution is built on top of gRPC C# and grpc-dotnet.
- ServiceModel.Grpc at a glance
- NuGet feed
- Benchmarks
- docs
- service and operation names
- service and operation bindings
- client configuration
- client code generation
- server code generation
- operations
- ASP.NET Core server configuration
- Grpc.Core server configuration
- exception handling general information
- global exception handling
- client filters
- server filters
- compatibility with native gRPC
- migrate from WCF to a gRPC
- migrate from WCF FaultContract to a gRPC global error handling
- examples
[ServiceContract]
public interface ICalculator
{
[OperationContract]
Task<long> Sum(long x, int y, int z, CancellationToken token = default);
[OperationContract]
ValueTask<(int Multiplier, IAsyncEnumerable<int> Values)> MultiplyBy(IAsyncEnumerable<int> values, int multiplier, CancellationToken token = default);
}
A proxy for the ICalculator service will be generated on demand via Reflection.Emit
.
PS> Install-Package ServiceModel.Grpc
// create a channel
var channel = new Channel("localhost", 5000, ...);
// create a client factory
var clientFactory = new ClientFactory();
// request the factory to generate a proxy for ICalculator service
var calculator = clientFactory.CreateClient<ICalculator>(channel);
// call Sum: sum == 6
var sum = await calculator.Sum(1, 2, 3);
// call MultiplyBy: multiplier == 2, values == [] {2, 4, 6}
var (multiplier, values) = await calculator.MultiplyBy(new[] {1, 2, 3}, 2);
A proxy for the ICalculator service will be generated in the source code.
PS> Install-Package ServiceModel.Grpc.DesignTime
// request ServiceModel.Grpc to generate a source code for ICalculator service proxy
[ImportGrpcService(typeof(ICalculator))]
internal static partial class MyGrpcServices
{
// generated code ...
public static IClientFactory AddCalculatorClient(this IClientFactory clientFactory, Action<ServiceModelGrpcClientOptions> configure = null) {}
}
// create a channel
var channel = new Channel("localhost", 5000, ...);
// create a client factory
var clientFactory = new ClientFactory();
// register ICalculator proxy generated by ServiceModel.Grpc.DesignTime
clientFactory.AddCalculatorClient();
// create a new instance of the proxy
var calculator = clientFactory.CreateClient<ICalculator>(channel);
// call Sum: sum == 6
var sum = await calculator.Sum(1, 2, 3);
// call MultiplyBy: multiplier == 2, values == [] {2, 4, 6}
var (multiplier, values) = await calculator.MultiplyBy(new[] {1, 2, 3}, 2);
internal sealed class Calculator : ICalculator
{
public Task<long> Sum(long x, int y, int z, CancellationToken token) => x + y + z;
public ValueTask<(int Multiplier, IAsyncEnumerable<int> Values)> MultiplyBy(IAsyncEnumerable<int> values, int multiplier, CancellationToken token)
{
var multiplicationResult = DoMultiplication(values, multiplier, token);
return new ValueTask<(int, IAsyncEnumerable<int>)>((multiplier, multiplicationResult));
}
private static async IAsyncEnumerable<int> DoMultiplication(IAsyncEnumerable<int> values, int multiplier, [EnumeratorCancellation] CancellationToken token)
{
await foreach (var value in values.WithCancellation(token))
{
yield return value * multiplier;
}
}
}
PS> Install-Package ServiceModel.Grpc.AspNetCore
internal sealed class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// enable ServiceModel.Grpc
services.AddServiceModelGrpc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoints =>
{
// bind Calculator service
endpoints.MapGrpcService<Calculator>();
});
}
}
Integrate with Swagger, see example
PS> Install-Package ServiceModel.Grpc.SelfHost
var server = new Grpc.Core.Server
{
Ports = { new ServerPort("localhost", 5000, ...) }
};
// bind Calculator service
server.Services.AddServiceModelTransient(() => new Calculator());
see example
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// setup filter life time
services.AddSingleton<LoggingServerFilter>();
// attach the filter globally
services.AddServiceModelGrpc(options =>
{
options.Filters.Add(1, provider => provider.GetRequiredService<LoggingServerFilter>());
});
}
internal sealed class LoggingServerFilter : IServerFilter
{
private readonly ILoggerFactory _loggerFactory;
public LoggingServerFilter(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public async ValueTask InvokeAsync(IServerFilterContext context, Func<ValueTask> next)
{
// create logger with a service name
var logger = _loggerFactory.CreateLogger(context.ServiceInstance.GetType().Name);
// log input
logger.LogInformation("begin {0}", context.ContractMethodInfo.Name);
foreach (var entry in context.Request)
{
logger.LogInformation("input {0} = {1}", entry.Key, entry.Value);
}
try
{
// invoke all other filters in the stack and the service method
await next().ConfigureAwait(false);
}
catch (Exception ex)
{
// log exception
logger.LogError("error {0}: {1}", context.ContractMethodInfo.Name, ex);
throw;
}
// log output
logger.LogInformation("end {0}", context.ContractMethodInfo.Name);
foreach (var entry in context.Response)
{
logger.LogInformation("output {0} = {1}", entry.Key, entry.Value);
}
}
}
Name | Package | Supported platforms | Description |
---|---|---|---|
ServiceModel.Grpc | netstandard2.0/2.1, net461+ | main functionality, basic Grpc.Core.Api extensions and ClientFactory. ClientFactory is fully compatible with Grpc.Net.Client. | |
ServiceModel.Grpc.AspNetCore | net7.0, net6.0, net5.0, .net core 3.0/3.1+ | Grpc.AspNetCore.Server extensions | |
ServiceModel.Grpc.AspNetCore.Swashbuckle | net7.0, net6.0, net5.0, .net core 3.0/3.1+ | Swagger integration, based on Swashbuckle.AspNetCore | |
ServiceModel.Grpc.AspNetCore.NSwag | net7.0, net6.0, net5.0, .net core 3.0/3.1+ | Swagger integration, based on NSwag | |
ServiceModel.Grpc.SelfHost | netstandard2.0/2.1, net461+ | Grpc.Core extensions for self-hosted Grpc.Core.Server | |
ServiceModel.Grpc.DesignTime | netstandard2.0/2.1, net461+ | C# code generator | |
ServiceModel.Grpc.MessagePackMarshaller | netstandard2.0, net7.0, net6.0, net5.0, .net core 3.1 | marshaller factory, based on MessagePack serializer | |
ServiceModel.Grpc.ProtoBufMarshaller | netstandard2.0/2.1, net7.0, net6.0, net5.0, .net core 3.1, net461+ | marshaller factory, based on protobuf-net serializer |
ServiceModel.Grpc is a tiny layer on top of grpc-dotnet, which helps to adapt code-first to gRPC protocol. A serializer makes a picture of the performance.
Benchmark code is available here.
The following benchmarks show the performance for unary call
on client and server.
[ServiceContract]
public interface ITestService
{
[OperationContract]
Task<SomeObject> PingPong(SomeObject value);
}
value = new SomeObject
{
StringScalar = "some meaningful text",
Int32Scalar = 1,
DateScalar = DateTime.UtcNow,
SingleScalar = 1.1f,
Int32Array = new int[100],
SingleArray = new float[100],
DoubleArray = new double[100]
};
-
ServiceModelGrpc.DataContract
test uses DataContractSerializer -
ServiceModelGrpc.Protobuf
test uses protobuf-net serializer -
ServiceModelGrpc.MessagePack
test uses MessagePack serializer -
ServiceModelGrpc.proto-emulation
test uses Google protobuf serialization, the same asgrpc-dotnet
. This test is designed to compare numbers betweenServiceModelGrpc
andgrpc-dotnet
without the influence of a serializer. -
grpc-dotnet
is a baseline:
service TestServiceNative {
rpc PingPong (SomeObjectProto) returns (SomeObjectProto);
}
message SomeObjectProto {
string stringScalar = 1;
google.protobuf.Timestamp dateScalar = 2;
float singleScalar = 3;
int32 int32Scalar = 4;
repeated float singleArray = 5 [packed=true];
repeated int32 int32Array = 6 [packed=true];
repeated double doubleArray = 7 [packed=true];
}
BenchmarkDotNet=v0.13.2, OS=ubuntu 20.04
Intel Xeon CPU E5-2673 v4 2.30GHz, 1 CPU, 2 logical and 2 physical cores
.NET SDK=7.0.100-rc.2.22477.23
[Host] : .NET 7.0.0 (7.0.22.47203), X64 RyuJIT AVX2
ShortRun : .NET 7.0.0 (7.0.22.47203), X64 RyuJIT AVX2
Job=ShortRun Platform=X64 Force=True
Server=False IterationCount=15 LaunchCount=1
RunStrategy=Throughput WarmupCount=3
Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Message size | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
ServiceModelGrpc.DataContract | 271.24 μs | 7.712 μs | 7.214 μs | 3,686.8 | 16.81 | 1.08 | 6.55 KB | 1.9531 | 51.81 KB | 7.73 |
ServiceModelGrpc.Protobuf | 28.95 μs | 0.653 μs | 0.579 μs | 34,544.6 | 1.79 | 0.08 | 1.33 KB | 0.3662 | 9.82 KB | 1.47 |
ServiceModelGrpc.MessagePack | 18.96 μs | 0.899 μs | 0.841 μs | 52,735.3 | 1.17 | 0.08 | 1.52 KB | 0.3662 | 10.02 KB | 1.49 |
ServiceModelGrpc.proto-emulation | 15.23 μs | 0.627 μs | 0.587 μs | 65,680.3 | 0.95 | 0.06 | 1.32 KB | 0.2441 | 6.76 KB | 1.01 |
grpc-dotnet | 16.20 μs | 0.840 μs | 0.744 μs | 61,744.8 | 1.00 | 0.00 | 1.32 KB | 0.2441 | 6.7 KB | 1.00 |
BenchmarkDotNet=v0.13.2, OS=ubuntu 20.04
Intel Xeon CPU E5-2673 v4 2.30GHz, 1 CPU, 2 logical and 2 physical cores
.NET SDK=7.0.100-rc.2.22477.23
[Host] : .NET 7.0.0 (7.0.22.47203), X64 RyuJIT AVX2
ShortRun : .NET 7.0.0 (7.0.22.47203), X64 RyuJIT AVX2
Job=ShortRun Platform=X64 Force=True
Server=False IterationCount=15 LaunchCount=1
RunStrategy=Throughput WarmupCount=3
Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Message size | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
ServiceModelGrpc.DataContract | 428.39 μs | 22.895 μs | 21.416 μs | 2,334.3 | 5.84 | 0.53 | 6.55 KB | 1.9531 | 60.96 KB | 3.83 |
ServiceModelGrpc.Protobuf | 96.46 μs | 8.084 μs | 6.750 μs | 10,366.7 | 1.31 | 0.12 | 1.33 KB | 0.7324 | 19.09 KB | 1.20 |
ServiceModelGrpc.MessagePack | 74.88 μs | 8.302 μs | 7.766 μs | 13,354.7 | 1.02 | 0.10 | 1.52 KB | 0.7324 | 19.26 KB | 1.21 |
ServiceModelGrpc.proto-emulation | 67.42 μs | 5.998 μs | 5.611 μs | 14,832.5 | 0.92 | 0.11 | 1.32 KB | 0.4883 | 16.05 KB | 1.01 |
grpc-dotnet | 73.74 μs | 5.848 μs | 5.471 μs | 13,561.8 | 1.00 | 0.00 | 1.32 KB | 0.4883 | 15.93 KB | 1.00 |
BenchmarkDotNet=v0.13.2, OS=ubuntu 20.04
Intel Xeon CPU E5-2673 v4 2.30GHz, 1 CPU, 2 logical and 2 physical cores
.NET SDK=7.0.100-rc.2.22477.23
[Host] : .NET 7.0.0 (7.0.22.47203), X64 RyuJIT AVX2
ShortRun : .NET 7.0.0 (7.0.22.47203), X64 RyuJIT AVX2
Job=ShortRun Platform=X64 Force=True
Server=False IterationCount=15 LaunchCount=1
RunStrategy=Throughput WarmupCount=3
Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Message size | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
ServiceModelGrpc.DataContract | 782.7 μs | 32.40 μs | 30.31 μs | 1,277.7 | 7.48 | 0.56 | 6.55 KB | 1.9531 | 98.51 KB | 5.11 |
ServiceModelGrpc.Protobuf | 147.8 μs | 11.29 μs | 10.57 μs | 6,764.2 | 1.41 | 0.15 | 1.33 KB | 0.9766 | 25.52 KB | 1.32 |
ServiceModelGrpc.MessagePack | 127.1 μs | 17.26 μs | 16.15 μs | 7,865.8 | 1.22 | 0.17 | 1.52 KB | 0.9766 | 25.5 KB | 1.32 |
ServiceModelGrpc.proto-emulation | 105.5 μs | 7.92 μs | 7.41 μs | 9,474.5 | 1.01 | 0.09 | 1.32 KB | 0.7324 | 19.41 KB | 1.01 |
grpc-dotnet | 105.0 μs | 6.18 μs | 5.78 μs | 9,524.3 | 1.00 | 0.00 | 1.32 KB | 0.7324 | 19.28 KB | 1.00 |