Skip to content

MS_JSONRPCService

nishi_74322014 edited this page Sep 1, 2026 · 1 revision

JSONを受信するJSON-RPCサービスを作成する方法

概要

WCF や ASP.NET Web API で、指定の JSON を受ける(受信する)方法を説明する。

補足(ページ名の「JSON-RPC」は一般名詞としての用法): 標準仕様の
JSON-RPC 2.0({"jsonrpc":"2.0","method":"...","params":...} という
形式を定めたもの)とは異なり、本ページが扱っているのは
**「JSON を POST で受け取るサービス」**という広い意味である。

【JSON-RPC 2.0(仕様)】
  POST /endpoint
  {"jsonrpc":"2.0","method":"getUser","params":{"id":1},"id":1}
     ↑ 単一のエンドポイントに「メソッド名」を渡す

【本ページの例】
  POST /JSONService.svc/PostAnswersKeyValue?userName=...
  [{"key":"k1","value":"v1"}, ...]
     ↑ URL がメソッドを表し、Body に JSON を載せる(REST 寄り)

対になるJSONを送信するRESTサービスを作成する方法が
「返す側」、本ページが**「受ける側」**という対応になっている。

JSONを受信するサービスを作成する

WCF の場合

以下のように WCF を定義する。

WebAPIと同様、WCF でもイイ感じに Binding してくれる。

  • KeyValRequest が POST された JSON で、
  • ソレ以外のパラメタは QueryString
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class JSONService : IJSONService
{
  public StringResponse PostAnswersKeyValue(
    KeyValRequest[] akv,
    string userName, string enterpriseID, string storeID, string deviceID,
    string screenID, string initializeScreenInfoID, string time)
  {
    ・・・

    // 基本的に正常系の戻り値を返す。
    return new StringResponse()
    {
      IsError = isError,
      Message = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff")
    };
  }
}

StringResponse で JSON も返せる。

補足(この引数の割り当てが「イイ感じ」の中身): WCF が
どの引数をどこから取るかを自動で決めている点が要点である。

POST /JSONService.svc/PostAnswersKeyValue?userName=u1&storeID=s1&...
Content-Type: application/json

[{"key":"k1","value":"v1"},{"key":"k2","value":"v2"}]
  ↑ この配列が akv にバインドされる

・複合型(配列・クラス)が 1 つ  → Body から
・単純型(string, int 等)       → UriTemplate / QueryString から

規則は単純で、

引数の型 どこから取るか
複合型(クラス、配列) Body(1 つだけ許される)
単純型(string、int、DateTime 等) URL(UriTemplate かクエリ文字列)

複合型を 2 つ以上引数に取れないという制約があるため、
「JSON を 2 種類受け取りたい」場合は
1 つのラッパー クラスにまとめる必要がある。

なお、AspNetCompatibilityRequirementsMode.Allowed が
指定されている理由は
JSONを送信するRESTサービスを作成する方法の
「注意点」で述べたとおりである
(Allowed は互換モードの有無にかかわらず動く)。

補足(受信側では「常に正常系を返す」設計に注意): コード中の
**「基本的に正常系の戻り値を返す。」**というコメントは、
意図を理解しておく必要がある。

この例はWeb Storage に溜めたデータを送信する処理であり
(後掲の JavaScript を参照)、
**「送信が成功したら Web Storage をクリアする」**という
動作になっている。

【HTTP 500 を返す設計】
  サーバ側で業務エラー → 500 を返す
     ↓ クライアントは error コールバックへ
  Web Storage をクリアしない → 次回も同じデータを送る → 永久に失敗し続ける

【本文の設計(200 + IsError フラグ)】
  サーバ側で業務エラー → 200 で IsError=true を返す
     ↓ クライアントは success コールバックへ
  「受け取った」ことは確定 → クリアするか否かを業務判断で決められる

つまり、「通信の失敗」と「業務の失敗」を分けている。
これ自体は妥当な設計だが、注意点がある。

注意 内容
監視で異常が見えない 常に 200 なので、エラー率の監視に引っかからない(JmeterによるWebアプリの負荷テストの「HTTP 200 で返ってくるエラー画面」と同じ問題)
REST の作法から外れる HTTP のステータス コードを使わない
クライアントの実装漏れ IsError を見忘れると失敗に気付かない

現在の設計指針としては、

  • 通信・入力の誤り(4xx)、サーバの障害(5xx)は HTTP で表す、
  • 業務上の結果(在庫切れ、承認却下)は 200 + 本文で表す

と切り分け、エラー本文は
**RFC 9457(Problem Details for HTTP APIs)**の形式に揃えるのが
標準的である。

呼び出し側の実装(jQuery)

function PostJsonFromWebStorage(url) {
    // Web Storageのすべての情報の取得
    var jsonArray = new Array();

    for (var i = 0; i < storage.length; i++) {
        var _key = storage.key(i);

        // Web Storageのキーと値を表示
        var jsonBean = {
            key: _key,
            value: storage.getItem(_key)
        };

        jsonArray.push(jsonBean);
    }

    // <p id="url"></p> に表示
    if (document.getElementById("url") != null) {
        $("#url").text(url);
    }
    // <p id="request"></p> に表示
    if (document.getElementById("request") != null) {
        $("#request").text("request:" + JSON.stringify(jsonArray).toString());
    }

    CallService("POST", url, JSON.stringify(jsonArray), "application/json; charset=utf-8", "JSON", false);
}

// ---------------------------------------------------------------
// ajax
// ---------------------------------------------------------------
// 引数
//         Type : GET or POST or PUT or DELETE verb
//         Url : Location of the service
//         Data : Data sent to server
//         ContentType : Content type sent to server
//         DataType : Expected data format from server
//         ProcessData : True or False
// 戻り値  -
// ---------------------------------------------------------------
function CallService(Type, Url, Data, ContentType, DataType, ProcessData) {
    $.ajax({
        type: Type,
        url: Url,
        data: Data,
        cache: false,
        contentType: ContentType,
        dataType: DataType,
        processdata: ProcessData,
        success: function (data) {
            // On Successfull service call
            ServiceSucceeded(data);
        },
        error: function (data) {
            // When Service call fails
            ServiceFailed(data);
        }
    });
}

// ---------------------------------------------------------------
// $.ajaxのコールバック(success)
// ---------------------------------------------------------------
// 引数    data
// 戻り値  -
// ---------------------------------------------------------------
function ServiceSucceeded(data) {
    // Success
    if (document.getElementById("response") != null) {
        // <p id="response">response</p> に結果を表示
        $("#response").text("response-success:" + JSON.stringify(data).toString());
    }
    ClearWebStorage(); // 送信成功のため、WebStorageをクリア
}

// ---------------------------------------------------------------
// $.ajaxのコールバック(error)
// ---------------------------------------------------------------
// 引数    data
// 戻り値  -
// ---------------------------------------------------------------
function ServiceFailed(data) {
    // Error
    if (document.getElementById("response") != null) {
        // <p id="response">response</p> に結果を表示
        $("#response").text("response-error:" + JSON.stringify(data).toString());
    }
    // 送信失敗のため、WebStorageをクリアしない。
}

補足($.ajax のオプションで押さえるべき 3 点): このコードには
JSON を POST する際の必須設定が含まれている。

オプション 値 なぜ必要か
contentType application/json 既定は application/x-www-form-urlencoded。これを指定しないとサーバ側で JSON として解釈されない
data JSON.stringify(...) オブジェクトのまま渡すと jQuery がフォーム形式に変換してしまう
dataType "JSON" 応答を JSON として解釈する

contentType の指定漏れは非常に多い誤りで、
「サーバ側で引数が null になる」という症状で現れる。

なお、processdata(小文字)は
正しくは processData(大文字の D)である。
jQuery のオプションは大文字小文字を区別する
ため、
この記述では既定値(true)のままになっている。
ただし、data が既に文字列であれば processData は
実質的に影響しないため、動作としては問題にならない。

補足(この「Web Storage に溜めて後で送る」構造): コード全体が
オフライン対応の実装になっている点が興味深い。

① 入力のたびに Web Storage(localStorage)に保存
     ↓ 通信できない状況でも入力を続けられる
② 通信可能なときに、溜まった分をまとめて POST
     ↓
③ 成功したら Web Storage をクリア

これは Store and Forward と呼ばれる古典的な方式で、
現在のモバイル Web でも有効な考え方である。

ただし、現在はより整理された手段がある。

手段 内容
Service Worker + Background Sync ブラウザを閉じても、通信可能になった時点で自動送信
IndexedDB localStorage より大容量・非同期・構造化データ向き
navigator.sendBeacon() 離脱時の送信(IE、WWWブラウザのいろいろ)

また、localStorage に業務データを保存することには
セキュリティ上の注意が要る。

  • XSS があれば JavaScript から全部読める(Cookie の HttpOnly のような保護が無い)、
  • 端末に平文で残る

ため、個人情報や機密情報を置かない、
送信後は確実にクリアするという運用が前提になる
(Webアプリケーション脆弱性対策)。

ASP.NET Web API の場合

移行メモ: 原典ではこの節は見出しのみで、本文が存在しない。

補足(Web API での受信): 未記載であるため、要点を補っておく。
WCF の場合と同じく、属性でバインド元を指定する形になる。

public class AnswersController : ApiController
{
    // POST api/answers?userName=u1&storeID=s1
    public StringResponse Post(
        [FromBody] KeyValRequest[] akv,      // Body の JSON
        [FromUri] string userName,           // クエリ文字列
        [FromUri] string storeID)
    {
        ...
    }
}
属性 バインド元
[FromBody] 要求本文(1 つだけ)
[FromUri] URL(ルート テンプレート/クエリ文字列)
省略時 単純型は URL、複合型は Body(WCF と同じ既定)

[FromBody] を 2 つ指定できないという制約は WCF と同じで、
理由も同じ(要求本文は 1 回しか読めないストリームであるため)。

なお、ASP.NET Core では属性名が変わる
([FromBody]、[FromQuery]、[FromRoute]、[FromForm])。
移行時には**[FromUri] → [FromQuery]** の置き換えが必要になる。


Tags: 移行, .NET開発, 通信技術, .NET Core, ASP.NET, ASP.NET Web API

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONE / TODO

Clone this wiki locally