ValidationContext

C#.NET検証データアノテーションASP.NET Coreカスタム検証IValidatableObject属性ベース検証

GitHub概要

dotnet/runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

スター17,028
ウォッチ458
フォーク5,203
作成日:2019年9月24日
言語:C#
ライセンス:MIT License

トピックス

dotnethacktoberfesthelp-wanted

スター履歴

dotnet/runtime Star History
データ取得日時: 2025/10/22 10:03

ValidationContext

ValidationContextは、.NETのSystem.ComponentModel.DataAnnotations名前空間に含まれる重要なクラスで、検証チェックが実行されるコンテキストを記述します。これにより、検証対象のオブジェクト全体へのアクセスや、サービスプロバイダーを通じたカスタム検証の実装が可能になります。

主な特徴

1. 検証コンテキストの提供

ValidationContextは、検証が実行される際の詳細な情報を提供します:

  • 検証対象のオブジェクトインスタンス
  • 検証対象のプロパティ名
  • サービスプロバイダーへのアクセス
  • カスタムアイテムディクショナリ

2. オブジェクト全体へのアクセス

単一のプロパティだけでなく、オブジェクト全体にアクセスできるため、複数のプロパティに依存する複雑な検証ロジックを実装できます。

3. サービスインジェクション対応

IServiceProviderインターフェースを実装しているため、DIコンテナに登録されたサービスを検証ロジック内で利用できます。

基本的な使い方

ValidationContextの作成

// 基本的なコンストラクタ
var context = new ValidationContext(objectToValidate);

// サービスプロバイダー付き
var context = new ValidationContext(objectToValidate, serviceProvider, items);

// アイテムディクショナリ付き
var context = new ValidationContext(objectToValidate, items);

シンプルな検証例

public class Person
{
    [Required(ErrorMessage = "名前は必須です。")]
    [StringLength(100, MinimumLength = 2, ErrorMessage = "名前は2文字以上100文字以下で入力してください。")]
    public string Name { get; set; }
    
    [Range(18, 60, ErrorMessage = "年齢は18歳から60歳の間で入力してください。")]
    public int Age { get; set; }
    
    [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください。")]
    public string Email { get; set; }
}

// オブジェクトの検証
var person = new Person { Name = "", Age = 16, Email = "invalid-email" };
var context = new ValidationContext(person, null, null);
var results = new List<ValidationResult>();

if (!Validator.TryValidateObject(person, context, results, true))
{
    foreach (var error in results)
    {
        Console.WriteLine($"エラー: {error.ErrorMessage}");
        if (error.MemberNames.Any())
        {
            Console.WriteLine($"  対象プロパティ: {string.Join(", ", error.MemberNames)}");
        }
    }
}

カスタム検証属性の実装

基本的なカスタム検証属性

public class FutureDateAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is DateTime dateTime)
        {
            if (dateTime <= DateTime.Now)
            {
                return new ValidationResult("日付は未来の日付を指定してください。");
            }
        }
        
        return ValidationResult.Success;
    }
}

他のプロパティを参照するカスタム検証

public class DateRangeAttribute : ValidationAttribute
{
    private readonly string _startDateProperty;
    
    public DateRangeAttribute(string startDateProperty)
    {
        _startDateProperty = startDateProperty;
    }
    
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var endDate = (DateTime?)value;
        if (!endDate.HasValue)
            return ValidationResult.Success;
        
        // ValidationContextを使用して他のプロパティにアクセス
        var startDateProperty = validationContext.ObjectType.GetProperty(_startDateProperty);
        if (startDateProperty == null)
            return new ValidationResult($"プロパティ '{_startDateProperty}' が見つかりません。");
        
        var startDate = (DateTime?)startDateProperty.GetValue(validationContext.ObjectInstance);
        if (!startDate.HasValue)
            return ValidationResult.Success;
        
        if (startDate > endDate)
        {
            return new ValidationResult($"終了日は開始日より後の日付を指定してください。");
        }
        
        return ValidationResult.Success;
    }
}

// 使用例
public class Event
{
    public DateTime StartDate { get; set; }
    
    [DateRange("StartDate")]
    public DateTime EndDate { get; set; }
}

IValidatableObjectの実装

複雑な検証ロジックの実装

public class Order : IValidatableObject
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public DateTime? ShipDate { get; set; }
    public string Status { get; set; }
    public decimal TotalAmount { get; set; }
    public List<OrderItem> Items { get; set; } = new List<OrderItem>();
    
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // 注文日の検証
        if (OrderDate > DateTime.Now)
        {
            yield return new ValidationResult(
                "注文日は未来の日付にはできません。",
                new[] { nameof(OrderDate) });
        }
        
        // 配送日の検証
        if (ShipDate.HasValue && ShipDate < OrderDate)
        {
            yield return new ValidationResult(
                "配送日は注文日より前にはできません。",
                new[] { nameof(ShipDate) });
        }
        
        // ステータスに応じた検証
        if (Status == "Shipped" && !ShipDate.HasValue)
        {
            yield return new ValidationResult(
                "ステータスが'Shipped'の場合、配送日は必須です。",
                new[] { nameof(ShipDate), nameof(Status) });
        }
        
        // 注文アイテムの検証
        if (!Items.Any())
        {
            yield return new ValidationResult(
                "注文には少なくとも1つのアイテムが必要です。",
                new[] { nameof(Items) });
        }
        
        // 合計金額の検証
        var calculatedTotal = Items.Sum(i => i.Quantity * i.UnitPrice);
        if (Math.Abs(TotalAmount - calculatedTotal) > 0.01m)
        {
            yield return new ValidationResult(
                $"合計金額が正しくありません。期待値: {calculatedTotal:C}",
                new[] { nameof(TotalAmount) });
        }
    }
}

public class OrderItem
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

サービスプロバイダーの活用

サービスを利用したカスタム検証

public interface IUserService
{
    bool IsEmailUnique(string email, int? excludeUserId = null);
    bool IsUsernameAvailable(string username);
}

public class UniqueEmailAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var email = value as string;
        if (string.IsNullOrEmpty(email))
            return ValidationResult.Success;
        
        // サービスプロバイダーからサービスを取得
        var userService = validationContext.GetService(typeof(IUserService)) as IUserService;
        if (userService == null)
        {
            throw new InvalidOperationException("IUserService が登録されていません。");
        }
        
        // 更新時は自身のIDを除外
        var userIdProperty = validationContext.ObjectType.GetProperty("Id");
        var userId = userIdProperty?.GetValue(validationContext.ObjectInstance) as int?;
        
        if (!userService.IsEmailUnique(email, userId))
        {
            return new ValidationResult("このメールアドレスは既に使用されています。");
        }
        
        return ValidationResult.Success;
    }
}

// 使用例
public class User
{
    public int? Id { get; set; }
    
    [Required(ErrorMessage = "ユーザー名は必須です。")]
    public string Username { get; set; }
    
    [Required(ErrorMessage = "メールアドレスは必須です。")]
    [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください。")]
    [UniqueEmail]
    public string Email { get; set; }
}

ASP.NET Coreとの統合

コントローラーでの利用

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;
    
    public UsersController(IUserService userService)
    {
        _userService = userService;
    }
    
    [HttpPost]
    public IActionResult Create([FromBody] UserCreateDto user)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        
        // ビジネスロジックの実行
        var createdUser = _userService.CreateUser(user);
        return CreatedAtAction(nameof(GetById), new { id = createdUser.Id }, createdUser);
    }
    
    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
        var user = _userService.GetUser(id);
        if (user == null)
            return NotFound();
        
        return Ok(user);
    }
}

カスタムモデルバインダーとの連携

public class CustomValidationModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // モデルバインディングロジック
        var model = // ... モデルの作成
        
        // ValidationContextの作成
        var validationContext = new ValidationContext(
            model,
            bindingContext.HttpContext.RequestServices,
            null);
        
        // カスタム検証の実行
        var validationResults = new List<ValidationResult>();
        if (!Validator.TryValidateObject(model, validationContext, validationResults, true))
        {
            foreach (var result in validationResults)
            {
                foreach (var memberName in result.MemberNames)
                {
                    bindingContext.ModelState.TryAddModelError(memberName, result.ErrorMessage);
                }
            }
        }
        
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

高度な使用例

条件付き検証

public class ConditionalValidationAttribute : ValidationAttribute
{
    private readonly string _conditionProperty;
    private readonly object _conditionValue;
    
    public ConditionalValidationAttribute(string conditionProperty, object conditionValue)
    {
        _conditionProperty = conditionProperty;
        _conditionValue = conditionValue;
    }
    
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var conditionProperty = validationContext.ObjectType.GetProperty(_conditionProperty);
        if (conditionProperty == null)
            return new ValidationResult($"条件プロパティ '{_conditionProperty}' が見つかりません。");
        
        var conditionValue = conditionProperty.GetValue(validationContext.ObjectInstance);
        
        // 条件が満たされない場合は検証をスキップ
        if (!Equals(conditionValue, _conditionValue))
            return ValidationResult.Success;
        
        // 条件が満たされた場合は値の検証を実行
        if (value == null || (value is string str && string.IsNullOrWhiteSpace(str)))
        {
            return new ValidationResult(ErrorMessage ?? "この項目は必須です。");
        }
        
        return ValidationResult.Success;
    }
}

// 使用例
public class PaymentInfo
{
    public string PaymentMethod { get; set; }
    
    [ConditionalValidation("PaymentMethod", "CreditCard", 
        ErrorMessage = "クレジットカード番号は必須です。")]
    public string CreditCardNumber { get; set; }
    
    [ConditionalValidation("PaymentMethod", "BankTransfer", 
        ErrorMessage = "銀行口座番号は必須です。")]
    public string BankAccountNumber { get; set; }
}

非同期検証

public class AsyncUniqueValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        // 同期的な検証の場合、非同期操作を実行できないため、
        // IValidatableObjectの実装で非同期検証を行うか、
        // カスタムアクションフィルターを使用することを推奨
        return ValidationResult.Success;
    }
}

// 非同期検証を含むサービス
public class AsyncValidationService
{
    private readonly IUserRepository _userRepository;
    
    public AsyncValidationService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }
    
    public async Task<IEnumerable<ValidationResult>> ValidateUserAsync(User user)
    {
        var results = new List<ValidationResult>();
        
        // 非同期でメールアドレスの重複チェック
        if (!await _userRepository.IsEmailUniqueAsync(user.Email, user.Id))
        {
            results.Add(new ValidationResult(
                "このメールアドレスは既に使用されています。",
                new[] { nameof(user.Email) }));
        }
        
        // 非同期でユーザー名の重複チェック
        if (!await _userRepository.IsUsernameAvailableAsync(user.Username, user.Id))
        {
            results.Add(new ValidationResult(
                "このユーザー名は既に使用されています。",
                new[] { nameof(user.Username) }));
        }
        
        return results;
    }
}

ベストプラクティス

1. 検証の階層化

  • 属性ベース検証: 単純なプロパティレベルの検証
  • IValidatableObject: 複数プロパティに関わる複雑な検証
  • カスタムバリデーター: 再利用可能な検証ロジック

2. エラーメッセージの管理

public class ValidationMessages
{
    public const string RequiredField = "{0}は必須項目です。";
    public const string StringLength = "{0}は{2}文字以上{1}文字以下で入力してください。";
    public const string InvalidEmail = "有効なメールアドレスを入力してください。";
    public const string DateRange = "{0}は{1}から{2}の間で指定してください。";
}

// 使用例
[Required(ErrorMessage = ValidationMessages.RequiredField)]
[StringLength(100, MinimumLength = 2, ErrorMessage = ValidationMessages.StringLength)]
public string Name { get; set; }

3. 検証の実行順序

  1. データ型の変換(モデルバインディング)
  2. 属性ベースの検証(Required、StringLengthなど)
  3. IValidatableObjectの検証(Validateメソッド)

4. パフォーマンスの考慮

public class CachedValidationService
{
    private readonly IMemoryCache _cache;
    private readonly IUserService _userService;
    
    public CachedValidationService(IMemoryCache cache, IUserService userService)
    {
        _cache = cache;
        _userService = userService;
    }
    
    public bool IsEmailUnique(string email, int? excludeUserId)
    {
        var cacheKey = $"email_unique_{email}_{excludeUserId}";
        
        return _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromMinutes(5);
            return _userService.IsEmailUnique(email, excludeUserId);
        });
    }
}

トラブルシューティング

よくある問題と解決方法

  1. 検証が実行されない

    • Validator.TryValidateObjectの第4引数をtrueに設定
    • 属性が正しく適用されているか確認
  2. サービスが取得できない

    • DIコンテナにサービスが登録されているか確認
    • ValidationContextにServiceProviderが設定されているか確認
  3. カスタム検証属性が動作しない

    • ValidationAttributeを正しく継承しているか確認
    • IsValidメソッドがオーバーライドされているか確認
  4. エラーメッセージが表示されない

    • MemberNamesが正しく設定されているか確認
    • ModelStateにエラーが追加されているか確認

まとめ

ValidationContextは、.NETにおける柔軟で強力な検証メカニズムの中核を担うクラスです。単純な属性ベースの検証から、複雑なビジネスルールの実装まで、様々な検証シナリオに対応できます。ASP.NET Coreと組み合わせることで、堅牢なWebアプリケーションの構築が可能になります。適切に活用することで、データの整合性を保ちながら、メンテナンス性の高いコードを実現できます。