To say that gRPC services are a mixed blessing is an understatement. But there are solutions for the two major issues that will let you grasp the benefits of faster, more powerful services. Used together, ASP.NET Core 7 and AutoMapper will let you unlock the power of gRPC services.
There’s good and bad to implementing gRPC services. The good: because gRPC services use a binary message format, you get faster performance. And, because gRPC services run on HTTP/2 or HTTP/3, you also get the potential to issue more than six parallel requests (plus asynchronous and prioritized requests), client-side and server-side streaming, and out-of-the-box bidirectional communication.
The bad has two aspects. First: much of that speed improvement comes from not using JSON as the format for a message’s body and not using HTTP endpoints. The result is that clients that can call RESTful services can’t call your gRPC services. If you want to create a gRPC web service that’s to be used by your business partners/customers/vendors, then you’re effectively forcing them to move to gRPC … a move they may not want to make (or to make right now).
The second bad aspect is that if you use the gRPC tool set, you’ll end up with a new set of objects that are different from the objects you’ve been using in your application up until now. Converting from one set of objects to another can be a pain.
Fortunately, .NET 7 provides a solution for the RESTful interoperability problem and a third-party tool provides a solution for working with the new set of objects.
In some ways, gRPC harks back to the earliest web service tools: WSDL and SOAP. With RESTful services, basically, you just start typing in code to define your service (though, if you write an OpenAPI document for your application, you can generate the skeleton of your service from it).
With gRPC services, you first describe your service in a text file using the protocol buffer format (“protobuf” for short). From that description, various tools generate both the base for your gRPC service and a proxy for your clients to use to call your service (just like the old WSDL files! I regard this as a good thing).
In the protobuf file (which generally has the .proto extension), you describe your service, its methods, and the message formats used both to pass data to and return data from the methods. In ASP.NET Core, those message formats are used to generate class files that, in turn, you use as objects in your code.
Typically, the protobuf definition of a service method looks like the following example which:
service ProductQuery {
rpc GetAll(nomessage)
returns(AllProducts);
rpc GetById(ProductId)
returns(svcProduct);
}
To enable calling these methods as if they were RESTful services—i.e., to enable clients to call these methods using HttpClient or to surf to a method in your browser—you just need to add an option statement after each object’s returns statement, enclosed in curly braces. Inside the option statement, you need to reference the google.api.http .proto file, specify the HTTP verb to used when calling this method (e.g., get, post, delete, etc.), and provide a URL to be used in calling the method.
This example specifies that the GetAll method is to be called with a GET request sent to <host url>/products
and the GetById method is to be called (also as a GET request) at <host url>/Products/{id}
:
service ProductQuery {
rpc GetAll(nomessage)
returns(AllProducts)
{
option (google.api.http) = {get: "/products"};
};
rpc GetById(ProductId)
returns(svcProduct)
{
option (google.api.http) = {get: "/Products/{id}"};
};
}
As the GetById method demonstrates, the URL for your method can contain parameters, using the same curly brace–based syntax that ASP.NET routing uses. The parameter name must correspond to a field in the method’s input message. That means, for example, that since the URL for the GetById method has a parameter called “id,” the message that the GetById method uses (ProductId) must have a field called “id” also. This would do the trick:
message ProductId{
int32 id=1;
…more fields…
}
This whole package is referred to as “JSON transcoding” because it provides a translation mechanism between the JSON messages typical of RESTful services and the format used by gRPC services.
But now you have to set up your project to support that option statement: Your project needs to have a file at the path “google/api/http.proto”. To compound this problem, that http.proto file, in turn, references another .proto file called “annotations.proto”—your project needs that file also.
You’ll need to make sure that both of those files can be found by Visual Studio when you build your application.
My solution was to download the http.proto and annotations.proto files to my project. I then recreated the folder structure that the references require inside my service’s Protos folder. I put my service’s .proto file in a folder of its own (that’s the warehousemanagement folder in this graphic):
As I said, one of the good/bad parts of gRPC service is that the provided tools will convert your message formats into objects that you can use in your C# code. The reason that’s a mixed blessing is because, unless you have much better planning skills than I have, the objects generated from your .proto file are not going to be the objects you’re using in the rest of your application. It’s not hard to imagine, for example, that the svcProduct object generated from the .proto file isn’t the Product object you’ve been using in the rest of your application, for example.
You could write your own code to convert your gRPC object to your “native” object but you have a better choice: AutoMapper. Automapper lets you generate a map between properties of two classes and, passed an instance (object) of one class, will create the “other” object by copying the values of the properties from the first object to the second. If you’ve been careful about giving equivalent properties on the two classes the same names, generating a map can be as simple as this code, which creates a map between objects of type svcProduct and Product:
MapperConfiguration gRpcNativeCfg = new MapperConfiguration(cfg => {
cfg.CreateMap<svcProduct, Product>().ReverseMap();
});
gRpcNativeMap = new Mapper(gRpcNativeCfg);
The call to ReverseMap in this code causes AutoMapper to generate a bidirectional map: from svcProduct to Product and from Product to svcProduct.
Even if you’re careful about naming properties, this solution is probably too simple: It’s likely, for example, that your classes use other classes. My Product class, for example, includes a collection of Warehouse objects … and that Warehouse class includes a property of type Address.
All that’s necessary to handle this, though, is to include those other classes when generating your map. That’s what this code does:
MapperConfiguration gRpcNativeCfg = new MapperConfiguration(cfg => {
cfg.CreateMap<svcProduct, Product>().ReverseMap();
cfg.CreateMap<svcWarehouse, Warehouse>().ReverseMap();
cfg.CreateMap<svcAddress, Address>().ReverseMap();
});
gRpcNativeMap = new Mapper(gRpcNativeCfg);
Now, creating a Product object from an svcProduct is as simple as this:
Product prd = gRpcNativeMap.Map<Product>(svcPrd);
Automapper recommends that you only create your mapper once in your application. In an ASP.NET Core application, you could define your mapper in the program.cs file and then add it to your application’s services collection.
Alternatively, you could create a static class and initialize your mapper in the static class’s constructor (the constructor on a static class is guaranteed to be only called once). Here’s an example of a conversion class that builds the mapper in the constructor and provides overloaded methods for each conversion:
internal class GrpcConversions
{
static IMapper gRpcNativeMap;
static GrpcConversions()
{
MapperConfiguration gRpcNativeCfg = new MapperConfiguration(cfg => {
cfg.CreateMap<svcProduct, Product>().ReverseMap();
cfg.CreateMap<svcAddress, Address>().ReverseMap();
cfg.CreateMap<svcWarehouse, Warehouse>().ReverseMap();
});
gRpcNativeMap = new Mapper(gRpcNativeCfg);
}
internal static Product Convert(svcProduct svcPrd)
{
return gRpcNativeMap.Map<Product>(svcPrd);
}
internal static svcProduct Convert(Product prd)
{
svcProduct svcPrd = gRpcNativeMap.Map<svcProduct>(prd);
}
}
With this static class available to you, actual conversions just look like this:
Product prd = GrpcConversions.Convert(svcPrd));
Using gRPC does usher you into a different world but, thanks to JSON transcoding, you don’t have to force that move on your partners. And, with a little help from AutoMapper, you won’t have to rewrite any existing code to work with gRPC, either. Given the benefits of gRPC, that’s not bad at all.
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 also writes courses and teaches for Learning Tree International.