Share via


Interceptores de gRPC en .NET

Por Ernest Nguyen

Los interceptores son un concepto de gRPC que permite a las aplicaciones interactuar con llamadas gRPC entrantes o salientes. Ofrecen una manera de enriquecer la canalización de procesamiento de solicitudes.

Los interceptores se configuran para un canal o servicio, y se ejecutan automáticamente para cada llamada a gRPC. Como los interceptores son transparentes para la lógica de aplicación del usuario, son una excelente solución para casos comunes, como los de registro, supervisión, autenticación y validación.

Tipo de Interceptor

Los interceptores se pueden implementar para servidores y clientes gRPC mediante la creación de una clase que hereda del tipo Interceptor:

public class ExampleInterceptor : Interceptor
{
}

De forma predeterminada, la clase base Interceptor no hace nada. Para agregar comportamiento a un interceptor, invalide los métodos de clase base adecuados en una implementación de interceptor.

Interceptores de cliente

Los interceptores de cliente gRPC interceptan las invocaciones de RPC salientes. Proporcionan acceso a la solicitud enviada, la respuesta entrante y el contexto de una llamada del lado cliente.

Métodos Interceptor que se invalidarán para el cliente:

  • BlockingUnaryCall: intercepta una invocación de bloqueo de una RPC unaria.
  • AsyncUnaryCall: intercepta una invocación asincrónica de una RPC unaria.
  • AsyncClientStreamingCall: intercepta una invocación asincrónica de una RPC de streaming de cliente.
  • AsyncServerStreamingCall: intercepta una invocación asincrónica de una RPC de streaming de servidor.
  • AsyncDuplexStreamingCall: intercepta una invocación asincrónica de una RPC de streaming bidireccional.

Advertencia

Aunque tanto BlockingUnaryCall como AsyncUnaryCall hacen referencia a RPC unarias, no son intercambiables. Una invocación de bloqueo no es interceptada por AsyncUnaryCally una invocación asincrónica no es interceptada por BlockingUnaryCall.

Creación de un interceptor gRPC de cliente

En el código siguiente se presenta un ejemplo básico de interceptación de una invocación asincrónica de una llamada unaria:

public class ClientLoggingInterceptor : Interceptor
{
    private readonly ILogger _logger;

    public ClientLoggingInterceptor(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<ClientLoggingInterceptor>();
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting call. Type/Method: {Type} / {Method}",
            context.Method.Type, context.Method.Name);
        return continuation(request, context);
    }
}

El reemplazo de AsyncUnaryCall:

  • Intercepta una llamada unaria asincrónica.
  • Registra detalles sobre la llamada.
  • Llama al parámetro continuation pasado al método. Esto invoca el siguiente interceptor de la cadena o el invocador de llamada subyacente si este es el último interceptor.

Los métodos de Interceptor para cada tipo de método de servicio tienen signaturas diferentes. Pero el concepto subyacente a los parámetros continuation y context sigue siendo el mismo:

  • continuation es un delegado que invoca el siguiente interceptor en la cadena o el invocador de llamada subyacente (si no queda ningún interceptor en la cadena). No es un error llamarlo cero o varias veces. No es necesario que los interceptores devuelvan una representación de llamada (AsyncUnaryCall en caso de RPC unaria) devuelta desde el delegado continuation. Si se omite la llamada de delegado y se devuelve una instancia de representación de llamada propia, se interrumpe la cadena de los interceptores y se devuelve la respuesta asociada inmediatamente.
  • context lleva valores con ámbito asociados a la llamada del lado cliente. Use context para pasar metadatos, como entidades de seguridad, credenciales o datos de seguimiento. Además, context contiene información sobre las fechas límite y la cancelación. Para más información, consulte Servicios gRPC confiables con fechas límite y cancelación.

Espera de respuesta en el interceptor de cliente

Un interceptor puede esperar la respuesta en llamadas de streaming unarias y de cliente mediante la actualización del valor AsyncUnaryCall<TResponse>.ResponseAsync o AsyncClientStreamingCall<TRequest, TResponse>.ResponseAsync.

public class ErrorHandlerInterceptor : Interceptor
{
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var call = continuation(request, context);

        return new AsyncUnaryCall<TResponse>(
            HandleResponse(call.ResponseAsync),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose);
    }

    private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> inner)
    {
        try
        {
            return await inner;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("Custom error", ex);
        }
    }
}

El código anterior:

  • Crea un interceptor que invalida AsyncUnaryCall.
  • El reemplazo de AsyncUnaryCall:
    • Llama al parámetro continuation para invocar el siguiente elemento de la cadena del interceptor.
    • Crea una instancia de AsyncUnaryCall<TResponse> basada en el resultado de la continuación.
    • Encapsula la tarea ResponseAsync mediante el método HandleResponse.
    • Espera la respuesta con HandleResponse. La espera de la respuesta permite agregar lógica después de que el cliente haya recibido la respuesta. Al esperar la respuesta en un bloque try-catch, se pueden registrar los errores de las llamadas.

Para más información sobre cómo crear un interceptor de cliente, vea el ejemplo ClientLoggerInterceptor.cs en el repositorio de grpc/grpc-dotnet GitHub.

Configuración de interceptores de cliente

Los interceptores de cliente gRPC se configuran en un canal.

El código siguiente:

  • Crea un canal mediante GrpcChannel.ForAddress.
  • Usa el método de extensión Intercept a fin de configurar el canal para usar el interceptor. Observe que este método devuelve un objeto CallInvoker. Los clientes gRPC fuertemente tipados se pueden crear a partir de un invocador como un canal.
  • Crea un cliente a partir del invocador. Las llamadas gRPC realizadas por el cliente ejecutan automáticamente el interceptor.
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());

var client = new Greeter.GreeterClient(invoker);

El método de extensión Intercept se puede encadenar para configurar varios interceptores para un canal. Como alternativa, hay una sobrecarga de Intercept que acepta varios interceptores. Se puede ejecutar cualquier número de interceptores para una sola llamada gRPC, como se muestra en el ejemplo siguiente:

var invoker = channel
    .Intercept(new ClientTokenInterceptor())
    .Intercept(new ClientMonitoringInterceptor())
    .Intercept(new ClientLoggerInterceptor());

Los interceptores se invocan en orden inverso de los métodos de extensión Intercept encadenados. En el código anterior, los interceptores se invocan en el orden siguiente:

  1. ClientLoggerInterceptor
  2. ClientMonitoringInterceptor
  3. ClientTokenInterceptor

Para obtener información sobre cómo configurar interceptores con el generador de cliente gRPC, vea Integración de la fábrica de cliente gRPC en .NET.

Interceptores de servidor

Los interceptores de servidor gRPC interceptan las solicitudes de RPC entrantes. Proporcionan acceso a la solicitud entrante, la respuesta saliente y el contexto de una llamada del lado servidor.

Métodos Interceptor que se invalidarán para el servidor:

  • UnaryServerHandler: intercepta una RPC unaria.
  • ClientStreamingServerHandler: intercepta una RPC de streaming de cliente.
  • ServerStreamingServerHandler: intercepta una RPC de streaming de servidor.
  • DuplexStreamingServerHandler: intercepta una RPC de streaming bidireccional.

Creación de un interceptor gRPC de servidor

En el código siguiente se presenta un ejemplo de una interceptación de una RPC unaria entrante:

public class ServerLoggerInterceptor : Interceptor
{
    private readonly ILogger _logger;

    public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting receiving call. Type/Method: {Type} / {Method}",
            MethodType.Unary, context.Method);
        try
        {
            return await continuation(request, context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error thrown by {context.Method}.");
            throw;
        }
    }
}

El reemplazo de UnaryServerHandler:

  • Intercepta una llamada unaria entrante.
  • Registra detalles sobre la llamada.
  • Llama al parámetro continuation pasado al método. Esto invoca el siguiente interceptor de la cadena o el controlador de servicio si este es el último interceptor.
  • Registra las excepciones. La espera de la continuación permite agregar lógica después de que se haya ejecutado el método de servicio. Al esperar la continuación en un bloque try-catch, se pueden registrar los errores de los métodos.

La signatura de los métodos interceptores de cliente y servidor es similar:

  • continuation equivale a un delegado para una RPC entrante que llama al siguiente interceptor de la cadena o al controlador de servicio (si no queda ningún interceptor en la cadena). De forma similar a los interceptores de cliente, puede llamarlo en cualquier momento y no es necesario devolver una respuesta directamente desde el delegado de continuación. La lógica de salida se puede agregar después de que se haya ejecutado un controlador de servicio esperando la continuación.
  • context lleva metadatos asociados a la llamada del lado servidor, como metadatos de solicitud, fechas límite y cancelación, o resultado de RPC.

Para más información sobre cómo crear un interceptor de servidor, vea el ejemplo ServerLoggerInterceptor.cs en el repositorio de grpc/grpc-dotnet GitHub.

Configuración de interceptores de servidor

Los interceptores de servidor gRPC se configuran durante el inicio. El código siguiente:

  • Agrega gRPC a la aplicación con AddGrpc.
  • Configura ServerLoggerInterceptor para todos los servicios agregándole a la colección Interceptors de la opción de servicio.
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });
}

Un interceptor también se puede configurar para un servicio específico mediante AddServiceOptions y la especificación del tipo de servicio.

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddGrpc()
        .AddServiceOptions<GreeterService>(options =>
        {
            options.Interceptors.Add<ServerLoggerInterceptor>();
        });
}

Los interceptores se ejecutan en el orden en que se agregan a InterceptorCollection. Si se configuran interceptores de servicio únicos y globales, los interceptores configurados globalmente se ejecutan antes que los configurados para un único servicio.

De manera predeterminada, los interceptores de servicio de gRPC tienen una duración por solicitud. La invalidación de este comportamiento es posible mediante el registro del tipo de interceptor con inserción de dependencias. En el ejemplo siguiente se registra ServerLoggerInterceptor con una duración singleton:

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });

    services.AddSingleton<ServerLoggerInterceptor>();
}

Interceptores de gRPC frente a middleware

El middleware de ASP.NET Core ofrece funcionalidades similares a los interceptores de aplicaciones gRPC basadas en C-core. El middleware y los interceptores de ASP.NET Core son conceptualmente similares. Ambos:

  • Se usan para construir una canalización que controla una solicitud gRPC.
  • Permiten que se realicen tareas antes o después del siguiente componente de la canalización.
  • Proporcionan acceso a HttpContext:
    • En el middleware, HttpContext es un parámetro.
    • En los interceptores, se puede acceder a HttpContext mediante el parámetro ServerCallContext con el método de extensión ServerCallContext.GetHttpContext. Esta característica es específica de los interceptores que se ejecutan en ASP.NET Core.

Diferencias entre los interceptores de gRPC y el middleware de ASP.NET Core:

  • Interceptores:
    • Operan en el nivel de abstracción de gRPC mediante la clase ServerCallContext.
    • Proporcionan acceso a los elementos siguientes:
      • Al mensaje deserializado que se envía a una llamada.
      • Al mensaje que se devuelve de la llamada antes de que se serialice.
    • Pueden detectar y controlar las excepciones que se producen en los servicios gRPC.
  • Middleware:
    • Se ejecuta para todas las solicitudes HTTP.
    • Se ejecuta antes que los interceptores de gRPC.
    • Opera en los mensajes HTTP/2 subyacentes.
    • Solo puede acceder a los bytes de las secuencias de solicitud y respuesta.

Recursos adicionales