With the release of ASP.NET Core v3.0, amongst many other features, gRPC has become a first-class citizen of the ecosystem with Microsoft officially supporting and embracing it.
What this means for developers is that ASP.NET Core 3.0 now ships with templates for building gRPC services, tooling for defining service contracts using protocol buffers, tooling to generate client/server stubs from said proto files and integration with Kestrel/HttpClient.
After a brief introduction to gRPC, this tutorial provides an overview on how gRPC services can be created with ASP.NET Core and how to invoke these services from .NET Core. Next, it will take a look at the cross-language nature of gRPC by integrating with a Node.js service. We will finish by exploring gRPC built-in security features based on TLS/SSL.
Companion code can be found in GitHub.
1. The 5 minutes introduction to gRPC
GRPC is a framework designed by Google and open sourced in 2015, which enables efficient Remote Procedure Calls (RPC) between services. It uses the HTTP/2 protocol to exchange binary messages, which are serialized/deserialized using Protocol Buffers.
Protocol buffers is a binary serialization protocol also designed by Google. It is highly performant by providing a compact binary format that requires low CPU usage.
Developers use proto files to define service and message contracts which are then used to generate the necessary code to establish the connection and exchange the binary serialized messages. When using C#, tooling will generate strongly typed message POCO classes, client stubs and service base classes. Being a cross-language framework, its tooling lets you combine clients and services written in many languages like C#, Java, Go, Node.js or C++.
Protocol buffers also allow services to evolve while remaining backwards compatible. Because each field in a message is tagged with a number and a type, a recipient can extract the fields they know, while ignoring the rest.
Figure 1, gRPC basics
The combination of the HTTP/2 protocol and binary messages encoded with Protocol Buffers lets gRPC achieve much smaller payloads and higher performance, than traditional solutions like HTTP REST services.
But traditional RPC calls are not the only scenario enabled by gRPC!
The framework takes advantage of HTTP/2 persistent connections by allowing both server and client streaming, resulting in four different styles of service methods:
- Unary RPC, where client performs a traditional RPC call, sending a request message and receiving a response.
- Server streaming RPC, where clients send an initial request but get a sequence of messages back.
- Client streaming RPC, where clients send a sequence of messages, wait for the server to process them and receive a single response back.
- Bidirectional streaming RPC, where both client and server send a sequence of messages. The streams are independent, so client and server can decide in which order to read/write the messages
So far, it’s all great news. So now you might then be wondering why isn’t gRPC everywhere, replacing traditional HTTP REST services?
It is due to a major drawback, its browser support!
The HTTP/2 support provided by browsers is insufficient to implement gRPC, requiring solutions like gRPC-Web based on an intermediate proxy that translates standard HTTP requests into gRPC requests. For more information, see this Microsoft comparison between HTTP REST and gRPC services.
Narrowing our focus back to .NET, there has been a port of gRPC for several years that lets you work with gRPC services based on proto files.
So, what exactly is being changed for ASP.NET Core 3.0?
The existing Grpc.Core package is built on top of unmanaged code (the chttp2 library), and does not play nicely with managed libraries like HttpClient and Kestrel that are widely used in .NET Core. Microsoft is building new Grpc.Net.Client and Grpc.AspNetCore.Server packages that will avoid unmanaged code, integrating instead with HttpClient and Kestrel. Tooling is also been improved to support a code-first approach and both proto2 and proto3 syntax. Checkout this excellent talk from Marc Gravell for more information.
Let’s stop our overview of gRPC here. If you were completely new to gRPC, hopefully there was enough to pique your interest. Those of you who have been around for a while might have recognized some WCF ideas! Let’s then start writing some code so we can see these concepts in action.
2. Creating a gRPC Service
As mentioned in the introduction, ASP.NET Core 3.0 ships with a gRPC service template. This makes creating a new service a very straightforward task.
If you are still following using a preview release of ASP.NET Core, remember to enable .NET Core SDK preview features in Visual Studio options:
Figure 2, enabling .NET Core SDK preview features
Start by creating a new ASP.NET Core project in Visual Studio 2019. Use Orders as the name of the project, and a different name for the solution since we will be adding more projects later. Then select the gRPC template from the ASP.NET Core 3.0 framework:
Figure 3, using the gRPC Service template with a new ASP.NET Core application
Congratulations you have created your first fully working gRPC service! Let’s take a moment to take a look and understand the generated code. Apart from the traditional Program and Startup classes, you should see a Protos folder containing a file named greet.proto and a Services folder containing a GreeterService class.
Figure 4, gRPC service generated by the template
These two files define and implement a sample Greeter service, the equivalent of a “Hello World” program for a gRPC service. The greet.proto file defines the contract: i.e. which methods does the service provide, and what are the request/response message exchanged by client and server:
syntax = "proto3";
option csharp_namespace = "Orders";
package Greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
The generated GreeterService class contains the implementation of the service:
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
As you can see, it inherits from a suspicious looking class named Greeter.GreeterBase, while HelloRequest and HelloReply classes are used as request and response respectively. These classes are generated by the gRPC tooling, based on the greet.proto file, providing all the functionality needed to send/receive messages so you just need to worry about the method implementation!
You can use F12 to inspect the class and you will see the generated code, which will be located at obj/Debug/netcoreapp3.0/GreetGrpc.cs (service) and obj/Debug/netcoreapp3.0/Greet.cs (messages).
It is automatically generated at build time, based on your project settings. If you inspect the project file, you will see a reference to the Grpc.AspNetCore package and a Protobuf item:
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="0.1.22-pre22.23.1" />
</ItemGroup>
Grpc.AspNetCore is a meta-package including the tooling needed at design time and the libraries needed at runtime. The Protobuf item points the tooling to the proto files it needs to generate the service and message classes. The GrpcServices="Server" attribute indicates that service code should be generated, rather than client code.
Let’s finish our inspection of the generated code by taking a quick look at the Startup class. You will see how the necessary ASP.NET Core services to host a gRPC service are added with services.AddGrpc(), while the GreeterService is mapped to a specific endpoint of the ASP.NET Core service by using endpoints.MapGrpcService<GreeterService>(); .
2.1 Defining a contract using proto files
Let’s now replace the sample service contract with our own contract for an imaginary Orders service. This will be a simple service that lets users create orders and later ask for the status:
syntax = "proto3";
option csharp_namespace = "Orders";
package Orders;
service OrderPlacement {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderReply) {}
rpc GetOrderStatus (GetOrderStatusRequest) returns (stream GetOrderStatusResponse) {}
}
message CreateOrderRequest {
string productId = 1;
int32 quantity = 2;
string address = 3;
}
message CreateOrderReply {
string orderId = 1;
}
message GetOrderStatusRequest {
string orderId = 1;
}
message GetOrderStatusResponse {
string status = 1;
}
As you can see, CreateOrder is a standard Unary RPC method while GetOrderStatus is a Server streaming RPC method. The request/response messages are fairly self-descriptive but notice how each field has a type and an order, allowing backwards compatibility when creating new service versions.
Let’s now rename the file as orders.proto and let’s move it to a new Protos folder located at the solution root, since we will share this file with a client project we will create later. Once renamed and moved to its new folder, you will need to edit the Orders.csproj project file and update the Protobuf item to include the path to the updated file:
<Protobuf Include="..\Protos\orders.proto" GrpcServices="Server" />
Let’s now implement the OrderPlacement service we just defined.
2.2 Implementing the service
If you build the project now, you should receive a fair number of errors since the tooling will no longer generate classes for the previous Greeter service and instead will generate them for the new OrderPlacement service.
Rename the GreeterService.cs file as OrdersService.cs and replace its contents with the following:
public class OrderPlacementService: OrderPlacement.OrderPlacementBase
{
private readonly ILogger<OrderPlacementService> _logger;
public OrderPlacementService(ILogger<OrderPlacementService> logger)
{
_logger = logger;
}
public override Task<CreateOrderReply> CreateOrder(CreateOrderRequest request, ServerCallContext context)
{
var orderId = Guid.NewGuid().ToString();
this._logger.LogInformation($"Created order {orderId}");
return Task.FromResult(new CreateOrderReply {
OrderId = orderId
});
}
public override async Task GetOrderStatus(GetOrderStatusRequest request, IServerStreamWriter<GetOrderStatusResponse> responseStream, ServerCallContext context)
{
await responseStream.WriteAsync(
new GetOrderStatusResponse { Status = "Created" });
await Task.Delay(500);
await responseStream.WriteAsync(
new GetOrderStatusResponse { Status = "Validated" });
await Task.Delay(1000);
await responseStream.WriteAsync(
new GetOrderStatusResponse { Status = "Dispatched" });
}
}
The implementation of CreateOrder is fairly similar to the previous Greeter one. The GetOrderStatus one is a bit more interesting. Being a server streaming method, it receives a ResponseStream parameter that lets you send as many response messages back to the client as needed. The implementation above simulates a real service by sending some messages after certain delays.
2.3 Hosting the service with ASP.NET Core
The only bit we need to modify from the generated project in order to host our new OrdersPlacement service is to replace the endpoints.MapGrpcService<GreeterService>(); in the Startup class with endpoints.MapGrpcService<OrderPlacementService>();.
The template is already adding all the gRPC infrastructure needed to host the service in ASP.NET via the existing services.AddGrpc() call. You should be able to start the server with the dotnet run command:
Figure 5, running the first gRPC service
The server will start listening on port 5001 using HTTPS (and 5000 using HTTP) as per the default settings. Time to switch our focus to the client side.
3. Creating a gRPC Client
While there is no template to generate a client, the new Grpc.Net.Client and Grpc.Net.ClientFactory packages combined with gRPC tooling makes it easy to create the code needed to invoke gRPC service methods.
3.1 Using Grpc.Net.Client in a Console project
First, let’s create a simple console application that lets us invoke the methods of the OrdersPlacement service we defined in the previous section. Start by adding a new project named Client to your solution, using the .NET Core Console Application template.
Once generated, install the following NuGet packages: Grpc.Tools, Grpc.Net.Client and Google.Protobuf. (If using the GUI in Visual Studio, remember to tick the “Include prerelease” checkbox)
Next, manually edit the Client.csproj project file. First update the Grpc.Tools reference with the PrivateAssets attribute, so .NET knows not to include this library in the generated output:
<PackageReference Include="Grpc.Tools" Version="1.22.0-pre12.23.0" PrivateAssets="All" />
Then, include a new ItemGroup with a Protobuf item that references the orders.proto file defined in the previous section:
<ItemGroup>
<Protobuf Include="..\Protos\orders.proto" GrpcServices="Client" />
</ItemGroup>
Notice the GrpcServices attribute telling Grpc.Tools that this time we need client stub code instead of service base classes!
Once you are done, rebuild the project. You should now be able to use the generated client code under the Orders namespace. Let’s write the code needed to instantiate a client of our service:
var channel = GrpcChannel.ForAddress(("https://localhost:5001");
var ordersClient = new OrderPlacement.OrderPlacementClient(channel);
We simply instantiate a GrpcChannel using the URL of our service using the factory method provided by the Grpc.Net.Client package, and use it to create the instance of the generated class OrderPlacementClient.
You can then use the ordersClient instance to invoke the CreateOrder method:
Console.WriteLine("Welcome to the gRPC client");
var reply = await ordersClient.CreateOrderAsync(new CreateOrderRequest
{
ProductId = "ABC1234",
Quantity = 1,
Address = "Mock Address"
});
Console.WriteLine($"Created order: {reply.OrderId}");
Now build and run the project while your server is also running. You should see your first client-server interaction using gRPC.
Figure 6, client invoking a service method
Invoking the server streaming method is equally simple. The generated GetOrderStatus method of the client stub returns an AsyncServerStreamingCall<GetOrderStatusResponse>. This is an object with an async iterable that lets you process every message received by the server:
using (var statusReplies = ordersClient.GetOrderStatus(new GetOrderStatusRequest { OrderId = reply.OrderId }))
{
while (await statusReplies.ResponseStream.MoveNext())
{
var statusReply = statusReplies.ResponseStream.Current.Status;
Console.WriteLine($"Order status: {statusReply}");
}
}
If you build and run the client again, you should see the streamed messages:
Figure 7, invoking the server streaming method
While this does not cover the four method styles available, it should give you enough information to explore the remaining two on your own. You can also explore the gRPC examples in the official grpc-dotnet repo, covering the different RPC styles and more.
3.2 Using Grpc.Net.ClientFactory to communicate between services
We have seen how the Grpc.Net.Client package allows you to create a client using the well-known HttpClient class. This might be enough for a sample console application like the one we just created. However, in a more complex application you would rather use HttpClientFactory to manage your HttpClient instances.
The good news is that there is a second package Grpc.Net.ClientFactory which contains the necessary classes to manage gRPC clients using HttpClientFactory. We will take a look by exploring a not so uncommon scenario, a service that invokes another service. Let’s imagine for a second that our Orders service needs to communicate with another service, Shippings, when creating an Order.
3.2.1 Adding a second gRPC service
Begin by adding a new shippings.proto file to the Protos folder of the solution. This should look familiar by now; it is another sample service exposing a traditional Unary RPC method.
syntax = "proto3";
option csharp_namespace = "Shippings";
package Shippings;
service ProductShipment {
rpc SendOrder (SendOrderRequest) returns (SendOrderReply) {}
}
message SendOrderRequest {
string productId = 1;
int32 quantity = 2;
string address = 3;
}
message SendOrderReply {
}
The only interesting bit is the empty reply message. It is expected for services to always return a message, even if empty. This is just an idiom in gRPC, it will help with versioning if/when fields are added to the response.
Now repeat the steps in section 2 of the article to add a new Shippings project to the solution that implements the service defined by this proto file. Update its launchSettings.json so this service gets started at https://localhost:5003 instead of its 5001 default.
If you have any trouble, check the source code on GitHub.
3.2.2 Registering a client with the HttpClientFactory
We will now update the Orders service so it invokes the SendOrder method of the Shippings service as part of its CreateOrder method. Update the Orders.csproj file to include a new Protobuf item referencing the shippings.proto file we just created. Unlike the existing server reference, this will be a client reference so Grpc.Tools generates the necessary client classes:
<ItemGroup>
<Protobuf Include="..\Protos\orders.proto" GrpcServices="Server" />
<Protobuf Include="..\Protos\shippings.proto" GrpcServices="Client" />
</ItemGroup>
Save and rebuild the project. Next install the Grpc.Net.ClientFactory NuGet package. Once installed, you will be able to use the services.AddGrpcClient extension method to register the client. Update the ConfigureServices method of the startup class as follows:
services.AddGrpc();
services
.AddGrpcClient<ProductShipment.ProductShipmentClient>(opts =>
{
opts.Address = new Uri("https://localhost:5003");
});
This registers a transient instance of the ProductShipmentClient, where its underlying HttpClient is automatically managed for us. Since it is registered as a service in the DI container, we can easily request an instance in the constructor of the existing OrdersPlacement service:
private readonly ILogger<OrderPlacementService> _logger;
private readonly ProductShipment.ProductShipmentClient _shippings;
public OrderPlacementService(ILogger<OrderPlacementService> logger, ProductShipment.ProductShipmentClient shippings)
{
_logger = logger;
_shippings = shippings;
}
Updating the existing CreateOrder method implementation so it invokes the new service is pretty easy as well:
public override async Task<CreateOrderReply> CreateOrder(CreateOrderRequest request, ServerCallContext context)
{
var orderId = Guid.NewGuid().ToString();
await this._shippings.SendOrderAsync(new SendOrderRequest
{
ProductId = request.ProductId,
Quantity = request.Quantity,
Address = request.Address
});
this._logger.LogInformation($"Created order {orderId}");
return new CreateOrderReply {
OrderId = orderId
};
}
Once you are done with the changes, start each service on its own console and run the client on a third:
Figure 8, trying our client and the 2 services
This concludes the basics for both implementing and calling gRPC services in .NET. We have just but scratched the surface of the different method styles available. For more information on Authentication, Deadlines, Cancellation, etc. check the Microsoft docs and the gRPC examples in the official grpc-dotnet repo.
4. Cross-language gRPC client and service
One of the key aspects of gRPC is its cross-language nature. Given a proto file; client and service can be generated using any combination of the supported languages. Let’s explore this by creating a gRPC service using Node.js, a new Products service we can use to get the details of a given product.
4.1 Creating a gRPC server in Node
Like we did with the previous Orders and Shippings services, let’s begin by adding a new products.proto file inside the Protos folder. It will be another straightforward contract with a Unary RPC method:
syntax = "proto3";
option csharp_namespace = "Products";
package products;
service ProductsInventory {
rpc Details(ProductDetailsRequest) returns (ProductDetailsResponse){}
}
message ProductDetailsRequest {
string productId = 1;
}
message ProductDetailsResponse {
string productId = 1;
string name = 2;
string category = 3;
}
Next create a new Products folder inside the solution and navigate into it. We will initialize a new Node.js project by running the npm init command from this folder. Once finished, we will install the necessary dependencies to create a gRPC service using Node:
npm install --save grpc @grpc/proto-loader
Once installed, add a new file server.js and update package.json main property as "main": "server.js". This way we will be able to start the gRPC server running the npm run command. It is now time to implement the service.
The actual method implementation is a fairly simple function:
const serviceImplementation = {
Details(call, callback) {
const { productId } = call.request;
console.log('Sending details for:', productId);
callback(null, {productId, name: 'mockName', category: 'mockCategory'});
}
};
Now we just need a bit of plumbing code using the installed grpc dependencies in order to get an HTTP/2 server started that implements the service defined by the proto file and the concrete implementation:
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
// Implement service
const serviceImplementation = { ... };
// Load proto file
const proto = grpc.loadPackageDefinition(
protoLoader.loadSync('../Protos/products.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
})
);
// Define server using proto file and concrete implementation
const server = new grpc.Server();
server.addService(proto.products.ProductsInventory.service, { Details: getdetails });
// get the server started
server.bind('0.0.0.0:5004', grpc.ServerCredentials.createInsecure());
server.start();
Even though we are using a different language, the code listing we saw should result pretty familiar after the .NET services. Developers are responsible for writing the service implementation. Standard gRPC libraries are then used to load the proto file and create a server able to handle the requests, by routing them to the appropriated method of the implementation and handling the serialization/deserialization of messages.
There is however an important difference!
Note the second argument grpc.ServerCredentials.createInsecure() to the server.bind function. We are starting the server without using TLS, which means traffic won’t be encrypted across the wire. While this is fine for development, it would be a critical risk in production. We will come back to this topic in Section 5.
4.2 Communicate with the Node service
Now that we have a new service, let’s update our console application to generate the necessary client stub code to invoke its methods. Edit the Client.csproj project file adding a new Protobuf element that references the new products.proto file:
<Protobuf Include="..\Protos\products.proto" GrpcServices="Client" />
Once you save and rebuild the project, let’s add the necessary code to our client application in order to invoke the service. Feel free to get a bit creative for your Console application to provide some interface that lets you invoke either the Products or Order services.
The basic code to invoke the Details method of the products service is as follows:
var channel = GrpcChannel.ForAddress("http://localhost:5004");
var productsClient = new ProductsInventory.ProductsInventoryClient(channel);
var reply = await productsClient.DetailsAsync(new ProductDetailsRequest { ProductId = "ABC1234" });
Console.WriteLine($"Product details received. Name={reply.Name}, Category={reply.Category}");
This is very similar to the code that sends a request to the Orders service. Note however that since our server does not have TLS enabled, we need to define the URL using the http:// protocol instead of https://. If you now try to run the client and send a request to the server you will receive the following exception:
Figure 9, exception trying to invoke a service without TLS
This is because when using the HTTP/2 protocol, the HttpClient used by the service client will enforce TLS. Since the Node server does not have TLS enabled, the connection cannot be established. In order to allow this during development, we need to use the following AppContext switch as advised in the official docs:
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
Once we make that change, we are able to invoke the Node service from our .NET client:
Figure 10, invoking the Node.js gRPC service from the .NET client
As you can see, it does not matter on which language (or platform for that matter) are the client and server running. As long as both adhere to the contract defined in the proto files, gRPC will make the transport/language choice transparent for the other side.
The final section of the article will look at TLS/SSL support in greater detail.
5. Understanding TLS/SSL with gRPC
5.1 Server credentials for TLS/SSL
gRPC is designed with TLS/SSL (Transport Layer Security/Secure Sockets Layer) support in mind in order to encrypt the traffic sent through the wire. This requires the gRPC servers to be configured with valid SSL credentials.
If you have paid attention throughout the article, you might have noticed that both services implemented in .NET and hosted in ASP.NET Core applications (Orders and Shippings) used TLS/SSL out of the box without us having to do anything. Whereas in the case of the Node.js app, we used unsecure connections without TLS/SSL since we didn’t supply valid SSL credentials.
5.1.1 ASP.NET Core development certificates
When hosting a gRPC server within an ASP.NET Core application using Kestrel, we get the same default TLS/SSL features for free as with any other ASP.NET Core application. This includes out of the box development certificates and default HTTPS support in project templates, as per the official docs.
By default, development certificates are installed in ${APPDATA}/ASP.NET/Https folder (windows) or the ${HOME}/.aspnet/https folder (mac and linux). There is even a CLI utility named dotnet dev-certs that comes with .NET Core and lets you manage the development certificate. Check out this article from Scott Hanselman from more information.
Essentially, this means when implementing gRPC services with ASP.NET Core, the SSL certificate is automatically managed for us and TLS/SSL is enabled even in our local development environments.
5.1.2 Manually generating development certificates
You might find yourself working on a platform that does not provide you with development certificates by default, like when we created the Node.js server.
In those cases, you will need to generate yourself a self-signed set of SSL credentials that you can then provide to your server during startup. Using openssl, it is relatively easy to create a script that will generate a new CA root certificate, and a public/private key pair for the server. (If you are in Windows, installing git and the git bash is the easiest way to get it). Such a script will look like this:
openssl genrsa -passout pass:1234 -des3 -out ca.key 4096
openssl req -passin pass:1234 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=CL/ST=RM/L=Santiago/O=Test/OU=Test/CN=ca"
openssl genrsa -passout pass:1234 -des3 -out server.key 4096
openssl req -passin pass:1234 -new -key server.key -out server.csr -subj "/C=CL/ST=RM/L=Santiago/O=Test/OU=Server/CN=localhost"
openssl x509 -req -passin pass:1234 -days 365 -in server.csr -CA ca.crt -Cakey ca.key -set_serial 01 -out server.crt
openssl rsa -passin pass:1234 -in server.key -out server.key
The script first generates a self-signed CA(Certificate Authority) root, then generates the public (server.crt) and private key (server.key) parts of a X509 certificate signed by that CA. Check the GitHub repo for full mac/linux and windows scripts.
Once the necessary certificates are generated, you can now update the code starting the Node.js gRPC service to load the SSL credentials from these files. That is, we will replace the line:
server.bind('0.0.0.0:5004', grpc.ServerCredentials.createInsecure());
..with:
const fs = require('fs');
...
const credentials = grpc.ServerCredentials.createSsl(
fs.readFileSync('./certs/ca.crt'),
[{
cert_chain: fs.readFileSync('./certs/server.crt'),
private_key: fs.readFileSync('./certs/server.key')
}],
/*checkClientCertificate*/ false);
server.bind(SERVER_ADDRESS, credentials);
If you restart the Node service and update the address in the console client to https://localhost:5004 , you will realize something is still not working:
Figure 11, the self-signed certificate for the Node service is not trusted
This is because we have self-signed the certificate, and it is not trusted by our machine. We could add it to our trusted store, but we could also disable the validation of the certificate in development environments. We can provide our own HttpClient instance used by the GrpcChannel through the optional GrpcChannelOptions parameter:
var httpHandler = new HttpClientHandler();
httpHandler.ServerCertificateCustomValidationCallback =
(message, cert, chain, errors) => true;
var httpClient = new HttpClient(httpHandler);
var channel = GrpcChannel.ForAddress("http://localhost:5004", new GrpcChannelOptions { HttpClient = httpClient });
var productsClient = new ProductsInventory.ProductsInventoryClient(channel);
And this way we can use SSL/TLS with our Node.js service.
5.2 Mutual authentication with client provided credentials
gRPC services can be configured to require client certificates within the requests, providing mutual authentication between client and server.
Begin by updating the previous certificate generation script so it also produces client certificates using the same CA certificate:
openssl genrsa -passout pass:1234 -des3 -out client.key 4096
openssl req -passin pass:1234 -new -key client.key -out client.csr -subj "/C=CL/ST=RM/L=Santiago/O=Test/OU=Client/CN=localhost"
openssl x509 -passin pass:1234 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
openssl rsa -passin pass:1234 -in client.key -out client.key
openssl pkcs12 -export -password pass:1234 -out client.pfx -inkey client.key -in client.crt
Now edit the Client.csproj project file to copy the generated certificate files to the output folder:
<ItemGroup>
<None Include="..\Products\certs\*.*" LinkBase="ProductCerts">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Note that the folder from which you Include them will depend on where your script creates the certificate files. In my case, it was a folder named certs inside the Products service.
Now we need to provide the generated client.pfx certificate when connecting with the Products GRPC server. In order to do so, we just need to load it as an X509Certificate2 and tell the HttpClientHandler used by the GrpcChannel to supply said certificate as the client credentials.
This might sound complicated, but it doesn’t change much the code that initialized the Products client instance:
var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);
var certificate = new X509Certificate2(
Path.Combine(basePath, "ProductCerts", "client.pfx"), "1234"))
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);
var httpClient = new HttpClient(handler);
var channel = GrpcChannel.ForAddress(serverUrl, new GrpcChannelOptions { HttpClient = httpClient });
return new ProductsInventory.ProductsInventoryClient(channel);
Note that you need to supply the same password to the X509Certificate2 constructor that you used to generate the client.pfx file. For the purposes of the article we are hardcoding it as 1234!
Once the client is updated to provide the credentials, you can update the checkClientCertificate parameter of the Node server startup and verify that the client provides a valid certificate as well.
5.3 Using development certificates with Docker for local development
To finish the article, let’s take a quick look at the extra challenges that Docker presents in regards to enabling SSL/TLS during local development.
To begin with, the ASP.NET Core development certificates are not automatically shared with the containers. In order to do so, we first need to export the certificate as a pfx file as per the commands described in this official samples. For example, in Windows you can run:
dotnet dev-certs https -ep ${APPDATA}/ASP.NET/Https/aspnetapp.pfx -p mypassword
Exporting the certificate is just the beginning. When running your containers, you need to make this certificate available inside the container. For example, when using docker-compose you can mount the folder in which the pfx file was generated as a volume:
volumes:
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
Then setup the environment variables to override the certificate location and its password:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+;http://+
- ASPNETCORE_HTTPS_PORT=5001
- ASPNETCORE_Kestrel__Certificates__Default__Password=mypassword
- ASPNETCORE_Kestrel__Certificates__Default__Path=/root/.aspnet/https/aspnetapp.pfx
This should let you run your ASP.NET Core gRPC services using docker with TLS/SSL enabled.
Figure 12, using docker for local development with SSL enabled
One last caveat though! The approach above works as long as you map the container ports to your localhost, since the self-signed SSL certificates are created for localhost. However, when two containers communicate with each other (as in the Orders container sending a request to the Shippings service), they will use the name of the container as the host in the URL (i.e., the Orders service will send a request to shippings:443 rather than localhost:5003). We need to bypass the host validation or the SSL connection will fail.
How the validation is disabled depends on how the client is created. When using the Grpc.Net.Client, you can supply an HttpClientHandler with a void implementation of its ServerCertificateCustomValidationCallback. We have already seen this in section 5.1.2 in order to accept the self-signed certificate created for the Node server.
When using the Grpc.Net.ClientFactory, there is a similar approach that lets you configure the HttpClientHandler after the call to services.AddGrpcClient:
services
.AddGrpcClient<ProductShipment.ProductShipmentClient>(opts =>
{
opts.Address = new Uri(shippings_url);
}).ConfigurePrimaryHttpMessageHandler(() => {
return new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
};
});
The second approach would be needed if you want to run both the Orders and Shippings services inside docker.
Figure 13, services communicating with each other using SSL inside docker
Before we finish, let me remark that you only want to disable this validation for local development purposes. Make sure these certificates and disabled validations do not end up being deployed in production!
Conclusion
A first-class support of gRPC in the latest ASP.NET Core 3.0 release is great news for .NET developers. They will get easier than ever access to a framework that provides efficient, secure and cross-language/platform Remote Procedure Calls between servers.
While it has a major drawback in the lack of browser support, there is no shortage of scenarios in which it can shine or at least become a worthy addition to your toolset. Building Microservices, native mobile apps or Internet of Things (IoT) are just a few of the examples where gRPC would be a good fit.
This article was technically reviewed by Dobromir Nikolov.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Daniel Jimenez Garciais a passionate software developer with 10+ years of experience who likes to share his knowledge and has been publishing articles since 2016. He started his career as a Microsoft developer focused mainly on .NET, C# and SQL Server. In the latter half of his career he worked on a broader set of technologies and platforms with a special interest for .NET Core, Node.js, Vue, Python, Docker and Kubernetes. You can
check out his repos.