WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

アクションメソッドと構造不定の JSON (CORE)

by WebSurfer 23. August 2021 18:54

クライアントから ASP.NET Core MVC や Web API のアクションメソッドに送信されてくる JSON 文字列の構造が不定の場合、どのように受け取って処理できるかという話を書きます。(.NET Core 3.x 以降の話です。.NET Framework および .NET Core 2.x 以前は未検証・未確認です)

JSON 文字列から指定した name の value を取得

(注: 以下は MVC のアクションメソッドを例に取って書いていますが、Web API のアクションメソッドでもモデルバインディングの関係は全く同じです)

クライアントから送信されてくる JSON 文字列の構造が常に同じなら、先の記事「JSON 文字列から C# のクラス定義生成」に書いたような手段で C# のクラス定義を生成し、それをアクションメソッドの引数に設定してやれば、フレームワーク組み込みのモデルバインダが JSON 文字列を C# のオブジェクトにデシリアライズしてバインドしてくれます。

しかし、JSON 文字列の構造が不定の場合は C# のクラス定義ができません。それでも ASP.NET のフレームワークがモデルバインディングしてくれるようにするにはどのようにしたらいいでしょう?

構造が不定とは言っても必要な情報のある JSON 文字列 {"name" : "value"} の name は事前に分かっているのであれば、それに該当する value はJsonElement オブジェクトとして取得できます(詳しくは先の記事「JSON 文字列から指定した name の value を取得」を見てください)。それで目的が果たせるのであればその方向で進めるのが良さそうです。

クライアントから送信されてきた構造不定の JSON 文字列を、アクションメソッドでどのように受け取って JsonElement オブジェクトにデシリアライズするかですが、それはフレームワーク組み込みのモデルバインダが自動的に行ってくれます。

具体例は下のサンプルコードの Full アクションメソッドを見てください。JsonElement 型の引数を設定するだけで、自動的にクライアントから送信されてきた JSON 文字列をデシリアライズしてバインドしてくれます。

using Microsoft.AspNetCore.Mvc;
using MvcCore5App4.Models;
using System.Text.Json;

namespace MvcCore5App4.Controllers
{
    public class JsonController : Controller
    {
        [HttpPost]
        public IActionResult Partial([FromBody] Rootobject postedObject)
        {
            JsonElement element = postedObject.Response.Result.ObjectArray;
            return Content(element.ToString());
        }

        [HttpPost]
        public IActionResult Full([FromBody] JsonElement postedObject)
        {
            JsonElement? element = FindElementByName(postedObject, "ObjectArray");

            string returnValue = (element != null) ?
                element.Value.ToString() : "ObjectArray は取得できません";

            return Content(returnValue);
        }


        private JsonElement? FindElementByName(JsonElement jelem, string name)
        {
            if (jelem.ValueKind == JsonValueKind.Object)
            {
                foreach (JsonProperty jprop in jelem.EnumerateObject())
                {
                    if (jprop.Name == name)
                    {
                        return jprop.Value;
                    }
                    else
                    {
                        JsonElement? retVal = FindElementByName(jprop.Value, name);
                        if (retVal != null)
                        {
                            return retVal;
                        }
                    }
                }
            }
            else if (jelem.ValueKind == JsonValueKind.Array)
            {
                foreach (JsonElement jelemInArray in jelem.EnumerateArray())
                {
                    JsonElement? retVal = FindElementByName(jelemInArray, name);
                    if (retVal != null)
                    {
                        return retVal;
                    }
                }
            }
            else
            {
                return null;
            }
            return null;
        }
    }
}

送信した JSON 文字列のサンプルは以下の通りです。先の記事「JSON 文字列から指定した name の value を取得」に書いたものと同じです。

{
  "Title": "This is my title",
  "Response": {
    "Version": 1,
    "StatusCode": "OK",
    "Result": {
      "Profile": {
        "UserName": "SampleUser",  
        "IsMobileNumberVerified": false,
        "MobilePhoneNumber": null
      },
      "ObjectArray" : [
          {"Code": 2000,"Description": "Fail"},
          {"Code": 3000,"Description": "Success"}
      ],
      "lstEnrollment": "2021-2-5"
    },
    "Message": {
      "Code": 1000,      
      "Description": "OK"
    }
  },
  "StringArray" : ["abc", "def", "ghi"]
}

JSON 文字列まるまる全部の構造が不定なわけではなく、特定の項目だけが不定の場合は別の方法があります。例えば、上の JSON 文字列の中で "ObjectArray" の value のみが不定だとします。

その場合は、上の JSON 文字列から Visual Studio の機能を利用して生成したクラス定義の中の ObjectArray プロパティの型を JsonElement に書き換えてやります。具体的には以下の通りです。

using System.Text.Json;

namespace MvcCore5App4.Models
{
    // Visual Studio を利用して JSON 文字列から生成したクラス定義
    public class Rootobject
    {
        public string Title { get; set; }
        public Response Response { get; set; }
        public string[] StringArray { get; set; }
    }

    public class Response
    {
        public int Version { get; set; }
        public string StatusCode { get; set; }
        public Result Result { get; set; }
        public Message Message { get; set; }
    }

    public class Result
    {
        public Profile Profile { get; set; }

        // ObjectArray の value の構造が不定ということで書き換え
        //public Objectarray[] ObjectArray { get; set; }
        public JsonElement ObjectArray { get; set; }
        
        public string lstEnrollment { get; set; }
    }

    public class Profile
    {
        public string UserName { get; set; }
        public bool IsMobileNumberVerified { get; set; }
        public object MobilePhoneNumber { get; set; }
    }

    // ObjectArray クラスの定義は不要なのでコメントアウト
    //public class Objectarray
    //{
    //    public int Code { get; set; }
    //    public string Description { get; set; }
    //}

    public class Message
    {
        public int Code { get; set; }
        public string Description { get; set; }
    }
}

上の Rootobject クラスをアクションメソッドの引数に設定してやれば、ObjectArray プロパティには JsonElement 型、それ以外は上の定義に指定した通りの型にデシリアライズしてくれます。

そのアクションメソッドの具体例は上のコントローラのサンプルコードの Partial アクションメソッドを見てください。アクションメソッドの実行結果が上の画像です。

Tags: , , , , ,

CORE

Web API でファイルアップ/ダウンロード (CORE)

by WebSurfer 17. January 2021 17:41

ファイルをアップロード/ダウンロードする相手が ASP.NET Core 3.1 Web API の場合はどのようにすれば良いかについて書きます。

Web API へファイルアップロード

コードを書いてみましたが、先の記事「ASP.NET Core 3.1 Web API」に書いた、(1) Controller は ControllerBase クラスを継承、(2) ApiControllerAttribute 属性を付与、(3) ルーティングは RouteAttibute 属性を付与して設定、アクションメソッドに [HttpGet], [HttpPost] 属性を付与する以外は MVC の場合とほとんど変わりませんでした。

(MVC の場合は、先の記事「ASP.NET Core MVC でファイルアップロード」と「ASP.NET Core MVC でファイルダウンロード」を見てください)

それでこの記事の話は終わってしまうのですが、それではちょっとブログの記事としては寂しいし、今後の参考になるかもしれないので検証に使ったコードを下にアップしておきます。

Web API コントローラー/アクションメソッド

using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;

namespace WebAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class FileUpDownloadController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileUpDownloadController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]
        public async Task<IActionResult> PostFile(IFormFile postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得。ブラウザが IE 
                // の場合 postedFile.FileName はクライアント側でのフ
                // ルパスになることがあるので Path.GetFileName を使う
                string filename = Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string contentRootPath = _hostingEnvironment.ContentRootPath;
                string filePath = contentRootPath + "\\" + 
                                  "UploadedFiles\\" + filename;

                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(stream);
                }

                result = filename + " (" + postedFile.ContentType +
                         ") - " + postedFile.Length +
                         " bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return Content(result);
        }

        [HttpGet]
        [ResponseCache(Duration = 0, 
            Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult GetFile(string filename = "sample1.jpg")
        {
            if (string.IsNullOrEmpty(filename))
            {
                return NotFound("引数が null または空");
            }

            // アプリケーションルートの物理パスを取得
            string contentRootPath = _hostingEnvironment.ContentRootPath;

            // ダウンロードするファイルの物理パス
            string physicalPath = contentRootPath + "\\" + 
                                  "Files\\" + filename;

            if (!System.IO.File.Exists(physicalPath))
            {
                return NotFound("指定されたパスにファイルが無い");
            }

            // Content-Disposition ヘッダを設定(RFC 6266 対応してない)
            Response.Headers.Append("Content-Disposition",
                "attachment;filename="+filename);
            
            return new PhysicalFileResult(physicalPath, "image/jpeg");
        }
    }
}

アップロードの検証に使った View

@{
    ViewData["Title"] = "Upload";
}

<h1>Upload</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <div>
                <div>
                    <p>Upload file using this form:</p>
                    @* name 属性はモデルのクラスのプロパティ名と同じ
                       にしないとサーバー側でモデルバインディングさ
                       れないので注意。大文字小文字は区別しない。*@
                    <input type="file" name="postedfile" />
                </div>
            </div>

        </form>
        <div>
            <div>
                <input type="button" id="ajaxUpload" value="Upload" />
                <div id="result"></div>
            </div>
        </div>
    </div>
</div>

<script src="~/Scripts/jquery.js"></script>
<script type="text/javascript">
    //<![CDATA[
    $(function () {
        $('#ajaxUpload').on('click', function (e) {
            // FormData オブジェクトの利用
            var fd = new FormData(document.querySelector("form"));

            $.ajax({
                url: '/FileUpDownload',
                method: 'post',
                data: fd,
                processData: false, // jQuery にデータを処理させない
                contentType: false  // contentType を設定させない
            }).done(function(response) {
                $("#result").empty;
                $("#result").text(response);
            }).fail(function( jqXHR, textStatus, errorThrown ) {
                $("#result").empty;
                $("#result").text('textStatus: ' + textStatus +
                    ', errorThrown: ' + errorThrown);
            });
        });
    });
    //]]>
</script>

ダウンロードの検証はブラウザのアドレスバーにコントローラーの URL を入力して FileUpDownload を GET 要求すれば可能です。ファイル名はデフォルトで "sample1.jpg" となっていますが、クエリ文字列で別のファイルを指定できます。

Tags: , , ,

Upload Download

Web API に XML データを送信

by WebSurfer 9. August 2020 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

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2021  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar