DotNet gRPC Internals
08/14/2022
6 minutes
In the this series of posts I will look into the details of gRPC in ASP.NET Core. In the previous post I created a simple service and a corresponding client. In this posts I will focus on the internal implementation of ASP.NET Core's gRPC extension. gRPC (gRPC Remote Procedure Calls is an open source remote procedure call implementation that is based on modern (web) standards. gRPC leverages HTTP2 as transport protocol, and uses Protocol Buffer as a data format and interface definition language. It is typically used for back channel (service-to-service) communication due to its efficiency. However, the efficiency comes at a cost: debugging/decoding messages are not as straightforward as with other protocols.
The Internals
In this post I will look into how Grpc.AspNetCore nuget package handles gRPC requests. As mentioned in the previous post, the package includes tooling, that generates the base service code for the our real service implementation. During startup AddGrpc()
registers dependencies to the DI container, but there is only a handful of classes registered today. The main activity happens during mapping the service with MapGrpcService<T>()
call.
Internally this method creates the HTTP call handlers for the service method. Firstly, this method checks if the AddGrpc()
call has registered the service dependencies, and delegates the rest of the work to ServiceRouteBuilder<T>
. ServiceRouteBuilder<T>
is responsible for creating the ASP.NET Core endpoints, which has to be done before app.Run();
is called in Program.cs.
Building the endpoints consists of two actions:
exploring the metadata for the endpoints and creating a
RequestDelegate
handling the requestsregistering the metadata and
RequestDelegate
with ASP.NET Core's endpoint builder.
ServiceRouteBuilder<T>
also registers an additional endpoint: if a gRPC request does not match the pattern of any previously registered routes, then an HTTP 404 response with unimplemented grpc-status header (or trailer) is returned.
Exploring Endpoints Metadata
Exploring Endpoints Metadata is a coordination if multiple classes:
an
IServiceMethodProvider<TService>
provides the methods of the registered serviceServerCallHandlerFactory<TService>
creates the ASP.NET Core call handlers for the service methodsServiceMethodProviderContext<TService>
context provides a plumbing
At this time of writing this post there is one service method provider implementation: BinderServiceMethodProvider<TService>
. This class initiates a binding process. It uses reflection to find a method responsible for binding the generated code. Then it invokes bind method with reflection and passes a ServiceBinderBase
implementation to it.
Note 1: the generated code is generated by the protoc.exe using the grpc_csharp_plugin switch. It is generated by the tooling at build time.Note 2: while this code is using reflection, binding happens during application startup, hence it will not cause performance issues during serving the actual requests.
In the generated code, the method responsible for binding, calls back a service binder with metadata and a delegate of each gRPC service defined in the proto file. The delegate is associated with the corresponding virtual method in xxxServiceBase
. This is the base service and its method that a developer needs to override in its gRPC service. The base service is generated along with the binder method by the tooling.
The metadata includes differenciates streaming requests and responses from non streaming ones. Metadata details are driven by the provided proto file. The code below shows a sample for the binding method generated for a proto file.I am
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] public static void BindService(Grpc::ServiceBinderBase serviceBinder, SuperServiceBase serviceImpl) { serviceBinder.AddMethod(__Method_DoWork, serviceImpl == null ? null : new Grpc::UnaryServerMethod<global::Super.RequestData, global::Super.ResponseData>(serviceImpl.DoWork)); serviceBinder.AddMethod(__Method_StreamWork, serviceImpl == null ? null : new Grpc::ClientStreamingServerMethod<global::Super.RequestData, global::Super.ResponseData>(serviceImpl.StreamWork)); }
The service binder implementation in Gprc.AspNetCore is implemented by ProviderServiceBinder
. It extracts metadata from the callback argumments. As the binding provider does not pass any service for the binding, the callback also does not return a delegate. Hence while extracting the metadata a callback also need to be created which points to the base implementation. Finally, it calls the ServiceMethodProviderContext<TService>
with the new delegate and metadata arguments.
ServiceMethodProviderContext<TService>
uses ServerCallHandlerFactory<TService>
to create the ASP.NET Core endpoint handler. The endpoint handler will store a reference to the delegate and provides the RequestDelegate
for ASP.NET Core endpoints. ServerCallHandlerFactory<TService>
then stores the metadata and the endpoint handler in a collection. This collection is iterated by ServiceRouteBuilder<T>
(mentioned above) when registering the endpoint handlers with ASP.NET Core.
Invoking an Endpoint
While everything above happens at startup time, during endpoint invocation a vastly different stack is executed. The entrypoint invokes ServerCallHandlerBase<T>
's Task HandleCallAsync(HttpContext httpContext)
method.
Note, that I am omitting everything else that is executed by the HTTP pipeline set up in Program.cs, such as Routing, Authentication, etc.
HandleCallAsync method validates that the incoming call is a valid a gRPC call, initializes a call context, and delegates the work to HandleCallAsyncCore method. This is an abstract method which is implemented by derived classes. Currently, there are four derived classes, each matching a corresponding gRPC method type.
UnaryServerCallHandler
ClientStreamingServerCallHandler
ServerStreamingServerCallHandler
DuplexStreamingServerCallHandler
These call handlers are responsible for reading and deserializing the request message(s) as well serializing the response message(s) and writing the data to the HTTP response stream. A message may be compressed, decompression happens while they are deserializing the input stream.
When a message is deserialized a method invoker class helps to invoke the user's implementation of this method. The method invoker activates the service (creates the type) responsible for handing the request. If the user specified interceptor pipeline, that is also invoked. Next, the delegate is invoked that has been created by ProviderServiceBinder<T>
. Finally, the class (service) handling the request is released and exceptions thrown by the user code are handled.
Summary
ASP.NET Core provides one of the fastest gRPC implementation today. It uses a clever implementation to prepare all metadata and message handler during application startup. This way handling an actual request can be as light as possible, avoiding any reflection and less scalable calls. This comes at a trade that loading proto files and services handles during runtime is hardly possible.