With WCF (Windows Communication Foundation) no longer being actively developed, gRPC (remote procedure call) appears to be the natural replacement when it comes to developing greenfield service applications on .NET Core and .NET 5. Unlike ASP.NET Core Web APIs, it supports both bidirectional streaming over HTTP/2 and has a small and efficient default binary message format, which makes it an ideal choice for implementing low latency and high throughput real-time services.
gRPC was originally developed and used internally by Google (hence the little g) and is now fully open sourced on GitHub (http://bit.ly/2YZRNOJ). It's being described as “a modern open source high performance RPC framework that can run in any environment” and includes code generators that produce client and server-side stubs for a variety of programming languages, including C#.
Bindings
In WCF, you define one or several endpoints to expose your services. Each endpoint consists of an address, a binding, and a contract. The address tells where to find the service and the binding defines the transport protocol to use when communicating with it, how to encode the messages, and what security mechanisms to use. You can choose among a number of system-provided bindings or create your own custom ones.
In the .NET implementations of gRPC, the communication is done over HTTP/2. No other transport protocols are supported. On top of the transport layer, there's the concept of a channel. Under the hood, the channel takes care of connecting to the server and handles things such as load balancing and connection pooling. It provides a single virtual connection, which may be backed by several physical connections internally, to a conceptual endpoint. As an application developer, you don't really need to bother with the details of how this is implemented. You write your code against the generated stubs rather than dealing with the channel directly.
Contracts
The stubs are generated based on a .proto file, which is nothing but a plain text file with a .proto file extension. It defines the service contract and the structure of the payload messages to be exchanged between the client and the server. In WCF, there's a concept of a Web service description language (WSDL) and a metadata exchange (MEX) endpoint that both describe the metadata of a service. gRPC supports a server reflection protocol for the same purpose, but the common and preferred way to expose a service's methods and how to call them is to simply share the .proto file with the consuming clients.
The serialization format in gRPC is agnostic, but the default language interface definition language (IDL) that's used to describe the service and the payload messages is called the protocol buffer language. A protocol compiler, called protoc, is used to compile the .proto file and generate code in any of the supported programming languages. For C#, it generates a .cs source file with a class for each message type that's defined in the file and another source file that contains the base classes for the gRPC service and client.
Let's take a quick look at how to migrate the following WCF service from the classic getting started sample in the official docs to gRPC:
[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples")
public interface ICalculator
{
[OperationContract]
double Add(double n1, double n2);
[OperationContract]
double Subtract(double n1, double n2);
[OperationContract]
double Multiply(double n1, double n2);
[OperationContract]
double Divide(double n1, double n2);
}
The first step is to create a .proto file. I prefer to put it in a .NET Standard class library that can be referenced from both client and server applications. The contents of the file might look something like this:
syntax = "proto3";
option csharp_namespace = "SharedLib.Generated";
service CalculatorService {
rpc Add (CalculatorRequest) returns (CalculatorReply) {}
rpc Subtract (CalculatorRequest) returns (CalculatorReply) {}
rpc Multiply (CalculatorRequest) returns (CalculatorReply) {}
rpc Divide (CalculatorRequest) returns (CalculatorReply) {}
}
message CalculatorRequest {
double n1 = 1;
double n2 = 2;
}
message CalculatorReply {
double result = 1;
}
Note that each method accepts only a single payload message, similar to a WCF message contract. You then define fields with unique numbers in each message. The numbers are required to be able to encode and decode binary messages over the wire without breaking backward compatibility. These binary messages are known as protocol buffers (or Protobuf for short) - a fast language and platform-neutral serialization mechanism that was also developed by Google. Unlike both XML and JSON, the messages are encoded in a binary format, which makes them small and easy to write and fast to read. According to the docs, protocol buffers are, for example, three to 10 times smaller and 20 to 100 times faster than using XML for serializing structured data. You'll find more information about how to write .proto files, including a list of all support data types, in the protocol buffers language guide at https://bit.ly/2Z5bl8x.
Code Generation
There are two different sets of libraries that provide gRPC support in .NET. Grpc.Core is Google's original implementation. It's a managed wrapper that invokes the functionality of a native C library via P/Invoke. grpc-dotnet is Microsoft's new implementation of gRPC in ASP.NET Core 3. It uses the cross-platform Kestrel Web server on the server-side and the System.Net.HttpClient class on the client-side and doesn't rely on any native library. Unlike Grpc.Core, it targets .NET Standard 2.1 and isn't compatible with the .NET Framework or any previous version of .NET Core.
If you have any server or client applications to be migrated to gRPC but are yet to be upgraded to .NET Core 3, you should install the required Grpc and Google.Protobuf NuGet packages and optionally also the Grpc.Tools package into your class library project after you've created the .proto file. Grpc.Tools is a development dependency that integrates with MSBuild to provide automatic code generation of the .proto file as part of the build process. In order for any code to be generated, you also need to add a <Protobuf>
element to your project file where you reference the .proto file. The complete contents of the .csproj file should look something like Listing 1. Grpc is a metapackage for Grpc.Core
.
Listing 1: The project file with all required packages
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard1.5</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.9.1" />
<PackageReference Include="Grpc" Version="2.23.0" />
<PackageReference Include="Grpc.Tools" Version="2.23.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native;contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="calculator.proto" />
</ItemGroup>
</Project>
If you use grpc-dotnet and .NET Core 3, there's a new dotnet-grpc tool that installs all required packages and modifies the project file for you. You can use it from the command-line or by choosing Project > Add Service Reference
in Visual Studio 2019 16.3 and selecting the “Add new gRPC service reference” option, as shown in Figure 1.
If you build the project and look in the obj/Debug/netstandard2.x
folder (replace Debug with Release or whatever build configuration you may be using) inside the project directory, you should see two generated files named Calculator.cs
and CalculatorGrpc.cs
. The former contains the CalculatorRequest
and CalculatorReply
message types and the latter contains the stubs for the gRPC service and client. If you prefer to include these source files in your project, you can add the following attributes to the <Protobuf>
element in the .csproj file. You shouldn't edit the source files manually though.
<Protobuf Include="calculator.proto"
OutputDir="%(RelativePath)"
CompileOutputs="false" />
Services
The next step is to implement the service methods. Create another class library that targets the same version of .NET Standard as the previously created library where the .proto file and the generated code files are located and add a reference to this project. Then add a class that extends the abstract CalculatorService.CalculatorServiceBase
class in CalculatorGrpc.cs
. Unlike in WCF, there's no service interface to be implemented in gRPC. Instead, code generation using protoc and overriding methods of the generated base class is the standard practice. In this case, the actual implementation looks something like Listing 2.
Listing 2: The service implementation
public class CalculatorService : CalculatorServiceBase
{
public override Task<CalculatorReply> Add(CalculatorRequest request, ServerCallContext context) =>
Task.FromResult(new CalculatorReply() { Result = request.N1 + request.N2 });
public override Task<CalculatorReply> Subtract(CalculatorRequest request, ServerCallContext context) =>
Task.FromResult(new CalculatorReply() { Result = request.N1 - request.N2 });
public override Task<CalculatorReply> Multiply(CalculatorRequest request, ServerCallContext context) =>
Task.FromResult(new CalculatorReply() { Result = request.N1 * request.N2 });
public override Task<CalculatorReply> Divide(CalculatorRequest request, ServerCallContext context)
{
if (request.N2 == 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Divide by zero attempt."));
return Task.FromResult(new CalculatorReply() {Result = request.N1 / request.N2 });
}
}
If you look at the virtual methods in the base class, you'll see that they throw an exception by default. This is, for example, how the Add method is implemented in CalculatorServiceBase
:
public virtual Task<CalculatorReply> Add(CalculatorRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, ""));
}
The compiler won't force you to provide an implementation for all methods as it does when you implement an interface. So if you forget to override any method that's defined in the .proto file, a client will get a runtime exception when trying to call this particular method.
Also note that the method signatures are asynchronous, and all methods must return a Task
or Task<T>
. There's no support or configuration setting for synchronous implementations or overloads of service methods in the C# variants of gRPC. If you don't use the await operator anywhere in the method, you can simply wrap the synchronously computed return value in a completed Task using the Task.FromResult
method, as shown in Listing 2.
Hosts
Once you have implemented the service, you need to host it in a process. Hosting a Grpc.Core service in a managed process is very similar to hosting a WCF service. Instead of creating a ServiceHost to which you add endpoints, you create a Grpc.Core.Server to which you add services and ports, like in Listing 3. A single server process may host several service implementations and listen for requests on several different ports.
Listing 3: The Grpc.Core service host
class Program
{
const string Host = "localhost";
const int Port = 50051;
static async Task Main(string[] args)
{
Server server = new Server();
server.Services.Add(CalculatorService.BindService(ServiceLib.CalculatorService()));
server.Ports.Add(new ServerPort(Host, Port, ServerCredentials.Insecure));
server.Start();
Console.WriteLine($"The service is ready at {Host} on port {Port}.");
Console.WriteLine("Press any key to stop the service...");
Console.ReadLine();
await server.ShutdownAsync();
}
}
Grpc-dotnet provides a new gRPC Service project template that you'll find under File > New > Project
in the latest version of Visual Studio. Once you've created the project, you can remove the Protos and Services folders that contain some default files and add a reference to your service library where the CalculatorService
class is implemented. If you then replace the reference to the removed GreeterService
with your service type in the Configure method of the Startup class, you should be able to build and run the app:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapGrpcService<CalculatorService>());
}
Besides the managed implementation, one of the benefits of using grpc-dotnet is that you get the built-in support for dependency injection, configuration and logging that ASP.NET Core provides.
Clients
Next, you need a client. Create another Console App (.NET Core) project and add a reference to the class library where the .proto file is located. You can then connect to the server by creating an instance of the generated CalculatorServiceClient
class and pass a Grpc.Core.ChannelBase
to its constructor, as shown in Listing 4. ChannelBase
is an abstract class that Grpc.Core
provides a concrete Channel implementation of. For grpc-dotnet, there's a fully managed client API that you can use by installing the Grpc.Net.Client
NuGet package into your client console app. It provides a static factory method called GrpcChannel.ForAddress
that accepts an address and creates a HttpClient
under the hood. Listing 5 contains an example of how to use it to call the CalculatorService
.
Listing 4: The Grpc.Core client
class Program
{
static async Task Main(string[] args)
{
Channel channel = new Channel("localhost:50051", ChannelCredentials.Insecure);
CalculatorService.CalculatorServiceClient client = new CalculatorService.CalculatorServiceClient(channel);
const double N1 = 1;
const double N2 = 1;
CalculatorReply result = await client.AddAsync(new CalculatorRequest() { N1 = N1, N2 = N2 });
Console.WriteLine($"{N1} + {N2} = {result.Result}");
}
}
Listing 5: The grpc-dotnet client
static async Task Main(string[] args)
{
using (GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001"))
{
CalculatorService.CalculatorServiceClient client = new CalculatorService.CalculatorServiceClient(channel);
const double N1 = 1;
const double N2 = 1;
CalculatorReply result = await client.AddAsync(new CalculatorRequest() { N1 = N1, N2 = N2 });
Console.WriteLine($"{N1} + {N2} = {result.Result}");
}
}
Once you have an instance of a client, it's simply a matter of calling the methods on it just like you would do with a WCF client proxy. These methods come with both asynchronous and synchronous overloads.
If you now start the server and the client (in that order) and see 1 + 1 = 2 getting printed to the console in the client app, you have successfully migrated the sample service from WCF to gRPC.
Transfer Modes
gRPC supports four types of service calls. The migrated calculator service uses unary RPCs where the client sends a single CalculatorRequest
to the server and gets a single CalculatorReply back. This corresponds to the default buffered transfer mode in WCF.
Then there are streaming service methods where the client sends a single request to the server and gets a stream of response messages back. This is similar to the behavior that you get in WCF when you return a Stream from a service operation and use a binding that supports the streamed transfer mode. Client streaming, where the client writes a sequence of request messages and sends them to the server using a provided stream and getting a single response back, is also supported.
The final option is bidirectional streaming where the client and the server operate independently on the same duplex channel using two concurrent streams. Each gRPC call is single HTTP request and the server could either wait until it has received all the request messages from the client before responding or it could write response messages back to the client while still receiving requests. gRPC guarantees that the order of messages in each stream is preserved.
You enable streaming RPCs by simply adding the keyword stream in front of the message type(s) in the service definition in the .proto file. To use server streaming, you add the stream keyword before the response type; to add client streaming, you add it before the request type; and for bidirectional streaming, you add it both before the request type and the response type:
service SampleService {
rpc ServerStream (Request) returns (stream Reply) {}
rpc ClientStream (stream Request) returns (Reply) {}
rpc BirectionalStream (stream Request) returns (stream Reply) {}
}
Let's now take a look at how you could migrate another example from MSDN that uses a duplex service contract to be able to send messages back to the client from the server:
[ServiceContract(Namespace = "http://...", SessionMode = SessionMode.Required, CallbackContract = typeof(ICalculatorDuplexCallback))]
public interface ICalculatorDuplex
{
[OperationContract(IsOneWay = true)]
void Clear();
[OperationContract(IsOneWay = true)]
void AddTo(double n);
[OperationContract(IsOneWay = true)]
void SubtractFrom(double n);
[OperationContract(IsOneWay = true)]
void MultiplyBy(double n);
[OperationContract(IsOneWay = true)]
void DivideBy(double n);
}
The callback interface is defined like this:
public interface ICalculatorDuplexCallback
{
[OperationContract(IsOneWay = true)]
void Equals(double result);
[OperationContract(IsOneWay = true)]
void Equation(string eqn);
}
Lifetime
When migrating this service, you may be tempted to define the five methods of the ICalculatorDuplex
contract in a new .proto file. There's one caveat here. The WCF service implementation is decorated with a ServiceBehaviorAttribute
with an InstanceContextMode
of InstanceContextMode.PerSession
. This means that a new instance of the service class is created for each client session. In Grpc.Core
, a service always behaves like a singleton; in other words, a single instance of the service class is shared by all clients. This is how the native C-core library that's used internally by the C# code in the Grpc.Core
package is implemented. ASP.NET Core and the Grpc.AspNetCore.Server
package, which is referenced by default in the project template, creates a new instance of the gRPC service class per client request by default. You can configure the lifetime of a service in the ConfigureServices method of the Startup class just like you would do with any other dependency in any other ASP.NET Core app; regardless of how the service class is instantiated, a client session in gRPC is always equivalent to a single call.
Regardless of how the service class is instantiated, a client session in gRPC is always equivalent to a single call.
There's a UserState
property of the ServerCallContext
parameter that returns an IDictionary<object, object>
that you can use to store any state, but it will only be persisted for the duration of the - potentially long-lived - call to that particular service method.
So how can you implement this service? One option is to define a single service method and use a field in the request message to specify the arithmetic operation to be performed on the server-side. Luckily protocol buffers support enumerations:
enum Operation {
ADD = 0;
SUBTRACT = 1;
MULTIPLY = 2;
DIVIDE = 3;
CLEAR = 4;
}
message BidirectionalCalculatorRequest {
Operation operation = 1;
double n = 2;
}
message BidirectionalCalculatorReply {
double result = 1;
string eqn = 2;
}
The generated method from the gRPC service in the next snippet now accepts a stream reader and a stream writer - both are asynchronous - and may be implemented something like in Listing 6.
service BidirectionalCalculatorService {
rpc Calculate (stream BidirectionalCalculatorRequest) returns (stream BidirectionalCalculatorReply) {}
}
Listing 6: The bidirectonal service
public class BidirectionalCalculatorService : BidirectionalCalculatorServiceBase
{
public override async Task Calculate(IAsyncStreamReader<BidirectionalCalculatorRequest> requestStream, IServerStreamWriter<BidirectionalCalculatorReply> responseStream, ServerCallContext context)
{
double result = 0.0D;
string equation = result.ToString();
while (await requestStream.MoveNext().ConfigureAwait(false))
{
var request = requestStream.Current;
double n = request.N;
switch (request.Operation)
{
case Operation.Add:
result += n;
equation += " + " + n.ToString();
break;
case Operation.Subtract:
result -= n;
equation += " - " + n.ToString();
break;
case Operation.Multiply:
result *= n;
equation += " * " + n.ToString();
break;
case Operation.Divide:
if (n == 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Divide by zero attempt."));
result /= n;
equation += " / " + n.ToString();
break;
case Operation.Clear:
equation += " = " + result.ToString();
break;
default:
continue;
}
await responseStream.WriteAsync(new BidirectionalCalculatorReply()
{
Result = result,
Eqn = equation
}).ConfigureAwait(false);
//reset state
if (request.Operation == Operation.Clear)
{
result = 0.0D;
equation = result.ToString();
}
}
}
}
If you target .NET Standard 2.1, you can replace the call to MoveNext and make use of the new async streams support that was introduced in C# 8.0:
await foreach (var message in requestStream.ReadAllAsync()) { ... }
Calling the Calculate()
method on the client-side gets you back an AsyncDuplexStreamingCall<BidirectionalCalculatorRequest, BidirectionalCalculatorReply>
. This class provides a ResponseStream property that returns an IAsyncStreamReader<BidirectionalCalculatorReply>
that you can use to consume response messages from the service. It also has a RequestStream property that returns an IClientStreamWriter<BidirectionalCalculatorRequest>
that you can use to send requests the other way.
Callbacks
To be able to read data while you're writing, you need to consume the response stream on another thread than the one on which you write to the request stream. WCF uses the thread pool and synchronization contexts to handle this. In a gRPC client app, you can create your own CallbackHandler
class. The one in Listing 7 takes an IAsyncStreamReader<BidirectionalCalculatorReply>
and uses it to consume the stream in a Task. You create an instance of it in the client app and await this Task, as shown in Listing 8. Calling CompleteAsync()
on the IClientStreamWriter<BidirectionalCalculatorRequest>
returned from the RequestStream
property terminates the connection. If you run the sample code, you should see the results getting printed to the console.
Listing 7: The callback handler
internal class CallbackHandler
{
readonly IAsyncStreamReader<BidirectionalCalculatorReply> _responseStream;
readonly CancellationToken _cancellationToken;
public CallbackHandler(IAsyncStreamReader<BidirectionalCalculatorReply> responseStream) : this(responseStream, CancellationToken.None) { }
public CallbackHandler(IAsyncStreamReader<BidirectionalCalculatorReply> responseStream, CancellationToken cancellationToken)
{
_responseStream = responseStream ?? throw new ArgumentNullException(nameof(responseStream));
_cancellationToken = cancellationToken;
Task = Task.Run(Consume, _cancellationToken);
}
public Task Task { get; }
async Task Consume()
{
while (await _responseStream.MoveNext(_cancellationToken).ConfigureAwait(false))
Console.WriteLine("Equals({0}), Equation({1})", _responseStream.Current.Result, _responseStream.Current.Eqn);
}
}
Listing 8: The bidirectional client
BidirectionalCalculatorServiceClient bidirectionalClient = new BidirectionalCalculatorServiceClient(channel);
using (var duplexStream = bidirectionalClient.Calculate())
{
var callbackHandler = new CallbackHandler(duplexStream.ResponseStream);
await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
{
Operation = Operation.Add,
N = 2
});
await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
{
Operation = Operation.Multiply,
N = 2
});
await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
{
Operation = Operation.Subtract,
N = 2
});
await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
{
Operation = Operation.Clear
});
await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
{
Operation = Operation.Add,
N = 10
});
await duplexStream.RequestStream.CompleteAsync();
await callbackHandler.Task;
}
Security
Passing a ChannelCredentials.Insecure
to the constructor of the Grpc.Core.Channel
class unsurprisingly creates an unsecure channel with no encryption. There is an SslCredentials
class that you can use to apply transport security on the channel using SSL/TLS. Using it requires you to create a certificate and a key file.
The project template for grpc-dotnet uses TLS with a default HTTPS development certificate that's installed with the .NET Core SDK. You are prompted to trust it when you start the ASP.NET Core host process unless you have done this before. How to create and install certificates is out of the scope of this article, but you can refer to the docs at https://bit.ly/2LktGpq for how to configure Kestrel.
Besides channel credentials, you can also apply call credentials to an individual service method call. Each method in the generated client class has an overload that accepts a CallOptions object, which in turn accepts a CallCredentials as one of the arguments in its constructor. CallCredentials is an abstract class and the only built-in-concrete implementation is a private AsyncAuthInterceptorCredentials
class that you create using the static CallCredentials.FromInterceptor
method. This, in turn, accepts an AsyncAuthInterceptor
that can attach metadata, such as for example a bearer token, to outgoing calls:
AsyncAuthInterceptor asyncAuthInterceptor = new AsyncAuthInterceptor((context, metadata) =>
{
metadata.Add("key", "value");
return Task.FromResult(default(object));
});
CallCredentials callCredentials = CallCredentials.FromInterceptor(asyncAuthInterceptor);
CallOptions callOptions = new CallOptions(credentials: allCredentials);
using (var duplexStream = bidirectionalClient.Calculate(callOptions))
{
...
}
Note that the interceptor won't ever get called and the call credentials will be ignored if you don't secure the channel.
You can also compose channel and call credentials using the static ChannelCredentials.Create
method. It returns a ChannelCredentials
that applies the call credentials for each call made on the channel. For more information and an example of how to use these types and methods, I recommend that you check out the tests in GitHub repository. The API documentation at https://bit.ly/2H6jCit may also be helpful.
If your applications live inside an enterprise with firewalls around it, an option to using certificates and securing the transport channel may be to just send metadata in the requests. If you register your app with Azure Active Directory, you could, for example, use MSAL.NET to acquire a token and implement Windows authentication this way.
You can attach metadata to a service call using the CallOptions
class:
CallOptions callOptions = new CallOptions(new Metadata() {
new Metadata.Entry("key", "value")
});
Retrieving it on the server-side is easy using the RequestHeaders
property of the ServerCallContext
, which returns an IList<Metadata.Entry>
(or actually a Metadata object that implements IList<Metadata.Entry>
) where Entry has a Key and Value property.
Settings
When it comes to settings, such as, for example, specifying the maximum accepted message length that the channel can send or receive, the Channel class in Grpc.Core accepts an IEnumerable<ChannelOption>
in some of its constructor overloads. There is a ChannelOptions
(note the plural noun) that defines constants for the names of the most commonly used options:
Channel channel = new Channel("127.0.0.1:50051", ChannelCredentials.Insecure, new ChannelOption[]
{
new ChannelOption(ChannelOptions.MaxSendMessageLength, 100),
new ChannelOption(ChannelOptions.MaxReceiveMessageLength, 100)
});
All supported options are defined in the native grpc_types.h
header file.
In ASP.NET Core, you configure gRPC settings using the overload of the AddGrpc method that accepts an AddGrpcGrpcServiceOptions
:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.ReceiveMaxMessageSize = 100;
options.SendMaxMessageSize = 100;
});
}
Here, you can also add interceptors that intercept the execution of an RPC call to perform things, such as logging, validation and collection of metrics.
Summary
In this article, you've seen how to use protocol buffers and gRPC to build microservices or migrate existing service-oriented applications built using WCF and the .NET Framework to .NET Core and the cloud. Regardless of whether you use the native C-core library or ASP.NET Core 3, gRPC provides an easy setup and an appealing asynchronous API that abstracts away the details of the underlying communication protocol in a nice way. Originally developed by Google and now ported and contributed to by Microsoft, it should definitely be on the list when choosing the technology stack for your next-generation services. At least, it should be if you care about efficiency and streaming response or requests.