.NET 上の gRPC インターセプター

作成者: Ernest Nguyen

インターセプターとは、アプリが受信または送信の gRPC 呼び出しと対話できるようにする gRPC の概念です。 要求処理パイプラインを強化する方法が用意されています。

インターセプターはチャネルやサービスに対して構成され、各 gRPC 呼び出しに対して自動的に実行されます。 インターセプターはユーザーのアプリケーション ロジックに対して透過的なので、ログ、監視、認証、検証などの一般的なケースに最適なソリューションです。

Interceptor

Interceptor 型から継承したクラスを作成することで、gRPC サーバーとクライアントの両方にインターセプターを実装できます。

public class ExampleInterceptor : Interceptor
{
}

既定では、Interceptor 基底クラスは何も行いません。 インターセプターの実装で適切な基底クラスのメソッドをオーバーライドすることで、インターセプターに動作を追加します。

クライアント インターセプター

gRPC クライアント インターセプターは、送信の RPC 呼び出しを取得します。 これで、送信された要求、受信応答、クライアント側の呼び出しのコンテキストにアクセスできるようになります。

クライアントに対してオーバーライドする Interceptor メソッド:

  • BlockingUnaryCall: 単項 RPC のブロッキング呼び出しを取得します。
  • AsyncUnaryCall: 単項 RPC による非同期呼び出しを取得します。
  • AsyncClientStreamingCall: クライアントストリーミング RPC の非同期呼び出しを取得します。
  • AsyncServerStreamingCall: サーバーストリーミング RPC の非同期呼び出しを取得します。
  • AsyncDuplexStreamingCall: 双方向ストリーミング RPC の非同期呼び出しを取得します。

警告

BlockingUnaryCallAsyncUnaryCall はともに単項 RPC を指しますが、互換性はありません。 ブロッキング呼び出しは AsyncUnaryCall によって取得されません。また、非同期呼び出しは BlockingUnaryCall によって取得されません。

クライアント gRPC インターセプターを作成する

単項呼び出しの非同期呼び出しを取得する基本的なコード例を次に示します。

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

AsyncUnaryCall のオーバーライド:

  • 非同期単項呼び出しを取得します。
  • 呼び出しに関する詳細をログに記録します。
  • メソッドに渡された continuation パラメーターを呼び出します。 これにより、チェーン内の次のインターセプター、またはこれが最後のインターセプターである場合は基の呼び出しの呼び出し元が呼び出されます。

Interceptor に対するメソッドは、サービス メソッドの種類によってシグネチャが異なります。 ただし、continuationcontext のパラメーターの概念は同じままです。

  • continuation はデリゲートです。チェーン内の次のインターセプター、または (チェーン内にインターセプターが残っていない場合は) 基の呼び出しの呼び出し元を呼び出すものです。 これを 0 回または複数回呼び出すことはエラーではありません。 インターセプターは continuation のデリゲートから返された呼び出し表現 (単項 RPC の場合は AsyncUnaryCall) を返すことを必要はありません。 デリゲート呼び出しを省略し、呼び出し表現の独自のインスタンスを返すと、インターセプターのチェーンが中断され、関連付けられた応答がすぐに返されます。
  • context は、クライアント側の呼び出しに関連付けられたスコープ値を伝達します。 context は、セキュリティ プリンシパル、資格情報、トレース データなどのメタデータを渡すために使います。 さらに、context は期限とキャンセルに関する情報を伝達します。 詳細については、「期限とキャンセルを使用した信頼性の高い gRPC サービス」を参照してください。

クライアント インターセプターで応答を待機する

インターセプター側で単項およびクライアント ストリーミング呼び出しを待機するには、AsyncUnaryCall<TResponse>.ResponseAsync または 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);
        }
    }
}

上記のコードでは次の操作が行われます。

  • AsyncUnaryCall をオーバーライドする新しいインターセプターを作成します。
  • AsyncUnaryCall のオーバーライド:
    • continuation パラメーターを呼び出して、インターセプター チェーンの次の項目を呼び出します。
    • 継続の結果に基づいて、新しい AsyncUnaryCall<TResponse> インスタンスを作成します。
    • HandleResponse メソッドを使って ResponseAsync タスクをラップします。
    • HandleResponse を使って応答を待機します。 応答を待つと、クライアントが応答を受信した後にロジックを追加できます。 try-catch ブロックで応答を待つことにより、呼び出しからのエラーをログに記録できます。

クライアント インターセプターの作成方法の詳細については、grpc/grpc-dotnet GitHub リポジトリの ClientLoggerInterceptor.cs の例を参照してください。

クライアント インターセプターを構成する

gRPC クライアント インターセプターはチャネル上に構成されます。

コード例を次に示します。

  • GrpcChannel.ForAddress を使ってチャネルを作成します。
  • Intercept 拡張メソッドを使って、インターセプターを使うようにチャネルを構成します。 このメソッドは CallInvoker を返すことに注意してください。 厳密に型指定された gRPC クライアントは、チャネルと同じように呼び出し元から作成できます。
  • 呼び出し元からクライアントを作成します。 このクライアントが行う gRPC 呼び出しによって、インターセプターが自動実行されます。
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());

var client = new Greeter.GreeterClient(invoker);

Intercept 拡張メソッドをチェーンして、1 つのチャネルに複数のインターセプターを構成することができます。 または、複数のインターセプターを受け入れる Intercept オーバーロードもあります。 次の例で示すように、1 つの gRPC 呼び出しに対して任意の数のインターセプターを実行できます。

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

インターセプターは、チェーンした Intercept 拡張メソッドの逆順で呼び出されます。 先ほどのコードの場合、インターセプターは次の順序で呼び出されます。

  1. ClientLoggerInterceptor
  2. ClientMonitoringInterceptor
  3. ClientTokenInterceptor

gRPC クライアント ファクトリを使ってインターセプターを構成する方法については、「.NET での gRPC クライアント ファクトリの統合」を参照してください。

サーバー インターセプター

gRPC サーバー インターセプターは受信 RPC 要求を取得します。 これで、受信要求、送信応答、サーバー側の呼び出しのコンテキストにアクセスできるようになります。

サーバーに対してオーバーライドする Interceptor メソッド:

  • UnaryServerHandler: 単項 RPC を取得します。
  • ClientStreamingServerHandler: クライアントストリーミング RPC を取得します。
  • ServerStreamingServerHandler: サーバーストリーミング RPC を取得します。
  • DuplexStreamingServerHandler: 双方向ストリーミング RPC を取得します。

サーバー gRPC インターセプターを作成する

受信単項 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;
        }
    }
}

UnaryServerHandler のオーバーライド:

  • 受信単項の呼び出しを取得します。
  • 呼び出しに関する詳細をログに記録します。
  • メソッドに渡された continuation パラメーターを呼び出します。 これにより、チェーン内の次のインターセプター、またはこれが最後のインターセプターである場合はサービス ハンドラーが呼び出されます。
  • すべての例外をログに記録します。 継続を待つと、サービス メソッドの実行後にロジックを追加できます。 try-catch ブロックで継続を待つことにより、メソッドからのエラーをログに記録できます。

クライアント インターセプターとサーバー インターセプターのメソッドのシグネチャは似ています。

  • continuation は、チェーン内の次のインターセプター、またはサービス ハンドラー (チェーン内にインターセプターが残っていない場合) を呼び出す受信 RPC のデリゲートを表します。 クライアント インターセプターと同様に、いつでも呼び出すことができます。また、継続デリゲートから応答を直接返す必要はありません。 送信ロジックは、継続を待ってからのサービス ハンドラーの実行後に追加できます。
  • context は、要求のメタデータ、デッドライン、キャンセル、RPC の結果など、サーバー側の呼び出しに関連付けられたメタデータを伝達します。

サーバー インターセプターの作成方法の詳細については、grpc/grpc-dotnet GitHub リポジトリの ServerLoggerInterceptor.cs の例を参照してください。

サーバー インターセプターを構成する

gRPC サーバー インターセプターは起動時に構成されます。 コード例を次に示します。

  • AddGrpc を使ってアプリに gRPC を追加します。
  • サービス オプションの Interceptors コレクションに追加することで、すべてのサービスに対して ServerLoggerInterceptor を構成します。
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });
}

また、AddServiceOptions を使い、サービスの種類を指定することで、特定のサービスに対してインターセプターを構成することもできます。

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

インターセプターは、InterceptorCollection に追加された順番に実行されます。 グローバル インターセプターと単一サービスのインターセプターの両方が構成されている場合、グローバルに構成されたインターセプターが単一サービス用に構成されたものよりも先に実行されます。

gRPC サーバー インターセプターには、既定値として要求ごとの有効期間があります。 この動作をオーバーライドするには、依存関係の挿入を使ってインターセプターの種類を登録します。 次の例では、シングルトンの有効期間に ServerLoggerInterceptor を登録しています。

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

    services.AddSingleton<ServerLoggerInterceptor>();
}

gRPC インターセプターとミドルウェア

ASP.NET Core ミドルウェアは、C-core ベースの gRPC アプリのインターセプターと比較して、同様の機能を提供します。 ASP.NET Core ミドルウェアとインターセプターは概念的に似ています。 両方:

  • gRPC 要求を処理するパイプラインを構築するために使用されます。
  • パイプライン内の次のコンポーネントの前または後で作業が実行されます。
  • HttpContext へのアクセスを提供します。
    • ミドルウェアでは、HttpContext はパラメーターです。
    • インターセプターでは、ServerCallContext.GetHttpContext 拡張メソッドで ServerCallContext パラメーターを使用して、HttpContext にアクセスできます。 この機能は、ASP.NET Core で実行されているインターセプターに固有です。

gRPC インターセプターと ASP.NET Core ミドルウェアの違い:

  • インターセプター:
    • ServerCallContext を使用して、抽象化の gRPC レイヤーで動作します。
    • 次にアクセスできます。
      • 呼び出しに送信された逆シリアル化されたメッセージ。
      • シリアル化される前に、呼び出しから返されたメッセージ。
    • GRPC サービスからスローされた例外をキャッチして処理できます。
  • ミドルウェア:
    • すべての HTTP 要求に対して実行されます。
    • gRPC インターセプターの前に実行されます。
    • 基になる HTTP/2 メッセージを操作します。
    • 要求ストリームと応答ストリームからのバイトのみにアクセスできます。

その他の技術情報