Foolproof
ライブラリ
Foolproof
概要
FoolproofはASP.NET MVC向けの条件付きバリデーションライブラリです。標準のData Annotationsでは実現困難な条件付きバリデーションを属性ベースで簡単に実装できます。RequiredIf、RequiredIfNot、RequiredIfTrue、RequiredIfFalseなどの豊富な条件付きバリデーション属性を提供し、クライアントサイド検証も自動的に生成します。複雑なビジネスルールを宣言的に表現でき、フォームの動的な検証要件に対応できます。
詳細
Foolproofは、ASP.NET MVCにおいて条件付きバリデーションを実現するための専用ライブラリです。他のフィールドの値に基づいて動的にバリデーションルールを適用でき、複雑なフォームロジックを属性レベルで定義できます。クライアントサイド検証の自動生成により、サーバーサイドとクライアントサイドで一貫した検証を実現します。jQuery Unobtrusive Validationと連携し、リアルタイムでのユーザー体験を向上させます。RequiredIfEmpty、RequiredIfNotEmpty、RequiredIfRegExMatchなどの多様な条件パターンをサポートします。
主な特徴
- 条件付きバリデーション: 他のフィールドの値に基づく動的検証
- 豊富な条件パターン: True/False、正規表現、空値チェックなど
- クライアントサイド対応: 自動的なJavaScript検証生成
- MVC統合: ASP.NET MVCとの完全な統合
- 宣言的構文: 属性による直感的な条件定義
- 既存コード互換: Data Annotationsとの共存が可能
メリット・デメリット
メリット
- 複雑な条件付きバリデーションを簡潔に記述
- クライアントサイド検証の自動生成
- 属性ベースの宣言的プログラミング
- ASP.NET MVCとの深い統合
- 豊富な条件パターンの提供
- 既存のData Annotationsとの互換性
デメリット
- ASP.NET MVCに依存(他のフレームワークでは使用不可)
- 複雑すぎる条件では可読性が低下
- 動的なバリデーションルールの実装が困難
- Entity Frameworkとの統合に制約
- 現在のメンテナンス状況が不明確
- 最新の.NET Coreバージョンへの対応状況
参考ページ
書き方の例
基本的な条件付きバリデーション
using System;
using System.ComponentModel.DataAnnotations;
using Foolproof;
// 基本的な条件付きバリデーション
public class UserRegistrationModel
{
[Required(ErrorMessage = "名前は必須項目です")]
[Display(Name = "名前")]
public string Name { get; set; }
[Required(ErrorMessage = "メールアドレスは必須項目です")]
[EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください")]
[Display(Name = "メールアドレス")]
public string Email { get; set; }
[Display(Name = "結婚している")]
public bool IsMarried { get; set; }
// 結婚している場合のみ、配偶者の名前が必須
[RequiredIfTrue("IsMarried", ErrorMessage = "結婚している場合は配偶者の名前を入力してください")]
[Display(Name = "配偶者の名前")]
public string SpouseName { get; set; }
[Display(Name = "年齢")]
[Range(0, 120, ErrorMessage = "年齢は0歳から120歳の間で入力してください")]
public int Age { get; set; }
// 18歳未満の場合、保護者の同意が必要
[RequiredIf("Age", Operator.LessThan, 18, ErrorMessage = "18歳未満の場合は保護者の同意が必要です")]
[Display(Name = "保護者の同意")]
public bool ParentalConsent { get; set; }
[Display(Name = "運転免許証を持っている")]
public bool HasDriverLicense { get; set; }
// 運転免許証を持っている場合、免許証番号が必須
[RequiredIfTrue("HasDriverLicense", ErrorMessage = "運転免許証を持っている場合は免許証番号を入力してください")]
[Display(Name = "免許証番号")]
public string DriverLicenseNumber { get; set; }
[Display(Name = "住所タイプ")]
public string AddressType { get; set; }
// 住所タイプが「自宅」の場合、住所が必須
[RequiredIf("AddressType", "自宅", ErrorMessage = "住所タイプが「自宅」の場合は住所を入力してください")]
[Display(Name = "住所")]
public string Address { get; set; }
[Display(Name = "電話番号タイプ")]
public string PhoneType { get; set; }
// 電話番号タイプが空でない場合、電話番号が必須
[RequiredIfNotEmpty("PhoneType", ErrorMessage = "電話番号タイプを選択した場合は電話番号を入力してください")]
[Display(Name = "電話番号")]
public string PhoneNumber { get; set; }
[Display(Name = "国籍")]
public string Country { get; set; }
// 国籍が「日本」でない場合、ビザ情報が必須
[RequiredIfNot("Country", "日本", ErrorMessage = "日本国籍でない場合はビザ情報を入力してください")]
[Display(Name = "ビザ情報")]
public string VisaInfo { get; set; }
}
// 雇用情報モデル
public class EmploymentModel
{
[Required(ErrorMessage = "名前は必須項目です")]
[Display(Name = "名前")]
public string Name { get; set; }
[Display(Name = "雇用状況")]
public string EmploymentStatus { get; set; }
// 雇用状況が「正社員」の場合、会社名が必須
[RequiredIf("EmploymentStatus", "正社員", ErrorMessage = "正社員の場合は会社名を入力してください")]
[Display(Name = "会社名")]
public string CompanyName { get; set; }
// 雇用状況が「正社員」の場合、年収が必須
[RequiredIf("EmploymentStatus", "正社員", ErrorMessage = "正社員の場合は年収を入力してください")]
[Display(Name = "年収")]
public decimal? AnnualSalary { get; set; }
// 雇用状況が「学生」の場合、学校名が必須
[RequiredIf("EmploymentStatus", "学生", ErrorMessage = "学生の場合は学校名を入力してください")]
[Display(Name = "学校名")]
public string SchoolName { get; set; }
// 雇用状況が「退職」の場合、退職理由が必須
[RequiredIf("EmploymentStatus", "退職", ErrorMessage = "退職の場合は退職理由を入力してください")]
[Display(Name = "退職理由")]
public string RetirementReason { get; set; }
[Display(Name = "副業をしている")]
public bool HasSideJob { get; set; }
// 副業をしている場合、副業内容が必須
[RequiredIfTrue("HasSideJob", ErrorMessage = "副業をしている場合は副業内容を入力してください")]
[Display(Name = "副業内容")]
public string SideJobDescription { get; set; }
[Display(Name = "リモートワーク希望")]
public bool WantsRemoteWork { get; set; }
// リモートワークを希望する場合、在宅勤務環境の説明が必須
[RequiredIfTrue("WantsRemoteWork", ErrorMessage = "リモートワークを希望する場合は在宅勤務環境を説明してください")]
[Display(Name = "在宅勤務環境")]
public string RemoteWorkEnvironment { get; set; }
}
// 注文情報モデル
public class OrderModel
{
[Required(ErrorMessage = "顧客名は必須項目です")]
[Display(Name = "顧客名")]
public string CustomerName { get; set; }
[Display(Name = "配送方法")]
public string DeliveryMethod { get; set; }
// 配送方法が「宅配」の場合、配送先住所が必須
[RequiredIf("DeliveryMethod", "宅配", ErrorMessage = "宅配の場合は配送先住所を入力してください")]
[Display(Name = "配送先住所")]
public string DeliveryAddress { get; set; }
// 配送方法が「宅配」の場合、配送希望日が必須
[RequiredIf("DeliveryMethod", "宅配", ErrorMessage = "宅配の場合は配送希望日を入力してください")]
[Display(Name = "配送希望日")]
public DateTime? DeliveryDate { get; set; }
[Display(Name = "支払い方法")]
public string PaymentMethod { get; set; }
// 支払い方法が「クレジットカード」の場合、カード番号が必須
[RequiredIf("PaymentMethod", "クレジットカード", ErrorMessage = "クレジットカードの場合はカード番号を入力してください")]
[Display(Name = "クレジットカード番号")]
public string CreditCardNumber { get; set; }
// 支払い方法が「銀行振込」の場合、振込口座が必須
[RequiredIf("PaymentMethod", "銀行振込", ErrorMessage = "銀行振込の場合は振込口座を入力してください")]
[Display(Name = "振込口座")]
public string BankAccount { get; set; }
[Display(Name = "ギフト包装希望")]
public bool WantsGiftWrap { get; set; }
// ギフト包装を希望する場合、メッセージカードが必須
[RequiredIfTrue("WantsGiftWrap", ErrorMessage = "ギフト包装を希望する場合はメッセージカードを入力してください")]
[Display(Name = "メッセージカード")]
public string GiftMessage { get; set; }
[Display(Name = "緊急性")]
public string Urgency { get; set; }
// 緊急性が「緊急」の場合、緊急連絡先が必須
[RequiredIf("Urgency", "緊急", ErrorMessage = "緊急の場合は緊急連絡先を入力してください")]
[Display(Name = "緊急連絡先")]
public string EmergencyContact { get; set; }
}
複雑な条件付きバリデーション
using System;
using System.ComponentModel.DataAnnotations;
using Foolproof;
// 複雑な条件を持つ保険申請モデル
public class InsuranceApplicationModel
{
[Required(ErrorMessage = "申請者名は必須項目です")]
[Display(Name = "申請者名")]
public string ApplicantName { get; set; }
[Required(ErrorMessage = "年齢は必須項目です")]
[Range(0, 120, ErrorMessage = "年齢は0歳から120歳の間で入力してください")]
[Display(Name = "年齢")]
public int Age { get; set; }
[Display(Name = "性別")]
public string Gender { get; set; }
[Display(Name = "既往歴がある")]
public bool HasMedicalHistory { get; set; }
// 既往歴がある場合、詳細が必須
[RequiredIfTrue("HasMedicalHistory", ErrorMessage = "既往歴がある場合は詳細を入力してください")]
[Display(Name = "既往歴の詳細")]
public string MedicalHistoryDetails { get; set; }
[Display(Name = "職業")]
public string Occupation { get; set; }
// 職業が「パイロット」「レーサー」「登山家」の場合、特別な書類が必要
[RequiredIfRegExMatch("Occupation", @"パイロット|レーサー|登山家",
ErrorMessage = "危険な職業の場合は特別な書類が必要です")]
[Display(Name = "特別書類")]
public string SpecialDocuments { get; set; }
[Display(Name = "保険金額")]
[Range(1000000, 100000000, ErrorMessage = "保険金額は100万円から1億円の間で入力してください")]
public decimal InsuranceAmount { get; set; }
// 保険金額が5000万円以上の場合、所得証明が必要
[RequiredIf("InsuranceAmount", Operator.GreaterThanOrEqualTo, 50000000,
ErrorMessage = "保険金額が5000万円以上の場合は所得証明が必要です")]
[Display(Name = "所得証明")]
public string IncomeProof { get; set; }
[Display(Name = "喫煙者")]
public bool IsSmoker { get; set; }
// 喫煙者の場合、1日の喫煙本数が必須
[RequiredIfTrue("IsSmoker", ErrorMessage = "喫煙者の場合は1日の喫煙本数を入力してください")]
[Display(Name = "1日の喫煙本数")]
public int? CigarettesPerDay { get; set; }
[Display(Name = "飲酒習慣")]
public string DrinkingHabits { get; set; }
// 飲酒習慣が「毎日」の場合、1日の飲酒量が必須
[RequiredIf("DrinkingHabits", "毎日", ErrorMessage = "毎日飲酒する場合は1日の飲酒量を入力してください")]
[Display(Name = "1日の飲酒量")]
public string AlcoholConsumption { get; set; }
[Display(Name = "運動習慣")]
public string ExerciseHabits { get; set; }
// 運動習慣が「なし」の場合、理由が必須
[RequiredIf("ExerciseHabits", "なし", ErrorMessage = "運動習慣がない場合は理由を入力してください")]
[Display(Name = "運動をしない理由")]
public string NoExerciseReason { get; set; }
[Display(Name = "保険金受取人")]
public string Beneficiary { get; set; }
// 保険金受取人が「その他」の場合、関係性の説明が必須
[RequiredIf("Beneficiary", "その他", ErrorMessage = "保険金受取人が「その他」の場合は関係性を説明してください")]
[Display(Name = "受取人との関係")]
public string BeneficiaryRelationship { get; set; }
[Display(Name = "海外在住経験")]
public bool HasOverseasExperience { get; set; }
// 海外在住経験がある場合、国と期間が必須
[RequiredIfTrue("HasOverseasExperience", ErrorMessage = "海外在住経験がある場合は国と期間を入力してください")]
[Display(Name = "海外在住詳細")]
public string OverseasDetails { get; set; }
}
// 医療情報モデル
public class MedicalInfoModel
{
[Required(ErrorMessage = "患者名は必須項目です")]
[Display(Name = "患者名")]
public string PatientName { get; set; }
[Display(Name = "症状の種類")]
public string SymptomType { get; set; }
// 症状の種類が「アレルギー」の場合、アレルゲンが必須
[RequiredIf("SymptomType", "アレルギー", ErrorMessage = "アレルギーの場合はアレルゲンを入力してください")]
[Display(Name = "アレルゲン")]
public string Allergen { get; set; }
// 症状の種類が「感染症」の場合、感染経路が必須
[RequiredIf("SymptomType", "感染症", ErrorMessage = "感染症の場合は感染経路を入力してください")]
[Display(Name = "感染経路")]
public string InfectionRoute { get; set; }
[Display(Name = "緊急度")]
public string Urgency { get; set; }
// 緊急度が「緊急」の場合、症状発症時刻が必須
[RequiredIf("Urgency", "緊急", ErrorMessage = "緊急の場合は症状発症時刻を入力してください")]
[Display(Name = "症状発症時刻")]
public DateTime? SymptomOnsetTime { get; set; }
[Display(Name = "妊娠中")]
public bool IsPregnant { get; set; }
// 妊娠中の場合、妊娠週数が必須
[RequiredIfTrue("IsPregnant", ErrorMessage = "妊娠中の場合は妊娠週数を入力してください")]
[Display(Name = "妊娠週数")]
public int? PregnancyWeeks { get; set; }
[Display(Name = "服用中の薬")]
public string CurrentMedication { get; set; }
// 服用中の薬が「あり」の場合、薬名が必須
[RequiredIf("CurrentMedication", "あり", ErrorMessage = "服用中の薬がある場合は薬名を入力してください")]
[Display(Name = "薬名")]
public string MedicationNames { get; set; }
[Display(Name = "手術歴")]
public bool HasSurgeryHistory { get; set; }
// 手術歴がある場合、手術内容が必須
[RequiredIfTrue("HasSurgeryHistory", ErrorMessage = "手術歴がある場合は手術内容を入力してください")]
[Display(Name = "手術内容")]
public string SurgeryDetails { get; set; }
[Display(Name = "家族歴")]
public string FamilyHistory { get; set; }
// 家族歴が「あり」の場合、詳細が必須
[RequiredIf("FamilyHistory", "あり", ErrorMessage = "家族歴がある場合は詳細を入力してください")]
[Display(Name = "家族歴の詳細")]
public string FamilyHistoryDetails { get; set; }
[Display(Name = "血液型")]
public string BloodType { get; set; }
// 血液型が「不明」の場合、血液型検査の希望が必須
[RequiredIf("BloodType", "不明", ErrorMessage = "血液型が不明の場合は血液型検査の希望を入力してください")]
[Display(Name = "血液型検査希望")]
public bool WantsBloodTypeTest { get; set; }
}
// 複雑な条件の組み合わせ例
public class ComplexValidationModel
{
[Display(Name = "年齢")]
[Range(0, 120)]
public int Age { get; set; }
[Display(Name = "性別")]
public string Gender { get; set; }
[Display(Name = "職業")]
public string Occupation { get; set; }
[Display(Name = "年収")]
public decimal? Income { get; set; }
// 年齢が65歳以上かつ性別が男性の場合、年金受給証明が必要
[RequiredIfTrue("IsElderly", ErrorMessage = "65歳以上の男性は年金受給証明が必要です")]
[Display(Name = "年金受給証明")]
public string PensionProof { get; set; }
// 計算プロパティ(バリデーション用)
public bool IsElderly => Age >= 65 && Gender == "男性";
// 職業が「会社員」で年収が1000万円以上の場合、所得証明が必要
[RequiredIfTrue("IsHighIncomeEmployee", ErrorMessage = "年収1000万円以上の会社員は所得証明が必要です")]
[Display(Name = "所得証明")]
public string IncomeProof { get; set; }
// 計算プロパティ(バリデーション用)
public bool IsHighIncomeEmployee => Occupation == "会社員" && Income >= 10000000;
[Display(Name = "結婚状況")]
public string MaritalStatus { get; set; }
[Display(Name = "子供の数")]
public int NumberOfChildren { get; set; }
// 結婚していて子供がいる場合、配偶者の職業が必要
[RequiredIfTrue("IsMarriedWithChildren", ErrorMessage = "結婚していて子供がいる場合は配偶者の職業を入力してください")]
[Display(Name = "配偶者の職業")]
public string SpouseOccupation { get; set; }
// 計算プロパティ(バリデーション用)
public bool IsMarriedWithChildren => MaritalStatus == "既婚" && NumberOfChildren > 0;
[Display(Name = "住所タイプ")]
public string AddressType { get; set; }
[Display(Name = "居住年数")]
public int YearsOfResidence { get; set; }
// 賃貸で居住年数が1年未満の場合、前住所が必要
[RequiredIfTrue("IsShortTermRenter", ErrorMessage = "賃貸で居住年数が1年未満の場合は前住所を入力してください")]
[Display(Name = "前住所")]
public string PreviousAddress { get; set; }
// 計算プロパティ(バリデーション用)
public bool IsShortTermRenter => AddressType == "賃貸" && YearsOfResidence < 1;
}
ASP.NET MVCでの実装例
using System;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using Foolproof;
// MVCコントローラーでの使用例
public class RegistrationController : Controller
{
// GET: Registration
public ActionResult Create()
{
var model = new UserRegistrationModel();
return View(model);
}
// POST: Registration
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(UserRegistrationModel model)
{
if (ModelState.IsValid)
{
// カスタムバリデーション(ビジネスロジック)
if (model.IsMarried && string.IsNullOrEmpty(model.SpouseName))
{
ModelState.AddModelError("SpouseName", "結婚している場合は配偶者の名前を入力してください");
}
if (model.Age < 18 && !model.ParentalConsent)
{
ModelState.AddModelError("ParentalConsent", "18歳未満の場合は保護者の同意が必要です");
}
// 重複チェック
if (IsEmailDuplicate(model.Email))
{
ModelState.AddModelError("Email", "このメールアドレスは既に登録されています");
}
if (ModelState.IsValid)
{
// データベースに保存
SaveUserRegistration(model);
TempData["Success"] = "登録が完了しました";
return RedirectToAction("Success");
}
}
// バリデーションエラーがある場合は再度フォームを表示
return View(model);
}
// Ajax バリデーション
[HttpPost]
public JsonResult ValidateEmail(string email)
{
bool isValid = !IsEmailDuplicate(email);
return Json(new { isValid = isValid, message = isValid ? "" : "このメールアドレスは既に登録されています" });
}
private bool IsEmailDuplicate(string email)
{
// 実際の実装ではデータベースをチェック
var existingEmails = new[] { "[email protected]", "[email protected]" };
return Array.Exists(existingEmails, e => e.Equals(email, StringComparison.OrdinalIgnoreCase));
}
private void SaveUserRegistration(UserRegistrationModel model)
{
// 実際の実装ではデータベースに保存
// 簡略化のため、ここでは何もしない
}
public ActionResult Success()
{
return View();
}
}
// Razorビューでの使用例(Views/Registration/Create.cshtml)
/*
@model UserRegistrationModel
@{
ViewBag.Title = "ユーザー登録";
}
<h2>ユーザー登録</h2>
@using (Html.BeginForm("Create", "Registration", FormMethod.Post, new { @class = "form-horizontal" }))
{
@Html.AntiForgeryToken()
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Age, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Age, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Age, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
@Html.EditorFor(model => model.IsMarried)
@Html.LabelFor(model => model.IsMarried, "結婚している")
</div>
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.SpouseName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.SpouseName, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.SpouseName, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
@Html.EditorFor(model => model.HasDriverLicense)
@Html.LabelFor(model => model.HasDriverLicense, "運転免許証を持っている")
</div>
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.DriverLicenseNumber, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.DriverLicenseNumber, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.DriverLicenseNumber, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.AddressType, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(model => model.AddressType, new SelectList(new[] { "自宅", "勤務先", "その他" }), "選択してください", new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.AddressType, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Address, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Address, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Address, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Country, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(model => model.Country, new SelectList(new[] { "日本", "アメリカ", "イギリス", "その他" }), "選択してください", new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Country, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.VisaInfo, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.VisaInfo, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.VisaInfo, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="登録" class="btn btn-primary" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script src="~/Scripts/mvcfoolproof.unobtrusive.min.js"></script>
}
*/