WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

Web API に XML データを送信

by WebSurfer 2020年8月9日 15:08

ASP.NET Web API 2 に XML 形式のデータを送信し、サーバー側のアクションメソッドで受け取る方法を書きます。(注: .NET Framework 版の話です。Core 版は未検証・未確認)

UTF-8 の場合

ASP.NET Web API で XML データを送信するというのは需要がなさそうで、あまり役には立たないかもしれませんが、せっかく考えたことなので整理して備忘録として残しておくことにしました。

(1) UTF-8 の場合

元の話は Teratail のスレッド「ASP.NET Web APIでPOSTされたXMLデータをXML形式で取得する方法」のものです。

サンプル XML データは以下の通りです。

<?xml version="1.0" encoding="utf-8"?>
<sample code="1.0">
  <data>
   <id>100</id>
  </data>
  <value>
    <name>日本語</name>
    <version>1.0.0</version>
  </value>
</sample>

この XML データを ASP.NET Web API に POST 送信し、サーバー側でアクションメソッド Post(SampleData value) の引数 value にモデルバインドするにはどうしたらよいかという話です。SampleData クラスの定義は以下の通り。属性を付与した理由は下に書きます。

namespace WebApi.Models
{
    [System.Xml.Serialization.XmlRoot("sample")]
    public class SampleData
    {
        [System.Xml.Serialization.XmlAttribute("code")]
        public string Code { get; set; }

        [System.Xml.Serialization.XmlElement("data")]
        public Data Data { get; set; }

        [System.Xml.Serialization.XmlElement("value")]
        public Value Value { get; set; }
    }

    public class Data
    {
        [System.Xml.Serialization.XmlElement("id")]
        public string Id { get; set; }
    }

    public class Value
    {
        [System.Xml.Serialization.XmlElement("name")]
        public string Name { get; set; }

        [System.Xml.Serialization.XmlElement("version")]
        public string Version { get; set; }
    }
}

XML データの文字コードが UTF-8 であれば以下の 3 つの設定でこの記事の一番上の画像の通りモデルバインドできます。

  1. 要求ヘッダに Content-Type: application/xml; charset=UTF-8 の設定

    Microsoft のドキュメント How WebAPI does Parameter Binding には content-type の設定だけで済みそうなことが書いてありますが、自分が試した限りでは下の 2, 3 も必要でした。
  2. App_Start/WebApiConfig.cs へ以下の設定の追加

    config.Formatters.XmlFormatter.UseXmlSerializer = true;  
  3. モデルのクラス、プロパティに XmlRoot, XmlAttribute, XmlElement 属性の付与

    xml コードの要素名とそれをバインドするクラス、プロパティ名を関連付けるために必要です。たとえ同名でも、上の例のように大文字小文字の違いがある場合は、プロパティにXmlElement 属性を付与してその引数に xml の要素名を設定しないとバインドされません。また、xml コードの属性値(この記事の例では code)をバインドする場合は、それに該当するプロパティへの XmlAttribute 属性の付与が必要です。

(2) Shift_JIS の場合

XML データの文字コードが Shift_JIS の場合はどうでしょう? (これも Teratail のスレッド「ASP.NET Web API POSTされた文字エンコードShift_JISのXMLデータをモデルバインドできない」の話です)

上の「(1) UTF-8 の場合」のような方法ではうまくいかないようです(null がバインドされる)。もちろん XML データを encoding="shift_jis" とし、要求ヘッダを Content-Type: application/xml; charset=shift_jis としたのですがダメでした。

検索するなどして調べてみましたが成果がなかったです。Shift_JIS ではヒットしないので UTF-16 に範囲を広げてググってみましたが、stackoverflow のスレッド Web API not able to bind model for POST with utf-16 encoded XML など、できなかったという記事しか見つかりませんでした。

そこを何とかするには、Microsoft のドキュメント How WebAPI does Parameter Binding の最初の方に書いてあるように、HttpRequestMessage クラスを引数に持つメソッドを使う他には手がなさそうな感じです。

HttpRequestMessage.Content プロパティで HttpContent オブジェクトを取得し、HttpContent.ReadAsStringAsync メソッドで POST されてきた XML 文字列を読んで、XDocument.Load メソッドで XDocument オブジェクトを作ってそれを操作するようにしてみました。

なお、この方法を取った場合、上の「UTF-8 の場合」で行った「2. App_Start/WebApiConfig.cs へ以下の設定の追加」と「3. モデルのクラス、プロパティに XmlRoot, XmlAttribute, XmlElement 属性の付与」は必要ありません。

検証は以下のようにしました。

まず Fiddler の Composer を使って、要求ヘッダに Content-Type: application/xml; charset=shift_jis を付与し、Request Body に XML データを設定して送信します。

Fiddler の Composer で送信

Fiddler の Inspectors で HexView を選択し、Request Body に設定して送信した XML データの 16 進数表現をチェックします。XML データの中の <name>日本語</name> の「日本語」の文字コードは 93 FA 96 7B 8C EA と Shift_JIS になっているのが分かります。

要求ヘッダとボディの 16 進数データ

Visual Studio のデバッガでアクションメソッドの Local 変数を確認します。「日本語」は文字化けせず期待通り取得できています。

アクションメソッドの Local 変数

ちょっと予想外だったのは、Fiddler の Composer の Request Body に書いた「日本語」の文字コードが Shift_JIS になることと、HttpContent.ReadAsStringAsync メソッドがそれを正しく「日本語」と読んでくれることです。そんなに都合よくできているというのが信じ難く、何か見落としがあるかも。(汗)

Tags: , , ,

Web API

Web API と要求ヘッダの application/xml

by WebSurfer 2020年6月7日 14:12

.NET Framework 版の ASP.NET Web API を要求する際、要求ヘッダの Accept に application/xml が含まれると応答は JSON ではなく XML になります。

ASP.NET Web API の応答

知ってました? 実は自分は知らなかったです。(汗) ちなみに、MVC の Json メソッド、および Core 3.1 Web API と MVC の Json メソッドは、要求ヘッダの Accept に application/xml が含まれていても JSON が返ってきます。

Web API の応答をチェックするには Postman などのツールを使うのが一般的なようです。でも、GET 要求ならブラウザのアドレスバーに直接 URL を入力して応答を見ることでも十分だったので自分は IE11 を使ってそうしてました。それで期待した通り JSON 文字列が返ってきてました。

ところが、Chrome, Edge, Firefox では応答が JSON ではなく XML になってしまいます。理由は要求ヘッダの Accept に application/xml が含まれるからのようです。ハマったのは上の画像のようなサーバーエラーとなってしまったことです(HTTP 500 応答が返ってきます。上の画像は形式は XML ですが中身はエラーメッセージです)。

エラーメッセージ "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'." によるとシリアライズに失敗したということです。

IE11 や jQuery ajax を使った検証ではシリアライズには問題はないことは確認しており、ブラウザに Chrome などを使ったからと言ってそれがシリアライズの部分に影響があるとは考えられなかったです。何故なのでしょう?

循環参照の問題でした。

上の画像の InnerException のエラーメッセージにある Blog_E36679EB... D5AD0 という文字列が、先の記事「JSON シリアライズの際の循環参照エラー」にもあったのを思い出して気が付きました。

.NET Framework 版の ASP.NET Web API の JSON シリアライザは Newtonsoft の Json.NET のもので、JsonIgnoreAttribute クラスという属性が利用できます。なので、問題のプロパティに [JsonIgnore] を付与してシリアライズの際の循環参照エラーは回避していました。

でも [JsonIgnore] が有効なのは JSON にシリアライズするときのみで、XML にシリアライズするときは循環参照の問題は回避できません。

Chrome, Edge などを使った時は、ASP.NET Web API は要求ヘッダの Accept に application/xml が含まれるのを見て、XML にシリアライズしようとして循環参照の問題に陥ったということのようです。

Tags: , , ,

Web API

ASP.NET Core 3.1 Web API の .NET 版との違い

by WebSurfer 2020年5月15日 12:45

ASP.NET Web API の Core 3.1 版のアプリが .NET Framework 版のアプリと異なる点を備忘録としてまとめておきます。あくまで自分的に大きな違いがあると思っている点のみです。

  1. Controller は ControllerBase クラスを継承する。(.NET Framework の ApiController ではなく)
  2. Controller に ApiControllerAttribute 属性を付与する。
  3. ルーティングは Controller に RouteAttibute 属性を付与して設定する。(.NET Framework 版プロジェクトで生成される "api/{controller}/{id}" というようなルーティング設定はないようです)
  4. アクションメソッドに [HttpGet], [HttpGet("{id}")], [HttpPost] 等の属性を付与する。(付与しないと HTTP 動詞でどれを呼ぶかの判別ができないようです)
  5. アクションメソッドの引数、例えば [FromBody] string name の name に文字列をバインドする場合、.NET Framework 版では「=文字列」という文字列をコンテンツとして送信すれば可能でしたが、Core 版ではそれができない。
  6. .NET のクラスのプロパティ名の最初の文字は大文字にするのが普通ですが、大文字にしておいてもシリアライズされた結果の応答の JSON 文字列のキー名の最初の文字が小文字になる。

詳しくは Microsoft のドキュメント「ASP.NET Core を使って Web API を作成する」に書いてありますのでそちらを見てください。

コード例を示すと以下のようになります。先の記事「ASP.NET Web API のバインディング」に .NET Framework 版のコードがありますので、それと比較してみてください。

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using WebAPI.Models;

namespace WebAPI.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
          };

        // GET api/values (Read...すべてのレコードを取得)
        [HttpGet]
        public List<Hero> Get()
        {
            return heroes;
        }

        // GET api/values/5 (Read...id 指定のレコード取得)
        [HttpGet("{id}")]
        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        // POST api/values  (Create...レコード追加)
        [HttpPost]
        public List<Hero> Post(Hero postedHero)
        {
            heroes.Add(postedHero);
            return heroes;
        }

        // PUT api/values/5 (Update...id 指定のレコード更新)
        [HttpPut("{id}")]
        public List<Hero> Put(int id, Hero postedHero)
        {
            heroes[id - 1].Name = postedHero.Name;
            return heroes;
        }

        // DELETE api/values/5 (Delete...id 指定のレコード削除)
        [HttpDelete("{id}")]
        public List<Hero> Delete(int id)
        {
            heroes.RemoveAt(id - 1);
            return heroes;
        }
    }
}

上に列挙した 5 番目の項目に書いた件を説明します。上のコード例の Put の場合を見てください。.NET Framework 版の記事ではアクションメソッドの引数を Put(int id, [FromBody] string name) とし、それを呼び出す jQuery ajax のコードでは data: encodeURI("=ガッチャマン") として無理やり(?)引数 name にバインドできていました。Core 版ではそれはできないようで、以下のコードのようにしないとダメでした。

// .NET Framework ベースとは異なり、以下のようにしないとダメ
function apiHeroesPut5() {
    var j = { Id: 5, Name: "ガッチャマン" };
    var jsonString = JSON.stringify(j);
    $.ajax({
        type: "PUT",
        url: "/values/5",
        data: jsonString,
        contentType: "application/json; charset=utf-8",
        success: function (data) {
            $('#heroes').empty();
            $.each(data, function (key, val) {
                var str = val.id + ': ' + val.name;
                $('<li/>', { html: str }).appendTo($('#heroes'));
            });
        },
        error: function (jqXHR, textStatus, errorThrown) {
            $('#heroes').empty();
            $('#heroes').text('textStatus: ' + textStatus +
                ', errorThrown: ' + errorThrown);
        }
    });
}

理由は不明です。バインディング ソース パラメーター推論を読むと分かるかも。手抜きですみません。

最後に、上に列挙した 6 番目の項目に書いた、シリアライズされた結果の応答の JSON 文字列のキー名の最初の文字が小文字になる件を説明します。

上の 1 番目のコード例を見てください。Hero クラスのプロパティ名 Id, Name の最初の文字は大文字にしています。しかし、Web API が Hero オブジェクトを JSON 文字列にシリアライズすると何故かキー名は id, name というように小文字になります。

これは Camel Casing と言うそうです。Core 3.0 以降の MVC でも同様で、MVC や Web API のフレームワークでデフォルトでそのようにする設定がされているようです(コンソールアプリで試したときは Camel Casing にはなりませんでしたので、JsonSerializer クラス自体の設定ではなさそうです)。Camel Casing になるのを回避する方法はあります。詳しくは別の記事「JsonSerializer の Camel Casing」を見てください。

2 番目のコード例の success: function (data) の data には応答の JSON 文字列をデシリアライズした JavaScript オブジェクトが渡されますが、キー名から値を取得する際、最初の文字を小文字にしないと値を取得できず undefined になってしまいます。(なので上のコードでは val.id, val.name としています)

何を隠そう自分はここにハマって 1 ~ 2 時間悩みました。(笑)

Tags: ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar