(Translated by https://www.hiragana.jp/)
GitHub - max-ieremenko/ServiceModel.Grpc: Code-first for gRPC
Skip to content

max-ieremenko/ServiceModel.Grpc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ServiceModel.Grpc

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.

Links

ServiceModel.Grpc at a glance

Declare a service contract

[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);
}

Client call (Reflection.Emit)

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);

Client call (source code generation)

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);

Implement a service

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;
        }
    }
}

Host the service in the asp.net core application

PS> Install-Package ServiceModel.Grpc.AspNetCore
var builder = WebApplication.CreateBuilder();

// enable ServiceModel.Grpc
builder.Services.AddServiceModelGrpc();

var app = builder.Build();

// bind Calculator service
app.MapGrpcService<Calculator>();

Integrate with Swagger, see example

UI demo

Host the service in Grpc.Core.Server

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());

Server filters

see example

var builder = WebApplication.CreateBuilder();

// setup filter life time
builder.Services.AddSingleton<LoggingServerFilter>();

// attach the filter globally
builder.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);
        }
    }
}

NuGet feed


Name Package Supported platforms Description
ServiceModel.Grpc Version netstandard2.0/2.1, net462 main functionality, basic Grpc.Core.Api extensions and ClientFactory. ClientFactory is fully compatible with Grpc.Net.Client.
ServiceModel.Grpc.Client.DependencyInjection Version netstandard2.0/2.1, net8.0, net7.0, net6.0 Dependency injection extensions for ClientFactory and Grpc.Net.ClientFactory
ServiceModel.Grpc.AspNetCore Version net8.0, net7.0, net6.0 Grpc.AspNetCore.Server extensions
ServiceModel.Grpc.AspNetCore.Swashbuckle Version net8.0, net7.0, net6.0 Swagger integration, based on Swashbuckle.AspNetCore
ServiceModel.Grpc.AspNetCore.NSwag Version net8.0, net7.0, net6.0 Swagger integration, based on NSwag
ServiceModel.Grpc.SelfHost Version netstandard2.0/2.1, net462+ Grpc.Core extensions for self-hosted Grpc.Core.Server
ServiceModel.Grpc.DesignTime Version netstandard2.0 C# code generator
ServiceModel.Grpc.MessagePackMarshaller Version netstandard2.0, net8.0, net7.0, net6.0 marshaller factory, based on MessagePack serializer
ServiceModel.Grpc.ProtoBufMarshaller Version netstandard2.0/2.1, net8.0, net7.0, net6.0, net462+ marshaller factory, based on protobuf-net serializer

Benchmarks

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 as grpc-dotnet. This test is designed to compare numbers between ServiceModelGrpc and grpc-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];
}

Client async unary call, server is stub


BenchmarkDotNet v0.13.10, Ubuntu 22.04.3 LTS (Jammy Jellyfish)
AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.100
  [Host]   : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
  ShortRun : .NET 8.0.0 (8.0.23.53103), 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 131.854 μみゅーs 14.1959 μみゅーs 12.5843 μみゅーs 7,584.2 21.45 2.07 6.55 KB - 51.85 KB 7.76
ServiceModelGrpc.Protobuf 12.382 μみゅーs 0.0817 μみゅーs 0.0724 μみゅーs 80,760.2 2.01 0.01 1.33 KB 0.1068 9.07 KB 1.36
ServiceModelGrpc.MessagePack 7.079 μみゅーs 0.0272 μみゅーs 0.0241 μみゅーs 141,262.9 1.15 0.01 1.52 KB 0.1221 10.06 KB 1.51
grpc-dotnet 6.147 μみゅーs 0.0251 μみゅーs 0.0223 μみゅーs 162,690.8 1.00 0.00 1.32 KB 0.0763 6.68 KB 1.00
ServiceModelGrpc.proto-emulation 6.383 μみゅーs 0.0444 μみゅーs 0.0394 μみゅーs 156,667.2 1.04 0.01 1.32 KB 0.0763 6.8 KB 1.02

Server async unary call, client is stub


BenchmarkDotNet v0.13.10, Ubuntu 22.04.3 LTS (Jammy Jellyfish)
AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.100
  [Host]   : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
  ShortRun : .NET 8.0.0 (8.0.23.53103), 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 Allocated Alloc Ratio
ServiceModelGrpc.DataContract 215.75 μみゅーs 34.677 μみゅーs 30.740 μみゅーs 4,635.0 4.44 0.62 6.55 KB 60.76 KB 3.86
ServiceModelGrpc.Protobuf 65.24 μみゅーs 2.010 μみゅーs 1.569 μみゅーs 15,327.2 1.34 0.07 1.33 KB 18.11 KB 1.15
ServiceModelGrpc.MessagePack 62.09 μみゅーs 17.085 μみゅーs 15.982 μみゅーs 16,105.7 1.29 0.33 1.52 KB 19.1 KB 1.21
grpc-dotnet 48.57 μみゅーs 1.943 μみゅーs 1.723 μみゅーs 20,589.0 1.00 0.00 1.32 KB 15.76 KB 1.00
ServiceModelGrpc.proto-emulation 48.03 μみゅーs 1.581 μみゅーs 1.402 μみゅーs 20,821.3 0.99 0.05 1.32 KB 15.89 KB 1.01

Client plus server async unary call, without stubs


BenchmarkDotNet v0.13.10, Ubuntu 22.04.3 LTS (Jammy Jellyfish)
AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.100
  [Host]   : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
  ShortRun : .NET 8.0.0 (8.0.23.53103), 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 Allocated Alloc Ratio
ServiceModelGrpc.DataContract 382.49 μみゅーs 62.427 μみゅーs 55.340 μみゅーs 2,614.4 5.90 0.79 6.55 KB 98.45 KB 5.14
ServiceModelGrpc.Protobuf 90.63 μみゅーs 2.444 μみゅーs 1.908 μみゅーs 11,033.3 1.41 0.12 1.33 KB 23.88 KB 1.25
ServiceModelGrpc.MessagePack 68.14 μみゅーs 3.780 μみゅーs 2.951 μみゅーs 14,676.7 1.06 0.10 1.52 KB 25.48 KB 1.33
grpc-dotnet 64.90 μみゅーs 5.299 μみゅーs 4.697 μみゅーs 15,407.8 1.00 0.00 1.32 KB 19.14 KB 1.00
ServiceModelGrpc.proto-emulation 63.76 μみゅーs 4.117 μみゅーs 3.214 μみゅーs 15,683.4 1.00 0.12 1.32 KB 19.38 KB 1.01