🚀

WebAPI(.NET)をJSON-RPCで実装してみる

に公開

はじめに

JSON-RPCについて調べる機会があり、.NET WebAPIでもJSON-RPCを使ってみたい!などと思い、EdjCase.JsonRpcライブラリに出会いました。
想像以上にJSON-RPCを使用したWebAPIが簡単に実装できたので、情報整理も兼ねて本記事作成に至ります。

JSON-RPCとは

https://www.jsonrpc.org/

JSON-RPCは、ステートレスで軽量なリモート手順呼び出し(RPC)プロトコルです。主にこの仕様は、いくつかのデータ構造とその処理に関するルールを定義しています。概念が同じプロセス内、ソケット上、http上、またはさまざまなメッセージパス環境内で使用できるという点で、トランスポートに依存しない。JSON(RFC 4627)をデータ形式として使用します。

https://ja.wikipedia.org/wiki/JSON-RPC

以下のサンプルでは、サーバへのリクエストを --> で、サーバからのレスポンスを <-- で示しています。(ただしレスポンスはJSON-RPCのバージョンによっては必須ではありません)

リクエストとレスポンス(v 2.0)

--> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 3}
<-- {"jsonrpc": "2.0", "result": 19, "id": 3}

要するに、ネットワーク越しにJSONで関数(メソッド)を指定して呼び出しを行うプロトコルです。
クライアントがメソッド名とパラメータを送信し、サーバが処理結果を返す仕組みで、言語や環境に依存せず利用できます。RESTのようにリソース指向ではなく、「関数呼び出し」をネットワーク越しに行う点が特徴です。

特徴 メリット
JSON形式のみを使用 実装がシンプルで軽量
言語・環境に依存しない 幅広いシステム間で利用可能
通知やバッチリクエストに対応 柔軟で効率的な通信が可能
明確なエラーハンドリング 安定したAPI設計ができる

環境

開発環境

  • Visual Studio 2022 IDE

https://visualstudio.microsoft.com/ja/vs/

  • .NET 9 SDK

https://dotnet.microsoft.com/ja-jp/download/dotnet/9.0

NuGet

以下のNuGetライブラリ EdjCase.JsonRpcを使用してJSON-RPCを.NETで実装します。

  • EdjCase.JsonRpc.Router (サーバー)

https://www.nuget.org/packages/EdjCase.JsonRpc.Router

  • EdjCase.JsonRpc.Client (クライアント)

https://www.nuget.org/packages/EdjCase.JsonRpc.Client

構築するアプリ

サーバーはWebAPI(ASP.NET Core)、クライアントはBlazor WASMで構築します。
以下は簡単なアーキテクチャ図です。

実装

いざいざEdjCase.JsonRpcを使用して.NETでJSON-RPCを実装します。

以下のようにBlazor WASMプロジェクト(BlazorAppJRPC)を作成し、さらにWebAPIプロジェクト(WebAPI-JRPC)を追加します。

EdjCase.JsonRpcサーバー機能追加

WebAPI側でProgram.csを以下のように編集し、EdjCase.JsonRpcサーバー機能を追加します。

Program.cs
using EdjCase.JsonRpc.Router;
using EdjCase.JsonRpc.Router.Defaults;
using System.Text;
Console.OutputEncoding = Encoding.UTF8;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
//builder にUseUrlsに受信URLをセットする
builder.WebHost.UseUrls("http://localhost:5001;");

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

// EdjCase.JsonRpc を用いた JSON-RPC サーバー機能を追加
builder.Services.AddJsonRpc(config =>
{
    // バッチリクエストの最大数を設定(クライアントが一度に送信できるリクエスト数の上限)
    config.BatchRequestLimit = 5;

    // サーバー内部の例外をクライアントに返すかどうか
    // true にするとスタックトレースを含む詳細が返される(開発時向け)
    config.ShowServerExceptions = true;

    // JSON シリアライザーのオプション設定
    config.JsonSerializerSettings = new System.Text.Json.JsonSerializerOptions
    {
        WriteIndented = true // JSON 出力をインデント付きで整形する(可読性向上)
    };

    // RPC メソッド呼び出し時に例外が発生した場合の処理を定義
    config.OnInvokeException = (context) =>
    {
        // 特定の例外型 (InvalidOperationException) をキャッチしてカスタムレスポンスを返す
        if (context.Exception is InvalidOperationException)
        {
            int customErrorCode = 1; // 独自のエラーコード
            var customData = new
            {
                Field = "Value" // 追加データをエラーレスポンスに含める
            };
            // JSON-RPC 規約に基づいたエラーレスポンスを構築
            RpcMethodErrorResult response = new(customErrorCode, "Custom message", customData);
            // 例外を処理済みとしてレスポンスを返す
            return OnExceptionResult.UseObjectResponse(response);
        }

        // 上記以外の例外は処理せずスローし続ける
        return OnExceptionResult.DontHandle();
    };
})
// JSON-RPC に認証と認可機能を追加
.AddAuthorization().AddAuthentication(); 

WebApplication app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    _ = app.MapOpenApi();
}

// app.UseHttpsRedirection();

// RpcController を継承したすべてのクラスを自動的に登録し、
// それらの公開インスタンスメソッドを JSON-RPC のエンドポイントとして利用可能にする
app.UseJsonRpc();

app.UseAuthorization();
app.MapControllers();
app.Run();

※証明書の準備が面倒だったので、本記事ではhttpで通信で事足りるように調整しています。ですので、実際のProgram.csとは差があるかもしれませんが、悪しからず。

単一呼び出し

EdjCase.JsonRpcを使用したJSON-RPCを実装します。まずは、シンプルにWebAPIの1メソッドを呼び出すJSON-RPCを実装します。

サーバー

RpcControllerを継承したコントローラークラスを準備、追加します。RpcControllerを継承することで、publicメソッドがEdjCase.JsonRpcで公開されます。
コントローラークラスはRpcRoute属性を使用してルート情報を設定します。

JRPCController.cs
using EdjCase.JsonRpc.Router;

namespace WebAPI_JRPC.Controllers;
// EdjCase.JsonRpc.Routerを使用してRpcControllerを継承したコントローラーを作成する。
[RpcRoute("/api/jrpc")]
public class JRPCController : RpcController
{
    public async Task<string> HelloWorldAsync()
    {
        return "Hello World!";
    }
}

クライアント

TestPage1.razor,TestPage1.razor.csを追加し、NavManue.razorから画面遷移ができるようになっている前提とします。
以下のようにリクエスト用のボタン「SendBatchRequest」をTestPage1に追加します。

TestPage1.razor
@page "/testpage1"

<h3>TestPage1</h3>
<div>
    <button class="btn btn-primary" @onclick="OnClickSingleReq">SendSingleRequest</button>
</div>

ボタン押下時に呼び出されるリクエスト処理メソッドOnClickSingleReqを以下のように追加します。
JRPCController.HelloWorldAsyncメソッドをrpcClient.SendAsyncメソッドの第一引数で指定してJSON-RPC呼び出しを行います。

TeatPage1.razor.cs
using EdjCase.JsonRpc.Client;
using EdjCase.JsonRpc.Common;
using Microsoft.AspNetCore.Components;
using Sotsera.Blazor.Toaster;
namespace BlazorAppJRPC.Pages;

public partial class TestPage1
{
    [Inject]
    private IToaster _toaster { get; set; } = default!;

    // SendSingleRequestボタンから実行されるメソッド
    private async Task OnClickSingleReq()
    {
        // 送信先URLを指定してRpcClientをインスタンス化
        string baseUrlStr = "http://localhost:5001";
        RpcClient rpcClient = RpcClient
            .Builder(new Uri(baseUrlStr))
            .Build();
        // EdjCase.JsonRpc.ClientでJSON-RPC呼び出し
        RpcResponse<string> response = await rpcClient.SendAsync<string>("HelloWorldAsync", "/api/jrpc");
        // レスポンスの受け取り
        string res = response.HasError ? $"Error: {response.Error?.Message}" : response.Result;
        // トーストメッセージ表示
        _toaster.Success($"res:[{res}]");
    }
}

動作確認

JSON-RPCでの単一呼び出しの動作確認を行います。


左側がWebAPI、右側がクライアントです。
gif

無事、WebAPIのメソッドがEdjCase.JsonRpcにより実行されていることが確認できました。

バッチ呼び出し

JSON-RPCでは複数の関数(メソッド)を一度に指定して、RPC実行させることができます。以降にバッチ呼び出しの例を記載します。

サーバー

以下のようにJRPCControllerに適当なメソッドを追加します。
汎用性を意識して(?)、partialクラスで実装とします。
前項で追加したJRPCController.csはpublic partial class JRPCController : RpcControllerとし、さらにpartialクラスで以下のJRPCControllerPatial1.csファイルを追加します。

JRPCControllerPatial1.cs
using EdjCase.JsonRpc.Router;
namespace WebAPI_JRPC.Controllers;
// EdjCase.JsonRpc.Routerを使用してRpcControllerを継承したコントローラーを作成する。
public partial class JRPCController : RpcController
{
    public async Task<int> CalcAddAsync(int a, int b)
    {
        return a + b;
    }
    public async Task<int> CalcMultiplyAsync(int a, int b)
    {
        return a * b;
    }
    public async Task<string> EchoAsync(string msg)
    {
        return $"echo : [{msg}]";
    }
}

クライアント

以下のようにバッチリクエスト用のボタン「SendBatchRequest」をTestPage1に追加します。

TestPage1.razor
@page "/testpage1"

<h3>TestPage1</h3>
<div>
    <button class="btn btn-primary" @onclick="OnClickSingleReq">SendSingleRequest</button>
</div>
<br>
<div>
    <button class="btn btn-primary" @onclick="OnClickBatchReq">SendBatchRequest</button>
</div>

ボタン押下時に呼び出されるバッチリクエスト処理メソッドOnClickBatchReqを以下のように追加します。
以下コメントの// RpcBulkRequestの準備にて、JSON-RPCで実行したいJRPCControllerのメソッド名を指定しています。
EdjCase.jsonrpcでは、RpcBulkRequestをSendAsyncメソッドに引数として実行します。実行結果はRpcBulkResponseで受け取ります。取り出しは、RpcIdをキーとして取り出します。

TestPage1.razor.cs
//~省略
    // EdjCase.JsonRpc でバッチ呼び出し
    private async Task OnClickBatchReq()
    {
        // 送信先URLを指定してRpcClientをインスタンス化
        string baseUrlStr = "http://localhost:5001";
        RpcClient rpcClient = RpcClient
            .Builder(new Uri(baseUrlStr))
            .Build();

        // RpcBulkRequestの準備
        List<(RpcRequest request, Type type)> typedRequests =
        [
            (RpcRequest.WithParameterList("HelloWorldAsync", Array.Empty<object>(), new RpcId(1L)), typeof(string)),
            (RpcRequest.WithParameterList("CalcAddAsync", new object[] { 1, 2 }, new RpcId(2L)), typeof(int)),
            (RpcRequest.WithParameterList("CalcMultiplyAsync", new object[] { 3, 4 }, new RpcId(3L)), typeof(int)),
            (RpcRequest.WithParameterList("EchoAsync", new object[] { "こんにちは" }, new RpcId(4L)), typeof(string))
        ];
        RpcBulkRequest bulkRequest = new(typedRequests);

        // RpcBulkRequestをSendAsyncでJSON-RPC実行
        RpcBulkResponse bulkResponse = await rpcClient.SendAsync(bulkRequest, "/api/jrpc");

        // RpcBulkResponseからRpcIdをもとに結果を取り出し
        RpcResponse<string> helloResp = bulkResponse.GetResponse<string>(new RpcId(1L));
        RpcResponse<int> addResp = bulkResponse.GetResponse<int>(new RpcId(2L));
        RpcResponse<int> mulResp = bulkResponse.GetResponse<int>(new RpcId(3L));
        RpcResponse<string> echoResp = bulkResponse.GetResponse<string>(new RpcId(4L));

        // トーストメッセージで結果を表示
        string hello = helloResp.HasError ? $"Error: {helloResp.Error?.Message}" : helloResp.Result;
        string add = addResp.HasError ? $"Error: {addResp.Error?.Message}" : addResp.Result.ToString();
        string mul = mulResp.HasError ? $"Error: {mulResp.Error?.Message}" : mulResp.Result.ToString();
        string echo = echoResp.HasError ? $"Error: {echoResp.Error?.Message}" : echoResp.Result;
        _toaster.Success($"res HelloWorld:[{hello}]");
        _toaster.Success($"res CalcAdd:[{add}]");
        _toaster.Success($"res CalcMultiply:[{mul}]");
        _toaster.Success($"res Echo:[{echo}]");

    }
//~省略

動作確認

JSON-RPCでのバッチ呼び出しの動作確認を行います。


左側がWebAPI、右側がクライアントです。
gif

無事、WebAPIのメソッドがEdjCase.JsonRpcにより複数実行されていることが確認できました。

まとめ

というわけで、EdjCase.JsonRpcを使用して、JSON-RPCを.NET WebAPIで実装しました。

意外と簡単に実装でき拡張もしやすそう、と感じました。
特にWebAPI側はProgram.csにEdjCase.JsonRpc機能追加後、RpcControllerを継承していればpublicメソッドは公開されるので、シンプルに実装が進められそうな印象です。

本記事が少しでも何かのお役に立てれば幸いです。

参考

  • GitHub EdjCase.jsonrpc

https://github.com/edjCase/JsonRpc

  • Qiita json-rpcのAPIをASP.NET Coreで作る

https://qiita.com/chickenramens/items/822d02d5121ab46c361d

  • Qiita JSON-RPCって何?

https://qiita.com/oohira/items/35e6eaaf4b44613ad7d3

  • Zenn JSON-RPC サクッと入門

https://zenn.dev/hachimada/articles/jsonrpc-basic

  • ときどきWEB REST APIとRPCによるアーキテクチャの違いをまとめてやんよ!!!

https://tokidoki-web.com/2021/03/rest-apirpcweb/

Discussion