Practical ASP.NET
Writing the Code for a gRPC Service and Client in ASP.NET Core 3.0
Once you've got a contract that describes a gRPC service, creating the service itself and a client that can call the service is easy. In fact, Visual Studio will do most of the work for you ... once you've got your projects set up correctly, that is.
In an earlier column, I described both how to set up a gRPC Web Service project and how to add gRPC service support to an existing project using ASP.NET Core 3.0 and Visual Studio 2019. I also walked through creating a .proto file that drives the creation of that service. This column takes you to the end of that process by adding the code for the service and then creating a client that calls the service.
First, a quick review (if you read the previous column, you can skip this bit). This is the .proto file in my gRPC service project that defines my service:
service CustomerService{
rpc GetCustomerById (CustomerIdRequest) returns (CustomerResponse);
}
message CustomerIdRequest {
int32 custId = 1;
}
message CustomerResponse {
int32 custId = 1;
string FullName = 2;
}
This proto file defines a service (called CustomerService) with a single method (GetCustomerById). A client that calls this service must provide a message in the format described by CustomerIdRequest, and the service will return a message in the format described by CustomerResponse. A CustomerIdRequest has a single int32 field called custId in its first position; a CustomerResponse message has two fields: an int32 field called custId in its first position and a string field called FullName in its second position (you can find a list of all the datatypes you can use in a .proto file in the Protobuffer specification).
Creating a gRPC Service
If you've set up your gRPC service project correctly then, when you build your application, Visual Studio will generate a base CustomerService class based on the description in your .proto file. You start the process of creating your service by inheriting from that base class. For my CustomerService class, that looks like this:
using static CustomerManagement.Protos.CustomerService;
namespace CustomerManagement.Services
{
public class CustomerService: CustomerServiceBase
{
The methods that make up your service are already defined in that base class -- you just have to override each method and add the code that does the actual work. In addition, the request and response message formats defined in your .proto file will have been converted into classes, with the fields in the messages becoming properties on those classes. This makes writing the code for your service almost trivial.
To override the GetCustomerById method defined in my .proto file to accept a CustomerIdRequest object and return a CustomerResponse object, I add the following code to my CustomerService class. As you can see, the base class generated from my .proto file has set this method up as an asynchronous method returning a CustomerResponse object wrapped inside a Task object:
public async override Task<CustomerResponse> GetCustomerById(
CustomerIdRequest request, ServerCallContext context)
{
}
Within that method, I use the custId property in the CustomerIdRequest object passed to the method to retrieve a Customer object. I then create a CustomerResponse object and fill its properties with data from the Customer object I've retrieved. Finally, I return that CustomerResponse object to whatever client had called my method. Because I have to return a CustomerResponse object wrapped inside a Task object, I use the Task object's static FromResult method, passing my CustomerResponse object, to generate that result.
Here's the code (what there is of it):
Customer cust = await CustomerRepository.GetCustomerByIdAsync(request.custId);
CustomerResponse cr = new CustomerResponse();
cr.CustId = cust.CustId;
cr.FullName = cust.FullName;
return Task.FromResult(cr);
Now that I've created a service, I should test it by creating a client.
Configuring a gRPC Client
For my client project type I can use any project template that I want. For old times' sake, I used the Windows Forms template to add another project to my solution.
A gRPC client needs a copy of the .proto file that defines the service (Visual Studio uses that copy to generate a class that handles communication with the service). So, after creating my client project, I add a Protos folder to my project and copy the .proto file from my service into that folder.
My next step is to add three NuGet packages to my client project: Grpc.Net.Client (various classes required by the client); Google.Protobuf (various classes required by the Grpc.Net.Client classes); and Grpc.Tools (the compilers and code generators that will create the class that handles communication with my service).
My last configuration step is to tell my project (and Grpc.Tools) about my .proto file. To do that, in Solution Explorer, I double-click on my client project's line to open my project's .csproj file. I then add an ItemGroup element containing a Protobuf element to the csproj that tells the project where to find my .proto file and what to do with it. That requires that I add two attributes to that Protobuf element:
- An Include attribute with the relative path to the .proto file in my client
- A GrpcServices attribute set to "Client" to indicate that I want the tools to generate a class that will call the service
To use the .proto file I copied from my CustomerService (and put in the Protos folder of my client project), I add this to my client's csproj file:
<ItemGroup>
<Protobuf Include="Protos\CustomerService.proto" GrpcServices="Client" />
</ItemGroup>
I can now check to see if I've done everything right by building my client project. If all has gone well, I'll have a new class (created from my client's copy of the .proto file) called <name of my service>Client. In my case, that will be a class called CustomerServiceClient. I can check for the existence of that class by writing some code that uses it.
Calling the Service
But, before writing the code to call my service, I need to add some using statements at the top of my client's code file. Here's what I used:
using Grpc.Net.Client;
using gRPCV3.Protos;
using static gRPCV3.Protos.CustomerService;
Note that the third using statement has the static keyword and finishes with the name of my service (CustomerService). That last using statement is actually referring to the namespace for the communication client that the gRPC tools generated for me.
I decided to put the code to call my service in a Windows Form in the form's OnLoad event. Because the classes used to call a gRPC service use asynchronous methods, when I overrided the OnLoad method, I had to add the async keyword to that method's description. Here's the result:
protected async override void OnLoad(EventArgs e)
{
base.OnLoad(e);
}
To actually call my service, I first use the GrpcChannel object that I got through NuGet, calling its ForAddress method and passing the URL for my service (by default, a gRPC service running in Visual Studio has the URL https://localhost:5001). I next create my CustomerServiceClient object (the communication class generated from my .proto file). When I create that communication client, I must pass it that GprcChannel object I just created.
That means that the code to create my communication client object and point it at my gRPC service looks like this:
GrpcChannel gChan = GrpcChannel.ForAddress("https://localhost:5001");
CustomerServiceClient client = new CustomerServiceClient(gChan);
I'm now ready to call my service. I create a CustomerIdRequest object and set its CustId property to the customer I want to retrieve. I pass that object to the GetCustomerById method on my communication client object and catch the CustomerResponse message that's returned from my service. Finally, I extract the data I want from the CustomerResponse message.
Again, the resulting code is pretty straightforward:
CustomerIdRequest crId = new CustomerIdRequest();
crId.CustId = 1;
CustomerResponse cr = await client.GetCustomerByIdAsync(crId);
MessageBox.Show(cr.FullName);
To test both my client and my service, I right-click on the Solution line right at the top of Solution Explorer and select Set Startup Projects. In the resulting dialog, I select the Multiple startup projects option and set the Action column for both of my projects to Start. After clicking the OK button, I just press the F5 key to run both my client and my service.
I recognize that, like using a Windows Form to test my service, I may just like gRPC services because it reminds me of the "good old days" of WSDL and SOAP Web Services. But I do like the idea of having a contract (the .proto file) that's shared between the service and the client to ensure that they will, in fact, work together. On that basis, the better performance I get with gRPC services is just icing on the cake.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.