Preview gRPC support

Happy to announce that gRPC support has now been published to MyGet, looking forward for any early preview adopters to test it out and report back any issues before the next release.

gRPC support is implemented in these 2 new packages below which requires netcoreapp3.0 on the server and either netstandard2.1 or netcoreapp3.0 on the client:

  • ServiceStack.Extensions - Server Support in GrpcFeature (requires netcoreapp3.0)
  • ServiceStack.GrpcClient - .NET GrpcServiceClient (requires netstandard2.1 or netcoreapp3.0)

Happy to report that gRPC integration went as well as I could’ve hoped where you can now call your existing ServiceStack Services via gRPC which in addition for reusing your existing Services, ServiceStack’s gRPC’s support ends up providing a nicer development experience then the default gRPC development experience in .NET Core 3.0 which requires maintaining an external .proto specification, and tooling to generate code-generated classes used in both server implementations and generated client proxies.

Instead we make use of marc gravell’s protobuf-net.Grpc library to enable ServiceStack’s normal code-first development experience of using typed Request/Response DTOs to implement and consume gRPC Services using the C# GrpcServiceClient which implements IServiceClientAsync so client applications will be able to easily switch between different .NET ServiceClients which all implement IServiceClientAsync (despite their implementations being very different), e.g:

//IServiceClientAsync client = new JsonServiceClient(BaseUrl);
IServiceClientAsync client = new GrpcServiceClient(BaseUrl);
var response = await client.GetAsync(new Hello { Name = "gRPC!" });

GrpcServiceClient is a full-featured ServiceClient which supports the same WebServiceException structured Error Handling including the same built-in Auth support for UserName/Password, SessionId and JWT/API Key BearerToken including seamless auto-retry support for RefreshTokens.

Limitations

As gRPC mandates a static service contract (i.e. can only return the same Response DTO Type) your Request DTOs are required to implement IReturn<TResponse> or IReturnVoid which is also the best practices for ServiceStack Services which enables the nicest development experience in typed generic service clients. In order to receive structured Error Responses your IReturn<T> Response DTOs requires ResponseStatus property as normal.

The other limitations is required to support ProtoBuf serialization which requires a numerical index for each field, to avoid your DTOs needing a dependency to protobuf-net Attributes you can define this using [DataContract] and [DataMember(Order=N)] attributes, all together this looks like:

[Route("/hello/{Name}")]
[DataContract]
public class Hello : IReturn<HelloResponse>
{
    [DataMember(Order = 1)]
    public string Name { get; set; }
}

[DataContract]
public class HelloResponse
{
    [DataMember(Order = 1)]
    public string Result { get; set; }
    
    [DataMember(Order = 2)]
    public ResponseStatus ResponseStatus { get; set; }
}

Basically these are the same attributes required for your Services to support SOAP.

Note: only Services that implement IReturn* and annotated with ProtoBuf attributes are registered as gRPC Services. If you get an error that your Service you’re trying to call doesn’t exist that’s likely the issue.

To enable gRPC support you need to register the GrpcFeature from the ServiceStack.Extensions netcoreapp3.0 package in your AppHost:

public override void Configure(Container container)
{
    Plugins.Add(new GrpcFeature(App));
}

gRPC uses .NET Core 3’s endpoint routing which you need to enable in your Startup class with app.UseRouting() and services.AddServiceStackGrpc() which enables ServiceStack’s code-first gRPC support:

public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration) => 
        Configuration = configuration;

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddServiceStackGrpc();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();

        app.UseRouting(); //enable endpoint routing

        app.UseServiceStack(new AppHost {
            AppSettings = new NetCoreAppSettings(Configuration)
        });
    }
}

Also note that gRPC works over HTTP/2 and TLS which requires extra TLS configuration when deployed to production, in development it uses the default development certificate.

Get Started

The easiest way to get started is to start from a new grpc template which is effectively a copy of the web project template pre-configured with gRPC support:

$ web new grpc GrpcApp

Tests

The IntegrationTest project shows an example of calling your App’s gRPC Service:

[Test] // Requires Host project running on https://localhost:5001 (default)
public async Task Can_call_Hello_Service_WebHost()
{
    var client = new GrpcServiceClient("https://localhost:5001");

    var response = await client.GetAsync(new Hello { Name = "World" });

    Assert.That(response.Result, Is.EqualTo("Hello, World!"));
}

As well as an example of calling a stand-alone self-host integration test which shows how to call an unencrypted gRPC Service:

public IServiceClientAsync CreateClient() => new GrpcServiceClient(BaseUri);

[Test]
public async Task Can_call_Hello_Service()
{
    GrpcClientFactory.AllowUnencryptedHttp2 = true;
    var client = CreateClient();

    var response = await client.GetAsync(new Hello { Name = "World" });

    Assert.That(response.Result, Is.EqualTo("Hello, World!"));
}

More Examples

For more GrpcServiceClient examples check out the integration tests at:

Feedback

Happy to answer any questions about the implementation and looking forward to hearing any early adopter feedback or reported issues you run into.

Implementation Notes

RpcGateway

gRPC executes ServiceStack Services via the new AppHost.RpcGateway that enables calling all ServiceStack Services via a single API:

Task<TResponse> ExecuteAsync<TResponse>(object requestDto, IRequest req)

That unlike MQ’s which uses ServiceController.ExecuteMessage to execute internal/trusted Services, the RpcGateway executes the full HTTP Request Pipeline where it goes through the same global Request/Response filters that normal HTTP ServiceStack Services execute, e.g. the same Auth validation and any other Request/Response filters.

The difference between HTTP Services is that instead of ServiceStack handling serializing the response, it returns the Response DTO back to ASP .NET Core gRPC which handles sending the response back to the HTTP/2 connected client. The RpcGateway also handles filters short-circuiting the Request Pipeline which gets converted into an Error Response Status that’s injected into the Response DTO’s ResponseStatus.

ProtoBuf Inheritance

Google’s protocol buffers doesn’t have any support for inheritance and protobuf-net has to retrofit its inheritance support by embedding sub classes as different fields in the base class type. This is awkward since all known sub classes needs to be defined on the base type using a consistent and non-conflicting numerical field id. Since AutoQuery requires inheritance it was important to be able to provide a seamless OOB solution which we’ve enabled by generating a Murmur2 hash (best overall for speed/randomness) of the types name that’s modded to within the 2^29-1 range of valid field ids in protocol buffers so the possibility of a hash collision is very very low but is still possible.

You may instead want to use your own user-defined field id for inherited classes using the [Id] attribute, for AutoQuery Services it should at least start from 10 to avoid id conflicts with base class properties, e.g:

[Route("/query/rockstars")]
[DataContract, Id(10)]
public class QueryRockstars : QueryDb<Rockstar>
{
    [DataMember(Order = 1)]
    public int? Age { get; set; }
}

The [Id] needs to be unique for all sub classes and must not conflict with base class field ids, so since AutoQuery Services share the same base-class, any user-defined [Id(N)] on AutoQuery Services needs to be unique across all your AutoQuery Services.

2 Likes

Hey @mythz this is looking good. Does the implementation play well with other gRPC clients? Is there a way to generate a proto file from the SS instance for client generation?

The gRPC support goes through the same ASP.NET Core’s gRPC HTTP/2 endpoint routing channel so I don’t expect there to be an issue from being called by other gRPC clients (which it supports) however it doesn’t dynamically generate a .proto specification yet, that’s one of the major TODO items to look at before release, in the meantime I’d expect you could manually create it based on the Request / Response DTOs definitions (the operation names are just {Verb}{RequestDtoName}, e.g. GetHello). Although the proxy generated clients aren’t going to have same built-in OOB features + dev experience of ServiceStack’s Service Clients which would need to be generated per endpoint instead of being able to use the same generic Service client to call any ServiceStack Service (i.e. the ServiceStack way).

The first step will be to provide a dynamic .proto (similar to Add ServiceStack Reference) to be able to generate a client proxy and later develop richer Service Clients for popular alt. languages that will consume ServiceStack Services (will likely need feature requests to determine popularity).

Great, with .proto generation I’m hoping we can support PHP clients without requiring anything from SS. When involving 3rd parties it’s easier for us to support this way. Basically allowing for a modern SOAP Web Services like experience. We will continue to use Service Clients internally. We need to consider the compatibility impact of the numerical indexing limitation first though.

Great news! Will be doing lots of stuff with gRPC and the new Worker Service.