WebAPI(.NET)をJSON-RPCで実装してみる
はじめに
JSON-RPCについて調べる機会があり、.NET WebAPIでもJSON-RPCを使ってみたい!などと思い、EdjCase.JsonRpcライブラリに出会いました。
想像以上にJSON-RPCを使用したWebAPIが簡単に実装できたので、情報整理も兼ねて本記事作成に至ります。
JSON-RPCとは
JSON-RPCは、ステートレスで軽量なリモート手順呼び出し(RPC)プロトコルです。主にこの仕様は、いくつかのデータ構造とその処理に関するルールを定義しています。概念が同じプロセス内、ソケット上、http上、またはさまざまなメッセージパス環境内で使用できるという点で、トランスポートに依存しない。JSON(RFC 4627)をデータ形式として使用します。
以下のサンプルでは、サーバへのリクエストを --> で、サーバからのレスポンスを <-- で示しています。(ただしレスポンスは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
- .NET 9 SDK
NuGet
以下のNuGetライブラリ EdjCase.JsonRpcを使用してJSON-RPCを.NETで実装します。
- EdjCase.JsonRpc.Router (サーバー)
- 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サーバー機能を追加します。
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属性を使用してルート情報を設定します。
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に追加します。
@page "/testpage1"
<h3>TestPage1</h3>
<div>
<button class="btn btn-primary" @onclick="OnClickSingleReq">SendSingleRequest</button>
</div>
ボタン押下時に呼び出されるリクエスト処理メソッドOnClickSingleReqを以下のように追加します。
JRPCController.HelloWorldAsyncメソッドをrpcClient.SendAsyncメソッドの第一引数で指定してJSON-RPC呼び出しを行います。
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ファイルを追加します。
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に追加します。
@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をキーとして取り出します。
//~省略
// 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
- Qiita json-rpcのAPIをASP.NET Coreで作る
- Qiita JSON-RPCって何?
- Zenn JSON-RPC サクッと入門
- ときどきWEB REST APIとRPCによるアーキテクチャの違いをまとめてやんよ!!!
Discussion