From d94d3f8ed9debd52d3c95bfc1f3a83070303ab08 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 18 Aug 2023 22:11:29 +0200 Subject: [PATCH] move grpc embedded snippets to projects files (#30077) * move grpc embedded snippets to projects files * build: use range instead of id for proto files * reword --- aspnetcore/grpc/services.md | 252 ++---------------- aspnetcore/grpc/services/DownloadResults.cs | 28 ++ aspnetcore/grpc/services/ExampleService.cs | 98 +++++++ aspnetcore/grpc/services/GreeterBase.cs | 23 ++ aspnetcore/grpc/services/GreeterService.cs | 13 + .../services/PerformLongRunningWorkAsync.cs | 36 +++ aspnetcore/grpc/services/Program.cs | 12 + aspnetcore/grpc/services/Protos/example.proto | 23 ++ aspnetcore/grpc/services/Protos/greeter.proto | 13 + 9 files changed, 265 insertions(+), 233 deletions(-) create mode 100644 aspnetcore/grpc/services/DownloadResults.cs create mode 100644 aspnetcore/grpc/services/ExampleService.cs create mode 100644 aspnetcore/grpc/services/GreeterBase.cs create mode 100644 aspnetcore/grpc/services/GreeterService.cs create mode 100644 aspnetcore/grpc/services/PerformLongRunningWorkAsync.cs create mode 100644 aspnetcore/grpc/services/Program.cs create mode 100644 aspnetcore/grpc/services/Protos/example.proto create mode 100644 aspnetcore/grpc/services/Protos/greeter.proto diff --git a/aspnetcore/grpc/services.md b/aspnetcore/grpc/services.md index b1c838a3d4d0..87636bafaf17 100644 --- a/aspnetcore/grpc/services.md +++ b/aspnetcore/grpc/services.md @@ -27,63 +27,21 @@ The following `.proto` file: * The `Greeter` service defines a `SayHello` call. * `SayHello` sends a `HelloRequest` message and receives a `HelloReply` message -```protobuf -syntax = "proto3"; - -service Greeter { - rpc SayHello (HelloRequest) returns (HelloReply); -} - -message HelloRequest { - string name = 1; -} - -message HelloReply { - string message = 1; -} -``` +:::code language="protobuf" source="~/grpc/services/Protos/greeter.proto" ::: C# tooling generates the C# `GreeterBase` base type: -```csharp -public abstract partial class GreeterBase -{ - public virtual Task 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; } -} -``` +:::code language="csharp" source="~/grpc/services/GreeterBase.cs" id="snippet_GreeterBase" ::: 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`: -```csharp -public class GreeterService : GreeterBase -{ - public override Task SayHello(HelloRequest request, ServerCallContext context) - { - return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" }); - } -} -``` +:::code language="csharp" source="~/grpc/services/GreeterService.cs" id="snippet_GreeterService" ::: The `ServerCallContext` gives the context for a server-side call. 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. -```csharp -app.MapGrpcService(); -``` +:::code language="csharp" source="~/grpc/services/Program.cs" id="snippet_MapGrpcService" ::: See for more information. @@ -98,23 +56,7 @@ A gRPC service can have different types of methods. How messages are sent and re 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. -```protobuf -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); -} -``` +:::code language="protobuf" source="~/grpc/services/Protos/example.proto" range="1-15" ::: 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. @@ -122,103 +64,36 @@ Each call type has a different method signature. Overriding generated methods fr A unary method has the request message as a parameter, and returns the response. A unary call is complete when the response is returned. -```csharp -public override Task 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](xref:web-api/index). 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: - -```protobuf -message ExampleRequest { - int32 pageIndex = 1; - int32 pageSize = 2; - bool isDescending = 3; -} -``` +:::code language="csharp" source="~/grpc/services/ExampleService.cs" id="snippet_UnaryCall" ::: + +Unary calls are the most similar to [actions on web API controllers](xref:web-api/index). 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 adding fields to the request message: + +:::code language="protobuf" source="~/grpc/services/Protos/example.proto" range="19-23" ::: ### Server streaming method A server streaming method has 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. -```csharp -public override async Task StreamingFromServer(ExampleRequest request, - IServerStreamWriter responseStream, ServerCallContext context) -{ - for (var i = 0; i < 5; i++) - { - await responseStream.WriteAsync(new ExampleResponse()); - await Task.Delay(TimeSpan.FromSeconds(1)); - } -} -``` +:::code language="csharp" source="~/grpc/services/ExampleService.cs" id="snippet_StreamingFromServer" ::: 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](xref:System.Threading.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. -```csharp -public override async Task StreamingFromServer(ExampleRequest request, - IServerStreamWriter responseStream, ServerCallContext context) -{ - while (!context.CancellationToken.IsCancellationRequested) - { - await responseStream.WriteAsync(new ExampleResponse()); - await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); - } -} -``` +:::code language="csharp" source="~/grpc/services/ExampleService.cs" id="snippet_StreamingFromServerUsingCancellationToken" ::: ### 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: -```csharp -public override async Task StreamingFromClient( - IAsyncStreamReader 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.ReadAllAsync()` extension method reads all messages from the request stream: - -```csharp -public override async Task StreamingFromClient( - IAsyncStreamReader requestStream, ServerCallContext context) -{ - await foreach (var message in requestStream.ReadAllAsync()) - { - // ... - } - return new ExampleResponse(); -} -``` +:::code language="csharp" source="~/grpc/services/ExampleService.cs" id="snippet_StreamingFromClient" ::: ### 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 method returns: -```csharp -public override async Task StreamingBothWays(IAsyncStreamReader requestStream, - IServerStreamWriter responseStream, ServerCallContext context) -{ - await foreach (var message in requestStream.ReadAllAsync()) - { - await responseStream.WriteAsync(new ExampleResponse()); - } -} -``` +:::code language="csharp" source="~/grpc/services/ExampleService.cs" id="snippet_StreamingBothWays" ::: The preceding code: @@ -227,27 +102,7 @@ The preceding code: It is possible to support more complex scenarios, such as reading requests and sending responses simultaneously: -```csharp -public override async Task StreamingBothWays(IAsyncStreamReader requestStream, - IServerStreamWriter 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); - } -} -``` +:::code language="csharp" source="~/grpc/services/ExampleService.cs" id="snippet_StreamingBothWaysComplex" ::: 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. @@ -255,15 +110,7 @@ In a bi-directional streaming method, the client and service can send messages t 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`. -```csharp -public override Task UnaryCall(ExampleRequest request, ServerCallContext context) -{ - var userAgent = context.RequestHeaders.GetValue("user-agent"); - // ... - - return Task.FromResult(new ExampleResponse()); -} -``` +:::code language="csharp" source="~/grpc/services/ExampleService.cs" id="snippet_UnaryCallRequestHeaders" ::: ## Multi-threading with gRPC streaming methods @@ -275,36 +122,7 @@ There are important considerations to implementing gRPC streaming methods that u A safe way to enable multiple threads to interact with a gRPC method is to use the producer-consumer pattern with [System.Threading.Channels](/dotnet/core/extensions/channels). -```csharp -public override async Task DownloadResults(DataRequest request, - IServerStreamWriter responseStream, ServerCallContext context) -{ - var channel = Channel.CreateBounded(new BoundedChannelOptions(capacity: 5)); - - var consumerTask = Task.Run(async () => - { - // Consume messages from channel and write to response stream. - await foreach (var message in channel.Reader.ReadAllAsync()) - { - await responseStream.WriteAsync(message); - } - }); - - var dataChunks = request.Value.Chunk(size: 10); - - // Write messages to channel from multiple threads. - await Task.WhenAll(dataChunks.Select( - async c => - { - var message = new DataResult { BytesProcessed = c.Length }; - await channel.Writer.WriteAsync(message); - })); - - // Complete writing and wait for consumer to complete. - channel.Writer.Complete(); - await consumerTask; -} -``` +:::code language="csharp" source="~/grpc/services/DownloadResults.cs" ::: The preceding gRPC server streaming method: @@ -327,43 +145,11 @@ If a gRPC method starts background tasks that use these types, it must complete In the following example, the server streaming method could write to the response stream after the call has finished: -```csharp -public override async Task StreamingFromServer(ExampleRequest request, - IServerStreamWriter responseStream, ServerCallContext context) -{ - _ = Task.Run(async () => - { - for (var i = 0; i < 5; i++) - { - await responseStream.WriteAsync(new ExampleResponse()); - await Task.Delay(TimeSpan.FromSeconds(1)); - } - }); - - await PerformLongRunningWorkAsync(); -} -``` +:::code language="csharp" source="~/grpc/services/PerformLongRunningWorkAsync.cs" id="snippet_StreamingFromServer" ::: For the previous example, the solution is to await the write task before exiting the method: -```csharp -public override async Task StreamingFromServer(ExampleRequest request, - IServerStreamWriter responseStream, ServerCallContext context) -{ - var writeTask = Task.Run(async () => - { - for (var i = 0; i < 5; i++) - { - await responseStream.WriteAsync(new ExampleResponse()); - await Task.Delay(TimeSpan.FromSeconds(1)); - } - }); - - await PerformLongRunningWorkAsync(); - - await writeTask; -} -``` +:::code language="csharp" source="~/grpc/services/PerformLongRunningWorkAsync.cs" id="snippet_StreamingFromServerWriteTask" ::: ## Additional resources diff --git a/aspnetcore/grpc/services/DownloadResults.cs b/aspnetcore/grpc/services/DownloadResults.cs new file mode 100644 index 000000000000..18a47418268a --- /dev/null +++ b/aspnetcore/grpc/services/DownloadResults.cs @@ -0,0 +1,28 @@ +public override async Task DownloadResults(DataRequest request, + IServerStreamWriter responseStream, ServerCallContext context) +{ + var channel = Channel.CreateBounded(new BoundedChannelOptions(capacity: 5)); + + var consumerTask = Task.Run(async () => + { + // Consume messages from channel and write to response stream. + await foreach (var message in channel.Reader.ReadAllAsync()) + { + await responseStream.WriteAsync(message); + } + }); + + var dataChunks = request.Value.Chunk(size: 10); + + // Write messages to channel from multiple threads. + await Task.WhenAll(dataChunks.Select( + async c => + { + var message = new DataResult { BytesProcessed = c.Length }; + await channel.Writer.WriteAsync(message); + })); + + // Complete writing and wait for consumer to complete. + channel.Writer.Complete(); + await consumerTask; +} \ No newline at end of file diff --git a/aspnetcore/grpc/services/ExampleService.cs b/aspnetcore/grpc/services/ExampleService.cs new file mode 100644 index 000000000000..19bd777cc2de --- /dev/null +++ b/aspnetcore/grpc/services/ExampleService.cs @@ -0,0 +1,98 @@ +using Grpc.Core; + +namespace GrcpServices; + +public class ExamplesService : ExampleService.ExampleServiceBase +{ + // + public override Task UnaryCall(ExampleRequest request, + ServerCallContext context) + { + var response = new ExampleResponse(); + return Task.FromResult(response); + } + // + + + // + public override Task UnaryCall(ExampleRequest request, + ServerCallContext context) + { + var userAgent = context.RequestHeaders.GetValue("user-agent"); + // ... + + return Task.FromResult(new ExampleResponse()); + } + // + + + // + public override async Task StreamingFromServer(ExampleRequest request, + IServerStreamWriter responseStream, ServerCallContext context) + { + for (var i = 0; i < 5; i++) + { + await responseStream.WriteAsync(new ExampleResponse()); + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + // + + // + public override async Task StreamingFromServer(ExampleRequest request, + IServerStreamWriter responseStream, ServerCallContext context) + { + while (!context.CancellationToken.IsCancellationRequested) + { + await responseStream.WriteAsync(new ExampleResponse()); + await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); + } + } + // + + + // + public override async Task StreamingFromClient( + IAsyncStreamReader requestStream, ServerCallContext context) + { + await foreach (var message in requestStream.ReadAllAsync()) + { + // ... + } + return new ExampleResponse(); + } + // + + // + public override async Task StreamingBothWays(IAsyncStreamReader requestStream, + IServerStreamWriter responseStream, ServerCallContext context) + { + await foreach (var message in requestStream.ReadAllAsync()) + { + await responseStream.WriteAsync(new ExampleResponse()); + } + } + // + + // + public override async Task StreamingBothWays(IAsyncStreamReader requestStream, + IServerStreamWriter 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); + } + } + // +} \ No newline at end of file diff --git a/aspnetcore/grpc/services/GreeterBase.cs b/aspnetcore/grpc/services/GreeterBase.cs new file mode 100644 index 000000000000..2753afa48bc7 --- /dev/null +++ b/aspnetcore/grpc/services/GreeterBase.cs @@ -0,0 +1,23 @@ +using Grpc.Core; + +namespace GrcpServices; + +// +public abstract partial class GreeterBase +{ + public virtual Task 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; } +} +// diff --git a/aspnetcore/grpc/services/GreeterService.cs b/aspnetcore/grpc/services/GreeterService.cs new file mode 100644 index 000000000000..b816d726f8fc --- /dev/null +++ b/aspnetcore/grpc/services/GreeterService.cs @@ -0,0 +1,13 @@ +using Grpc.Core; + +namespace GrcpServices; + +// +public class GreeterService : GreeterBase +{ + public override Task SayHello(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" }); + } +} +// diff --git a/aspnetcore/grpc/services/PerformLongRunningWorkAsync.cs b/aspnetcore/grpc/services/PerformLongRunningWorkAsync.cs new file mode 100644 index 000000000000..787fadd2d218 --- /dev/null +++ b/aspnetcore/grpc/services/PerformLongRunningWorkAsync.cs @@ -0,0 +1,36 @@ +// +public override async Task StreamingFromServer(ExampleRequest request, + IServerStreamWriter responseStream, ServerCallContext context) +{ + _ = Task.Run(async () => + { + for (var i = 0; i < 5; i++) + { + await responseStream.WriteAsync(new ExampleResponse()); + await Task.Delay(TimeSpan.FromSeconds(1)); + } + }); + + await PerformLongRunningWorkAsync(); +} +// + + +// +public override async Task StreamingFromServer(ExampleRequest request, + IServerStreamWriter responseStream, ServerCallContext context) +{ + var writeTask = Task.Run(async () => + { + for (var i = 0; i < 5; i++) + { + await responseStream.WriteAsync(new ExampleResponse()); + await Task.Delay(TimeSpan.FromSeconds(1)); + } + }); + + await PerformLongRunningWorkAsync(); + + await writeTask; +} +// diff --git a/aspnetcore/grpc/services/Program.cs b/aspnetcore/grpc/services/Program.cs new file mode 100644 index 000000000000..2658ca8eed25 --- /dev/null +++ b/aspnetcore/grpc/services/Program.cs @@ -0,0 +1,12 @@ +using GrcpServices; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddGrpc(); + +var app = builder.Build(); + +// +app.MapGrpcService(); +// + +app.Run(); diff --git a/aspnetcore/grpc/services/Protos/example.proto b/aspnetcore/grpc/services/Protos/example.proto new file mode 100644 index 000000000000..f0d66ae48f8f --- /dev/null +++ b/aspnetcore/grpc/services/Protos/example.proto @@ -0,0 +1,23 @@ +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); +} + +message ExampleResponse {} + +message ExampleRequest { + int32 pageIndex = 1; + int32 pageSize = 2; + bool isDescending = 3; +} diff --git a/aspnetcore/grpc/services/Protos/greeter.proto b/aspnetcore/grpc/services/Protos/greeter.proto new file mode 100644 index 000000000000..083ba80b5cb5 --- /dev/null +++ b/aspnetcore/grpc/services/Protos/greeter.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} \ No newline at end of file