Partilhar via


interceptadores gRPC no .NET

Por Ernest Nguyen

Interceptadores são um conceito de gRPC que permite que os aplicativos interajam com chamadas gRPC de entrada ou saída. Eles oferecem uma maneira de enriquecer o pipeline de processamento de solicitação.

Os interceptadores são configurados para um canal ou serviço e executados automaticamente para cada chamada gRPC. Como os interceptadores são transparentes para a lógica do aplicativo do usuário, eles são uma excelente solução para casos comuns, como registro em log, monitoramento, autenticação e validação.

TipoInterceptor

Os interceptadores podem ser implementados para servidores gRPC e clientes criando uma classe que herda do tipo Interceptor:

public class ExampleInterceptor : Interceptor
{
}

Por padrão, a classe base Interceptor não faz nada. Adicione comportamento a um interceptador substituindo os métodos de classe base apropriados em uma implementação de interceptador.

Interceptadores de cliente

Interceptadores de cliente gRPC interceptam invocações RPC de saída. Eles fornecem acesso à solicitação enviada, à resposta de entrada e ao contexto de uma chamada do lado do cliente.

Métodos Interceptor a serem substituídos pelo cliente:

  • BlockingUnaryCall: intercepta uma invocação de bloqueio de um RPC unário.
  • AsyncUnaryCall: intercepta uma invocação assíncrona de um RPC unário.
  • AsyncClientStreamingCall: intercepta uma invocação assíncrona de um RPC de fluxo de dados de cliente.
  • AsyncServerStreamingCall: intercepta uma invocação assíncrona de um RPC de fluxo de dados de servidor.
  • AsyncDuplexStreamingCall: intercepta uma invocação assíncrona de um RPC de fluxo de dados bidirecional.

Aviso

Embora ambos BlockingUnaryCall e AsyncUnaryCall se refiram a RPCs unários, eles não são intercambiáveis. Uma invocação de bloqueio não é interceptada por AsyncUnaryCall e uma invocação assíncrona não é interceptada por um BlockingUnaryCall.

Criar um interceptador gRPC do cliente

O código a seguir apresenta um exemplo básico de interceptação de uma invocação assíncrona de uma chamada unária:

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);
    }
}

Substituindo AsyncUnaryCall:

  • Intercepta uma chamada unária assíncrona.
  • Registra detalhes sobre a chamada.
  • Chama o parâmetro continuation passado para o método. Isso invoca o próximo interceptador na cadeia ou no invocador de chamadas subjacente se este for o último interceptador.

Os métodos em Interceptor para cada tipo de método de serviço têm assinaturas diferentes. No entanto, o conceito por trás dos parâmetros continuation e context permanece o mesmo:

  • continuation é um delegado que invoca o próximo interceptador na cadeia ou o invocador de chamada subjacente (se não houver nenhum interceptador na cadeia). Não é um erro chamá-lo de zero ou várias vezes. Interceptadores não são necessários para retornar uma representação de chamada (AsyncUnaryCall no caso de RPC unário) retornada do delegado continuation. Omitir a chamada delegada e retornar sua própria instância de representação de chamada interrompe a cadeia de interceptadores e retorna a resposta associada imediatamente.
  • context carrega valores com escopo associados à chamada do lado do cliente. Use context para passar metadados, como entidades de segurança, credenciais ou dados de rastreamento. Além disso, context traz informações sobre prazos e cancelamento. Para obter mais informações, confira Serviços gRPC confiáveis com prazos finais e cancelamento.

Aguardando resposta no interceptador de cliente

Um interceptador pode aguardar a resposta em chamadas de fluxo de dados unárias e de cliente atualizando o valor AsyncUnaryCall<TResponse>.ResponseAsync ou 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);
        }
    }
}

O código anterior:

  • Cria um novo interceptador que substitui AsyncUnaryCall.
  • Substituindo AsyncUnaryCall:
    • Chama o parâmetro continuation para invocar o próximo item na cadeia de interceptadores.
    • Cria uma nova instância AsyncUnaryCall<TResponse> com base no resultado da continuação.
    • Encapsula a tarefa ResponseAsync usando o método HandleResponse.
    • Aguarda a resposta com HandleResponse. Aguardar a resposta permite que a lógica seja adicionada depois que o cliente recebeu a resposta. Ao aguardar a resposta em um bloco try-catch, erros de chamadas podem ser registrados em log.

Para obter mais informações sobre como criar um interceptador de cliente, confira o ClientLoggerInterceptor.cs exemplo no grpc/grpc-dotnet repositório GitHub.

Configurar interceptadores de cliente

Os interceptadores de cliente gRPC são configurados em um canal.

O seguinte código:

  • Cria um canal usando GrpcChannel.ForAddress.
  • Usa o método de extensão Intercept para configurar o canal para usar o interceptador. Observe que esse método retorna um CallInvoker. Clientes gRPC fortemente tipados podem ser criados a partir de um invocador, assim como um canal.
  • Cria um cliente no invocador. As chamadas gRPC feitas pelo cliente executam automaticamente o interceptador.
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());

var client = new Greeter.GreeterClient(invoker);

O método de extensão Intercept pode ser encadeado para configurar vários interceptadores para um canal. Como alternativa, há uma sobrecarga de Intercept que aceita vários interceptadores. Qualquer número de interceptadores pode ser executado para uma única chamada gRPC, como demonstra o exemplo a seguir:

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

Os interceptadores são invocados na ordem inversa dos métodos de extensão Intercept encadeados. No código anterior, os interceptadores são invocados na seguinte ordem:

  1. ClientLoggerInterceptor
  2. ClientMonitoringInterceptor
  3. ClientTokenInterceptor

Para obter informações sobre como configurar interceptadores com a fábrica de clientes gRPC, confira Integração de fábrica do cliente gRPC no .NET.

Interceptadores de servidor

Os interceptadores de servidor gRPC interceptam solicitações RPC de entrada. Eles fornecem acesso à solicitação de entrada, à resposta de saída e ao contexto de uma chamada do lado do servidor.

Métodos Interceptor a serem substituídos pelo cliente:

  • UnaryServerHandler: intercepta um RPC unário.
  • ClientStreamingServerHandler: intercepta um RPC de fluxo de dados de cliente.
  • ServerStreamingServerHandler: intercepta um RPC de fluxo de dados de servidor.
  • DuplexStreamingServerHandler: intercepta um RPC de fluxo de dados bidirecional.

Criar um interceptador gRPC do servidor

O código a seguir apresenta um exemplo de uma interceptação de um RPC unário de entrada:

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;
        }
    }
}

Substituindo UnaryServerHandler:

  • Intercepta uma chamada unária de entrada.
  • Registra detalhes sobre a chamada.
  • Chama o parâmetro continuation passado para o método. Isso invoca o próximo interceptador na cadeia ou no invocador de chamadas subjacente se este for o último interceptador.
  • Registra todas as exceções. Aguardar a continuação permite que a lógica seja adicionada após a execução do método de serviço. Ao aguardar a continuação em um bloco try-catch, os erros de métodos podem ser registrados em log.

A assinatura de métodos de interceptadores de cliente e servidor são semelhantes:

  • continuation significa um delegado para um RPC de entrada que chama o próximo interceptador na cadeia ou no manipulador de serviço (se não houver nenhum interceptador na cadeia). Semelhante aos interceptadores de cliente, você pode chamá-lo a qualquer momento e não há necessidade de retornar uma resposta diretamente do delegado de continuação. A lógica de saída pode ser adicionada depois que um manipulador de serviço for executado aguardando a continuação.
  • context carrega metadados associados à chamada do lado do servidor, como metadados de solicitação, prazos e cancelamento ou resultado de RPC.

Para obter mais informações sobre como criar um interceptador de servidor, veja o exemplo de ServerLoggerInterceptor.cs no do repositório GitHubgrpc/grpc-dotnet.

Configurar interceptadores de servidor

Os interceptadores de servidor gRPC são configurados na inicialização. O seguinte código:

  • Adiciona gRPC ao aplicativo com AddGrpc.
  • Configura ServerLoggerInterceptor para todos os serviços adicionando-o à coleção da opção Interceptors de serviço.
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });
}

Um interceptador também pode ser configurado para um serviço específico usando AddServiceOptions e especificando o tipo de serviço.

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

Os interceptadores são executados na ordem em que são adicionados ao InterceptorCollection. Se os interceptadores globais e de serviço único estiverem configurados, os interceptadores configurados globalmente serão executados antes daqueles configurados para um único serviço.

Por padrão, os interceptadores de servidor gRPC têm um tempo de vida por solicitação. Substituir esse comportamento é possível por meio do registro do tipo de interceptador com injeção de dependência. O exemplo a seguir registra o ServerLoggerInterceptor com um tempo de vida singleton:

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

    services.AddSingleton<ServerLoggerInterceptor>();
}

Interceptadores gRPC versus Middleware

O middleware do ASP.NET Core oferece funcionalidades semelhantes em comparação com interceptadores em aplicativos gRPC baseados em C-core. O middleware e interceptadores do ASP.NET Core são conceitualmente semelhantes. Ambos:

  • Eles são usados para construir um pipeline que manipula uma solicitação gRPC.
  • Eles permitem que o trabalho seja executado antes ou depois do próximo componente no pipeline.
  • Forneça acesso ao HttpContext:
    • No middleware, o HttpContext é um parâmetro.
    • Nos interceptadores, o HttpContext pode ser acessado usando o parâmetro ServerCallContext com o método de extensão ServerCallContext.GetHttpContext. Esse recurso é específico para interceptadores em execução no ASP.NET Core.

Diferenças do interceptador gRPC no middleware ASP.NET Core:

  • Interceptadores:
    • Opere na camada gRPC de abstração usando o ServerCallContext.
    • Forneça acesso:
      • À mensagem desserializada enviada a uma chamada.
      • À mensagem retornada da chamada antes de ser serializada.
    • É possível capturar e manipular exceções geradas nos serviços gRPC.
  • Middleware:
    • É executado para todas as solicitações HTTP.
    • É executado antes dos interceptadores gRPC.
    • Opera nas mensagens HTTP/2 subjacentes.
    • Só é possível acessar bytes nos fluxos de solicitação e resposta.

Recursos adicionais