アプリ開発ときどきアウトドア

主にJavaを使ったアプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

1. システムエンジニアリング ASP.NET Core 実装技術

ASP.NET Core: 入力検証のエラーメッセージ日本語化

投稿日:

ASP.NET Coreでは入力値を検証するための[Required]等の検証属性が提供されていますが、エラーメッセージが英語になっています。
検証属性以外でも画面の実装方法によってはモデルバインディングと言われる機能により、ユーザに英語のエラーメッセージが表示される場合があります。
ここでは、このようなユーザ向けエラーメッセージを日本語化します。

前提

  • Visual Studio 2019 + ASP.NET Core 3.1(MVC + Razor)を使用します。
  • 検証属性のユーザ向けエラーメッセージを日本語化します。
    ASP.NET Core標準の検証属性群が使用するメッセージは、ユーザが入力した値の検証に失敗した場合に使用されるものと、プログラマが検証属性に不正なパラメータを指定した場合に使用されるものに分けられます。(後者の例として、例えば[Range(100, 1)]のように大小関係を間違えて指定した場合が考えられます。)
    前者はユーザに対して表示するメッセージであるため日本語化の対象とします。後者はプログラマ向けのものなので、日本語化の対象外とします。
  • モデルバインディングのエラーメッセージも日本語化します。
    ASP.NET Coreではモデルバインディングという仕組みがあり、ユーザが入力した値はサーバ側でモデル(クラス)のプロパティに自動的に設定されます。この際、例えば「int型のプロパティに入力値”abc”を設定」などのように、変換できない場合はエラーメッセージが表示される場合があります。
    クライアント側の入力検証が有効であれば、このようなモデルバインディングでのエラーは発生しませんが、念のためのモデルバインディングのエラーメッセージも日本語化します。
  • 実行環境のカルチャに応じた使用言語の変更は行いません。
    .NET Coreのリソースマネージャは実行環境や要求に含まれるカルチャー情報に基づいて、使用する言語を変更することができます。
    日本の業務アプリでは多言語に対応するアプリは少ないことや実装を簡略化するために、固定で日本語を使用する前提とします。
  • ここで説明するサンプルは、こちらのプロジェクトに含まれています。

実現方針

  • 結構時間をかけて調べたのですが…スマートなやり方が分かりませんでした。
    結果として、他の方も参考にされている次の記事の内容をベースに対応します。
  • 検証属性のエラーメッセージ変更方式
    • 検証失敗時のエラー情報を提供するためのプロバイダクラス(IValidationMetadataProviderを実装したクラス)を作成します。検証失敗時にこのクラス(メソッド)が呼び出されるので、必要に応じてエラーメッセージを設定します。
    • 上記記事のサンプルは次の検証属性に対応していない(メッセージを上書きできない)ため、独自に条件を修正しています。
      1. CreditCardAttribute
      2. EmailAddressAttribute
      3. PhoneAttribute
      4. UrlAttribute
    • 標準の検証属性は、次の例のように属性個別にメッセージを設定できます。このように個別に設定されたメッセージがある場合、そのメッセージを優先して表示するようにします。
      [Display(Name = "正規表現2")]
      [RegularExpression("A-Z", ErrorMessage = "{0}は大文字アルファベットを指定してください。")]
      public string RegularExpressionItem2 { get; set; }
      
  • モデルバインディングのエラーメッセージ変更方式
    startupのAddMvc/AddControllers/AddControllersWithViews()使用時のMVCオプションのModelBindingMessageProviderを使用して、モデルバインディング時の各種エラーに対応するエラーメッセージを設定します。
  • 日本語メッセージのリソースはResources.resxに定義します。
    詳細は「リソース定義」節をご覧ください。

エラーメッセージの日本語化方法

検証属性のエラーメッセージを日本語化する方法と、モデルバインディングのエラーメッセージを日本語化する方法について説明します。

検証属性のエラーメッセージ

検証属性でユーザ向けに使用するエラーメッセージを日本語化します。
これは、検証失敗時のエラー情報を提供するためのプロバイダクラスの作成と、サービスへのプロバイダクラスの登録で実現します。

プロバイダクラスの実装

IValidationMetadataProviderを実装したクラスを定義します。
検証属性がエラーメッセージを使用する際にCreateValidationMetadata()メソッドが実行されます。主に、サーバ側での入力検証の時に実行されますが、クライアント側検証が有効になっている場合は、クライアント側で使用するエラーメッセージをページに埋め込むためにページ生成時にも実行されます。
このメソッドの処理で、各検証属性のエラーメッセージを必要に応じて変更します。検証属性のエラーメッセージ(やリソース)が既存から変更されていた場合、そのエラーメッセージを使用します。既存からの変更がなく、その検証属性に対応するエラーメッセージ(日本語)が定義されている場合、そのエラーメッセージを使用します。
日本語のエラーメッセージはResourceで定義しています。詳細は「リソース定義」節をご覧ください。

public class CustomValidationMetadataProvider : IValidationMetadataProvider
{
    private const string RESOURCE_KEY_PREFIX = "Validator_";

    private ResourceManager resourceManager;

    private Type resourceType;

    private Dictionary<Type, string> defaultMessageDic;

    public CustomValidationMetadataProvider()
    {
        // サテライトアセンブリ等で動的に言語を変更する予定はないので固定で指定
        resourceType = typeof(Resource);
        string baseName = resourceType.FullName ?? string.Empty;
        Assembly ass = resourceType.GetTypeInfo().Assembly;
        resourceManager = new ResourceManager(baseName, ass);

        // 既定でメッセージが設定されている属性と既定メッセージ
        // (後の「既存メッセージからの変更有無」を判定するために使用)
        var dic = new Dictionary<Type, string>();
        dic.Add(typeof(CreditCardAttribute), @"The {0} field is not a valid credit card number.");
        dic.Add(typeof(EmailAddressAttribute), @"The {0} field is not a valid e-mail address.");
        dic.Add(typeof(PhoneAttribute), @"The {0} field is not a valid phone number.");
        dic.Add(typeof(UrlAttribute), @"The {0} field is not a valid fully-qualified http, https, or ftp URL.");
        this.defaultMessageDic = dic;
    }

    public void CreateValidationMetadata(
        ValidationMetadataProviderContext context)
    {
        var metaData = context.ValidationMetadata.ValidatorMetadata;

        // int/Decimal/DateTime等の値型の場合、
        // 暗黙的に必須属性が追加されるので、そのメッセージも置き換え
        if (context.Key.ModelType.GetTypeInfo().IsValueType &&
            metaData.Where(m => m.GetType() == typeof(RequiredAttribute)).Count() == 0)
        {
            metaData.Add(new RequiredAttribute());
        }

        // 対象プロパティに紐づく全ての属性に対して処理
        foreach (var obj in metaData)
        {
            if (!(obj is ValidationAttribute attr))
            {
                continue;
            }

            // リソースやメッセージが変更されている場合はそれを優先
            // (新旧メッセージが共にnullも「変更なし」とみなす)
            if (attr.ErrorMessageResourceName != null)
            {
                continue;
            }
            Type type = attr.GetType();
            string message = attr.ErrorMessage;
            string? defaultMessage = this.defaultMessageDic.GetValueOrDefault(type);
            if (!string.Equals(message, defaultMessage))
            {
                continue;
            }

            // メッセージが既定から変更されておらず、
            // 対応するメッセージが未定義の場合は既定の動作に任せる
            string name = RESOURCE_KEY_PREFIX + type.Name;
            string? newMessage = resourceManager.GetString(name);
            if (string.IsNullOrEmpty(newMessage) )
            {
                continue;
            }

            // メッセージが既定から変更されておらず、
            // 対応するメッセージが定義されている場合、それで上書き
            attr.ErrorMessageResourceType = resourceType;
            attr.ErrorMessageResourceName = name;
            attr.ErrorMessage = null;
        }
    }
}

エラーメッセージが既存から変更されたかどうかの判定に関する補足です。
当初、参考記事のサンプルのように「検証属性のエラーメッセージプロパティがnullの場合はメッセージは変更されていない。」という条件で良いと考えていました。実際にやってみると、CreditCard等の一部属性ではエラーメッセージを設定していないにも関わらず、エラーメッセージプロパティにメッセージが設定されており、誤判定されます。
そのため、次のように判定しています。(といっても、どうちらのケースも59行目のようにstring.Equals()の1行で判定しています。)

対象の属性 既定のエラーメッセージ
(ErrorMessageの値)
「変更なし」の判定方法
CreditCard
EmailAddress
Phone
Url
“The {0} field is not a valid credit card number.”等のようなメッセージが設定されています。 検証属性のエラーメッセージが左記の既定のエラーメッセージと一致する。
Compare
Range
RegularExpression
Required
StringLength
nullが設定されています。
(検証属性側でメッセージを生成)
検証属性のエラーメッセージがnullである。

プロバイダの登録

StartupのMVC系サービス登録時のMVCオプションとして上記のプロバイダクラスを指定します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options => {
		...
        options.ModelMetadataDetailsProviders.Add(
            new CustomValidationMetadataProvider()
        );
		...
    });

モデルバインディングのエラーメッセージ

モデルバインディングで使用するエラーメッセージを日本語化します。

MVCオプションのModelBindingMessageProviderを使って、各ケース毎のエラーメッセージを指定します。
string.formatをいちいち記載するのが嫌だったので、ラムダ式(Func)を使っています。
日本語のエラーメッセージはResourceで定義しています。詳細は「リソース定義」節をご覧ください。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options => {
        ...
        // モデルバインディング失敗時のエラーメッセージをカスタマイズ
        // (サーバ側でモデルに値を格納する際に発生する可能性がある。)
        Func<string, string, string> f1 = (f, a1) => string.Format(f, a1);
        Func<string, string, string, string> f2 = (f, a1, a2) => string.Format(f, a1, a2);
        var mp = options.ModelBindingMessageProvider;
        mp.SetAttemptedValueIsInvalidAccessor((x, y) =>
            f2(Resource.ModelBinding_AttemptedValueIsInvalid, x, y));
        mp.SetMissingBindRequiredValueAccessor((x) =>
            f1(Resource.ModelBinding_MissingBindRequiredValue, x));
        mp.SetMissingKeyOrValueAccessor(() => 
            Resource.ModelBinding_MissingKeyOrValue);
        mp.SetMissingRequestBodyRequiredValueAccessor(() =>
            Resource.ModelBinding_MissingRequestBodyRequiredValue);
        mp.SetNonPropertyAttemptedValueIsInvalidAccessor((x) =>
            f1(Resource.ModelBinding_NonPropertyAttemptedValueIsInvalid, x));
        mp.SetNonPropertyUnknownValueIsInvalidAccessor(() =>
            Resource.ModelBinding_NonPropertyUnknownValueIsInvalid);
        mp.SetNonPropertyValueMustBeANumberAccessor(() =>
            Resource.ModelBinding_NonPropertyValueMustBeANumber);
        mp.SetUnknownValueIsInvalidAccessor((x) =>
            f1(Resource.ModelBinding_UnknownValueIsInvalid, x));
        mp.SetValueIsInvalidAccessor((x) =>
            f1(Resource.ModelBinding_ValueIsInvalid, x));
        mp.SetValueMustBeANumberAccessor((x) =>
            f1(Resource.ModelBinding_ValueMustBeANumber, x));
        mp.SetValueMustNotBeNullAccessor((x) =>
            f1(Resource.ModelBinding_ValueMustNotBeNull, x));
        ...
    });

リソースの定義

前述の検証属性やモデルバインディングのエラーメッセージ日本語化で使用しているリソースは次の通りです。

  • “Resource.resx”をデザイナではなくテキストエディタで開いた例です。
    コピーする場合は同様に開けば容易かと思います。
  • 用途が分かるようリソース名は次のルールで決定しています。
    • 検証属性のリソース名: “Validator_” + (属性クラス名)
    • モデルバインディングのリソース名: “ModelBinding_”+(プロパティ名)
  • 参考としてコメントに既存のメッセージを定義しています。
    既存のメッセージの追跡方法は「参考」をご覧ください。
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema 
    ...
    -->
  ...
  <data name="ModelBinding_AttemptedValueIsInvalid" xml:space="preserve">
    <value>{1}では'{0}'は無効な値です。</value>
    <comment>The value '{0}' is not valid for {1}.</comment>
  </data>
  <data name="ModelBinding_MissingBindRequiredValue" xml:space="preserve">
    <value>{0}に対する値が指定されていません。</value>
    <comment>A value for the '{0}' parameter or property was not provided.</comment>
  </data>
  <data name="ModelBinding_MissingKeyOrValue" xml:space="preserve">
    <value>必須です。</value>
    <comment>A value is required.</comment>
  </data>
  <data name="ModelBinding_MissingRequestBodyRequiredValue" xml:space="preserve">
    <value>要求にボディの指定が必須です。</value>
    <comment>A non-empty request body is required.</comment>
  </data>
  <data name="ModelBinding_NonPropertyAttemptedValueIsInvalid" xml:space="preserve">
    <value>'{0}'は無効です。</value>
    <comment>The value '{0}' is not valid.</comment>
  </data>
  <data name="ModelBinding_NonPropertyUnknownValueIsInvalid" xml:space="preserve">
    <value>値は無効です。</value>
    <comment>The supplied value is invalid.</comment>
  </data>
  <data name="ModelBinding_NonPropertyValueMustBeANumber" xml:space="preserve">
    <value>数字を指定してください。</value>
    <comment>The field must be a number.</comment>
  </data>
  <data name="ModelBinding_UnknownValueIsInvalid" xml:space="preserve">
    <value>{0}の値は無効です。</value>
    <comment>The supplied value is invalid for {0}.</comment>
  </data>
  <data name="ModelBinding_ValueIsInvalid" xml:space="preserve">
    <value>'{0}'は無効です。</value>
    <comment>The value '{0}' is invalid.</comment>
  </data>
  <data name="ModelBinding_ValueMustBeANumber" xml:space="preserve">
    <value>{0}は数字を指定してください。</value>
    <comment>The field {0} must be a number.</comment>
  </data>
  <data name="ModelBinding_ValueMustNotBeNull" xml:space="preserve">
    <value>'{0}'は無効です。</value>
    <comment>The value '{0}' is invalid.</comment>
  </data>
  <data name="Validator_CompareAttribute" xml:space="preserve">
    <value>{0}と{1}が一致しません。</value>
    <comment>'{0}' and '{1}' do not match.</comment>
  </data>
  <data name="Validator_CreditCardAttribute" xml:space="preserve">
    <value>{0}は有効なカード番号ではありません。</value>
    <comment>The {0} field is not a valid credit card number.</comment>
  </data>
  <data name="Validator_EmailAddressAttribute" xml:space="preserve">
    <value>{0}は有効なメールアドレスではありません。</value>
    <comment>The {0} field is not a valid e-mail address.</comment>
  </data>
  <data name="Validator_PhoneAttribute" xml:space="preserve">
    <value>{0}は有効な電話番号ではありません。</value>
    <comment>The {0} field is not a valid phone number.</comment>
  </data>
  <data name="Validator_RangeAttribute" xml:space="preserve">
    <value>{0}は{1}から{2}の範囲で指定してください。</value>
    <comment>The field {0} must be between {1} and {2}.</comment>
  </data>
  <data name="Validator_RegularExpressionAttribute" xml:space="preserve">
    <value>{0}は正規表現'{1}'に一致するように指定してください。</value>
    <comment>The field {0} must match the regular expression '{1}'.</comment>
  </data>
  <data name="Validator_RequiredAttribute" xml:space="preserve">
    <value>{0}は必須です。</value>
    <comment>The {0} field is required.</comment>
  </data>
  <data name="Validator_StringLengthAttribute" xml:space="preserve">
    <value>{0}は{1}桁以内で指定してください。</value>
    <comment>The field {0} must be a string with a maximum length of {1}.</comment>
  </data>
  <data name="Validator_UrlAttribute" xml:space="preserve">
    <value>{0}は有効なURLではありません。</value>
    <comment>The {0} field is not a valid fully-qualified http, https, or ftp URL.</comment>
  </data>
</root>

参考

検証属性の既定のエラーメッセージ

ASP.NET Core標準の検証属性は次のように既定のエラーメッセージを参照しています。
この仕組みを追跡して既定のエラーメッセージを列挙しています。

  1. 検証属性の各コンストラクタにて、既定で使用するメッセージとしてのDataAnnotationsResourcesリソースのプロパティ名を指定している。
    public class RequiredAttribute : ValidationAttribute {
        public RequiredAttribute()
            : base(() => DataAnnotationsResources.RequiredAttribute_ValidationError) {
        }
    
  2. 上記のリソースはDataAnnotationsResourcesで定義されている。
    ...
      <data name="RequiredAttribute_ValidationError" xml:space="preserve">
        <value>The {0} field is required.</value>
      </data>
      <data name="StringLengthAttribute_InvalidMaxLength" xml:space="preserve">
        <value>The maximum length must be a nonnegative integer.</value>
      </data>
      <data name="StringLengthAttribute_ValidationError" xml:space="preserve">
        <value>The field {0} must be a string with a maximum length of {1}.</value>
      </data>
    ...
    

    このリソースには、入力値の検証エラー時に使用するユーザ向けのエラーメッセージと、検証属性に対するパラメータ不正時に使用されるエラーメッセージが定義されている。
    前者のリソース名は概ねRequiredAttribute_ValidationErrorのように(属性名)_ValidationErrorというネーミングになっている。

モデルバインディングの既定のエラーメッセージ

モデルバインディングでは次のように既定のエラーメッセージを参照しています。
この仕組みを追跡して既定のエラーメッセージを列挙しています。

  1. メッセージの設定で使用したMVCオプションのModelBindingMessageProviderプロパティの実体は、DefaultModelBindingMessageProviderである。このクラスのコンストラクタで各ケースで使用する既定のエラーメッセージをResourceのプロパティ名で指定している。
    public DefaultModelBindingMessageProvider()
    {
        ...
        SetValueMustNotBeNullAccessor(Resources.FormatModelBinding_NullValueNotValid);
        ...
    
  2. 上記で参照しているリソースのプロパティは、リソースのデザイナで生成されたResources.Designer.csで定義されている。
    ...
    internal static string FormatModelBinding_NullValueNotValid(object p0)
       => string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_NullValueNotValid"), p0);
    ...
    
  3. 上記のプロパティで参照しているリソース名はResources.resxで定義されている。
    ...
      <data name="ModelBinding_NullValueNotValid" xml:space="preserve">
        <value>The value '{0}' is invalid.</value>
      </data>
    ...
    

参考URL



(adsbygoogle = window.adsbygoogle || []).push({});


(adsbygoogle = window.adsbygoogle || []).push({});

-1. システムエンジニアリング, ASP.NET Core, 実装技術

執筆者:

関連記事

Javaでのパスワード付きzipファイルの圧縮/解凍方法(ZipCrypto/AES)

先日、JavaでのZIP暗号化の考察という記事を書きましたが、zip4jのメンテナンスが再開されており、バージョン2系が公開されていましたので、これを使って通常のzip圧縮/解凍、パスワード付きzip …

疎通確認用pingツール

新人君たちと本番環境の構築作業でデータセンターに入り。 構築したサーバから、既存の重要なサーバへの疎通確認を行うために、pingを何度も入力する予定とのこと。 作業時間の短縮や間違いの低減のために、こ …

slf4jとlog4j2を使たデバッグログの出力方法

Webアプリやスタンドアロンアプリの開発でデバッグログやトレースログを出したい場合があります。 とりあえず、ロガーのログレベルをdebugやtraceに下げればいいや、と設定してもログが出力されない場 …

OWASPが推奨する強力なパスワード

Webアプリケーションのセキュリティの標準化や推進を行うOWASPでは、強力なパスワードの使用を推奨しているので、それをまとめた。 cheatsheetseries.owasp.orgAuthenti …

wildflyへのwarデプロイの自動化

更新したWebアプリをWildflyにデプロイするのが面倒なのでスクリプトを作成してみました。 前提 実行環境はCentOS Linux 7です。 JavaEEのWebアプリの配布形式であるwarファ …

プロフィール ゆっきーです。
都内でシステムエンジニアをやっています。
もっと詳細を見る