【C#】MagicOnionの例外処理

サーバーで発生した例外はクライアントに RpcException として通知される。

基本の動作

// サーバー側
public class SampleService : ServiceBase<ISample>, ISample
{
    public async UnaryResult<int> SumAsync(int x, int y)
    {
        throw new NotImplementedException("oh!"); // 例外を返す
    }
}

// クライアント側
public async Task Run()
{
    try
    {
        GrpcChannel channel = CreateChannel();
        var client = MagicOnionClient.Create<ISample>(channel);

        // awaitすると結果がそのまま戻り値として取れる
        var result = await client.SumAsync(10, 20); // ★この行でGrpc.Core.RpcException発生
        Console.WriteLine($"Sum={result}");

        // asyncしないとUnaryResultからawaitで結果を取る
        MagicOnion.UnaryResult<int> ret = client.SumAsync(10, 20);
        int result2 = await ret.ResponseAsync; // ★この行でGrpc.Core.RpcException発生
        Console.WriteLine($"Sum={result2}");
    }
    catch (Exception ex) // ★Grpc.Core.RpcExceptionを受け取る
    {
        Console.WriteLine(ex.ToString());
        throw;
    }
}
// 以下のようなメッセージが表示される
// Grpc.Core.RpcException:
//     Status(StatusCode="Unknown", Detail="Exception was thrown by handler.")
//   at MagicOnion.Client.ResponseContext`1.Deserialize()
//   at MagicOnion.UnaryResult`1.UnwrapResponse()
//   at GrpcClient.Sample.Run() in .\\MagicOnionSample\\GrpcClient\\AppMain.cs

Wait()して終了を同期で待つとSystem.AggregateExceptionとなります。

// 呼び出し側でWait()呼び出すとAggregateExceptionが発生 -> 通常のTask動作
var s = new Sample();
xxxx.Run().Wait(); // ★AggregateException

普通の async/await な Task の動きと同じです。

  • 例外は .NET の gRPC 標準の RpcException として扱われる
  • サーバーで適切に処理しないと標準の RpcException としてクライアントに返される
  • メッセージが「Exception was thrown by handler」で実質無意味

サーバー側で例外を処理してクライアントに返す

.NET WebAPI の ExceptionFilterAttribute みたいなことはできない。各メソッドの catch ブロックに処理を記述する。

// ★サーバー側
public async UnaryResult<int> SumAsync(int x, int y)
{
    try
    {
        throw new NotImplementedException("oh!");
    }
    catch (Exception ex) // ★★このブロックを追加
    {
        // InternalはHTTPでは500 Internal Server Error相当
        var statusCode = (int)Grpc.Core.StatusCode.Internal;

        return this.ReturnStatusCode<int>(statusCode, ex.Message); // ★★何か記述する
    }
}

// ★クライアント側
//....
    catch (Exception ex) // ★Grpc.Core.RpcExceptionを受け取る
    {
        Console.WriteLine(ex.ToString());
    }
// 以下のようにコードとメッセージを受け取る
// Grpc.Core.RpcException:
//     Status(StatusCode="Internal", Detail="oh!")
//   at MagicOnion.Client.ResponseContext`1.Deserialize()
//   at MagicOnion.UnaryResult`1.UnwrapResponse()
//   at GrpcClient.Sample.Run() in .\\MagicOnionSample\\GrpcClient\\AppMain.cs
  • statusCode は gRPC のステータスコードを返す
  • 数字 + 文字列で返すとクライアントの RpcException に反映される
  • そのまま使うと WCF に比べて送信できる情報が貧弱な

もし分散システムで多重転送時してて、メッセージでエラーをスタックして転送するような場合、別途仕組みが必要。どうせクライアントも .NET(というかC#) なので共有プロジェクト配布で良さそう。場合によっては DLL 配布でもよさそう。