This article demonstrates running gRPC functionality within ASP.NET Kestrel server and using various gRPC clients to access it. In particular, I describe using a multiplatform C# Console client, HTML/JavaScript in-browser client and non-Blazor WebAssembly C# client. The examples cover all gRPC paradigms including Single Request/Single Response, Single Request/Server Streaming, Client Streaming/Single Response and bi-directional Streaming.
Web gRPC Under ASP.NET 8.0, Kestrel with HTML/JavaScript and Avalonia Clients
Dedication
This dedication is my farewell present to my good friends at Kahua.com including
- Colin Whitlock - who on top of being a great CTO, is also a great manager, architect and developer in the company.
- Jon Sinsel - an extremely sharp C# guru who very patiently introduced me to Uno Platform and Kahua code.
- Adam Love - the best Web, Mobile wizard (also a great C# developer) without whose help, I'd still be figuring out things about deploying ASP.NET application.
- Jonas Mayor - terrific C# software engineer who introduced me to IIS URL Rewrite module and ASP.NET middleware.
Introduction
gRPC
gRPC is an abbreviation for "Google Remote Procedure Calls". It is a high performance multi-platform technology for communications between a server and clients.
Here are the gRPC advantages:
- gRPC messages are much smaller than those of the text based protocols (REST and SOAP), correspondingly gRPC communications are much faster.
- gRPC can be used for Web communications.
- gRPC supports Server side streaming This is important, because many other popular Web protocols e.g. REST do not support streaming.
- gRPC (except from a browser) supports Client side as well as the bidirectional streaming (both client and server exchanging information via established streams at the same time). gRPC in browser not supporting client side streaming is a pretty bad limitation that might require switching to other technologies e.g. SignalR for chatty clients.
- Both Server and Client gRPC can be run virtually on any platform and use various different software languages including, but not limited to C#, Java, JavaScript, Python.
- gRPC is very simple and easy to learn.
Kestrel and ASP.NET 8.0
The gRPC service is implemented as part of Kestrel ASP.NET server.
Kestrel is a Microsoft Web server written entirely in .NET Core and because of that it is 100% multiplatform.
Kestrel is great for handling ASP.NET and gRPC requests/responses, but misses some Web server features and because of that is often deployed behind another Web server which plays role of a reverse proxy for ASP and gRPC requests/responses.
On Windows the reverse proxy server is usually IIS while on Linux it can be Apache or anything else:
The integration between IIS and Kestrel comes naturally in .NET 8.0 and requires almost no code change.
Avalonia
Avalonia is a great open source .NET framework for writing UI on multiple platforms (including Windows, MacOS, Linux, WebAssembly, Android and iOS) using C# and XAML.
On top of being multiplatform, Avalonia framework is considerably better and more powerful than WPF or any JavaScript/TypeScript framework.
Avalonia allows creating applications of great speed and quality and has very few differences in the way it behaves on various platforms.
Avalonia can be used for creating Web applications using WebAssembly.
The Main Purpose of the Article
This article provides easy and well explained samples for building and deploying gRPC services using IIS/Kestrel ASP.NET functionality on Windows and consuming them by various gRPC Clients. Here are the gRPC clients presented in the article:
- C# Console Program (would work exactly the same in C# desktop applications on any platform).
- HTML/JavaScript Web broser client.
- C# Non-Blazor Avalonia WebAssembly client. The reason I prefer not using Blazor is because from different sources on the Web I read that non-Blazor technology for C# WebAssembly and its interaction with HTML/JavaScript is more stable.
Important Note on Using ASP.NET
I'd like underscore that ASP.NET is often used to generate the HTML/JavaScript pages on the fly.
For application speed sake and clarity reasons, I prefer using ASP.NET for providing backend services only without almost any HTML/JavaScript generation.
One exception to the rule above is - sometimes, I add the constant global configuration parameters to ASP.NET pages via code generation only at the initial state (when the page is loaded the first time).
The AWebPros.com Website Demonstrating the gRPC Samples running under IIS/Kestrel
Using the code described in this article, I built a Website ASP gRPC Samples demonstrating web HTML/JavaScript and Avalonia WebAssembly client with IIS/Kestrel web server.
This article concentrates primarily on gRPC related code.
My wonderful journey through building and deploying a real life APS.NET web site I plan to describe in future articles. In particular I plan covering the following topics:
- WebAssembly, Avalonia and ASP.NET.
- Installing Hosting Bundle so that your ASP.NET would run under IIS.
- Using IIS/Kestrel in and out of hosting model.
- Using Cors.
- Obtaining and deploying a free SSL certificate so that your website would not be marked as insecure by Web browsers.
- Wiring ASP.NET Response Compression for WebAssembly files to improve speed.
- Deploying ASP.NET web sites using Publish.
No gRPC Client Streaming in Browser (unfortunately)
There is a known limitation of grpc-web (the only gRPC framework that can be used for gRPC browser based clients) - it allows only server side streaming (gRPC-Web Streaming).
No client streaming or bidirectional streaming is allowed.
Using gRPC over WebAssembly has appearance of allowing Client side and bidirectional streaming, but in fact it is not correct.
As we shall learn from the samples below, when you try to stream from a Web Browser client, the messages are not sent one by one, but instead they are accumulated on the client until the client indicates the end of streaming. Then all the accumulated messages are sent to the server together.
gRPC ASP.NET Code Samples
The Source Code
The source code for this article is located under ASP.NET gRCP Samples.
In order to maximize the code reuse between various samples, I built an all encompassing solution - AspGrpcTests.sln (which you never have to open) and a number of filtered solutions (.slnf) files around it. The .slnf files filter in the functionality specific to each sample.
Kestrel only Code Sample
Opening and Running the Kestrel only Solution
Kestrel can be used without IIS as a stand-alone process or Windows service. This sample (located in GrpcKestrelOnlyImplementation.slnf solution filter) demonstrates gRPC communications between a Kestrel-only server running as a console process and a local C# console client.
To run this sample open GrpcKestrelOnlyImplementation.slnf in Visual Studio 2022. You can see the following projects and files in the solution explorer:
GrpcServerProcess is the project containing the Kestrel server and ConsoleTestClient is the project to start the local gRPC C# client.
Start the server process first (by right-clicking GrpcServerProcess and selecting Debug->Run Without Debugging). This will start the Kestrel server as a process on your windows. Note, that since the output of the server in not in HTML format, it will start a browser displaying an error something like (This local page can't be found). Do not worry about it and do not kill the browser if you want your server to continue running.
Then start the console client (project ConsoleTestClient) in the similar fashion. Here is what will be printed on the client console:
Unary server call sample:
Hello Joe Doe!!!
Streaming Server Sample:
Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Hello Joe Doe 12
Hello Joe Doe 13
Hello Joe Doe 14
Hello Joe Doe 15
Hello Joe Doe 16
Hello Joe Doe 17
Hello Joe Doe 18
Hello Joe Doe 19
Hello Joe Doe 20
Streaming Server Sample with Error:
Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Status(StatusCode="Internal", Detail="Error Status Detail (for the client)")
Streaming Client Sample:
Hello Client_1, Client_2, Client_3
Bidirectional Streaming Client/Server Sample:
Hello Client_1_1
Hello Client_1_2
Hello Client_1_3
Hello Client_1_4
Hello Client_2_1
Hello Client_2_2
Hello Client_2_3
Hello Client_2_4
Hello Client_3_1
Hello Client_3_2
Hello Client_3_3
Hello Client_3_4
There are five gRPC scenarios demo'ed by this sample:
- Unary sample - client sends "Joe Doe" string to the server and it returns "Hello Joe Doe!".
- Streaming server sample - client sends single string ("Joe Doe") and returns multiple greetings.
- Streaming server sample with error - same as the streaming server sample above, only the server throws an
RpcException
in the middle of the stream (after the 11th iteration). - Streaming client sample - client send multiple strings ("Client_1", "Client_2" and "Client_3") and the server concatenates them and returns the "Client_1, Client_2, Client_3" string.
- Bidirectional streaming sample - the client sends 3 requests within ("Client_1", "Client_2" and "Client_3") and the server responds with 4 responses for each of the client requests (everything is happening in simultaneously - new client requests can arrive while the server responds to the old ones).
A Note on the Code Overview
Since similar or identical code is used in the rest of the samples, I will provide a very detailed review of the gRPC server and client code in this sample, while in the future samples, I'll be only emphasizing the differences with the current code.
Source Code Overview
The source code for this sample consists of 4 projects:
- GrpcServerProcess - starting and running Kestrel server.
- GreeterImpl - a re-usable library containing the gRPC server functionality implementation (GrpcServerProcess depends on it).
- Protos - containing the Greeter.proto gRPC proto file shared between the server (via GreeterImpl) and the client.
- ConsoleTestClient - the console client for running all the client gRPC tests against the server (it depends on Protos project as well).
The project dependencies are reflected on the diagram below:
Protos project contains Greeter.proto file defining gRPC protos for the gRPC methods:
syntax = "proto3";
option csharp_namespace = "simple";
service Greeter
{
rpc SayHello (HelloRequest) returns (HelloReply);
rpc ServerStreamHelloReplies (HelloRequest) returns (stream HelloReply);
rpc ServerStreamHelloRepliesWithError (HelloRequest) returns (stream HelloReply);
rpc ClientStreamHelloRequests(stream HelloRequest) returns (HelloReply);
rpc ClientAndServerStreamingTest(stream HelloRequest) returns (stream HelloReply);
}
message HelloRequest
{
string name = 1;
}
message HelloReply
{
string msg = 1;
}
All the Greeter
methods take HelloRequest
message(s) as requests and return HelloReply
messages as replies. I could have used built-in string
protobuf Type for both, but wanted to show a more complex example with request and reply types defined in the proto file.
Note that there is an integer specified next to a field within HelloRequest
and HelloReply
protos, e.g.:
string name = 1;
This integer should uniquely define a protobuf field, so that if new fields are added to the type , they should have different integers - in that case, the new expanded type will be backward compatible.
The most interesting server code is located within GreeterImplementation.cs file under the reusable GreeterImpl project.
Take a look at GreeterImpl.csproj file. It shows how to add a reference to a proto file contained by a different project:
<ItemGroup>
<Protobuf Include="..\Protos\Greeter.proto" GrpcServices="Server" />
...
</ItemGroup>
We simply provide a relative path to the file.
The GrpcServices="Server"
means that the server skeleton will be generated for the proto methods. Note that GreeterImpl project also has a reference to Grpc.AspNetCore package which has references needs to the generated the server skeleton.
GreeterImplementation
class of GreeterImpl project inherits from the generated skeleton simple.Greeter.GreeterBase
class. It provides overriding implementations for all the gRPC server methods, e.g here is the override for the simplest - single request/single reply SayHello(...)
method:
public override async Task<helloreply> SayHello(HelloRequest request, ServerCallContext context)
{
string name = request.Name;
return new HelloReply { Msg = $"Hello {name}!!!" };
}
</helloreply>
GrpcServerProcess is the actual project that starts Kestrel server supporting the gRPC method implementations defined within GreeterImplementations
class.
The local server port is defined within its appsettings.json file:
"Kestrel": {
"Endpoints": {
"Grpc": {
"Url": "https://localhost:55003",
"Protocols": "Http2"
}
}
}
The rest of its functionality is contained within Program.cs file:
using GrpcServerProcess;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<GreeterImplementation>().RequireHost("*:55003");
app.Run();
ConsoleTestClient project also references Greeter.proto file but with GrpcServices="Client"
: That instructs the Visual Studio to generate client proxies for Greeter
methods.
ConsoleTestClient also needs to provide references to Grpc.Net.Client, Google.Protobuf and Grpc.Tools packages:
<ItemGroup>
<PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Grpc.Tools" Version="2.62.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<Protobuf Include="..\Protos\Greeter.proto" GrpcServices="Client" />
</ItemGroup>
The really interesting custom code is server implementations of gRPC methods and client calls to those methods. Server implementations are contained within GreeterImplementations.cs file (under the re-usable GreeterImpl project) and client calls are contained within Program.cs file of the ConsoleTestClient projects.
In the subsequent sections below I shall explain those methods one by one.
Single Request / Single Reply SayHello(...) method
This is the simplest method to implement and to call (since there is no either server or client side streaming).
Here is the protobuf code for this method from Greeter.proto file located under Protos project:
service Greeter
{
rpc SayHello (HelloRequest) returns (HelloReply);
...
}
Here is the client code (from ConsoleTestClient/Program.cs file):
var channel =
GrpcChannel.ForAddress("https://localhost:55003");
var greeterGrpcClient = new Greeter.GreeterClient(channel);
string greetingName = "Joe Doe";
Console.WriteLine($"Unary server call sample:");
var reply =
await greeterGrpcClient.SayHelloAsync(new HelloRequest { Name = greetingName });
Console.WriteLine(reply.Msg);
The code at the top of the snippet above connects the gRPC client to the grpc server at "https://localhost:55003" url and creates the greeterGrpcClient
object to call the server methods.
The server call itself takes only one line:
var reply = await greeterGrpcClient.SayHelloAsync(new HelloRequest { Name = greetingName });
We create the HelloRequest
object setting its name to "Joe Doe", send it over to the server and wait for the reply.
reply.Msg
will contain the "Hello Joe Doe!" greeting string.
The server implementation (located within GreeterImplementation.cs file) is also very simple:
public override async Task<helloreply> SayHello(HelloRequest request, ServerCallContext context)
{
string name = request.Name;
return new HelloReply { Msg = $"Hello {name}!!!" };
}
</helloreply>
The client call will result in the following msg printed to the console:
Hello Joe Doe!!!
Server Streaming Samples
There are two Server Streaming samples - one plain ServerStreamHelloReplies(...)
method and the other one with server throwing an RpcExeption
in the middle of the streaming responses being sent back to the client ServerStreamHelloRepliesWithError(...)
.
Here is the protobuf code for these two methods:
service Greeter
{
...
rpc ServerStreamHelloReplies (HelloRequest) returns (stream HelloReply);
rpc ServerStreamHelloRepliesWithError (HelloRequest) returns (stream HelloReply);
...
}
Here is the client code for the plain method (without server error):
var serverStreamingCall = greeterGrpcClient.ServerStreamHelloReplies(new HelloRequest { Name = greetingName });
await foreach(var response in serverStreamingCall.ResponseStream.ReadAllAsync())
{
Console.WriteLine(response.Msg);
}
The result of this client method call will be a stream of messages printed to the console:
Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Hello Joe Doe 12
Hello Joe Doe 13
Hello Joe Doe 14
Hello Joe Doe 15
Hello Joe Doe 16
Hello Joe Doe 17
Hello Joe Doe 18
Hello Joe Doe 19
Hello Joe Doe 20
The client code for the server streaming with error is very similar, only its is encased within try/catch
block:
var serverStreamingCallWithError = greeterGrpcClient.ServerStreamHelloRepliesWithError(new HelloRequest { Name = greetingName });
try
{
await foreach (var response in serverStreamingCallWithError.ResponseStream.ReadAllAsync())
{
Console.WriteLine(response.Msg);
}
}
catch(RpcException exception)
{
Console.WriteLine(exception.Message);
}
And here is what it prints to the console:
Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Status(StatusCode="Internal", Detail="Error Status Detail (for the client)")
The server throws an error after the 11th iteration and the error message intended for the client is "Error Status Detail (for the client)" (the server can write another, more detailed, error message to its log).
Now let us take a look at the server implementations of the server streaming methods within GreeterImplementation.cs file:
public override async Task ServerStreamHelloReplies
(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
await ServerStreamHelloRepliesImpl(request, false, responseStream, context);
}
public override async Task ServerStreamHelloRepliesWithError
(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
await ServerStreamHelloRepliesImpl(request, true, responseStream, context);
}
Both methods call ServerStreamHelloRepliesImp(...)
method, one passing the argument throwException
set to false
and the other set to true
.
private async Task ServerStreamHelloRepliesImpl
(
HelloRequest request,
bool throwException,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
string name = request.Name;
for (int i = 0; i < 20; i++)
{
await Task.Delay(200);
await responseStream.WriteAsync(new HelloReply { Msg = $"Hello {name} {i + 1}" });
context.CancellationToken.ThrowIfCancellationRequested();
if (i == 10 && throwException)
{
throw new RpcException(new Status(StatusCode.Internal, "Error Status Detail (for the client)"), "ERROR: Cannot Continue with streaming (server error)!");
}
}
}
Note that the line:
context.CancellationToken.ThrowIfCancellationRequested();
serves to cancel the server streaming from the client side. Client side cancellation will be demo'ed later when we discuss the web clients.
Client Streaming Sample
Next sample (method Greeter.ClientStreamHelloRequests(...)
) demonstrates async streaming from the client, accumulating the results on the server and returning the single value result to the client after the client streaming ended.
Here is the protobuf declaration of this methods:
service Greeter
{
...
rpc ClientStreamHelloRequests(stream HelloRequest) returns (HelloReply);
...
}
Here is the client code:
var clientSreamingCall = greeterGrpcClient.ClientStreamHelloRequests();
for(int i = 0; i < 3; i++)
{
await clientSreamingCall.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });
}
await clientSreamingCall.RequestStream.CompleteAsync();
var clientStreamingResponse = await clientSreamingCall;
Console.WriteLine(clientStreamingResponse.Msg);
The server returns string "Hello " followed by the comma separated concatenation of the messages from the client:
Hello Client_1, Client_2, Client_3
Here is the server code:
public override async Task<HelloReply> ClientStreamHelloRequests(IAsyncStreamReader<HelloRequest> requestStream, ServerCallContext context)
{
string message = "Hello ";
bool first = true;
await foreach (var inputMessage in requestStream.ReadAllAsync())
{
if (!first)
{
message += ", ";
}
message += inputMessage.Name;
first = false;
}
return new HelloReply { Msg = message };
}
Note, that the server receives each of the client messages as they are streamed, without waiting for the client streaming to end. You can observe it by running the server in a debugger and putting a breakpoint within the await foreach(...)
loop.
Bidirectional Streaming Sample
Finally we came the most interesting and complex sample - the one that demonstrates streaming both client requests and server replies simultaneusly: the server does not wait for the client to finish its streaming it starts replying after each client request arrives.
The protobuf method is called Greeter.ClientAndServerStreamingTest(...)
:
service Greeter
{
...
rpc ClientAndServerStreamingTest(stream HelloRequest) returns (stream HelloReply);
}
Note that both requests and replies are streamed.
Here is the client implementation of calling bidirectional streaming functionality:
var clientServerStreamingCall = greeterGrpcClient.ClientAndServerStreamingTest();
var readTask = Task.Run(async () =>
{
await foreach (var reply in clientServerStreamingCall.ResponseStream.ReadAllAsync())
{
Console.WriteLine(reply.Msg);
}
});
for (int i = 0; i < 3; i++)
{
await clientServerStreamingCall.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });
await Task.Delay(1000);
}
await Task.WhenAll(readTask, clientServerStreamingCall.RequestStream.CompleteAsync());
The readTask
defined at the top will be used to get the streaming replies, one by one.
Here is what the client prints to console:
Hello Client_1_1
Hello Client_1_2
Hello Client_1_3
Hello Client_1_4
Hello Client_2_1
Hello Client_2_2
Hello Client_2_3
Hello Client_2_4
Hello Client_3_1
Hello Client_3_2
Hello Client_3_3
Hello Client_3_4
For every string coming from the client, the server sends back 4 replies e.g. for Client_1, the replies will be Client_1_1, Client_1_2, Client_1_3 and Client_1_4.
Note that the interval between streaming client messages to the server is pretty large - 1 second and yet, there is almost no delay in server replies. This is because the server starts replying after the first client message is received and continues replying without waiting for the client streaming to be completed.
This is what the real bi-directional streaming is and unfortunately it will NOT work from the browser as will be explained below.
Overview of Browser Based Code Sample Code Samples
The rest of the ASP.NET gRPC code samples will be browser based. One sample demonstrates the HTML/JavaScript using gRPC to exchange information with the server and the other Avalonia WebAssembly.
The server gRPC code for those samples will be exactly the same as in the previous sample - located within GreeterImplementation.cs file under GreeterImpl project. Because of that we shall concentrate primarily on the client code and ASP.NET specific server code.
HTML/JavaScript ASP.NET gRPC Sample
Running the Sample
Start the solution filter AspGrpcServerWithRazorClient.slnf. Make AspGrpcServerWithRazorClient project to be your startup project. Build the project and run it under the debugger.
Here is the page you'll see:
You can try changing the name (or leaving it set to "Nick"), then clicking "Get Single Greeting" button and you'll get "Hello Nick!!!" printed in the browser:
Now try clicking "Get Multiple Greetings" button. You will see multiple greeting streaming from the server from "Hello Nick 1" to "Hello Nick 20" and at the end it will print "STREAMING ENDED" (if you let it run to the end):
You can also cancel the server streaming at any point by pressing button "Cancel Streaming".
Pressing button "Get Multiple Greetings with ERROR" will call the on the server method that will throw an RpcException
after 11th iteration. Here is how the resulting error is displayed:
Creating the ASP.NET Project for Hosting the Sample
I created the ASP.NET project AspGrpcServerWithRazorClient.csproj by choosing "ASP.NET Core Web App (Razor Pages)" project type:
Then I modified its Program.cs, Index.cshtml and _Layout.cshtml as will be detailed below.
Generating JavaScript Client Proxies from Protobuf Code
As a prerequisite for everything that follows, please, install nodeJS and npm e.g. by going to Node JS Install, downloading the node .msi installer and running it on your windows machine.
In order to build the server you need to
- Install protoc protobuffer compiler e.g. from Protoc Compiler Download by downloading and unzipping protoc-26.1-win64.zip (or some other file depending on your machine). Make sure protoc.exe is in your path.
- Generate the JavaScript client proxies from Greeter.proto protobuf file using protoc compiler. Protoc will generate nodeJS JavaScript proxies. Here is the command that needs to be run from a command line from inside the Protos folder (of the Protos project) to generate the client proxies:
protoc -I=. Greeter.proto --js_out=import_style=commonjs:..\AspGrpcServerWithRazorClient\wwwroot\dist --grpc-web_out=import_style=commonjs,mode=grpcwebtext:..\AspGrpcServerWithRazorClient\wwwroot\dist
This line is also contained in README.txt file within the same folder. - The files Greeter_grpc_web_pb.js and Greeter_pb.js will be created under AspGrpcServerWithRazorClient\wwwroot\dist folder.
These files are nodeJS modules and not fit to be run from the browser. - In order to convert the client proxies to ES modules (that can be run in the browser) I use "WebPack Task Runner" VS2022 extension:
You need to install this extension also. - There is a tiny JavaScript file AspGrpcServerWithRazorClient/wwwroot/dist/client.js that refers to all the functionality that the JavaScript client needs from the generated Greeter_..._pb.js files:
const { HelloRequest, HelloReply } = require('./Greeter_pb.js');
const { GreeterClient } = require('./Greeter_grpc_web_pb.js');
global.HelloRequest = HelloRequest;
global.HelloReply = HelloReply;
global.GreeterClient = GreeterClient;
This file is used by webpack for generating the main.js file that can be used by the browser client. - In order to use webpack file generation, we need to create webpack.config.js file at the project level. Here is its content:
const path = require('path');
module.exports = {
mode: 'development',
entry: './wwwroot/dist/client.js',
output: {
path: path.resolve(__dirname, 'wwwroot/dist'),
filename: 'main.js',
},
};
It will instruct webpack to take client.js and every file it depends on and based on them create main.js file (located under the same dist folder) which can be used by the JavaScript browser clients. - In order to do the conversion - right click webpack.config.js file in the solution explorer and choose "Task Runner Explorer":
It should open the Task Runner Explorer utility. Click "Run Development" or "Run Production" and choose "Run" menu option:
It will generate file main.js which can be used in browser JavaScript code.
HTML/JavaScript Client Code Overview
HTML/JavaScript client code that uses the proxies generated in the previous subsection is located within AspGrpcServerWithRazorClient/Pages/Index.cshtml file. The file is virtually all HTML/JavaScript with no ASP code generation.
The HTML part of the file defines labels, buttons and divs (as placeholders for adding text):
<h1>ASP gRPC Samples</h1>
<label>Enter Name:</label>
<input type="text" id="TheName" value="Nick">
<h2>Non-Streaming (Single Value) gRPC Sample:</h2>
<button type="button" id="GetSingleGreetingButton">Get Single Greeting</button><br /><br />
<div id="TheSingleValueResponse" style="font-weight:bold;font-size:20px;font-style:italic"></div>
<h2>Server Streaming gRPC Sample (multiple greetings from the Server):</h2>
<button type="button" id="GetMultipleGreetingsButton">Get Multiple Greeting</button>
<button type="button" id="GetMultipleGreetingsWithErrorButton">Get Multiple Greeting with ERROR</button>
<button type="button" id="CancelStreamingButton">Cancel Streaming</button>
<br /><br />
<div id="TheStreamingResponse" style="font-weight:bold;font-size:20px;font-style:italic"></div>
<label id="TheErrorLabel" style="visibility:collapse">Streaming Error:</label>
<div id="TheStreamingError"></div>
<div id="TheStreamingEnded"></div>
Then two modules are added -
- main.js file generated in the previous subsection. main.js module is used get the client proxy code).
- jquery.min.js - for finding HTML tree nodes and modifying them.
<script src="./dist/main.js"></script>
<script src="./dist/jquery.min.js"></script>
And then finally there is JavaScript client code within <script type="text/javascript">
HTML tag.
Client Code to Create the gRPC Greeter Client
var location = window.location;
var url = location.origin;
var greeterServiceClient = new GreeterClient(url);
After creating the client greeterServiceClient
, we use it for gRPC service calls.
Calling Single Request / Single Reply SayHello(...) Service from JavaScript Client
Here is how we call SayHello(...)
service from JavaScript client:
var request = new HelloRequest();
var name = $("#TheName").val();
request.setName(name);
greeterServiceClient.sayHello(request, {}, (err, response) => {
var msg = response.getMsg();
$("#TheSingleValueResponse").text(msg);
});
This code itself is assigned using jQuery to be a callback on the "#GetSingleGreetingButton" click:
$("#GetSingleGreetingButton").on(
"click",
() => {
...
});
Calling Streaming gRPC Services form the Client
For server streaming calls we define a global variable var stream = null;
and then use it to call the streaming services and to cancel them if needed.
For the sake of the code reuse there is a single JavaScript getStreamedGreetings(greeterServiceClient, throwError)
method whose second argument is a Boolean flag - that should be set to true
to call the gRPC streaming service that throws an error after the 11th iteration and false
for the one that allows the streaming service to run all the way to the end.
Here is how streaming service invocation callbacks are assigned to the corresponding buttons:
$("#GetMultipleGreetingsButton").on(
"click",
() => getStreamedGreetings(greeterServiceClient, false)
);
$("#GetMultipleGreetingsWithErrorButton").on(
"click",
() => getStreamedGreetings(greeterServiceClient, true)
);
Here is the getStreamedGreetings(...)
method implementation (with detailed comments):
getStreamedGreetings = (greeterServiceClient, throwError) => {
$("#TheStreamingEnded").text('');
$("#TheErrorLabel").css("visility", "collapse");
$("#TheStreamingError").text('');
var request = new HelloRequest();
var name = $("#TheName").val();
request.setName(name);
if (!throwError) {
stream = greeterServiceClient.serverStreamHelloReplies(request, {});
}
else {
stream = greeterServiceClient.serverStreamHelloRepliesWithError(request, {});
}
stream.on('data', (response) => {
var msg = response.getMsg();
$("#TheStreamingResponse").text(msg);
});
stream.on('status', (status) => {
});
stream.on('error', (err) => {
$("#TheErrorLabel").css("visility", "visible");
$("#TheStreamingError").text(err);
stream = null;
});
stream.on('end', () => {
$("#TheStreamingEnded").text("STREAMING ENDED");
stream = null;
})
Canceling Server Streaming
All one needs to do is to cancel a server stream is to call stream.cancel("Cancellation Message")
.
ASP.NET Code for Building and Starting ASP.NET Server
This code is located within well documented Program.cs file under AspGrpcServerWithRazorClient project:
using GrpcServerProcess;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddGrpc();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseGrpcWeb();
app.MapRazorPages();
app.MapGrpcService<GreeterImplementation>().EnableGrpcWeb();
app.Run();
ASP.NET Avalonia/WebAssembly gRPC Code Sample
Running the Sample
To run the sample, please, open AspGrpcWithAvaloniaClients.slnf solution filter file, make AspGrpcServer the start-up project, then re-build it and start it.
Here is the screen you are going to see (after a second or too):
The upper two samples (request/reply and server samples) will behave almost exactly the same as those of the previous section:
The two bottom samples correspond to Client streaming and bi-directional client-server streaming and the purpose is to show that they will NOT work correctly in browser.
Indeed, if you press "Test Streaming Client" or "Test Streaming Client and Server" buttons, the server will get the messages and start responding only when the client stream is completed (it is 5 seconds for Streaming Client sample and 3 second for bi-directional sample). Which essentially means that client streaming is not working (even though C# generates client proxies for those protobuf methods).
Creating Client C# Avalonia Projects
To create an Avalonia WebAssembly project I use instructions from Creating Avalonia Web Assembly Project:
- I install wasm-tools (or make sure they are installed) by running
dotnet workload install wasm-tools
from a command line. - I update to the latest avalonia dotnet templates by running command:
dotnet new install avalonia.templates
- I create a folder for the project (in my case it is called AvaGrpcClient) and cd to it using the command line.
- From within that folder, I run from the command line:
dotnet new avalonia.xplat
- This will create the shared project AvaGrpcClient (within the same-named folder) and a number of platform specific projects.
- I remove most of the platform specific projects leaving only AvaGrpcClient.Browser (for building the Avalonia WebAssembly bundle) and AvaGrpcClient.Display (for debugging and faster prototyping if needed).
Client Avalonia Code
In order for the project AvaGrpcClient to generate the gRPC client proxies, I add a reference to the protobuf file to AvaGrpcClient.csproj file with GrpcServices flag set to Client:
<Protobuf Include="..\..\Protos\Greeter.proto" GrpcServices="Client" />
I also add references to packages required by Grpc including Grpc.Net.Client.Web package needed specifically for the grpc to work in a browser:
<ItemGroup>
<PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Grpc.Net.Client.Web" Version="2.62.0" />
<PackageReference Include="Grpc.Tools" Version="2.62.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
...
</ItemGroup>
Almost all Sample specific client code is located within MainView.xaml.cs and MainView.xaml files. MainView.xaml specifies the visual layout while all the code interacting with the server is inside MainView.xaml.cs file.
The connection to the client is established within MainView
class constructor:
var channel =
GrpcChannel.ForAddress
(
CommonData.Url!,
new GrpcChannelOptions
{
HttpHandler = new GrpcWebHandler(new HttpClientHandler())
});
_greeterGrpcClient = new Greeter.GreeterClient(channel);
...
_serverStreamCancellationTokenSource = new CancellationTokenSource();
Note the CommonData.Url
static string property. It should contain the URL for connected to the Grpc calls ( nn my case it is the same as ASP.NET server URL).
This CommonData.Url
property is set to the first argument passed to the Program.Main(...)
method contained in AvaGrpcClient.Browser solution (solution that actually creates WebAssembly browser code as a number of .wasm files):
internal sealed partial class Program
{
private static async Task Main(string[] args)
{
CommonData.Url = args[0];
await BuildAvaloniaApp()
.WithInterFont()
.UseReactiveUI()
.StartBrowserAppAsync("out");
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>();
}
Note also that calling the following code
await BuildAvaloniaApp()
.WithInterFont()
.UseReactiveUI()
.StartBrowserAppAsync("out");
essentially replaces an HTML element with id "out" by the Avalonia Browser application (in our case it is MainView
object instance).
The callbacks fired when the corresponding buttons are pressed are defined as methods within MainView.xaml.cs file.
Here is the code for calling Single Request/Single Response (Unary) service SayHello
from the client:
private async void TestUnaryHelloButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var reply =
await _greeterGrpcClient.SayHelloAsync(new HelloRequest { Name = GreetingName });
HelloResultText.Text = reply.Msg;
}
where GreetingName
property is the Text
value from the TextBox
defined in MainView.xaml file:
private string GreetingName => NameToEnter.Text ?? string.Empty;
Here is the documented client code for testing server streaming:
private async void TestStreamingServerButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
StreamingServerResultsText.Text = string.Empty;
StreamingErrorText.Text = string.Empty;
try
{
var serverStreamingResponsesContainer =
_greeterGrpcClient.ServerStreamHelloReplies(new HelloRequest { Name = GreetingName });
await foreach (var response in serverStreamingResponsesContainer.ResponseStream.ReadAllAsync(_serverStreamCancellationTokenSource.Token))
{
StreamingServerResultsText.Text = response.Msg;
}
}
catch(RpcException exception)
{
StreamingErrorText.Text = $"ERROR: {exception.Message}";
}
}
Code for testing server streaming with error is exactly the same, only calls a different service ServerStreamHelloRepliesWithError(...)
:
var serverStreamingResponsesContainer = _greeterGrpcClient.ServerStreamHelloRepliesWithError(new HelloRequest { Name = GreetingName });
The method below cancels server streaming from the client
private void TestStreamingServerCancelButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_serverStreamCancellationTokenSource?.Cancel();
_serverStreamCancellationTokenSource = new CancellationTokenSource();
}
The next method demonstrates using the Client Streaming API. Because we work in C#, the Client Streaming API has been generated, and can be used - but, unfortunately it does not do the streaming from the browser. It waits until the client streaming is finished and then sends all the accumulated messages together:
private async void TestStreamingClientButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
StreamingClientResultsText.Text = string.Empty;
var clientStreamContainer = _greeterGrpcClient.ClientStreamHelloRequests();
for (int i = 0; i < 5; i++)
{
await clientStreamContainer.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });
await Task.Delay(1000);
}
await clientStreamContainer.RequestStream.CompleteAsync();
var clientStreamingResponse = await clientStreamContainer;
StreamingClientResultsText.Text = clientStreamingResponse.Msg;
}
And here is the code demonstrating bi-directional (client and server) streaming API suffering from the same problem (there is no Client side streaming from the browser - the browser accumulates client messages and sends the all together one the client closes the 'stream'):
private async void TestStreamingClientServerButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var clientServerStreamContainer = _greeterGrpcClient.ClientAndServerStreamingTest();
StreamingClientServerResultsText.Text = string.Empty;
var readTask = Task.Run(async () =>
{
await foreach (var reply in clientServerStreamContainer.ResponseStream.ReadAllAsync())
{
await Dispatcher.UIThread.InvokeAsync(() => { StreamingClientServerResultsText.Text += reply.Msg + "\n"; });
}
});
for (int i = 0; i < 3; i++)
{
await clientServerStreamContainer.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });
await Task.Delay(1000);
}
await Task.WhenAll(readTask, clientServerStreamContainer.RequestStream.CompleteAsync());
}
ASP.NET Server Code
The server ASP.NET code is located within AspGrpcServer project.
There are two things to know about the project:
- I made it build-dependent on AvaGrpcClient.Browser project by right-clicking the solution and choosing Project Dependencies menu item and then choosing the main project AspGrpcServer and making it dependent on AvaGrpcClient.Browser.
This ensures that every time you rebuild the server, the AvaGrpcClient.Browser project will be built before that and since the AvaGrpcClient.Browser project proj-depends on AvaGrpcClient, the AvaGrpcClient project will be built event before that. - There is a post-build event defined within the AspGrpcServer project (see the bottom of AspGrpcServer.csproj file). This event copies AppBundle/_framework folder created by AvaGrpcClient.Browser build over to AspGrpcServer/wwwroot/_framework folder. Note - this folder (_framework) should be ignored by the version control tool.
I modified the Shared/_Layout.cshtml code to be simpler and also I modified the Index.cshtml file to contain a <div id="out">
which will be replaced by Avalonia WebAssembly MainView
object instance (as was explained above).
Here is the code for Index.cshtml:
<body style="margin: 0px; overflow: hidden">
<!--
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2 class="purple">
Powered by
<a class="highlight" href="https://www.avaloniaui.net/" target="_blank">Avalonia UI</a>
</h2>
</div>
<img class="icon" src="~/Logo.svg" alt="Avalonia Logo" />
</div>
</div>
<!--
<script type='module' src="~/mainForAvalonia.js"></script>
</body>
On the last line of the above code - note that we are loading mainForAvalonia.js module. This module/file contains the JavaScript code that actually creates the Wasm and runs the wasm project in the browser.
Here is the JavaScript code contained inside mainForAvalonia.js file:
import { dotnet } from './_framework/dotnet.js'
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
const { getConfig, runMain } = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.create();
const config = getConfig();
await runMain(config.mainAssemblyName, [window.location.origin]);
Note the last line - this is where we we pass the current server URL to server also as the URL for creating gRPC client communication channel.
Now take a look at the server's start-up file AspGrpcServer/Program.cs. The difference from a similar file from the previous (HTML/JavaScript) sample is very small. The only difference is that we need to add Mime types to allow returning .wasm and some other files to the client. Here is how it is achieved:
var contentTypeProvider = new FileExtensionContentTypeProvider();
var dict = new Dictionary<string, string>
{
{".pdb" , "application/octet-stream" },
{".blat", "application/octet-stream" },
{".bin", "application/octet-stream" },
{".dll" , "application/octet-stream" },
{".dat" , "application/octet-stream" },
{".json", "application/json" },
{".wasm", "application/wasm" },
{".symbols", "application/octet-stream" }
};
foreach (var kvp in dict)
{
contentTypeProvider.Mappings[kvp.Key] = kvp.Value;
}
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = contentTypeProvider });
We create build a FileExtensionContentTypeProvider
object inserting needed new MIME types into it and then we pass it as ContentTypeProvider
property of StaticFileOptions
object to the app.UseStaticFiles(...)
method.
Conclusion
This is an article in which I provide easy samples and detailed explanations of all the use-cases for gRCP communications between various clients (including C# Console, HTML/JavaScritp and C# in Browser via WebAssembly clients), and gRPC-enabled ASP.NET server.