Overall Logic
Using ASP.NET Core gRPC is very straightforward; service registration and middleware require only two lines of code.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddGrpc();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
Registering services:
public static IGrpcServerBuilder AddGrpc(this IServiceCollection services)
{
ArgumentNullThrowHelper.ThrowIfNull(services);
#if NET8_0_OR_GREATER
// Prefer AddRoutingCore when available.
// AddRoutingCore doesn't register a regex constraint and produces smaller result from trimming.
services.AddRoutingCore();
services.Configure<RouteOptions>(ConfigureRouting);
#else
services.AddRouting(ConfigureRouting);
#endif
services.AddOptions();
services.TryAddSingleton<GrpcMarkerService>();
services.TryAddSingleton(typeof(ServerCallHandlerFactory<>));
services.TryAddSingleton(typeof(IGrpcServiceActivator<>), typeof(DefaultGrpcServiceActivator<>));
services.TryAddSingleton(typeof(IGrpcInterceptorActivator<>), typeof(DefaultGrpcInterceptorActivator<>));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<GrpcServiceOptions>, GrpcServiceOptionsSetup>());
// Model
services.TryAddSingleton<ServiceMethodsRegistry>();
services.TryAddSingleton(typeof(ServiceRouteBuilder<>));
services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IServiceMethodProvider<>), typeof(BinderServiceMethodProvider<>)));
return new GrpcServerBuilder(services);
}
GrpcMarkerService
is an empty service meant to determine whether developers have injected gRPC services via the container. If services are registered using .AddGrpc()
, GrpcMarkerService
can be accessed; otherwise, during middleware initialization, a direct check can be performed to throw an exception.
Each proto file generates a type, such as GreeterBase
, through the sg
technology, with each generated type having two BindService
methods and proto custom methods.
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static grpc::ServerServiceDefinition BindService(GreeterBase serviceImpl)
{
return grpc::ServerServiceDefinition.CreateBuilder()
.AddMethod(__Method_SayHello, serviceImpl.SayHello).Build();
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static void BindService(grpc::ServiceBinderBase serviceBinder, GreeterBase serviceImpl)
{
serviceBinder.AddMethod(__Method_SayHello, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::GrpcService1.HelloRequest, global::GrpcService1.HelloReply>(serviceImpl.SayHello));
}
These two methods are dynamically generated and will create .AddMethod()
code for the method names generated in the proto file. This way, when the middleware registers gRPC services, it does not need to reflectively scan service classes; instead, it directly calls the BindService
method to configure each route with its corresponding method.
Below are the details for each method:
internal class MethodModel
{
public MethodModel(IMethod method, RoutePattern pattern, IList<object> metadata, RequestDelegate requestDelegate)
{
Method = method;
Pattern = pattern;
Metadata = metadata;
RequestDelegate = requestDelegate;
}
public IMethod Method { get; }
public RoutePattern Pattern { get; }
public IList<object> Metadata { get; }
public RequestDelegate RequestDelegate { get; }
}
The following shows the configuration process for registering a gRPC service within the middleware:
Next, for each method of each service, routes and request delegates are bound.
When a request matches the route and method, it directs to the corresponding RequestDelegate
.
The generation of methods is first handled in BindService
.
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static void BindService(grpc::ServiceBinderBase serviceBinder, GreeterBase serviceImpl)
{
serviceBinder.AddMethod(__Method_SayHello, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::GrpcService1.HelloRequest, global::GrpcService1.HelloReply>(serviceImpl.SayHello));
}
// ProviderServiceBinder.cs
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, UnaryServerMethod<TRequest, TResponse>? handler)
{
var (invoker, metadata) = CreateModelCore<UnaryServerMethod<TService, TRequest, TResponse>>(
method.Name,
new[] { typeof(TRequest), typeof(ServerCallContext) });
_context.AddUnaryMethod<TRequest, TResponse>(method, metadata, invoker);
}
public void AddUnaryMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, IList<object> metadata, UnaryServerMethod<TService, TRequest, TResponse> invoker)
where TRequest : class
where TResponse : class
{
// ServerCallHandlerFactory.cs
var callHandler = _serverCallHandlerFactory.CreateUnary<TRequest, TResponse>(method, invoker);
AddMethod(method, RoutePatternFactory.Parse(method.FullName), metadata, callHandler.HandleCallAsync);
}
As seen, after several layers of encapsulation, the specified gRPC service methods are packaged into a RequestDelegate
.
public delegate Task RequestDelegate(HttpContext context);
Once the RequestDelegate
is built, the logic for handling requests can be found in ServerCallHandlerBase.cs
. The HandleCallAsync
method in ServerCallHandlerBase
aligns with the definition of RequestDelegate
, allowing direct utilization by the middleware.
It is important to note that the ServerCallHandlerBase
instance is created at the point of scanning the gRPC service and binding methods, meaning that for each request hitting the same route, it corresponds to the same ServerCallHandlerBase
instance, with only the HandleCallAsync
method receiving a new HttpContext
on each call.
文章评论