ValidationContext
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
スター履歴
データ取得日時: 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. 検証の実行順序
- データ型の変換(モデルバインディング)
- 属性ベースの検証(Required、StringLengthなど)
- 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);
});
}
}
トラブルシューティング
よくある問題と解決方法
-
検証が実行されない
Validator.TryValidateObjectの第4引数をtrueに設定- 属性が正しく適用されているか確認
-
サービスが取得できない
- DIコンテナにサービスが登録されているか確認
- ValidationContextにServiceProviderが設定されているか確認
-
カスタム検証属性が動作しない
- ValidationAttributeを正しく継承しているか確認
- IsValidメソッドがオーバーライドされているか確認
-
エラーメッセージが表示されない
- MemberNamesが正しく設定されているか確認
- ModelStateにエラーが追加されているか確認
まとめ
ValidationContextは、.NETにおける柔軟で強力な検証メカニズムの中核を担うクラスです。単純な属性ベースの検証から、複雑なビジネスルールの実装まで、様々な検証シナリオに対応できます。ASP.NET Coreと組み合わせることで、堅牢なWebアプリケーションの構築が可能になります。適切に活用することで、データの整合性を保ちながら、メンテナンス性の高いコードを実現できます。