AspNetCore.Docs/aspnetcore/grpc/services.md

9.3 KiB

title author description monikerRange ms.author ms.date no-loc uid
Create gRPC services and methods jamesnk Learn how to create gRPC services and methods. >= aspnetcore-3.0 jamesnk 08/25/2020
ASP.NET Core Identity
cookie
Cookie
Blazor
Blazor Server
Blazor WebAssembly
Identity
Let's Encrypt
Razor
SignalR
grpc/services

Create gRPC services and methods

By James Newton-King

This document explains how to create gRPC services and methods in C#. Topics include:

  • How to define services and methods in .proto files.
  • Generated code using gRPC C# tooling.
  • Implementing gRPC services and methods.

Create new gRPC services

gRPC services with C# introduced gRPC's contract-first approach to API development. Services and messages are defined in .proto files. C# tooling then generates code from .proto files. For server-side assets, an abstract base type is generated for each service, along with classes for any messages.

The following .proto file:

  • Defines a Greeter service.
  • The Greeter service defines a SayHello call.
  • SayHello sends a HelloRequest message and receives a HelloReply message
syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

C# tooling generates the C# GreeterBase base type:

public abstract partial class GreeterBase
{
    public virtual Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        throw new RpcException(new Status(StatusCode.Unimplemented, ""));
    }
}

public class HelloRequest
{
    public string Name { get; set; }
}

public class HelloReply
{
    public string Message { get; set; }
}

By default the generated GreeterBase doesn't do anything. Its virtual SayHello method will return an UNIMPLEMENTED error to any clients that call it. For the service to be useful an app must create a concrete implementation of GreeterBase:

public class GreeterService : GreeterBase
{
    public override Task<HelloReply> UnaryCall(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloRequest { Message = $"Hello {request.Name}" });
    }
}

The service implementation is registered with the app. If the service is hosted by ASP.NET Core gRPC, it should be added to the routing pipeline with the MapGrpcService method.

app.UseEndpoints(endpoints =>
{
    endpoints.MapGrpcService<GreeterService>();
});

See xref:grpc/aspnetcore for more information.

Implement gRPC methods

A gRPC service can have different types of methods. How messages are sent and received by a service depends on the type of method defined. The gRPC method types are:

  • Unary
  • Server streaming
  • Client streaming
  • Bi-directional streaming

Streaming calls are specified with the stream keyword in the .proto file. stream can be placed on a call's request message, response message, or both.

syntax = "proto3";

service ExampleService {
  // Unary
  rpc UnaryCall (ExampleRequest) returns (ExampleResponse);

  // Server streaming
  rpc StreamingFromServer (ExampleRequest) returns (stream ExampleResponse);

  // Client streaming
  rpc StreamingFromClient (stream ExampleRequest) returns (ExampleResponse);

  // Bi-directional streaming
  rpc StreamingBothWays (stream ExampleRequest) returns (stream ExampleResponse);
}

Each call type has a different method signature. Overriding generated methods from the abstract base service type in a concrete implementation ensures the correct arguments and return type are used.

Unary method

A unary method gets the request message as a parameter, and returns the response. A unary call is complete when the response is returned.

public override Task<ExampleResponse> UnaryCall(ExampleRequest request,
    ServerCallContext context)
{
    var response = new ExampleResponse();
    return Task.FromResult(response);
}

Unary calls are the most similar to actions on web API controllers. One important difference gRPC methods have from actions is gRPC methods are not able to bind parts of a request to different method arguments. gRPC methods always have one message argument for the incoming request data. Multiple values can still be sent to a gRPC service by making them fields on the request message:

message ExampleRequest {
    int pageIndex = 1;
    int pageSize = 2;
    bool isDescending = 3;
}

Server streaming method

A server streaming method gets the request message as a parameter. Because multiple messages can be streamed back to the caller, responseStream.WriteAsync is used to send response messages. A server streaming call is complete when the method returns.

public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    for (var i = 0; i < 5; i++)
    {
        await responseStream.WriteAsync(new ExampleResponse());
        await Task.Delay(TimeSpan.FromSeconds(1));
    }
}

The client has no way to send additional messages or data once the server streaming method has started. Some streaming methods are designed to run forever. For continuous streaming methods, a client can cancel the call when it's no longer needed. When cancellation happens the client sends a signal to the server and the ServerCallContext.CancellationToken is raised. The CancellationToken token should be used on the server with async methods so that:

  • Any asynchronous work is canceled together with the streaming call.
  • The method exits quickly.
public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    while (!context.CancellationToken.IsCancellationRequested)
    {
        await responseStream.WriteAsync(new ExampleResponse());
        await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken);
    }
}

Client streaming method

A client streaming method starts without the method receiving a message. The requestStream parameter is used to read messages from the client. A client streaming call is complete when a response message is returned:

public override async Task<ExampleResponse> StreamingFromClient(
    IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context)
{
    while (await requestStream.MoveNext())
    {
        var message = requestStream.Current;
        // ...
    }
    return new ExampleResponse();
}

When using C# 8 or later, the await foreach syntax can be used to read messages. The IAsyncStreamReader<T>.ReadAllAsync() extension method reads all messages from the request stream:

public override async Task<ExampleResponse> StreamingFromClient(
    IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context)
{
    await foreach (var message in requestStream.ReadAllAsync())
    {
        // ...
    }
    return new ExampleResponse();
}

Bi-directional streaming method

A bi-directional streaming method starts without the method receiving a message. The requestStream parameter is used to read messages from the client. The method can choose to send messages with responseStream.WriteAsync. A bi-directional streaming call is complete when the the method returns:

public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    await foreach (var message in requestStream.ReadAllAsync())
    {
        await responseStream.WriteAsync(new ExampleResponse());
    }
}

The preceding code:

  • Sends a response for each request.
  • Is a basic usage of bi-directional streaming.

It is possible to support more complex scenarios, such as reading requests and sending responses simultaneously:

public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    // Read requests in a background task.
    var readTask = Task.Run(async () =>
    {
        await foreach (var message in requestStream.ReadAllAsync())
        {
            // Process request.
        }
    });
    
    // Send responses until the client signals that it is complete.
    while (!readTask.IsCompleted)
    {
        await responseStream.WriteAsync(new ExampleResponse());
        await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken);
    }
}

In a bi-directional streaming method, the client and service can send messages to each other at any time. The best implementation of a bi-directional method varies depending upon requirements.

Access gRPC request headers

A request message is not the only way for a client to send data to a gRPC service. Header values are available in a service using ServerCallContext.RequestHeaders.

public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context)
{
    var userAgent = context.RequestHeaders.GetValue("user-agent");
    // ...

    return Task.FromResult(new ExampleResponse());
}

Additional resources