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

ASP.NET MVC5 で boorstrap datepicker 利用

by WebSurfer 16. August 2021 14:14

ASP.NET MVC5 で Controller で Model に日付を設定して View に渡し、Html ヘルパーの EditorFor を使ってテキストボックスにその日付を表示したとします。そのテキストボックスに bootstrap datepicker を適用してカレンダーを表示した際、 Model に設定した日付がカレンダー上で選択されるようにする方法を備忘録として残しておきます。

boorstrap datepicker

自動的に Model に設定した日付が bootstrap datepicker のカレンダー上で選択されて表示されると思っていたのですが、何もしないと Model に設定した日付ではなくてブラウザに表示された当日の日付になってしまいます。

その原因は、例えば new DateTime(2021, 3, 23) としてそれを Model に設定すると、テキストボックスに 2021/03/23 0:00:00 と表示される(当該 html input 要素で value="2021/06/24 0:00:00" となる)、即ち 0:00:00 部分が bootstrap datepicker にとって余計で日付を判定できなかったからのようです。

なので、当該 html input 要素で value="2021/03/23" となるようにすれば bootstrap datepicker のカレンダー上もその日付を選択して表示するようになります。

そのためには、Model の当該プロパティに DisplayFormatAttribute 属性を付与してやります。具体的には以下のようにします。その結果が上の画像です。

(1) Model

using System;
using System.ComponentModel.DataAnnotations;

namespace Mvc5App.Models
{
    public class BootstrapDatepickerModel
    {
        [Display(Name = "日付")]
        [DisplayFormat(ApplyFormatInEditMode = true,
            DataFormatString = "{0:yyyy/MM/dd}")]
        public DateTime? OrderDate { get; set; }
    }
}

Visual Studio 2019 のテンプレートを使って ASP.NET Web アプリを作ると、Bootstrap.js と Bootstrap.css が自動的にプロジェクトに含まれますが、bootstrap datepicker はその中には含まれていませんので注意してください。別途 github のサイト uxsolutions/bootstrap-datepicker 等からダウンロードする必要があります。この記事では、この記事を書いている時点での最新版 1.9.0 を使用しました。

また、bootstrap datepicker は Boorstrap 本体のバージョン 4 以上には対応してないそうです (Bootstrap 4 および 5 でも使う方法があるそうですが未検証・未確認です)。この記事で使った Bootstrap のバージョンは、MVC5 アプリのテンプレートに含まれている v3.4.1 です。

ご参考に View と Controller / Action Metod のコードも以下に載せておきます。

(2) View

@section Scripts { ... } 内の link 要素、script 要素による外部 bootstrap datepicker の js ファイル、css ファイルへの参照に注目してください。

@model Mvc5App.Models.BootstrapDatepickerModel

@{
    ViewBag.Title = "Datepicker";
}

<h2>Datepicker</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>BootstrapDatepickerModel</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.OrderDate,
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.OrderDate,
                    new { htmlAttributes = new { @class = "form-control datepicker" } })
                @Html.ValidationMessageFor(model => model.OrderDate, "",
                    new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    <link href="~/Content/bootstrap-datepicker.css" rel="stylesheet" />
    <script src="~/Scripts/bootstrap-datepicker.js"></script>
    <script src="~/Scripts/locales/bootstrap-datepicker.ja.min.js"></script>

    @Scripts.Render("~/bundles/jqueryval")

    <script type="text/javascript">
        //<![CDATA[
        $(function () {
            $('.datepicker').datepicker({
                format: 'yyyy/mm/dd',
                language: 'ja'
            });
        })
        //]]>
    </script>
}

(3) Controller / Action Method

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Mvc5App.DAL;
using System.Data.Entity;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using Mvc5App.Models;
using System;

namespace Mvc5App.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Datepicker()
        {
            var model = new BootstrapDatepickerModel();
            model.OrderDate = new DateTime(2021, 3, 23);

            // model.OrderDate が null なら今日の日付を表示する
            if (model.OrderDate == null)
            {
                model.OrderDate = DateTime.Now;
            }

            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Datepicker(BootstrapDatepickerModel model)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");

            }

            return View(model);
        }
    }
}

Tags: , , ,

MVC

Link Tag Helper と Script Tag Helper (CORE)

by WebSurfer 9. August 2021 11:22

ASP.NET Core アプリで Content Delivery Network (CDN) から css や JavaScript のリソースを取得する際、CDN からの取得に失敗した場合にフォールバック(代替えリソース)を取得するのに便利な Link Tag Helper, Script Tag Helper があります。

その概要は Mocrosoft のドキュメント「ASP.NET Core のリンク タグ ヘルパー」と「ASP.NET Core のスクリプト タグ ヘルパー」に書いてあるのを見つけました・・・が、それを読んだだけでは理解できませんでした。(汗)

なので、実際にコードを書いて動かしてどういう動きになるのかを調べて、その結果分かったことを以下に備忘録として残しておきます。

まず、上に紹介したドキュメントのサンプルコードにある integrity 属性と crossorigin 属性とは何かを書きます。それらは ASP.NET Core の Tag Helper の機能ではなく、html に備わっている改ざん防止機能です。詳しい説明は MDN のドキュメント「サブリソース完全性」と「HTML crossorigin 属性」にあります。

ユーザーがあらかじめ CDN から取得したリソースのハッシュ値を計算してそれを integrity 属性に設定しておくと、ブラウザが CDN に要求をかけて応答として取得したリソースのハッシュ値を計算して比較し、一致しない場合はブラウザはそのリソースをロードしないという動きになるようです。それにより悪意のある第三者による改ざん攻撃のリスクを軽減するものだそうです。

integrity 属性に設定するハッシュ値の取得方法は、MDN の記事に書いてあるように、オンラインで SRI Hash Generator というサービスから取得できます。自分も試してみましたが、期待通りの結果が得られました。

crossorigin 属性の方は、MDN のドキュメントでは自分には意味不明でした。(涙) いろいろ調べてみると、サブリソース完全性に書いてある "... ブラウザーはオリジン間リソース共有 (CORS) を使用してリソースに追加のチェックを行い ..." の CORS と関係があるようです。

実際に検証してみると、integrity 属性を付与した場合は crossorigin="anonymous" 属性も一緒に付与しないと、CDN から応答が返ってきてもブラウザはそれを取り込むことはないという結果になりました。

要するに、integrity 属性にハッシュ値を設定をして改ざん防止の効果を期待するなら、同時に crossorigin="anonymous" 属性の付与も必須ということのようです。

なお、ASP.NET Core の Link Tag Helper と Script Tag Helper にとって integrity 属性と crossorigin 属性の設定は必須ではありません。無くてもフォールバック機能は動きます。

次に、ASP.NET Core の Tag Helper 独自の asp-fallback-* という属性の説明をします。名前に fallback とあるように、それらの属性はすべてフォールバックを行うためのものです。

(1) Link Tag Helper の場合

asp-fallback-href: CDN の css ファイルがロードできなかった場合のフォールバック(代替え)css ファイルの URL を指定します。

asp-fallback-test-*: CDN の css ファイルに含まれる特定のクラス名、プロパティ名とその値を指定します。例えば、以下のクラスが含まれる場合、

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

asp-fallback-test-* は以下のように設定します。

<link rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
  asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
  asp-fallback-test-class="sr-only" 
  asp-fallback-test-property="position"
  asp-fallback-test-value="absolute"
  crossorigin="anonymous"
  integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" />

上の Link Tag Helper が html にレンダリングされると以下のようになります (一部略)。

<link rel="stylesheet" 
  href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
   crossorigin="anonymous" 
  integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" />

<meta name="x-stylesheet-fallback-test" content="" class="sr-only" />

<script>
  !function (a,b,c,d) {
    var e, f=document,
        g = f.getElementsByTagName("SCRIPT"),
        h = g[g.length - 1].previousElementSibling,
        i = f.defaultView && f.defaultView.getComputedStyle ? 
              f.defaultView.getComputedStyle(h) : h.currentStyle;

    if ( i && i[a] !== b) 
      for (e = 0; e < c.length; e++)
        f.write('<link href="'+c[e]+'" '+d+"/>")}
  ("position","absolute",["/lib/bootstrap/dist/css/bootstrap.min.css"], 
   "rel=\u0022stylesheet\u0022 crossorigin=\u0022anonymous\u0022 ... ");
</script>

meta タグとスクリプトを見てください。それらによって CDN の css が asp-fallback-test-* に設定したクラス名、プロパティ名とその値を含んでいるかがテストされ、テスト結果 NG と判断された場合は asp-fallback-href に指定される URL のフォールバック css ファイルを取得するよう link 要素を document に書き込みます。

なお、integrity 属性を使っての検証 NG の場合は CDN から送られてきた css はロードされませんので、テスト結果は NG と判断され、フォールバック css ファイルを取得するようになります。

(2) Script Tag Helper の場合

asp-fallback-src: CDN の js ファイルがロードできなかった場合のフォールバック(代替え)js ファイルの URL を指定します。

asp-fallback-test: CDN の js ファイルに含まれる特定の JavaScript オブジェクト名を指定します。例えば、window.jQuery という名前の JavaScript オブジェクトが含まれる場合、asp-fallback-test="window.jQuery" とします。以下の例を見てください。

<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"
  asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
  asp-fallback-test="window.jQuery"
  crossorigin="anonymous"
  integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2">
</script>

上の Script Tag Helper が html にレンダリングされると以下のようになります (一部略)。

<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"
  crossorigin="anonymous" 
  integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2">
</script>

<script>
  (window.jQuery || 
   document.write("\u003Cscript src=\u0022/lib/jquery/dist/jquery.min.js\u0022 ..."));
</script>

asp-fallback-test に設定した window.jQuery が定義されていない場合は CDN から送られてきた js がロードできなかったということなので、document.write(...) が実行されて、asp-fallback-src に指定される URL の js ファイルを取得するよう script 要素を document に書き込みます。

Link Tag Helper の場合と同様に、integrity 属性を使っての検証 NG の場合は CDN から送られてきた js はロードされませんので、上のスクリプトで window.jQuery は未定義となり、フォールバック js ファイルを取得するようになります。

Tags: , , ,

CORE

About this blog

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

Calendar

<<  September 2021  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar