@IT の連載に「自作の検証属性を定義する(クライアントサイド編)」という .NET Framework 版の MVC アプリにカスタム検証属性を実装する記事があります。その記事のポイントはクライアント側での JavaScript による検証をフレームワーク組み込みの検証システムにどのように統合するかということです。
この記事では、@IT の記事のクライアント側での JavaScript による検証と同等な機能を ASP.NET Core MVC ではどのように実装するかということを書きます。

(注:プロパティにまたがる検証を行うための CustomValidationAttribute の話ではなく、一つのフィールドの内容を独自の条件で検証するためのものです)
.NET Framework MVC との違いは IClientValidatable の代わりに IClientModelValidator インターフェイスを継承し AddValidation メソッドを実装するところです。
Model、検証属性の定義、クライアント側での検証用 JavaScript のコードを以下にアップしておきます。注意点はそれらに書きましたので
見てください。
Model
Model は .NET Framework MVC のものと全く同じです。自作の検証属性 InArrayAttribute は Publish プロパティに付与されています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
namespace MvcCoreApp.Models
{
public class Book
{
[Key]
[Display(Name = "ISBNコード")]
[Required(ErrorMessage = "{0}は必須です。")]
[RegularExpression(
"[0-9]{3}-[0-9]{1}-[0-9]{3,5}-[0-9]{3,5}-[0-9A-Z]{1}",
ErrorMessage = "{0}はISBNの形式で入力してください。")]
public string Isbn { get; set; }
[Display(Name = "書名")]
[Required(ErrorMessage = "{0}は必須です。")]
[StringLength(100,
ErrorMessage = "{0}は{1}文字以内で入力してください。")]
public string Title { get; set; }
[Display(Name = "価格")]
[Range(100, 10000,
ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
public int? Price { get; set; }
[Display(Name = "出版社")]
[StringLength(30,
ErrorMessage = "{0}は{1}文字以内で入力してください。")]
[InArray("翔泳社,技術評論社,秀和システム," +
"毎日コミュニケーションズ,日経BP社,インプレスジャパン")]
public string Publish { get; set; }
[Display(Name = "刊行日")]
[Required(ErrorMessage = "{0}は必須です。")]
public DateTime Published { get; set; }
}
}
カスタム検証属性
出版社のリストとの比較検証を行う InArrayAttribute を定義します。IClientModelValidator を継承しているところと、AddValidation メソッドの実装に注目してください。(ちなみに .NET Framework MVC では IClientValidatable インターフェイスの GetClientValidationRules メソッドを使います)
using System;
using System.Collections.Generic;
using System.Linq;
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;
namespace MvcCoreApp.Models
{
[AttributeUsage(AttributeTargets.Property,
AllowMultiple = false)]
public class InArrayAttribute :
ValidationAttribute, IClientModelValidator
{
// 値リストを表すプライベート変数
private string _opts;
// コンストラクタ(値リストとエラーメッセージを設定)
public InArrayAttribute(string opts)
{
this._opts = opts;
this.ErrorMessage =
"{0} は「{1}」のいずれかで指定してください。";
}
// プロパティの表示名と値リストでエラーメッセージ作成
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentCulture,
ErrorMessageString, name, _opts);
}
// サーバー側での検証
// 値リストに入力値が含まれているかをチェック
public override bool IsValid(object value)
{
// 入力値が空の場合は検証をスキップ
if (value == null) { return true; }
// カンマ区切りテキストを分解し、入力値valueと比較
if (Array.IndexOf(_opts.Split(','), value) == -1)
{
return false;
}
return true;
}
// IClientModelValidator が実装するメソッド。
// 検証対象の html 要素 (input) に控えめな JavaScript
// による検証のための属性 (data-val 他) と値を追加
public void AddValidation(
ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(
context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes,
"data-val-inarray", errorMessage);
MergeAttribute(context.Attributes,
"data-val-inarray-opts", this._opts);
}
// 上の AddValidation メソッドで使うヘルパーメソッド
private bool MergeAttribute(
IDictionary<string, string> attributes,
string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
}
}
検証用 JavaScript(View に組み込み)
View にインラインで書いていますがコード自体は @IT の記事のものと同じです。検証用 JavaScript ライブラリに自作の検証スクリプトをどのように追加し、連携を取って動くようにするかという点がキモです。後者については、Unobtrusive Client Validation in ASP.NET MVC 3 という記事の中の Single value validators というセクションに記述がありました。正直読んでもよく分からなかったですが。(汗)
@model MvcCoreApp.Models.Book
・・・中略・・・
<div class="form-group">
<label asp-for="Publish" class="control-label"></label>
<input asp-for="Publish" class="form-control" />
<span asp-validation-for="Publish" class="text-danger">
</span>
</div>
・・・中略・・・
@section Scripts {
@{
await
Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script type="text/javascript">
$.validator.addMethod("inarray",
function (value, element, parameters) {
// 入力値が空の場合は検証をスキップ
value = $.trim(value);
if (value === '') {
return true;
}
// カンマ区切りテキストを分解し、入力値valueと比較
if ($.inArray(value, parameters.split(',')) === -1) {
return false;
}
return true;
});
$.validator.unobtrusive.adapters.
addSingleVal('inarray', 'opts');
</script>
}