WebSurfer's Home

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

整数型プロパティの検証、エラーメッセージ (CORE)

by WebSurfer 2023年1月26日 11:49

.NET 7.0 の ASP.NET Core MVC で、モデルのプロパティが整数型(int, short, long 等)の場合の検証とエラーメッセージに関する注意点を書きます。

検証結果

.NET Framework の MVC5 の場合については先の記事「int 型プロパティの検証、エラーメッセージ (MVC5)」に書きましたのでそちらを見てください。

検証に使ったのは Visual Studio 2022 v17.4.4 のテンプレートで作成した .NET 7.0 の ASP.NET Core MVC アプリです。View には Html ヘルパーではなく入力 Tag ヘルパーを使いました。

Model のプロパティが整数型の場合 (下に載せた Model のコード例では ContractID, Code プロパティ)、Required 属性を付与しなくても、View の入力 Tag ヘルパーから生成される html の input 要素には data-val-required="The xxx field is required." (xxx はプロパティ名) という属性が付与されます。

(プロパティが int? 型のような null 許容型の場合は data-val-required 属性は付与されません。.NET Framework MVC5 の場合に付与された data-val-number という属性は .NET 7.0 ASP.NET Core MVC では付与されません)

View で検証用の JavaScript ファイルが取り込まれるように設定されていると (下に載せた View のコード例では @section Scripts の中のコード)、未入力の場合はクライアント側での検証により data-val-required 属性に設定された "The xxx field is required." というメッセージが表示されます。

これを任意の文字列に書き換えるには、Model の当該プロパティに Required 属性を付与し ErrorMessage に書き換えたい文字列を設定します。そうすると、html の input 要素の data-val-required 属性に設定される文字列が ErrorMessage に置き換わり、検証 NG の場合はそれが表示されます。

未入力の検証については上記の対応だけで特に不都合はないと思います。

しかし、未入力の検証に加えて、数字か否かの検証を行う場合はいろいろ問題があります。それを以下に説明します。

入力 Tag ヘルパーが以下のようになっている場合、html の input 要素の type 属性が "number" となり、ブラウザによる入力の制約と、ASP.NET による検証がかかります。

<input asp-for="Price2" class="form-control"/>

入力の制約はブラウザ依存です。自分が試した限りですが、Edge 109.0.1518.61, Chrome 109.0.5414.120, Opera 94.0.4606.76 では数字と + - . 以外の文字は受け付けなくなります。Firefox 109.0 では何でも入力できてしまいます。

クライアント側での JavaScript による検証はフレームワークに組み込みの jquery.validate.min.js により行われます。数字以外の文字を入力して送信しようとすると、"Please enter a valid number." というエラーメッセージが表示され送信はキャンセルされます。("Please enter a valid number." というエラーメッセージは jquery.validate.min.js にハードコーディングされています。それを書き換えれば日本語化はできます)

問題は、(1) 文字 + - はブラウザは受け付けるが jquery.validate.min.js による検証で NG になるという不整合、(2) エラーメッセージが英語になること、(3) さらにそれを解決するためプロパティに RegularExpression 属性を追加しても無視されることです。

上記 (1) ~ (3) を解決するには、html の input 要素が type="number" ではなく type="text" になるようにします。具体的には、View の当該入力 Tag ヘルパーに以下のように type="text" を追加します。

<input asp-for="Price2" class="form-control" type="text"/>

それにより RegularExpression 属性に設定した正規表現が期待通り動くようになり、検証結果 NG の場合は ErrorMessage に設定したエラーメッセージが表示されるようになります。

以上をまとめると:

  1. 未入力の検証には、Model の当該プロパティに Required 属性を付与し ErrorMessage にエラーメッセージを設定。
  2. 数字か否かの検証には、当該入力 Tag ヘルパーに type="text" を追加して html の input 要素の type 属性が "text" となるようにし、さらに
  3. RegularExpression 属性をプロパティに付与して正規表現を使っての検証を行う。ErrorMessage にエラーメッセージを設定する。

 

以上はクライアント側での検証の話です。

サーバー側での検証の問題は、未入力または整数型にパースできない文字列を送信した場合、Model のプロパティに付与した Required 属性、RegularExpression 属性が働かないことです。上の画像の「価格2 (int)」のエラーメッセージを見てください。

上の画像の例では、クライアント側での検証を無効にして "2000x" という文字列を送信していますが「The value '2000x' is not valid for 価格2 (int).」というエラーメッセージが出ています。RegularExpression 属性に設定したエラーメッセージとは異なっています。

未入力の場合は「The value '' is invalid.」というエラーメッセージが表示されます。やはり Required 属性に設定したエラーメッセージとは異なります。

これは、"2000x" という文字列や空白は int 型にパースできないので、検証属性による検証が行われる前にエラーとなって、そのエラーメッセージが出ているようです。(想像です)

検証属性の ErrorMessage に設定したメッセージが表示されて欲しいのですが、int 型にパースできない文字列が送信された場合は何ともならないようです。ただし、このエラーメッセージを書き換える方法はあります。

ModelStateDictionary に含まれる ModelStateEntry は同じ Key でマージしすると上書きされます。具体例は下の Controller コードの通りです。上の画像の ID (int) のテキストボックスの下のエラーメッセージを書き換えています。

Model

using System.ComponentModel.DataAnnotations;

namespace MvcNet7App.Models
{
    public class Contract
    {
        [Display(Name = "ID (int)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d+$", ErrorMessage = "数字のみ")]
        public int ContractID { get; set; }

        [Display(Name = "契約日 (DateTime)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d{4}/\d{2}/\d{2}( \d{1,2}:\d{2}:\d{2})?$",
            ErrorMessage = "yyyy/MM/dd 形式")]
        public DateTime ContractDate { get; set; }

        [Display(Name = "価格 (decimal)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d{1,5}$", 
            ErrorMessage = "数字 1 ~ 5 文字")]
        public decimal Price { get; set; }

        [Display(Name = "価格2 (int)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d+$", ErrorMessage = "数字のみ")]
        [Range(100, 10000, 
            ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
        public int Price2 { get; set; }
    }
}

View

@model MvcNet7App.Models.Contract

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

<div class="row">
    <div class="col-md-4">
        <form asp-action="ContractEdit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="ContractID" class="control-label"></label>
                <input asp-for="ContractID" class="form-control"/>
                <span asp-validation-for="ContractID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ContractDate" class="control-label"></label>
                <input asp-for="ContractDate" class="form-control" type="text"/>
                <span asp-validation-for="ContractDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" type="text"/>
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price2" class="control-label"></label>
                <input asp-for="Price2" class="form-control" type="text"/>
                <span asp-validation-for="Price2" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{
        // コメントアウトするとクライアント側での検証がかからない
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

Controller

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MvcNet7App.Models;
using System.Diagnostics;

namespace MvcNet7App.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult ContractEdit()
        {
            var model = new Contract 
            { 
                ContractID = 1, 
                ContractDate = DateTime.Now, 
                Price = 1000m, 
                Price2 = 2000
            };

            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult ContractEdit(Contract model)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");
            }

            // div asp-validation-summary に表示するために追加
            var newDictionary = new ModelStateDictionary();
            newDictionary.AddModelError("", 
                "ValidationSummary に表示するために追加。");
            ModelState.Merge(newDictionary);

            // エラーメッセージを書き換えることはできる。
            // 同じ Key でマージした方に上書きされる
            ModelStateEntry? state = ModelState["ContractID"];

            if (state != null && state.Errors.Count > 0)
            {
                string msg = state.Errors[0].ErrorMessage;
                if (msg.StartsWith("The value"))
                {
                    // マージすると RawValue が null になるので書き戻すた
                    // めに取得しておく
                    object? value = state.RawValue;

                    var newDictionary2 = new ModelStateDictionary();
                    newDictionary2.AddModelError("ContractID",
                        "書き換え。元は The value '1.0' is not valid for ID (int).");
                    ModelState.Merge(newDictionary2);

                    // RawValue を書き戻す。そうしないと再描画されたとき
                    // 元のユーザー入力が表示されず 0 になってしまう
                    state.RawValue = value;
                }
            }

            return View(model);
        }
    }
}

上に書いた「The value '2000x' is not valid for 価格2 (int).」とか「The value '' is invalid.」は Model Binding Error Messages というそうで、ローカライズする方法があるようです。上のように書き換えるよりローカライズする方が現実的かもしれません。

Stackoverflow の記事 ASP.NET Core Model Binding Error Messages Localization に方法が書いてあるのを見つけました。別途検証してみます。 ⇒ 2022/1/27 追記: 別の記事「Model Binding Error Messages の差替 (CORE)」に書きましたのでそちらを見てください。

Tags: , , , , ,

Validation

SQL Server の数値型を LIKE 句を使ってあいまい検索

by WebSurfer 2021年6月19日 12:55

SQL Server の数値型は、文字列型と同様に、そのまま直接 LIKE 句を使ってあいまい検索ができるようです。下の画像を見てください。UnitPrice 列は money 型ですが、[UnitPrice] LIKE '%2%' という条件が有効になっています。

SSMS での検索結果

知ってました? 実は何を隠そう自分は最近まで知らなかったです。(汗) 数字型はまず文字列型に変換してから、それに LIKE 句を使うものだと思ってました。

調べてみると、Microsoft のドキュメント「LIKE (Transact-SQL)」に、

"引数が文字列データ型でない場合、SQL Server データベース エンジン は可能であれば引数を文字列データ型に変換します。 If any one of the arguments isn't of character string data type, the SQL Server Database Engine converts it to character string data type, if it's possible."

・・・と書いてあります。実際に試してみると、上の画像のように money 型の UnitPrice 列も LIKE 句を使ってあいまい検索ができました。

Microsoft のドキュメントが言う「可能であれば」がどこまでの範囲か調べ切れていませんが、自分が SQL Server 2012 で試した限りでは int 型と money 型は可能な範囲に入るようです。

ADO.NET + SqlClient を使った .NET Framework のアプリケーションでも同じことができます。パラメータ化も可能です。ただし、パラメータ化する場合は、パラメータは文字列型として扱う必要がありますが。

上の画像と同様な LIKE 句を使って検索を行う .NET Framework コンソールアプリのサンプルコードを以下に載せておきます。ADO.NET + SqlClient を使い、SQL 文はパラメータ化しています。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;

namespace ConsoleAppLIKE
{
    public class Product
    {
        public int ProductID { get; set; }

        public string ProductName { set; get; }

        // UnitPrice 列は NULL 可なので Nullable とした
        public decimal? UnitPrice { set; get; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string connString = "接続文字列";
            string selectQuery =
                "SELECT [ProductID] ,[ProductName] ,[UnitPrice] FROM [Products] " +
                "WHERE [ProductName] LIKE N'%' + @ProductName + N'%' AND " +
                "[UnitPrice] LIKE N'%' + @UnitPrice + N'%'";

            string productName = "ch";
            string unitPrice = "2";            
            List<Product> productList = new List<Product>();

            using (var connection = new SqlConnection(connString))
            {
                using (var command = new SqlCommand(selectQuery, connection))
                {
                    var p1 = new SqlParameter("@ProductName", SqlDbType.NVarChar);
                    p1.Value = productName;

                    // UnitPrice 列は money 型だが LIKE 句を使ってあいまい検索
                    // する場合はパラメータの型は文字列とする
                    var p2 = new SqlParameter("@UnitPrice", SqlDbType.NVarChar);
                    p2.Value = unitPrice;

                    command.Parameters.Add(p1);
                    command.Parameters.Add(p2);

                    connection.Open();
                    using (var reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            var product = new Product
                            {
                                ProductID = reader.GetInt32(0),
                                ProductName = reader.GetString(1),

                                // UnitPrice 列は NULL 可なのでその対応
                                UnitPrice = reader.IsDBNull(2) ?
                                            null : (decimal?)reader.GetDecimal(2)
                            };

                            productList.Add(product);
                        }
                    }
                }
            }

            foreach (Product p in productList)
            {
                Console.WriteLine($"PriductID: {p.ProductID}, " +
                    $"ProductName: {p.ProductName}, UnitPrice: {p.UnitPrice}");
            }
        }
    }
}

サンプルコード中のコメントにも書きましたが、UnitPrice 列は money 型ですが LIKE 句を使ってあいまい検索する場合はパラメータの型は文字列とする必要がありますので注意してください (例えば、SqlDbType.NVarChar を SqlDbType.Deciaml にするとエラーになります)。

上のコードの実行結果は以下の通りで、上の画像の SSMS での実行結果と同じになっています。

ADO.NET での検索結果

Tags: , , ,

SQL Server

int 型プロパティの検証、エラーメッセージ (MVC5)

by WebSurfer 2019年3月24日 16:06

ASP.NET MVC で、モデルのプロパティが int 型の場合の検証とエラーメッセージに関する注意点を書きます。

NULL 許容参照型がデフォルトで有効になっている ASP.NET Core 6.0+ の場合については別の記事「整数型プロパティの検証、エラーメッセージ (CORE)」を見てください。

検証��エラーメッセージ

モデルのプロパティが int 型の場合、クライアント側の検証を有効にしておけば、RequiredAttribute, RegularExpressionAttribute は付与しなくても input 要素には data-val-required="xxx フィールドが必要です。", data-val-number="フィールド xxx には数字を指定してください。" という属性が付与され、入力に応じてそれらのエラーメッセージが表示されます。

プロパティに RequiredAttribute が付与され ErrorMessage が設定されている場合は、data-val-required 属性に設定される文字列が ErrorMessage に置き換わります。(ちなみに、プロパティが int? 型の場合は data-val-required 属性そのものが付与されません)

上記は TextBoxFor, EditorFor いずれを使っても同じです。

ただし、EditorFor を使うと input 要素の type 属性が "number" となるので、それによりブラウザ依存の動きが出るのに要注意です。(TextBoxFor を使った場合は type 属性は "text" となります)

input 要素の type 属性が "number" となると、例えば、Chrome は数字以外の入力は受け付けなくなりますが、IE11 は最初の文字が数字であれば後に続く文字は何でも入力できてしまうという違いが出ます。

さらに、プロパティに RegularExpressionAttribute を追加して数字か否かをチェックするようにしても、input 要素の type 属性が "number" となっていると無視されます。

その場合動きはブラウザ依存になり、"1x" というような入力を受け付ける IE11 では data-val-number 属性に設定されたメッセージが、Firefox では data-val-required 属性に設定されたメ���セージが表示されます。Chrome は "1x" というような文字は入力できませんが、"1..." という文字列は受け付けるので、その場合は Firefox と同様に data-val-required 属性に設定されたメッセージが表示されます。

EditorFor ではなく TextBoxFor を使えば input 要素の type 属性は "text" となって、RegularExpressionAttribute による検証が行われ、検証 NG の場合は ErrorMessage に設定したメッセージが表示されます。

input 要素の type 属性が "number" となることによりブラウザ依存の動きとなって期待と異なるエラーメッセージが出るのを避けるためには以下の対応が必要です:

  1. TextBoxFor を使って input 要素の type 属性が "text" となるようにし、さらに
  2. RegularExpressionAttribute で数字か否かの検証を行う。  

以上はクライアント側での検証の話です。サーバー側での検証によるエラーメッセージは上記とは異なります。上の画像の「価格2 (int)」のエラーメッセージを見てください。

クライアント側での検証を無効にして "2000x" という文字列を送信していますが「値 '2000x' は 価格2 (int) に対して無効です。」というエラーメッセージが出ています。

それは EditorFor (type="number") でも TextBoxFor (type="text") でも同じで、数字として不正な文字が混ざって POST されると、モデルバインディングの際 int 型にパースできないということで、RegularExpressionAttribute による検証が行われる前に検証 NG となって、そのエラーメッセージが出るようです。

RegularExpressionAttribute の ErrorMessage に設定したメッセージが表示されて欲しいのですが、int 型にパースできない文字列が POST されては何ともならないようです。ただし、このエラーメッセージを書き換える方法はあります。

マイクロソフト公式解説書「プログラミング ASP.NET MVC」の p186「エラーメッセージを制御する」に書いてあったことですが、ModelStateDictionary に含まれる ModelState は同じ Key でマージした方に上書きされます。具定例は以下のコードの通りです。上の画像の「ID (int)」がこのコードによる書き換え結果です。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult WankumaEdit(Keiyaku model)
{
  if (ModelState.IsValid)
  {
    // DB の編集処理
    return RedirectToAction("Index");
  }

  // デバッグ用
  ModelStateDictionary dictionary = ModelState;

  // ValidationSummary(true) に表示するために追加
  var newDictionary = new ModelStateDictionary();
  newDictionary.AddModelError("",
    "ValidationSummary に表示するために追加。");
  ModelState.Merge(newDictionary);

  // エラーメッセージを書き換えることはできる。
  // 「プログラミング ASP.NET MVC」の p186「エラーメッセージ
  // を制御する」参照。同じ Key でマージした方に上書きされる
  ModelState state = dictionary["KeiyakuID"];

  if (state.Errors.Count > 0)
  {
    string msg = state.Errors[0].ErrorMessage;
    if (msg.StartsWith("値"))
    {
      // マージすると Value が null になるので書き戻すために
      // 取得しておく
      ValueProviderResult value = state.Value;
      var newDictionary2 = new ModelStateDictionary();
      newDictionary2.AddModelError("KeiyakuID",
        "入力不正(デフォルトの「値 'xx' は ID に対して" +
        "無効です。」を書き換え)");
      ModelState.Merge(newDictionary2);

      // Value を書き戻す。そうしないと再描画されたとき元の
      // ユーザー入力が表示されず 0 になってしまう
      ModelState["KeiyakuID"].Value = value;
    }
  }
  return View(model);
}

Tags: , ,

Validation

About this blog

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

Calendar

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

View posts in large calendar