Data Annotations

バリデーションライブラリC#.NET標準ライブラリ属性MVCEntity Framework

ライブラリ

Data Annotations

概要

Data Annotationsは、.NET Frameworkの標準ライブラリの一部として提供される属性ベースのバリデーションシステムです。System.ComponentModel.DataAnnotations名前空間に含まれ、クラスのプロパティに属性を付与することで、宣言的にバリデーションルールを定義できます。ASP.NET MVC、Entity Framework、Blazorなど.NETエコシステム全体で広く使用されており、シンプルで直感的な構文により、データモデルの検証を効率的に実装できます。

詳細

Data Annotationsは、.NET標準ライブラリの一部として、クラスのプロパティやフィールドに属性(Attribute)を付与することで、バリデーションルールを定義する仕組みです。Required、StringLength、Range、RegularExpressionなどの豊富なビルトイン属性を提供し、カスタム属性の作成も可能です。ASP.NET MVCではModel Bindingと連携して自動的にバリデーションを実行し、Entity Frameworkではデータベース制約と連携します。属性ベースのアプローチにより、ビジネスロジックとバリデーションルールを分離し、保守性の高いコードを実現できます。

主な特徴

  • 標準ライブラリ: .NET Frameworkの標準機能として提供
  • 宣言的構文: 属性による直感的なバリデーション定義
  • フレームワーク統合: MVC、Entity Framework、Blazorとの深い連携
  • 拡張性: カスタム属性の作成が容易
  • 国際化対応: リソースファイルを使用した多言語エラーメッセージ
  • クライアントサイド対応: JavaScript生成による自動クライアント検証

メリット・デメリット

メリット

  • .NET標準ライブラリとして追加設定不要
  • 属性による宣言的で読みやすいコード
  • ASP.NET MVCとEntity Frameworkの完全統合
  • 豊富なビルトイン属性によるカバレッジ
  • カスタム属性の作成が簡単
  • クライアントサイド検証の自動生成

デメリット

  • 複雑な条件付きバリデーションが困難
  • 属性によるモデルクラスの汚染
  • 実行時バリデーションのパフォーマンス制約
  • 属性の組み合わせによる柔軟性の限界
  • テストが困難な場合がある
  • 動的なバリデーションルールの実装が困難

参考ページ

書き方の例

基本的な属性とバリデーション

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

// 基本的なデータモデル
public class User
{
    [Key]
    public int Id { get; set; }

    [Required(ErrorMessage = "名前は必須項目です")]
    [StringLength(50, MinimumLength = 2, ErrorMessage = "名前は2文字以上50文字以下で入力してください")]
    [Display(Name = "名前")]
    public string Name { get; set; }

    [Required(ErrorMessage = "メールアドレスは必須項目です")]
    [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください")]
    [Display(Name = "メールアドレス")]
    public string Email { get; set; }

    [Range(0, 120, ErrorMessage = "年齢は0歳から120歳の間で入力してください")]
    [Display(Name = "年齢")]
    public int Age { get; set; }

    [Phone(ErrorMessage = "有効な電話番号を入力してください")]
    [Display(Name = "電話番号")]
    public string PhoneNumber { get; set; }

    [Url(ErrorMessage = "有効なURLを入力してください")]
    [Display(Name = "ウェブサイト")]
    public string Website { get; set; }

    [DataType(DataType.Date)]
    [Display(Name = "生年月日")]
    public DateTime? BirthDate { get; set; }

    [DataType(DataType.Password)]
    [StringLength(100, MinimumLength = 8, ErrorMessage = "パスワードは8文字以上100文字以下で入力してください")]
    [Display(Name = "パスワード")]
    public string Password { get; set; }

    [Compare("Password", ErrorMessage = "パスワードが一致しません")]
    [Display(Name = "パスワード確認")]
    [NotMapped] // Entity Frameworkで永続化しない
    public string ConfirmPassword { get; set; }

    [RegularExpression(@"^[0-9]{3}-[0-9]{4}$", ErrorMessage = "郵便番号は000-0000の形式で入力してください")]
    [Display(Name = "郵便番号")]
    public string PostalCode { get; set; }

    [CreditCard(ErrorMessage = "有効なクレジットカード番号を入力してください")]
    [Display(Name = "クレジットカード番号")]
    public string CreditCardNumber { get; set; }
}

// 製品モデル
public class Product
{
    [Key]
    public int Id { get; set; }

    [Required(ErrorMessage = "製品名は必須項目です")]
    [StringLength(200, ErrorMessage = "製品名は200文字以下で入力してください")]
    [Display(Name = "製品名")]
    public string Name { get; set; }

    [Required(ErrorMessage = "価格は必須項目です")]
    [Range(0.01, double.MaxValue, ErrorMessage = "価格は0.01以上で入力してください")]
    [DataType(DataType.Currency)]
    [Display(Name = "価格")]
    public decimal Price { get; set; }

    [StringLength(1000, ErrorMessage = "説明は1000文字以下で入力してください")]
    [Display(Name = "説明")]
    public string Description { get; set; }

    [Range(0, int.MaxValue, ErrorMessage = "在庫数は0以上で入力してください")]
    [Display(Name = "在庫数")]
    public int Stock { get; set; }

    [Required(ErrorMessage = "カテゴリは必須項目です")]
    [Display(Name = "カテゴリ")]
    public string Category { get; set; }

    [Display(Name = "有効")]
    public bool IsActive { get; set; } = true;

    [DataType(DataType.DateTime)]
    [Display(Name = "作成日時")]
    public DateTime CreatedAt { get; set; } = DateTime.Now;

    [DataType(DataType.DateTime)]
    [Display(Name = "更新日時")]
    public DateTime UpdatedAt { get; set; } = DateTime.Now;
}

// バリデーション実行クラス
public class ValidationHelper
{
    public static ValidationResult ValidateModel(object model)
    {
        var validationResults = new List<ValidationResult>();
        var validationContext = new ValidationContext(model);
        
        bool isValid = Validator.TryValidateObject(model, validationContext, validationResults, true);
        
        return new ValidationResult
        {
            IsValid = isValid,
            Errors = validationResults.Select(vr => new ValidationError
            {
                PropertyName = vr.MemberNames.FirstOrDefault(),
                ErrorMessage = vr.ErrorMessage
            }).ToList()
        };
    }

    public static ValidationResult ValidateProperty(object model, string propertyName, object value)
    {
        var validationResults = new List<ValidationResult>();
        var validationContext = new ValidationContext(model) { MemberName = propertyName };
        
        bool isValid = Validator.TryValidateProperty(value, validationContext, validationResults);
        
        return new ValidationResult
        {
            IsValid = isValid,
            Errors = validationResults.Select(vr => new ValidationError
            {
                PropertyName = propertyName,
                ErrorMessage = vr.ErrorMessage
            }).ToList()
        };
    }
}

// バリデーション結果クラス
public class ValidationResult
{
    public bool IsValid { get; set; }
    public List<ValidationError> Errors { get; set; } = new List<ValidationError>();
}

public class ValidationError
{
    public string PropertyName { get; set; }
    public string ErrorMessage { get; set; }
}

// 使用例
class Program
{
    static void Main(string[] args)
    {
        // 有効なユーザーデータ
        var validUser = new User
        {
            Name = "田中太郎",
            Email = "[email protected]",
            Age = 30,
            PhoneNumber = "090-1234-5678",
            Website = "https://example.com",
            BirthDate = new DateTime(1993, 5, 15),
            Password = "SecurePassword123",
            ConfirmPassword = "SecurePassword123",
            PostalCode = "100-0001",
            CreditCardNumber = "4111111111111111"
        };

        // バリデーション実行
        var result = ValidationHelper.ValidateModel(validUser);
        
        if (result.IsValid)
        {
            Console.WriteLine("✓ ユーザーデータは有効です");
        }
        else
        {
            Console.WriteLine("✗ バリデーションエラー:");
            foreach (var error in result.Errors)
            {
                Console.WriteLine($"  {error.PropertyName}: {error.ErrorMessage}");
            }
        }

        // 無効なユーザーデータ
        var invalidUser = new User
        {
            Name = "A", // 短すぎる
            Email = "invalid-email", // 無効なメール
            Age = 150, // 範囲外
            PhoneNumber = "123", // 無効な電話番号
            Website = "not-a-url", // 無効なURL
            Password = "123", // 短すぎる
            ConfirmPassword = "456", // 一致しない
            PostalCode = "invalid", // 無効な郵便番号
            CreditCardNumber = "invalid" // 無効なクレジットカード番号
        };

        var invalidResult = ValidationHelper.ValidateModel(invalidUser);
        
        if (!invalidResult.IsValid)
        {
            Console.WriteLine("\n無効なユーザーデータのバリデーションエラー:");
            foreach (var error in invalidResult.Errors)
            {
                Console.WriteLine($"  {error.PropertyName}: {error.ErrorMessage}");
            }
        }

        // 製品データのバリデーション
        var product = new Product
        {
            Name = "サンプル製品",
            Price = 1000.50m,
            Description = "これはサンプル製品です",
            Stock = 100,
            Category = "電子機器"
        };

        var productResult = ValidationHelper.ValidateModel(product);
        
        if (productResult.IsValid)
        {
            Console.WriteLine("\n✓ 製品データは有効です");
        }
        else
        {
            Console.WriteLine("\n✗ 製品データのバリデーションエラー:");
            foreach (var error in productResult.Errors)
            {
                Console.WriteLine($"  {error.PropertyName}: {error.ErrorMessage}");
            }
        }
    }
}

カスタム属性とバリデーション

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;

// カスタム属性:年齢制限
public class MinimumAgeAttribute : ValidationAttribute
{
    private readonly int _minimumAge;

    public MinimumAgeAttribute(int minimumAge)
    {
        _minimumAge = minimumAge;
        ErrorMessage = $"年齢は{_minimumAge}歳以上である必要があります";
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null) return ValidationResult.Success;

        if (value is DateTime birthDate)
        {
            var age = DateTime.Today.Year - birthDate.Year;
            if (birthDate.Date > DateTime.Today.AddYears(-age))
            {
                age--;
            }

            if (age < _minimumAge)
            {
                return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
            }
        }

        return ValidationResult.Success;
    }
}

// カスタム属性:日本の郵便番号
public class JapanesePostalCodeAttribute : ValidationAttribute
{
    public JapanesePostalCodeAttribute()
    {
        ErrorMessage = "日本の郵便番号形式(000-0000)で入力してください";
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null || string.IsNullOrEmpty(value.ToString()))
        {
            return ValidationResult.Success; // 空値は他の属性で検証
        }

        var postalCode = value.ToString();
        var pattern = @"^\d{3}-\d{4}$";
        
        if (!System.Text.RegularExpressions.Regex.IsMatch(postalCode, pattern))
        {
            return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
        }

        return ValidationResult.Success;
    }
}

// カスタム属性:日本の電話番号
public class JapanesePhoneNumberAttribute : ValidationAttribute
{
    public JapanesePhoneNumberAttribute()
    {
        ErrorMessage = "日本の電話番号形式で入力してください";
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null || string.IsNullOrEmpty(value.ToString()))
        {
            return ValidationResult.Success;
        }

        var phoneNumber = value.ToString();
        var patterns = new[]
        {
            @"^0\d{1,4}-\d{1,4}-\d{4}$", // 固定電話
            @"^0\d{2,3}-\d{3,4}-\d{4}$", // 固定電話(市外局番)
            @"^0[7-9]0-\d{4}-\d{4}$", // 携帯電話
            @"^050-\d{4}-\d{4}$" // IP電話
        };

        bool isValid = patterns.Any(pattern => 
            System.Text.RegularExpressions.Regex.IsMatch(phoneNumber, pattern));

        if (!isValid)
        {
            return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
        }

        return ValidationResult.Success;
    }
}

// カスタム属性:禁止文字列
public class ForbiddenWordsAttribute : ValidationAttribute
{
    private readonly string[] _forbiddenWords;

    public ForbiddenWordsAttribute(params string[] forbiddenWords)
    {
        _forbiddenWords = forbiddenWords;
        ErrorMessage = "禁止されている文字が含まれています";
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null) return ValidationResult.Success;

        var text = value.ToString().ToLowerInvariant();
        
        foreach (var word in _forbiddenWords)
        {
            if (text.Contains(word.ToLowerInvariant()))
            {
                return new ValidationResult(
                    $"'{word}' は禁止されている文字です", 
                    new[] { validationContext.MemberName });
            }
        }

        return ValidationResult.Success;
    }
}

// カスタム属性:条件付き必須
public class RequiredIfAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;
    private readonly object _comparisonValue;

    public RequiredIfAttribute(string comparisonProperty, object comparisonValue)
    {
        _comparisonProperty = comparisonProperty;
        _comparisonValue = comparisonValue;
        ErrorMessage = "この項目は必須です";
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
        if (property == null)
        {
            return new ValidationResult($"プロパティ '{_comparisonProperty}' が見つかりません");
        }

        var comparisonValue = property.GetValue(validationContext.ObjectInstance);
        
        if (Equals(comparisonValue, _comparisonValue))
        {
            if (value == null || string.IsNullOrEmpty(value.ToString()))
            {
                return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
            }
        }

        return ValidationResult.Success;
    }
}

// カスタム属性:ファイルサイズ制限
public class FileSizeAttribute : ValidationAttribute
{
    private readonly int _maxSizeInMB;

    public FileSizeAttribute(int maxSizeInMB)
    {
        _maxSizeInMB = maxSizeInMB;
        ErrorMessage = $"ファイルサイズは{_maxSizeInMB}MB以下である必要があります";
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null) return ValidationResult.Success;

        if (value is Microsoft.AspNetCore.Http.IFormFile file)
        {
            var maxSizeInBytes = _maxSizeInMB * 1024 * 1024;
            if (file.Length > maxSizeInBytes)
            {
                return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
            }
        }

        return ValidationResult.Success;
    }
}

// カスタム属性を使用したモデル
public class AdvancedUser
{
    [Required(ErrorMessage = "名前は必須項目です")]
    [StringLength(100, MinimumLength = 2, ErrorMessage = "名前は2文字以上100文字以下で入力してください")]
    [ForbiddenWords("admin", "test", "spam", ErrorMessage = "禁止されている名前です")]
    public string Name { get; set; }

    [Required(ErrorMessage = "メールアドレスは必須項目です")]
    [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください")]
    public string Email { get; set; }

    [Required(ErrorMessage = "生年月日は必須項目です")]
    [MinimumAge(18, ErrorMessage = "18歳以上である必要があります")]
    public DateTime BirthDate { get; set; }

    [JapanesePostalCode]
    public string PostalCode { get; set; }

    [JapanesePhoneNumber]
    public string PhoneNumber { get; set; }

    [Display(Name = "運転免許証を持っている")]
    public bool HasDriverLicense { get; set; }

    [RequiredIf("HasDriverLicense", true, ErrorMessage = "運転免許証を持っている場合は免許証番号が必要です")]
    [StringLength(20, ErrorMessage = "免許証番号は20文字以下で入力してください")]
    public string DriverLicenseNumber { get; set; }

    [Display(Name = "プロフィール画像")]
    [FileSize(5, ErrorMessage = "プロフィール画像は5MB以下で選択してください")]
    public Microsoft.AspNetCore.Http.IFormFile ProfileImage { get; set; }

    [Display(Name = "年収")]
    [Range(0, 100000000, ErrorMessage = "年収は0円以上1億円以下で入力してください")]
    public decimal? AnnualIncome { get; set; }

    [Display(Name = "趣味")]
    [StringLength(500, ErrorMessage = "趣味は500文字以下で入力してください")]
    public string Hobbies { get; set; }

    [Display(Name = "自己紹介")]
    [StringLength(1000, ErrorMessage = "自己紹介は1000文字以下で入力してください")]
    [ForbiddenWords("広告", "宣伝", "spam", ErrorMessage = "不適切な内容が含まれています")]
    public string SelfIntroduction { get; set; }
}

// 会社情報モデル
public class Company
{
    [Required(ErrorMessage = "会社名は必須項目です")]
    [StringLength(200, ErrorMessage = "会社名は200文字以下で入力してください")]
    public string Name { get; set; }

    [Required(ErrorMessage = "郵便番号は必須項目です")]
    [JapanesePostalCode]
    public string PostalCode { get; set; }

    [Required(ErrorMessage = "住所は必須項目です")]
    [StringLength(500, ErrorMessage = "住所は500文字以下で入力してください")]
    public string Address { get; set; }

    [Required(ErrorMessage = "電話番号は必須項目です")]
    [JapanesePhoneNumber]
    public string PhoneNumber { get; set; }

    [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください")]
    public string Email { get; set; }

    [Url(ErrorMessage = "有効なURLを入力してください")]
    public string Website { get; set; }

    [Range(1, 10000, ErrorMessage = "従業員数は1人以上10,000人以下で入力してください")]
    public int EmployeeCount { get; set; }

    [Required(ErrorMessage = "設立年月日は必須項目です")]
    [DataType(DataType.Date)]
    public DateTime EstablishedDate { get; set; }

    [StringLength(1000, ErrorMessage = "事業内容は1000文字以下で入力してください")]
    public string BusinessDescription { get; set; }
}

// 高度なバリデーション実行クラス
public class AdvancedValidationHelper
{
    public static ValidationResult ValidateModel(object model)
    {
        var validationResults = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
        var validationContext = new ValidationContext(model);
        
        bool isValid = Validator.TryValidateObject(model, validationContext, validationResults, true);
        
        return new ValidationResult
        {
            IsValid = isValid,
            Errors = validationResults.Select(vr => new ValidationError
            {
                PropertyName = vr.MemberNames.FirstOrDefault(),
                ErrorMessage = vr.ErrorMessage
            }).ToList()
        };
    }

    public static ValidationResult ValidateModelWithGroups(object model, params string[] groups)
    {
        var validationResults = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
        var validationContext = new ValidationContext(model);
        
        // グループ指定されたプロパティのみ検証
        var properties = model.GetType().GetProperties();
        var errors = new List<ValidationError>();
        
        foreach (var property in properties)
        {
            var value = property.GetValue(model);
            var propertyValidationResults = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
            var propertyValidationContext = new ValidationContext(model) { MemberName = property.Name };
            
            bool propertyIsValid = Validator.TryValidateProperty(value, propertyValidationContext, propertyValidationResults);
            
            if (!propertyIsValid)
            {
                errors.AddRange(propertyValidationResults.Select(vr => new ValidationError
                {
                    PropertyName = property.Name,
                    ErrorMessage = vr.ErrorMessage
                }));
            }
        }
        
        return new ValidationResult
        {
            IsValid = errors.Count == 0,
            Errors = errors
        };
    }
}

// 使用例
class Program
{
    static void Main(string[] args)
    {
        // 高度なユーザーデータのバリデーション
        var advancedUser = new AdvancedUser
        {
            Name = "田中太郎",
            Email = "[email protected]",
            BirthDate = new DateTime(1990, 5, 15),
            PostalCode = "100-0001",
            PhoneNumber = "03-1234-5678",
            HasDriverLicense = true,
            DriverLicenseNumber = "123456789012",
            AnnualIncome = 5000000,
            Hobbies = "読書、映画鑑賞、プログラミング",
            SelfIntroduction = "こんにちは、田中太郎です。よろしくお願いします。"
        };

        var result = AdvancedValidationHelper.ValidateModel(advancedUser);
        
        if (result.IsValid)
        {
            Console.WriteLine("✓ 高度なユーザーデータは有効です");
        }
        else
        {
            Console.WriteLine("✗ 高度なユーザーデータのバリデーションエラー:");
            foreach (var error in result.Errors)
            {
                Console.WriteLine($"  {error.PropertyName}: {error.ErrorMessage}");
            }
        }

        // 無効なデータの例
        var invalidUser = new AdvancedUser
        {
            Name = "admin", // 禁止文字
            Email = "invalid-email",
            BirthDate = new DateTime(2010, 1, 1), // 18歳未満
            PostalCode = "invalid", // 無効な郵便番号
            PhoneNumber = "invalid", // 無効な電話番号
            HasDriverLicense = true,
            DriverLicenseNumber = "", // 必須だが空
            AnnualIncome = -1000000, // 負の値
            SelfIntroduction = "これは広告です" // 禁止文字
        };

        var invalidResult = AdvancedValidationHelper.ValidateModel(invalidUser);
        
        if (!invalidResult.IsValid)
        {
            Console.WriteLine("\n無効なユーザーデータのバリデーションエラー:");
            foreach (var error in invalidResult.Errors)
            {
                Console.WriteLine($"  {error.PropertyName}: {error.ErrorMessage}");
            }
        }

        // 会社情報のバリデーション
        var company = new Company
        {
            Name = "株式会社サンプル",
            PostalCode = "100-0001",
            Address = "東京都千代田区千代田1-1-1",
            PhoneNumber = "03-1234-5678",
            Email = "[email protected]",
            Website = "https://sample.com",
            EmployeeCount = 100,
            EstablishedDate = new DateTime(2000, 4, 1),
            BusinessDescription = "ソフトウェア開発およびITコンサルティング"
        };

        var companyResult = AdvancedValidationHelper.ValidateModel(company);
        
        if (companyResult.IsValid)
        {
            Console.WriteLine("\n✓ 会社情報は有効です");
        }
        else
        {
            Console.WriteLine("\n✗ 会社情報のバリデーションエラー:");
            foreach (var error in companyResult.Errors)
            {
                Console.WriteLine($"  {error.PropertyName}: {error.ErrorMessage}");
            }
        }
    }
}

ASP.NET Core統合とクライアントサイド検証

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.ComponentModel.DataAnnotations;

// ASP.NET Core MVCでの使用例
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateUser([FromBody] User user)
    {
        // モデルバリデーションの確認
        if (!ModelState.IsValid)
        {
            var errors = ModelState
                .Where(x => x.Value.Errors.Count > 0)
                .Select(x => new ValidationError
                {
                    PropertyName = x.Key,
                    ErrorMessage = string.Join(", ", x.Value.Errors.Select(e => e.ErrorMessage))
                })
                .ToList();

            return BadRequest(new
            {
                Message = "バリデーションエラーが発生しました",
                Errors = errors
            });
        }

        // バリデーション成功時の処理
        return Ok(new { Message = "ユーザーが正常に作成されました", User = user });
    }

    [HttpPut("{id}")]
    public IActionResult UpdateUser(int id, [FromBody] User user)
    {
        // 部分バリデーション(特定のプロパティのみ)
        var validationResults = new List<ValidationResult>();
        var validationContext = new ValidationContext(user);
        
        // 特定のプロパティのみバリデーション
        var propertiesToValidate = new[] { "Name", "Email", "Age" };
        
        foreach (var propertyName in propertiesToValidate)
        {
            var property = user.GetType().GetProperty(propertyName);
            if (property != null)
            {
                var value = property.GetValue(user);
                var propertyValidationContext = new ValidationContext(user) { MemberName = propertyName };
                var propertyValidationResults = new List<ValidationResult>();
                
                Validator.TryValidateProperty(value, propertyValidationContext, propertyValidationResults);
                validationResults.AddRange(propertyValidationResults);
            }
        }

        if (validationResults.Any())
        {
            var errors = validationResults.Select(vr => new ValidationError
            {
                PropertyName = vr.MemberNames.FirstOrDefault(),
                ErrorMessage = vr.ErrorMessage
            }).ToList();

            return BadRequest(new
            {
                Message = "部分バリデーションエラーが発生しました",
                Errors = errors
            });
        }

        return Ok(new { Message = "ユーザーが正常に更新されました", User = user });
    }

    [HttpPost("validate")]
    public IActionResult ValidateUserData([FromBody] User user)
    {
        // カスタムバリデーションロジック
        var customValidationResults = new List<ValidationError>();
        
        // 重複チェック(例:データベース確認)
        if (IsEmailDuplicate(user.Email))
        {
            customValidationResults.Add(new ValidationError
            {
                PropertyName = nameof(user.Email),
                ErrorMessage = "このメールアドレスは既に使用されています"
            });
        }

        // 年齢と生年月日の整合性チェック
        if (user.BirthDate.HasValue && user.Age > 0)
        {
            var calculatedAge = DateTime.Today.Year - user.BirthDate.Value.Year;
            if (user.BirthDate.Value.Date > DateTime.Today.AddYears(-calculatedAge))
            {
                calculatedAge--;
            }

            if (Math.Abs(user.Age - calculatedAge) > 1)
            {
                customValidationResults.Add(new ValidationError
                {
                    PropertyName = nameof(user.Age),
                    ErrorMessage = "年齢と生年月日が一致しません"
                });
            }
        }

        // Data Annotationsバリデーション
        var validationResults = new List<ValidationResult>();
        var validationContext = new ValidationContext(user);
        bool isValid = Validator.TryValidateObject(user, validationContext, validationResults, true);
        
        var allErrors = validationResults.Select(vr => new ValidationError
        {
            PropertyName = vr.MemberNames.FirstOrDefault(),
            ErrorMessage = vr.ErrorMessage
        }).ToList();
        
        allErrors.AddRange(customValidationResults);

        if (allErrors.Any())
        {
            return BadRequest(new
            {
                Message = "バリデーションエラーが発生しました",
                Errors = allErrors
            });
        }

        return Ok(new { Message = "バリデーションが成功しました" });
    }

    private bool IsEmailDuplicate(string email)
    {
        // 実際の実装ではデータベース確認
        var existingEmails = new[] { "[email protected]", "[email protected]" };
        return existingEmails.Contains(email);
    }
}

// カスタムバリデーション属性(ASP.NET Core固有)
public class UniqueEmailAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null) return ValidationResult.Success;

        var email = value.ToString();
        
        // 実際の実装ではDIコンテナからサービスを取得
        // var userService = validationContext.GetService<IUserService>();
        // bool isDuplicate = userService.IsEmailDuplicate(email);
        
        // 簡略化した例
        var existingEmails = new[] { "[email protected]", "[email protected]" };
        bool isDuplicate = existingEmails.Contains(email);

        if (isDuplicate)
        {
            return new ValidationResult(
                "このメールアドレスは既に使用されています",
                new[] { validationContext.MemberName });
        }

        return ValidationResult.Success;
    }
}

// 国際化対応のバリデーション
public class LocalizedUser
{
    [Required(ErrorMessageResourceType = typeof(Resources.ValidationMessages), 
              ErrorMessageResourceName = "NameRequired")]
    [StringLength(50, MinimumLength = 2, 
                  ErrorMessageResourceType = typeof(Resources.ValidationMessages), 
                  ErrorMessageResourceName = "NameLength")]
    public string Name { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources.ValidationMessages), 
              ErrorMessageResourceName = "EmailRequired")]
    [EmailAddress(ErrorMessageResourceType = typeof(Resources.ValidationMessages), 
                  ErrorMessageResourceName = "EmailFormat")]
    [UniqueEmail(ErrorMessageResourceType = typeof(Resources.ValidationMessages), 
                 ErrorMessageResourceName = "EmailDuplicate")]
    public string Email { get; set; }

    [Range(0, 120, ErrorMessageResourceType = typeof(Resources.ValidationMessages), 
           ErrorMessageResourceName = "AgeRange")]
    public int Age { get; set; }
}

// リソースファイル(Resources/ValidationMessages.resx)の例
public class ValidationMessages
{
    public static string NameRequired => "名前は必須項目です";
    public static string NameLength => "名前は2文字以上50文字以下で入力してください";
    public static string EmailRequired => "メールアドレスは必須項目です";
    public static string EmailFormat => "有効なメールアドレスを入力してください";
    public static string EmailDuplicate => "このメールアドレスは既に使用されています";
    public static string AgeRange => "年齢は0歳から120歳の間で入力してください";
}

// Blazor Serverでの使用例
@page "/user-form"
@using System.ComponentModel.DataAnnotations

<EditForm Model="@user" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="form-group">
        <label for="name">名前:</label>
        <InputText id="name" @bind-Value="user.Name" class="form-control" />
        <ValidationMessage For="@(() => user.Name)" />
    </div>

    <div class="form-group">
        <label for="email">メールアドレス:</label>
        <InputText id="email" @bind-Value="user.Email" class="form-control" />
        <ValidationMessage For="@(() => user.Email)" />
    </div>

    <div class="form-group">
        <label for="age">年齢:</label>
        <InputNumber id="age" @bind-Value="user.Age" class="form-control" />
        <ValidationMessage For="@(() => user.Age)" />
    </div>

    <button type="submit" class="btn btn-primary">送信</button>
</EditForm>

@code {
    private User user = new User();

    private async Task HandleValidSubmit()
    {
        // バリデーション済みデータの処理
        Console.WriteLine($"ユーザー作成: {user.Name}, {user.Email}, {user.Age}");
        
        // 実際の実装では保存処理など
        await Task.Delay(1000);
        
        // 成功メッセージ表示
        user = new User(); // フォームリセット
    }
}