WebSurfer's Home

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

MVC に JSON をバインドするには [FromBody] が必要 (CORE)

by WebSurfer 2021年4月12日 15:14

ASP.NET Core MVC のアクションメソッドに JSON 文字列をボディに含めて POST 送信する場合、アクションメソッドの引数に [FromBody] 属性を付与しないと、なぜかモデルバインディングに失敗するという話を書きます。(Core 3.1 と 5.0 で確認。1.x, 2.x. 6.0 は未検証・未確認です)

モデルバインディングに失敗

上の画像がその例で、アクションメソッドの引数には [FromBody] を付与していません。RequestJson は生成されて引数の requestJson に渡されますが、各プロパティには送信されてきた JSON 文字列の値が代入されていません。(各プロパティの型の既定値のまま)

引数の RequestJson クラスの定義は以下の通りです。

namespace MvcCore5App2.Models
{
    public class RequestJson
    {
        public string Method { get; set; }
        public bool MuteHttpExceptions { get; set; }
        public string ContentType { get; set; }
        public string Payload { get; set; }
    }
}

JSON 文字列の送信は以下の画像のように Fiddler の Composer を使いました。(注: RequestJson クラスの各プロパティ名と JSON の {"name";"value"} の "name" の大文字小文字が違いますがそこは関係ないです。先の記事「JsonSerializer の Camel Casing (CORE)」に書いたように、Core v3.x 以降で使われる System.Text.Json 名前空間の JsonSerializer クラスは、デフォルトではデシリアライズする際の大文字小文字を区別するのですが、アプリ内で区別しない設定にしているようです)

Fiddler の Composer で送信

.NET Framework 版の MVC5 では [FromBody] を付与しなくてもモデルバインディングされたのですが・・・

何故か ASP.NET Core MVC では [FromBody] を付与しないと、送信されてきたクエリ文字列、フォームボディ、ルートパラメータ、クッキー、要求ヘッダ、ファイルなどのどれから取得するかが分からず、モデルバインディング (JSON 文字列から値を取得して各プロパティに代入) できないということのようです。(想像です)

以下の画像のように、アクションメソッドの引数に [FromBody] を付与すればモデルバインディングに成功します。送信した JSON 文字列は上の Fiddler の Composer 画像のものと同じですが、JSON の value が RequestJson クラスの各プロパティに代入されてから引数の requestJson に渡されています。

モデルバインディングに成功

上にも書きましたが、.NET Framework 版の MVC では [FromBody] 無しでも問題なくモデルバインディングされます。Core 版 MVC でも同じだろうと思っているとハマります。何を隠そう自分もハマって 2 時間ぐらい悩みました。

さらに、Core 版でも Web API の場合は [FromBody] 無しでも問題なくモデルバインディングされます。以下の画像を見てください。

Web API の場合

Web API では、コンプレックス型(この記事の例では RequestJson クラス)の場合、デフォルトではボディからパラメータを取得するということになるそうで、それゆえ [FromBody] 無しでも問題ないということかもしれません(想像です)。

Web API にせよ MVC5 にせよ、JSON 文字列をボディに含めて POST 送信するなら、それを受けるアクションメソッドの引数には [FromBody] を付与しておくというのが正解と思いました。

Tags: , , , ,

CORE

CheckBox と DropDownList の検証 (CORE)

by WebSurfer 2021年3月7日 14:44

CheckBox には、例えばユーザーが条件を許諾したという意思表示のため、チェックを入れるのが必須というケースがあると思います。その検証をクライアント側とサーバー側の両方で行うためのカスタム検証属性を考えてみました。

CheckBox の検証

CheckBox の場合 RequiredAttribute ではクライアント側でもサーバー側でも検証はかかりません。RangeAttribute を使って [Range(typeof(bool), "true", "true")] というように設定するという手段がありますが、クライアント側での検証がかかりません。

というわけで、先の記事「ASP.NET Core MVC 検証属性の自作」とか「ファイルアップロード時の検証 (CORE)」で書いたようなカスタム検証属性を作って利用するのが良さそうです。

DropDownList にもそのようなカスタム検証属性を作ってみようと思いましたが、未選択で検証 NG とする場合は一番最初の option 要素が value="" となっていれば RequiredAttribute でクライアント/サーバー両方で検証がかかること、特定の項目を選ばなければならないというのは他の条件も絡むであろうから先の記事「CustomValidation 属性」で書いたようにすべきで、意味がなさそうなので止めました。

以下にそのコードをアップしておきます。上の画像を表示したものです。DropDownList 関係のコードもせっかく書いたので一緒に載せておきます。

Model とカスタム検証属性

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MvcCoreApp.Models
{
    // ビューモデル
    public class Partner
    {
        [Required(ErrorMessage = "{0} is required.")]
        public string Name { get; set; }

        // Required 属性では検証はかからない
        [Required(ErrorMessage = "{0} is required.")]
        [CheckBoxValidate(true)]
        public bool PartnerAccepted { get; set; }

        [Required(ErrorMessage = "{0} is required.")]
        public string Area { get; set; }

        public SelectList AreaList { get; set; }
    }

    // DropDownList に渡す SelectList 生成用のクラス
    // AreaId は int? または string 型にしておけば、SelectList を
    // 生成する際、下の Controller のコード例のように null を設定
    // できる。結果 option value="" となり未選択で検証 NG とできる
    public class Area
    {
        public int? AreaId { set; get; }
        public string AreaName { set; get; }
    }

    // CheckBox 用の検証属性
    // true/false を引数で指定(要チェックなら true とする)
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CheckBoxValidateAttribute : ValidationAttribute, 
                                             IClientModelValidator
    {
        private readonly bool option;

        public CheckBoxValidateAttribute(bool option)
        {
            this.option = option;
            this.ErrorMessage = "{0} must be set to {1}.";
        }

        public override string FormatErrorMessage(string name)
        {
            return String.Format(CultureInfo.CurrentCulture, 
                                 ErrorMessageString, 
                                 name, 
                                 option);
        }

        public override bool IsValid(object value)
        {
            // テキストボックスに入力された文字列が検証対象の場合は
            // ここで value が空白か否かをチェックして空白の場合は
            // true を返すようにしていた(空白の検証は Required だけで
            // 行い、その他の検証属性は空白の場合は true を返すのが
            // 原則なので)。チェックボックスの場合は ASP.NET MVC では
            // 必ず true/false のいずれかが返されるようになっている。
            // なので null が帰ってきた場合は何か良からぬことが起こって
            // いるかもしれないということで例外をスローすることにした
            if (value == null) 
            {
                throw new ArgumentNullException("CheckBoxValidate");
            }

            if ((bool)value == option)
            {
                return true;
            }
            return false;
        }

        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            var errorMessage = 
                FormatErrorMessage(context.ModelMetadata.GetDisplayName());
            MergeAttribute(context.Attributes, 
                           "data-val-checkboxvalidate", 
                           errorMessage);
            MergeAttribute(context.Attributes, 
                           "data-val-checkboxvalidate-option", 
                           this.option.ToString());
        }

        // 上の AddValidation メソッドで使うヘルパーメソッド
        private bool MergeAttribute(IDictionary<string, string> attributes, 
                                    string key, 
                                    string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
            attributes.Add(key, value);
            return true;
        }
    }
}

Controller / Action Method

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using MvcCoreApp.Models;
using Microsoft.AspNetCore.Http;
using System.IO;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MvcCoreApp.Controllers
{
    public class ValidationController : Controller
    {
        public IActionResult CheckBoxDropDown()
        {
            // DropDwonList に渡す SelectList の生成
            // AreaId を null に設定すると結果 option value="" となり
            // 未選択では RequiredAttribute による検証は NG となる
            var list = new List<Area>
            { 
                new Area { AreaId = null, AreaName = "--- Select One ---"},
                new Area { AreaId = 1, AreaName = "North" },
                new Area { AreaId = 2, AreaName = "South"},
                new Area { AreaId = 3, AreaName = "Eastt" },
                new Area { AreaId = 4, AreaName = "West" }
            };

            var partner = new Partner 
            { 
                AreaList = new SelectList(list, "AreaId", "AreaName")
            };

            return View(partner);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult CheckBoxDropDown(Partner partner)
        {
            if (!ModelState.IsValid)
            {
                // 検証 NG で再表示する場合は SelectList も再生成要
                var list = new List<Area>
                {
                    new Area { AreaId = null, AreaName = "--- Select One ---"},
                    new Area { AreaId = 1, AreaName = "North" },
                    new Area { AreaId = 2, AreaName = "South"},
                    new Area { AreaId = 3, AreaName = "Eastt" },
                    new Area { AreaId = 4, AreaName = "West" }
                };

                partner.AreaList = new SelectList(list, "AreaId", "AreaName");

                return View(partner);
            }

            return RedirectToAction("Create", "Validation");
        }
    }
}

View

@model MvcCoreApp.Models.Partner

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

<h1>CheckBoxDropDown</h1>

<h4>Pertner</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="CheckBoxDropDown">
            <div asp-validation-summary="ModelOnly" class="text-danger">
            </div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group form-check">
                <label class="form-check-label">
                    <input class="form-check-input" asp-for="PartnerAccepted" />
                    @Html.DisplayNameFor(model => model.PartnerAccepted)
                </label>
                <br />
                <span asp-validation-for="PartnerAccepted" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label asp-for="Area" class="control-label"></label>
                <select asp-for="Area" asp-items="Model.AreaList" 
                        class="form-control">
                </select>
                <span asp-validation-for="Area" class="text-danger"></span>
            </div>

            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}

<script type="text/javascript">
    //<![CDATA[
    $.validator.addMethod("checkboxvalidate",
        function (value, element, parameters) {
            // チェックされてないと value は undefined となり
            // value では判定できないので注意
            var result = element.checked;
            var isTrue = (parameters.toUpperCase() == 'TRUE');
            if (result == isTrue) {
                return true;
            }
            return false;
        });

    $.validator.unobtrusive.adapters.
        addSingleVal('checkboxvalidate', 'option');

    //]]>
</script>
}

Tags: , , , ,

Validation

EF Core で SQL Server に渡される SQL 文の確認

by WebSurfer 2021年2月19日 16:27

開発時に Entity Framework から SQL Server に渡される SQL 文を確認したいことがあると思います。EF6 と違って EF Core ではそれは少々面倒だったのですが、EF Core 5.0 で導入された「シンプルなログ」という機能を使えば EF6 並みに簡単にできるようになったとのことなので試してみました。

EF Core の SQL 文

その「シンプルなログ」機能を使って EF Core が SQL Server に発行した SQL 文を Visual Studio の出力ウィンドウに表示したのが上の画像です。

基本的には DbContextOptionsBuilder.LogTo メソッドを使ってコンテキストクラスにアクセスして EF Core ログを取得し、指定の場所にログを書き出すという操作になるようです。

NuGet パッケージの追加、構成の変更、コードの大幅な書き換えなどは必要なく、Microsoft のドキュメント「シンプルなログ」に書かれているように、コンテキストクラスの OnConfiguring メソッドに一行追加するだけで EF Core ログの取得が可能になります。

具体的には、例えば先の記事「スキャフォールディング機能 (CORE)」でリバースエンジニアリングで作成したコンテキストクラスを例に取ると、以下のように自動生成された OnConfiguring メソッドの中で LogTo メソッドを使って EF Core ログを取得し、それを Debug.WriteLine メソッドで書き出すという一行を追加すれば OK です。

using Microsoft.EntityFrameworkCore;
using MvcCore5App.Models;

namespace MvcCore5App.DAL
{
    public partial class NorthwindContext : DbContext
    {
        public NorthwindContext(DbContextOptions<NorthwindContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Category> Categories { get; set; }
        public virtual DbSet<Product> Products { get; set; }
        public virtual DbSet<Supplier> Suppliers { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // 出力ウィンドウに EF Core ログを表示
            optionsBuilder.LogTo(msg => System.Diagnostics.Debug.WriteLine(msg));

            // ・・・中略・・・
        }

        // ・・・中略・・・
    }
}

上の LogTo メソッドにより、Visual Studio でデバッグ実行した際に EF Core ログが出力ウィンドウに表示され、その中に EF Core から SQL Server に渡される SQL 文が含まれるようになります。

例えば、以下のような Linq to Entities 文をコントローラーのアクションメソッドに書いて Visual Studio でデバッグ実行し、そのアクションメソッドをブラウザから呼び出してから出力ウィンドウを見ると、この記事の一番上の画像の SQL 文が発行されているのが確認できます。

namespace MvcCore5App.Controllers
{
    public class ProductsController : Controller
    {
        private readonly NorthwindContext _context;

        public ProductsController(NorthwindContext context)
        {
            _context = context;
        }

        public IActionResult Index()
        {
            // どのような SQL 文が投げられるかの検証用
            var list = _context.Products.OrderBy(p => p.ProductId).
                                         Skip(20).Take(10).ToList();

            // ・・・中略・・・
        }

        // ・・・中略・・・
    }
}

上に紹介した「シンプルなログ」は開発時限定でログを取ることを目的としているようで、運用時などでのログの取得には別の機能があるそうです。詳しくは Microsoft のドキュメント「ログとインターセプトの概要」を見てください。


最後に、EF6 で SQL Server に渡される SQL 文を確認する方法もついでに書いておきます。下の画像が EF6 のログを出力ウィンドウに書き出して、その中に含まれる SQL 文の部分を示したものです。

EF6 の SQL 文

基本的には EF Core と同様にコンテキストクラスからログを得て Visual Studio の出力ウィンドウに書き出すのですが、それには DbContext.Database.Log プロパティ を以下のように使います。

namespace Mvc5App.Controllers
{
    public class ProductsController : Controller
    {
        private NORTHWINDEntities db = new NORTHWINDEntities();

        public ActionResult Index()
        {
            // 出力ウィンドウに EF6 ログを表示
            db.Database.Log = msg => System.Diagnostics.Debug.Write(msg);

            var list = db.Products.OrderBy(p => p.ProductID).
                                   Skip(20).Take(10).ToList();

            // ・・・中略・・・
        }
    }
}

元々はログを取るための機能で、SQL 文を確認するだけでなく他にもいろいろ情報が取得できるようです。詳しくは Microsoft のドキュメント「データベース操作のログ記録と受信」を見てください。

Tags: , , ,

ADO.NET

About this blog

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

Calendar

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

View posts in large calendar