How to get a simple stream of string using ServiceStack Grpc?

Viewed 458

fighting with the ServiceStack library since a while to get a basic "stream" of string to work in C#.

In short, I'm trying to replicate the basic example from "native" gRPC.

Proto buf

service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (stream HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

Server

public override async Task SayHello(HelloRequest request, IServerStreamWriter<HelloReply> responseStream,                                            ServerCallContext context)
    {
        foreach (var x in Enumerable.Range(1, 10))
        {
            await responseStream.WriteAsync(new HelloReply
            {
                Message = $"Hello {request.Name} {x}"
            });

            await Task.Delay(200);
        }
    }

Client

var replies = client.SayHello(new HelloRequest { Name = "Laurent" });

await foreach (var reply in replies.ResponseStream.ReadAllAsync())
{
    Console.WriteLine(reply.Message);
}

Then with the ServiceStack library, I'm not able to get the server piece done correctly. I always get a message telling me my function 'SayHello' isn't defined.

Let me know, thx !

2 Answers

ServiceStack gRPC implementation adopts a code-first implementation where your existing ServiceStack Services can be called from gRPC endpoints.

So instead of manually authoring a .proto file you would instead create Services using standard Request / Response DTOs and Service implementation for normal Request/Reply gRPC Services.

For Server Stream gRPC Services you would need to implement IStreamService interface in addition to inheriting from ServiceStack's Service base class.

An example of this is covered in Implementing Server Stream Services in the docs:

public class StreamFileService : Service, IStreamService<StreamFiles,FileContent>
{
    public async IAsyncEnumerable<FileContent> Stream(StreamFiles request, 
        CancellationToken cancel = default)
    {
        var i = 0;
        var paths = request.Paths ?? TypeConstants.EmptyStringList;
        while (!cancel.IsCancellationRequested)
        {
            var file = VirtualFileSources.GetFile(paths[i]);
            var bytes = file?.GetBytesContentsAsBytes();
            var to = file != null
                ? new FileContent {
                    Name = file.Name,
                    Type = MimeTypes.GetMimeType(file.Extension),
                    Body = bytes,
                    Length = bytes.Length,
                }
                : new FileContent {
                    Name = paths[i],
                    ResponseStatus = new ResponseStatus {
                        ErrorCode = nameof(HttpStatusCode.NotFound),
                        Message = "File does not exist",
                    }
                };

            yield return to;

            if (++i >= paths.Count)
                yield break;
        }
    }
}

You would also need to register your Stream Service implementation in RegisterServices:

Plugins.Add(new GrpcFeature(App) {
    RegisterServices = {
        typeof(StreamFileService)
    }
});

If you're using the smart C# generic gRPC Service Client you can avoid .proto descriptors and protoc generated classes entirely as you can reuse the Server DTOs in your ServiceModel project to enable an end-to-end API without code-gen:

var request = new StreamFiles {
    Paths = new List<string> {
        "/js/ss-utils.js",
        "/js/hot-loader.js",
        "/js/not-exists.js",
        "/js/hot-fileloader.js",
    }
};

var files = new List<FileContent>();
await foreach (var file in client.StreamAsync(request))
{
    files.Add(file);
}

An alternative to sharing your ServiceModel.dll you can use C# Add ServiceStack Reference to generate your C# DTOs on the client.

For protoc generated clients you can use the x dotnet tool to Generate protoc Dart gRPC Client

$ x proto-dart https://todoworld.servicestack.net -out lib

Where you can use the serverStreamFiles API stubs to invoke the server stream Service:

var stream = client.serverStreamFiles(StreamFiles()..paths.addAll([
  '/js/ss-utils.js',
  '/js/hot-loader.js',
  '/js/hot-fileloader.js',
]));

await for (var file in stream) {
  var text = utf8.decode(file.body);
  print('FILE ${file.name} (${file.length}): ${text.substring(0, text.length < 50 ? text.length : 50)} ...');
}

The todo-world/clients repo contains a number of gRPC test examples in different langauges.

In the end, here's what I end up doing as a simple POC of a stream, if this can help anyone else. No more proto-file either !

Client Program.cs :

        private async static Task GetBotStream()
        {
            var res = client.StreamAsync(new BotStreamRequest { });

            await foreach (var textReceived in res)
            {
                Console.WriteLine(textReceived.Result);
            }
        }

Client dtos.cs

   [DataContract]
    public partial class BotStreamRequest : IReturn<BotStreamReply>
    {
    }

    [DataContract]
    public partial class BotStreamReply
    {
        [DataMember(Order = 1)]
        public virtual string Result { get; set; }

        [DataMember(Order = 2)]
        public virtual ResponseStatus ResponseStatus { get; set; }
    }

Server Program.cs

        public async IAsyncEnumerable<BotStreamReply> Stream(BotStreamRequest request, [EnumeratorCancellation]CancellationToken cancel = default)
        {
            foreach (var x in Enumerable.Range(1, 10))
            {
                yield return new BotStreamReply { Result = $"My stream {x}" };
            }
        }
Related