こんにちは!
C#でWebアプリケーションを開発していると、ユーザーの入力データを検証(バリデーション)する必要性に迫られることがよくあります。
このような悩みを持つ開発者の方も多いのではないでしょうか?
この記事では、C#のIValidatableObject
インターフェースを使った、より柔軟なバリデーション方法について、基礎から応用まで、具体例を交えながら詳しくご紹介します。
この記事は次のような人におすすめです。
- C#でのバリデーション実装に悩んでいる方
- Data Annotationsの制限に困っている方
- 複数のプロパティ間の関連チェックを実装したい方
- バリデーションの仕組みをしっかり理解したい方
この記事を読めば、IValidatableObject
の使い方が分かるだけでなく、具体的な実装方法も理解できるようになります。
さらに、よくある課題への対処方法もお伝えしています。
それでは、順を追って詳しく見ていきましょう。
そもそもIValidatableObjectとは
IValidatableObject
は、.NET Frameworkが提供する、モデルクラスに対してカスタムバリデーションを実装するためのインターフェースです。
このインターフェースの最大の特徴は、プロパティ間の相関関係をチェックできるという点です。たとえば、次のようなチェックが簡単に実装できます。
- 開始日は終了日より前でなければならない
- パスワードと確認用パスワードが一致している
- 合計金額は内訳の合計と一致している
Data Annotationsだけでは実装が難しいこれらのチェックも、IValidatableObject
を使えば簡単に実装できます。
IValidatableObjectの基本構造
まず、IValidatableObject
の基本的な構造を見てみましょう。
public interface IValidatableObject
{
IEnumerable<ValidationResult> Validate(ValidationContext validationContext);
}
このインターフェースには、Validateというメソッドが1つだけ定義されています。このメソッドは以下のような特徴を持っています。
ValidationContext
を受け取り、検証のためのコンテキスト情報として使用するValidationResult
の列挙を返す- バリデーションエラーがない場合は空の列挙を返す
IValidatableObjectを使った簡単な実装例
具体的な例として、ユーザー登録フォームを想定したモデルを見てみましょう。
public class UserRegistration : IValidatableObject
{
public string Username { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
public int Age { get; set; }
public bool AcceptTerms { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
// パスワードの一致チェック
if (Password != ConfirmPassword)
{
results.Add(new ValidationResult(
"パスワードと確認用パスワードが一致しません",
new[] { nameof(Password), nameof(ConfirmPassword) }
));
}
// 年齢チェック
if (Age < 13)
{
results.Add(new ValidationResult(
"13歳未満の方は登録できません",
new[] { nameof(Age) }
));
}
// 利用規約の同意チェック
if (!AcceptTerms)
{
results.Add(new ValidationResult(
"利用規約への同意が必要です",
new[] { nameof(AcceptTerms) }
));
}
return results;
}
}
このコードについてポイントを説明します。
List<ValidationResult>
を使ってバリデーションエラーを収集します- 各
ValidationResult
には、エラーメッセージと関連するプロパティ名を指定します
nameof
演算子を使うことで、プロパティ名のタイプミスを防ぎます- 複数のプロパティを関連付けることもできます
- ユーザーにとって分かりやすい、具体的なメッセージを設定します
- 何が問題で、どうすれば解決できるのかを明確に示します
Data Annotationsとの組み合わせ
IValidatableObject
は、既存のData Annotationsと組み合わせて使うこともできます。
public class UserRegistration : IValidatableObject
{
[Required(ErrorMessage = "ユーザー名は必須です")]
[StringLength(50, ErrorMessage = "ユーザー名は50文字以内で入力してください")]
public string Username { get; set; }
[Required(ErrorMessage = "パスワードは必須です")]
[MinLength(8, ErrorMessage = "パスワードは8文字以上で入力してください")]
public string Password { get; set; }
[Required(ErrorMessage = "確認用パスワードは必須です")]
public string ConfirmPassword { get; set; }
[Range(0, 120, ErrorMessage = "年齢は0から120の間で入力してください")]
public int Age { get; set; }
public bool AcceptTerms { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (Password != ConfirmPassword)
{
results.Add(new ValidationResult(
"パスワードと確認用パスワードが一致しません",
new[] { nameof(Password), nameof(ConfirmPassword) }
));
}
if (Age < 13)
{
results.Add(new ValidationResult(
"13歳未満の方は登録できません",
new[] { nameof(Age) }
));
}
if (!AcceptTerms)
{
results.Add(new ValidationResult(
"利用規約への同意が必要です",
new[] { nameof(AcceptTerms) }
));
}
return results;
}
}
このように組み合わせることで、次のような役割分担が実現できます。
IValidatableObjectを使った具体的な実装例
ここからは、具体例を交えながら詳しく見ていきましょう。
日付の範囲チェック
イベント予約システムを例に、日付の範囲チェックを実装する方法を説明します。
public class EventReservation : IValidatableObject
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int ParticipantsCount { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
// 開始日は今日以降であることをチェック
if (StartDate.Date < DateTime.Today)
{
results.Add(new ValidationResult(
"開始日は今日以降の日付を指定してください",
new[] { nameof(StartDate) }
));
}
// 終了日は開始日以降であることをチェック
if (EndDate < StartDate)
{
results.Add(new ValidationResult(
"終了日は開始日以降の日付を指定してください",
new[] { nameof(EndDate) }
));
}
// 予約期間は最大30日までとする
var duration = (EndDate - StartDate).TotalDays;
if (duration > 30)
{
results.Add(new ValidationResult(
"予約期間は最大30日までです",
new[] { nameof(StartDate), nameof(EndDate) }
));
}
// 参加人数は期間に応じて制限
int maxParticipants = duration <= 7 ? 100 : 50;
if (ParticipantsCount > maxParticipants)
{
results.Add(new ValidationResult(
$"参加人数は{maxParticipants}人までです",
new[] { nameof(ParticipantsCount) }
));
}
return results;
}
}
このコードでは、以下のようなポイントがあります。
- 開始日が過去の日付でないことを確認します
- 終了日が開始日より前でないことを確認します
- 予約期間は30日までという制限を設けています
- 参加人数は期間によって制限が変わります
- 期間チェックでは
StartDate
とEndDate
の両方を指定します - エラーメッセージが両方のフィールドに表示されます
金額計算の整合性チェック
注文フォームを例に、金額計算の整合性をチェックする実装を見てみましょう。
public class OrderForm : IValidatableObject
{
public List<OrderItem> Items { get; set; }
public decimal SubTotal { get; set; }
public decimal TaxRate { get; set; }
public decimal TaxAmount { get; set; }
public decimal TotalAmount { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
// 注文アイテムのチェック
if (Items == null || !Items.Any())
{
results.Add(new ValidationResult(
"注文アイテムを1つ以上指定してください",
new[] { nameof(Items) }
));
return results; // これ以上のチェックは不要
}
// 小計のチェック
var calculatedSubTotal = Items.Sum(item => item.Price * item.Quantity);
if (Math.Abs(SubTotal - calculatedSubTotal) > 0.01m)
{
results.Add(new ValidationResult(
"小計が注文アイテムの合計と一致しません",
new[] { nameof(SubTotal) }
));
}
// 消費税のチェック
var calculatedTaxAmount = SubTotal * TaxRate;
if (Math.Abs(TaxAmount - calculatedTaxAmount) > 0.01m)
{
results.Add(new ValidationResult(
"消費税額が正しくありません",
new[] { nameof(TaxAmount) }
));
}
// 合計金額のチェック
var calculatedTotal = SubTotal + TaxAmount;
if (Math.Abs(TotalAmount - calculatedTotal) > 0.01m)
{
results.Add(new ValidationResult(
"合計金額が正しくありません",
new[] { nameof(TotalAmount) }
));
}
return results;
}
}
public class OrderItem
{
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
この実装におけるポイントを説明します。
- 致命的なエラー(注文アイテムがない)の場合は、それ以上のチェックを行いません
- パフォーマンスの改善とコードの簡潔さにつながります
decimal
型を使用して金額を扱います- 小数点以下の計算誤差を考慮して、差分の絶対値で比較します
- 小計、消費税、合計金額と、段階的にチェックします
- エラーの原因を特定しやすくなります
条件付きバリデーション
ユーザーの選択や入力値に応じて、バリデーションルールを変更する必要がある場合の実装を見ていきましょう。
public class DeliveryOrder : IValidatableObject
{
public string DeliveryType { get; set; } // "Standard" または "Express"
public DateTime? PreferredDate { get; set; }
public string PreferredTimeSlot { get; set; }
public string Address { get; set; }
public string BuildingName { get; set; }
public string RoomNumber { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
// 配送タイプに応じたバリデーション
switch (DeliveryType)
{
case "Standard":
// 通常配送の場合のチェック
if (PreferredDate.HasValue &&
PreferredDate.Value < DateTime.Now.AddDays(3))
{
results.Add(new ValidationResult(
"通常配送の場合、配送希望日は3日後以降を指定してください",
new[] { nameof(PreferredDate) }
));
}
break;
case "Express":
// 急配の場合のチェック
if (!PreferredDate.HasValue)
{
results.Add(new ValidationResult(
"急配の場合、配送希望日は必須です",
new[] { nameof(PreferredDate) }
));
}
if (string.IsNullOrEmpty(PreferredTimeSlot))
{
results.Add(new ValidationResult(
"急配の場合、配送希望時間帯は必須です",
new[] { nameof(PreferredTimeSlot) }
));
}
break;
default:
results.Add(new ValidationResult(
"配送タイプを選択してください",
new[] { nameof(DeliveryType) }
));
break;
}
// 住所関連のチェック
if (!string.IsNullOrEmpty(BuildingName) || !string.IsNullOrEmpty(RoomNumber))
{
if (string.IsNullOrEmpty(BuildingName))
{
results.Add(new ValidationResult(
"建物名を入力してください",
new[] { nameof(BuildingName) }
));
}
if (string.IsNullOrEmpty(RoomNumber))
{
results.Add(new ValidationResult(
"部屋番号を入力してください",
new[] { nameof(RoomNumber) }
));
}
}
return results;
}
}
この実装のポイントを説明します。
switch
文を使って、配送タイプごとに異なるバリデーションルールを適用します- それぞれの場合で必要なチェックを明確に分離します
- 建物名か部屋番号のどちらかが入力された場合、両方を必須とします
- 相互に依存する項目のバリデーションを実装します
DateTime?
のようなNULL許容型を適切にチェックしますHasValue
プロパティを使って、値の有無を確認します
ひとつひとつ真摯に向き合う企業
株式会社 ONE WEDGEでは、新たな仲間を募集しています!
私たちと一緒に、革新的で充実したキャリアを築きませんか?
当社は、従業員が仕事と私生活のバランスを大切にできるよう、充実した福利厚生を整えています。
- 完全週休2日制(土日休み)で、祝日や夏季休暇、年末年始休暇もしっかり保証!
- 様々な休暇制度(有給、慶弔、産前・産後、育児、バースデー休暇)を完備!
- 従業員の成長と健康を支援するための表彰制度、資格取得支援、健康促進手当など!
- 生活を支えるテレワーク手当、記事寄稿手当、結婚祝金・出産祝金など、様々な手当を提供!
- 自己啓発としての書籍購入制度や、メンバー間のコミュニケーションを深める交流費補助!
- 成果に応じた決算賞与や、リファラル採用手当、AI手当など、頑張りをしっかり評価!
- ワークライフバランスを重視し、副業もOK!
株式会社 ONE WEDGEでは、一人ひとりの従業員が自己実現できる環境を大切にしています。
共に成長し、刺激を与え合える仲間をお待ちしております。
あなたの能力と熱意を、ぜひ当社で発揮してください。
ご応募お待ちしております!
ホームページ、採用情報は下記ボタンからご確認ください!
応募、ご質問など、LINEでお気軽にご相談ください♪
まとめ
ここまで、IValidatableObject
の使い方について詳しく解説してきました。ポイントをおさらいしましょう。
- IValidatableObjectは柔軟なバリデーションを可能にします
- 複数のプロパティ間の相関チェックが実装できます
- 既存のData Annotationsと組み合わせて使えます
- パフォーマンスと責任分離を意識することが重要です
- 適切なエラーメッセージの設定が必要です
- テストがしやすい設計を心がけましょう
IValidatableObject
は、確かに実装の手間は必要です。しかし、複雑なバリデーションを求められる場面で、その真価を発揮します。
フォームの入力チェックやデータの整合性確認など、さまざまな用途で活用できます。ユーザー登録フォーム、注文フォーム、予約システムなど、複雑なビジネスルールを含むシステムで特に重宝します。
IValidatableObject
は、C#でのバリデーション実装における強力なツールの一つです。ぜひ、あなたのプロジェクトでも活用してみてください。