ServiceModel.Grpc

Code-first for gRPC

View on GitHub

ServiceModel.Grpc

ServiceModel.Grpc enables applications to communicate with gRPC services using code-first approach (no .proto files), helps to get around some limitations of gRPC protocol like “only reference types”, “exact one input”, “no nulls/value-types”. Provides exception handling. Helps to migrate existing WCF solution to gRPC with minimum efforts.

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.

Usage

Declare a service contract

[DataContract]
public class Person
{
    [DataMember]
    public string FirstName { get; set; }

    [DataMember]
    public string SecondName { get; set; }
}

[ServiceContract]
public interface IGreeter
{
    [OperationContract]
    Task<string> SayHello(Person person, CancellationToken token = default);

    [OperationContract]
    ValueTask<(string Greeting, IAsyncEnumerable<string> Greetings)> Greet(IAsyncEnumerable<Person> persons, string greeting, CancellationToken token = default);
}

Client call (Reflection.Emit)

A proxy for IGreeter 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 IGreeter service
var greeter = clientFactory.CreateClient<IGreeter>(channel);

// call SayHello
var greet = await greeter.SayHello(new Person { FirstName = "John", SecondName = "X" });

// call Greet
var (greeting, greetings) = await greeter.Greet(new[] { new Person { FirstName = "John", SecondName = "X" } }, "hello");

Client call (source code generation)

A proxy for IGreeter service will be generated in the source code.

PS> Install-Package ServiceModel.Grpc.DesignTime
// request ServiceModel.Grpc to generate a source code for IGreeter service proxy
[ImportGrpcService(typeof(IGreeter))]
internal static partial class MyGrpcServices
{
    // generated code ...
    public static IClientFactory AddGreeterClient(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 IGreeter proxy generated by ServiceModel.Grpc.DesignTime
clientFactory.AddGreeterClient();

// create a new instance of the proxy
var greeter = clientFactory.CreateClient<IGreeter>(channel);

// call SayHello
var greet = await greeter.SayHello(new Person { FirstName = "John", SecondName = "X" });

// call Greet
var (greeting, greetings) = await greeter.Greet(new[] { new Person { FirstName = "John", SecondName = "X" } }, "hello");

ServiceModel.Grpc.DesignTime uses roslyn source generators, which requires net5.0 sdk.

Implement a service

internal sealed class Greeter : IGreeter
{
    public Task<string> SayHello(Person person, CancellationToken token = default)
    {
        return string.Format("Hello {0} {1}", person.FirstName, person.SecondName);
    }

    public ValueTask<(string Greeting, IAsyncEnumerable<string> Greetings)> Greet(IAsyncEnumerable<Person> persons, string greeting, CancellationToken token)
    {
        var greetings = DoGreet(persons, greeting, token);
        return new ValueTask<(string, IAsyncEnumerable<string>)>((greeting, greetings));
    }

    private static async IAsyncEnumerable<string> DoGreet(IAsyncEnumerable<Person> persons, string greeting, [EnumeratorCancellation] CancellationToken token)
    {
        await foreach (var person in persons.WithCancellation(token))
        {
            yield return string.Format("{0} {1} {2}", greeting, person.FirstName, person.SecondName);
        }
    }
}

Host the service in asp.net core application

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 Greeter service
            endpoints.MapGrpcService<Greeter>();
        });
    }
}

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 Greeter service
server.Services.AddServiceModelTransient(() => new Greeter());

Data contracts

By default the DataContractSerializer is used for marshalling data between server an client. This behavior is configurable, see ProtobufMarshaller example.

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set;}

    [DataMember]
    public DateTime BirthDay { get; set;}
}

Service contracts

Service contract is a public interface marked with ServiceContractAttribute. Methods marked with OperationContractAttribute are gRPC calls.

for net461 System.ServiceModel.dll, for netstandard package System.ServiceModel.Primitives

[ServiceContract]
public interface IPersonService
{
    // gRPC call
    [OperationContract]
    Task Ping();

    // method is not gRPC call
    Task Ping();
}

Operation contracts

Any operation in a service contract is one of gRPC method: Unary, ClientStreaming, ServerStreaming or DuplexStreaming.

Context parameters

ServiceModel.Grpc.CallContext

// contract
[OperationContract]
Task Ping(CallContext context = default);

// client
await client.Ping(new CallOptions(....));

// server
Task Ping(CallContext context)
{
    // take ServerCallContext
    Grpc.Core.ServerCallContext serverContext = context;
    var token = serverContext.CancellationToken;
    var requestHeaders = serverContext.RequestHeaders;
}

Grpc.Core.CallOptions

// contract
[OperationContract]
Task Ping(CallOptions context = default);

// client
await client.Ping(new CallOptions(....));

// server
Task Ping(CallOptions context)
{
    // context is not available here (default)
}

Grpc.Core.ServerCallContext

// contract
[OperationContract]
Task Ping(ServerCallContext context = default);

// client
await client.Ping();

// server
Task Ping(ServerCallContext context)
{
    var token = context.CancellationToken;
    var requestHeaders = context.RequestHeaders;
}

System.Threading.CancellationToken

// contract
[OperationContract]
Task Ping(CancellationToken token = default);

// client
var tokenSource = new CancellationTokenSource();
await client.Ping(tokenSource.Token);

// server
Task Ping(CancellationToken token)
{
    if (!token.IsCancellationRequested)
    {
        // ...
    }
}

Limitations