gRPC-Interceptors in .NET

Von Ernest Nguyen

Interceptors sind ein gRPC-Konzept, das es Apps ermöglicht, mit ein- oder ausgehenden gRPC-Aufrufen zu interagieren. Sie bieten eine Möglichkeit zum Anreichern der Anforderungspipeline.

Interceptors werden für einen Kanal oder Dienst konfiguriert und bei jedem gRPC-Aufruf automatisch ausgeführt. Da Interceptors für die Anwendungslogik des Benutzers transparent sind, eignen sie sich hervorragend für gängige Zwecke wie Protokollierung, Überwachung, Authentifizierung und Validierung.

Interceptor-Typ

Interceptors können für gRPC-Server und -Clients implementiert werden, indem Sie eine Klasse erstellen, die vom Typ Interceptor erbt:

public class ExampleInterceptor : Interceptor
{
}

Standardmäßig hat die Basisklasse Interceptor keine Aufgabe. Fügen Sie einem Interceptor Verhalten hinzu, indem Sie die entsprechenden Methoden der Basisklasse in einer Implementierung des Interceptors überschreiben.

Clientinterceptors

gRPC-Clientinterceptors fangen ausgehende RPC-Aufrufe ab. Sie ermöglichen den Zugriff auf die gesendete Anforderung, die eingehende Antwort und den Kontext für einen Aufruf auf Clientseite.

Interceptor-Methoden, die für den Client überschrieben werden sollen:

  • BlockingUnaryCall: fängt einen blockierenden Aufruf eines unären RPC ab.
  • AsyncUnaryCall: fängt einen asynchronen Aufruf eines unären RPC ab.
  • AsyncClientStreamingCall: fängt einen asynchronen Aufruf eines RPC für Clientstreaming ab.
  • AsyncServerStreamingCall: fängt einen asynchronen Aufruf eines RPC für Serverstreaming ab.
  • AsyncDuplexStreamingCall: fängt einen asynchronen Aufruf eines RPC für bidirektionales Streaming ab.

Warnung

Obwohl sowohl BlockingUnaryCall als auch AsyncUnaryCall auf unäre RPCs verweisen, sind sie nicht austauschbar. Ein blockierender Aufruf wird nicht von AsyncUnaryCall abgefangen, ein asynchroner Aufruf nicht von BlockingUnaryCall abgefangen.

Erstellen eines gRPC-Interceptors für einen Client

Der folgende Code ist ein einfaches Beispiel für das Abfangen eines asynchronen Aufrufs eines unären Aufrufs:

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

Das Überschreiben von AsyncUnaryCall bewirkt Folgendes:

  • Ein asynchroner unärer Aufruf wird abgefangen.
  • Details zum Aufruf werden protokolliert.
  • Der an die Methode übergebene Parameter continuation wird aufgerufen. Dies ruft den nächsten Interceptor in der Kette oder den zugrunde liegenden Aufrufaufrufer auf, wenn dies der letzte Interceptor ist.

Methoden für Interceptor für jede Art von Dienstmethode haben unterschiedliche Signaturen. Das Konzept hinter den Parametern continuation und context bleibt jedoch unverändert:

  • continuation ist ein Delegat, der den nächsten Interceptor in der Kette oder den zugrundeliegenden Aufrufer aufruft (wenn es keinen Interceptor mehr in der Kette gibt). Es ist kein Fehler, ihn NULL oder mehrmals aufzurufen. Interceptors müssen keine Aufrufdarstellung (AsyncUnaryCall im Falle einer unären RPC) zurückgeben, die vom Delegaten continuation zurückgegeben wird. Wenn Sie den Aufruf des Delegaten weglassen und Ihre eigene Instanz der Aufrufdarstellung zurückgeben, wird die Kette der Interceptors unterbrochen und die zugehörige Antwort sofort zurückgegeben.
  • context enthält bereichsbezogene Werte, die mit dem Aufruf auf Clientseite verbunden sind. Übergeben Sie mithilfe von context Metadaten wie Sicherheitsprinzipale, Anmeldeinformationen oder Ablaufverfolgungsdaten. Darüber hinaus context enthält Informationen zu Fristen und Abbrüchen. Weitere Informationen finden Sie unter Zuverlässige gRPC-Dienste mit Fristen und Abbrüchen.

Warten auf Antwort im Clientinterceptor

Ein Interceptor kann die Antwort in unären und Clientstreamingaufrufen abwarten, indem er den Wert AsyncUnaryCall<TResponse>.ResponseAsync oder AsyncClientStreamingCall<TRequest, TResponse>.ResponseAsync aktualisiert.

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

Der obige Code:

  • Erstellt einen neuen Interceptor, der AsyncUnaryCall überschreibt.
  • Das Überschreiben von AsyncUnaryCall bewirkt Folgendes:
    • Der Parameter continuation wird aufgerufen, um das nächste Element in der Interceptorkette aufzurufen.
    • Es wird eine neue AsyncUnaryCall<TResponse>-Instanz basierend auf dem Ergebnis der Fortsetzung erstellt.
    • Die Aufgabe ResponseAsync wird mit der Methode HandleResponse umschlossen.
    • Die Antwort wird mit HandleResponse erwartet. Durch das Warten auf die Antwort kann Logik hinzugefügt werden, nachdem der Client die Antwort empfangen hat. Durch das Warten auf die Antwort in einem try-catch-Block können Fehler aus Aufrufen protokolliert werden.

Weitere Informationen zur Erstellung eines Clientinterceptors finden Sie im Beispiel zu ClientLoggerInterceptor.cs im grpc/grpc-dotnet GitHub-Repository.

Konfigurieren von Clientinterceptora

gRPC-Clientinterceptors werden für einen Kanal konfiguriert.

Der folgende Code führt folgende Aktionen aus:

  • Ein Kanal wird unter Verwendung von GrpcChannel.ForAddress erstellt.
  • Mithilfe der Erweiterungsmethode Intercept wird der Kanal für die Verwendung des Interceptors konfiguriert. Beachten Sie, dass diese Methode CallInvoker zurückgibt. Stark typisierte gRPC-Clients können wie ein Kanal über einen Aufrufer erstellt werden.
  • Ein Client wird über den Aufrufer erstellt. Vom Client ausgehende gRPC-Aufrufe führen automatisch den Interceptor aus.
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());

var client = new Greeter.GreeterClient(invoker);

Die Erweiterungsmethode Intercept kann verkettet werden, um für einen Kanal mehrere Interceptors zu konfigurieren. Alternativ dazu gibt es die Überladung Intercept, die mehrere Interceptors zulässt. Für einen einzelnen gRPC-Aufruf kann eine beliebige Anzahl von Interceptors ausgeführt werden, wie das folgende Beispiel zeigt:

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

Interceptors werden in umgekehrter Reihenfolge der verketteten Intercept-Erweiterungsmethoden aufgerufen. Im vorstehenden Code werden Interceptors in der folgenden Reihenfolge aufgerufen:

  1. ClientLoggerInterceptor
  2. ClientMonitoringInterceptor
  3. ClientTokenInterceptor

Informationen zum Konfigurieren von Interceptors mit der gRPC-Clientfactory finden Sie unter gRPC-Clientfactoryintegration in .NET.

Serverinterceptors

gRPC-Serverinterceptors fangen eingehende RPC-Anforderungen ab. Sie ermöglichen den Zugriff auf die eingehende Anforderung, die ausgehende Antwort und den Kontext für einen Aufruf auf Serverseite.

Interceptor-Methoden, die für den Server überschrieben werden sollen:

  • UnaryServerHandler: fängt einen unären RPC ab.
  • ClientStreamingServerHandler: fängt einen RPC für Clientstreaming ab.
  • ServerStreamingServerHandler: fängt einen RPC für Serverstreaming ab.
  • DuplexStreamingServerHandler: fängt einen RPC für bidirektionales Streaming ab.

Erstellen eines gRPC-Serverinterceptors

Der folgende Code zeigt ein Beispiel für das Abfangen eines eingehenden unären RPC:

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

Das Überschreiben von UnaryServerHandler bewirkt Folgendes:

  • Ein eingehender unärer Aufruf wird abgefangen.
  • Details zum Aufruf werden protokolliert.
  • Der an die Methode übergebene Parameter continuation wird aufgerufen. Dies ruft den nächsten Interceptor in der Kette oder den Diensthandler auf, wenn dies der letzte Interceptor ist.
  • Alle Ausnahmen werden protokolliert. Durch das Warten auf die Fortsetzung kann Logik hinzugefügt werden, nachdem die Dienstmethode ausgeführt wurde. Durch das Warten auf die Fortsetzung in einem try-catch-Block können Fehler aus Methoden protokolliert werden.

Die Signatur der Methoden von Client- und Serverinterceptors ist ähnlich:

  • continuation steht für einen Delegaten eines eingehenden RPC, der den nächsten Interceptor in der Kette oder den Diensthandler aufruft (wenn es in der Kette keinen Interceptor mehr gibt). Ähnlich wie bei Clientinterceptors können Sie ihn jederzeit aufrufen und müssen nicht direkt eine Antwort vom Fortsetzungsdelegaten zurückgeben. Ausgehende Logik kann hinzugefügt werden, nachdem ein Diensthandler durch Warten auf die Fortsetzung ausgeführt wurde.
  • context enthält Metadaten, die mit dem serverseitigen Aufruf verbunden sind, z. B. Anforderungsmetadaten, Fristen und Abbrüche oder RPC-Ergebnisse.

Weitere Informationen zur Erstellung eines Serverinterceptors finden Sie im Beispiel zu ServerLoggerInterceptor.cs im grpc/grpc-dotnet GitHub-Repository.

Konfigurieren von Serverinterceptors

gRPC-Serverinterceptors werden beim Start konfiguriert. Der folgende Code führt folgende Aktionen aus:

  • gRPC wird mit AddGrpc zur App hinzugefügt.
  • ServerLoggerInterceptor wird für alle Dienste konfiguriert, indem es der Sammlung Interceptors der Dienstoption hinzugefügt wird.
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });
}

Ein Interceptor kann auch für einen bestimmten Dienst konfiguriert werden, indem Sie AddServiceOptions verwenden und den Diensttyp angeben.

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

Interceptors werden in der Reihenfolge ausgeführt, in der sie InterceptorCollection hinzugefügt werden. Wenn sowohl globale als auch einzelne Interceptors für einen Dienst konfiguriert sind, werden die global konfigurierten Interceptors vor den für einen einzelnen Dienst konfigurierten Interceptors ausgeführt.

Standardmäßig haben gRPC-Serverinterceptors eine anforderungsbezogene Lebensdauer. Sie können dieses Verhalten überschreiben, indem Sie den Interceptortyp mit Dependency Injection registrieren. Das folgende Beispiel registriert ServerLoggerInterceptor mit einer Singletonlebensdauer:

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

    services.AddSingleton<ServerLoggerInterceptor>();
}

Vergleich von gRPC-Interceptors und Middleware

ASP.NET Core-Middleware bietet ähnliche Funktionen wie Interceptors in C-core-basierten gRPC-Apps. ASP.NET Core-Middleware und Interceptors sind konzeptionell ähnlich. Beide:

  • werden dafür verwendet, eine Pipeline zu konstruieren, die gRPC-Anforderungen verarbeitet.
  • ermöglichen das Ausführen von Arbeiten vor oder nach der nächsten Komponente in der Pipeline.
  • ermöglichen den Zugriff auf HttpContext:
    • In Middleware ist HttpContext ein Parameter.
    • Bei Interceptors kann auf HttpContext über den Parameter ServerCallContext mit der Erweiterungsmethode ServerCallContext.GetHttpContext zugegriffen werden. Dieses Feature ist spezifisch für Interceptors, die in ASP.NET Core ausgeführt werden.

Unterschiede zwischen gRPC-Interceptors und ASP.NET Core-Middleware:

  • Interceptors:
    • arbeiten auf der gRPC-Abstraktionsebene mit ServerCallContext.
    • stellen Zugriff bereit auf:
      • die deserialisierte, beim Aufruf gesendete Nachricht.
      • die beim Aufruf zurückgegebene Nachricht, bevor sie serialisiert wird.
    • können von gRPC-Diensten ausgelöste Ausnahmen abfangen und verarbeiten.
  • Middleware:
    • wird für alle HTTP-Anforderungen ausgeführt.
    • wird vor gRPC-Interceptors ausgeführt.
    • arbeitet mit den zugrundeliegenden HTTP/2-Nachrichten.
    • kann nur auf Bytes aus den Anforderungs- und Antwortdatenströmen zugreifen.

Zusätzliche Ressourcen