FluentValidation
ライブラリ
FluentValidation
概要
FluentValidationはC#/.NETエコシステムで最も信頼されているバリデーションライブラリです。「シンプルで読みやすい強型付けバリデーションルール」を理念として開発され、.NET開発者にとって最もパワフルで柔軟性の高いバリデーション体験を提供します。6億回以上のダウンロード実績を持ち、ASP.NET Core、Entity Framework、Blazorとの深い統合により、企業レベルのアプリケーション開発において標準的な選択肢となっています。
詳細
FluentValidation 12.0.0は2025年現在の最新版で、レガシーコードの削除と古いプラットフォームサポートの終了に焦点を当てた安定版リリースです。.NET 8以降に対応し、Linq Expressionを基盤とした強力な型安全バリデーションシステムを提供。AbstractValidatorベースのクラス設計により、ドメイン駆動設計(DDD)パターンと完全に統合され、複雑なビジネスルールの実装を直感的に実現します。シングルトンパターンでの使用を前提とした設計により、高いパフォーマンスを維持しています。
主な特徴
- 強力な型安全性: C#の型システムとLambda式を最大限活用
- ASP.NET Core統合: MVC、Web API、Blazorとの統一されたバリデーション
- エンタープライズ対応: 複雑なビジネスルールと大規模プロジェクト向け
- 高性能: シングルトンパターンとExpression Trees最適化
- 豊富なバリデーター: 40以上のビルトインバリデーション機能
- カスタマイズ性: カスタムバリデーターとルール拡張の容易さ
メリット・デメリット
メリット
- .NETエコシステムでの圧倒的な実績と信頼性(6億回以上ダウンロード)
- ASP.NET Core、Entity Framework、Blazorとの完璧な統合
- 企業レベルのアプリケーションでの豊富な採用事例
- 強力な型安全性とIntelliSenseによる開発効率向上
- 条件付きバリデーション、ルールセット、継承対応の高度な機能
- 充実した公式ドキュメントと活発なコミュニティサポート
デメリット
- C#/.NET専用(他言語での使用不可)
- 初期学習コストがやや高い(Expression Treesの理解が必要)
- 軽量なバリデーションには機能過多の可能性
- フロントエンド(JavaScript)との連携で追加設定が必要
- 複雑なビジネスルールではパフォーマンス調整が必要な場合
- .NET Framework古いバージョンのサポート終了
参考ページ
書き方の例
インストールと基本セットアップ
# FluentValidationのインストール
dotnet add package FluentValidation
# ASP.NET Core統合用(非推奨パッケージに注意)
dotnet add package FluentValidation.AspNetCore
# NuGetパッケージマネージャーを使用する場合
Install-Package FluentValidation
Install-Package FluentValidation.AspNetCore
基本的なスキーマ定義とバリデーション
using FluentValidation;
// 顧客クラスの定義
public class Customer
{
public int Id { get; set; }
public string Surname { get; set; }
public string Forename { get; set; }
public decimal Discount { get; set; }
public string Address { get; set; }
public bool HasDiscount { get; set; }
public string Postcode { get; set; }
}
// 基本的なバリデータークラス
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
// 基本的なバリデーションルール
RuleFor(customer => customer.Surname)
.NotEmpty()
.WithMessage("苗字は必須項目です");
RuleFor(customer => customer.Forename)
.NotEmpty()
.WithMessage("名前を入力してください")
.Length(2, 50)
.WithMessage("名前は2文字以上50文字以下で入力してください");
RuleFor(customer => customer.Discount)
.NotEqual(0)
.When(customer => customer.HasDiscount)
.WithMessage("割引が有効な場合は割引率を設定してください");
RuleFor(customer => customer.Address)
.Length(20, 250)
.WithMessage("住所は20文字以上250文字以下で入力してください");
RuleFor(customer => customer.Postcode)
.Must(BeAValidPostcode)
.WithMessage("有効な郵便番号を入力してください");
}
// カスタムバリデーションメソッド
private bool BeAValidPostcode(string postcode)
{
if (string.IsNullOrEmpty(postcode))
return false;
// 日本の郵便番号形式(例: 123-4567)
return System.Text.RegularExpressions.Regex.IsMatch(
postcode, @"^\d{3}-\d{4}$");
}
}
// バリデーションの実行
class Program
{
static void Main()
{
var customer = new Customer
{
Surname = "",
Forename = "太郎",
HasDiscount = true,
Discount = 0,
Address = "短すぎる住所",
Postcode = "無効な郵便番号"
};
var validator = new CustomerValidator();
// バリデーション実行
var results = validator.Validate(customer);
if (!results.IsValid)
{
foreach (var failure in results.Errors)
{
Console.WriteLine($"プロパティ {failure.PropertyName} でエラー: {failure.ErrorMessage}");
}
}
// 例外を投げるバリデーション
try
{
validator.ValidateAndThrow(customer);
}
catch (ValidationException ex)
{
Console.WriteLine($"バリデーションエラー: {ex.Message}");
}
}
}
高度なバリデーションルールとカスタムバリデーター
using FluentValidation;
using FluentValidation.Results;
// 複雑な製品注文モデル
public class ProductOrder
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal DiscountRate { get; set; }
public DateTime OrderDate { get; set; }
public Address ShippingAddress { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public OrderStatus Status { get; set; }
public Customer Customer { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; } = "日本";
}
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
// 高度なバリデータークラス
public class ProductOrderValidator : AbstractValidator<ProductOrder>
{
public ProductOrderValidator()
{
// 製品名の詳細バリデーション
RuleFor(order => order.ProductName)
.NotEmpty()
.WithMessage("製品名は必須項目です")
.Length(2, 100)
.WithMessage("製品名は2文字以上100文字以下で入力してください")
.Matches(@"^[a-zA-Z0-9\s\-_]+$")
.WithMessage("製品名は英数字、スペース、ハイフン、アンダースコアのみ使用可能です");
// 数量の条件付きバリデーション
RuleFor(order => order.Quantity)
.GreaterThan(0)
.WithMessage("数量は1以上である必要があります")
.LessThanOrEqualTo(1000)
.WithMessage("一度に注文できる数量は1000個までです")
.Must(BeValidQuantityForProduct)
.WithMessage("この製品の注文数量が制限を超えています");
// 価格の詳細バリデーション
RuleFor(order => order.UnitPrice)
.GreaterThan(0)
.WithMessage("単価は0より大きい必要があります")
.LessThanOrEqualTo(1000000)
.WithMessage("単価は100万円以下である必要があります")
.ScalePrecision(2, 10)
.WithMessage("単価は小数点以下2桁までです");
// 割引率のバリデーション
RuleFor(order => order.DiscountRate)
.InclusiveBetween(0, 1)
.WithMessage("割引率は0から1の間で入力してください");
// 注文日のバリデーション
RuleFor(order => order.OrderDate)
.GreaterThanOrEqualTo(DateTime.Today.AddDays(-30))
.WithMessage("注文日は30日以内である必要があります")
.LessThanOrEqualTo(DateTime.Today.AddDays(7))
.WithMessage("注文日は1週間後までしか設定できません");
// ネストしたオブジェクトのバリデーション
RuleFor(order => order.ShippingAddress)
.SetValidator(new AddressValidator())
.When(order => order.Status != OrderStatus.Cancelled);
// コレクションのバリデーション
RuleFor(order => order.Tags)
.Must(tags => tags.Count <= 5)
.WithMessage("タグは5個まで設定可能です");
RuleForEach(order => order.Tags)
.NotEmpty()
.WithMessage("空のタグは設定できません")
.Length(2, 20)
.WithMessage("各タグは2文字以上20文字以下である必要があります");
// 条件付きバリデーション(複数条件)
When(order => order.Status == OrderStatus.Processing, () =>
{
RuleFor(order => order.Customer)
.NotNull()
.WithMessage("処理中の注文には顧客情報が必要です");
RuleFor(order => order.ShippingAddress)
.NotNull()
.WithMessage("処理中の注文には配送先住所が必要です");
}).Otherwise(() =>
{
RuleFor(order => order.Status)
.NotEqual(OrderStatus.Shipped)
.WithMessage("顧客情報なしで配送状態にはできません");
});
// 依存ルール(先行ルールが成功した場合のみ実行)
RuleFor(order => order.ProductName)
.NotEmpty()
.DependentRules(() =>
{
RuleFor(order => order.Quantity)
.Must((order, quantity) => IsQuantityAvailableForProduct(order.ProductName, quantity))
.WithMessage("指定された製品の在庫が不足しています");
});
// ルールセットの定義
RuleSet("QuickValidation", () =>
{
RuleFor(order => order.ProductName).NotEmpty();
RuleFor(order => order.Quantity).GreaterThan(0);
});
RuleSet("CompleteValidation", () =>
{
Include("QuickValidation");
RuleFor(order => order.UnitPrice).GreaterThan(0);
RuleFor(order => order.ShippingAddress).SetValidator(new AddressValidator());
});
}
// カスタムバリデーションメソッド
private bool BeValidQuantityForProduct(int quantity)
{
// 製品固有の数量制限ロジック
return quantity <= 100; // 簡単な例
}
private bool IsQuantityAvailableForProduct(string productName, int quantity)
{
// 在庫確認ロジック(実際の実装ではデータベースアクセス等)
return !string.IsNullOrEmpty(productName) && quantity > 0;
}
}
// 住所バリデーター
public class AddressValidator : AbstractValidator<Address>
{
public AddressValidator()
{
RuleFor(address => address.Street)
.NotEmpty()
.WithMessage("住所は必須項目です")
.Length(5, 200)
.WithMessage("住所は5文字以上200文字以下で入力してください");
RuleFor(address => address.City)
.NotEmpty()
.WithMessage("市区町村は必須項目です");
RuleFor(address => address.PostalCode)
.NotEmpty()
.WithMessage("郵便番号は必須項目です")
.Matches(@"^\d{3}-\d{4}$")
.WithMessage("郵便番号は000-0000の形式で入力してください");
RuleFor(address => address.Country)
.NotEmpty()
.WithMessage("国名は必須項目です");
}
}
フレームワーク統合(ASP.NET Core、Blazor、Entity Framework等)
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
// ASP.NET Core統合例
// Startup.cs または Program.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// FluentValidationの登録(推奨方法)
services.AddValidatorsFromAssemblyContaining<CustomerValidator>();
// 個別バリデーターの登録
services.AddScoped<IValidator<Customer>, CustomerValidator>();
services.AddScoped<IValidator<ProductOrder>, ProductOrderValidator>();
// ASP.NET Core MVC設定
services.AddControllers(options =>
{
// モデルバリデーションの自動実行を無効化(FluentValidationを優先)
options.ModelValidatorProviders.Clear();
});
}
}
// ASP.NET Core WebAPIでの使用例
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
private readonly IValidator<Customer> _customerValidator;
public CustomersController(IValidator<Customer> customerValidator)
{
_customerValidator = customerValidator;
}
[HttpPost]
public async Task<IActionResult> CreateCustomer([FromBody] Customer customer)
{
// バリデーション実行
var validationResult = await _customerValidator.ValidateAsync(customer);
if (!validationResult.IsValid)
{
// エラーをモデルステートに追加
foreach (var error in validationResult.Errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
return BadRequest(ModelState);
}
// バリデーション成功時の処理
// ここで顧客をデータベースに保存等
return Ok(new { message = "顧客が正常に作成されました", customer });
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateCustomer(int id, [FromBody] Customer customer)
{
// 特定のルールセットのみ実行
var validationResult = await _customerValidator.ValidateAsync(customer, options =>
{
options.IncludeRuleSets("UpdateValidation");
options.IncludeProperties(x => x.Surname, x => x.Forename);
});
if (!validationResult.IsValid)
{
return BadRequest(validationResult.Errors.Select(e => new
{
Property = e.PropertyName,
Error = e.ErrorMessage,
AttemptedValue = e.AttemptedValue
}));
}
return Ok();
}
}
// Blazorでの使用例
@page "/customer-form"
@using FluentValidation
@inject IValidator<Customer> CustomerValidator
<EditForm Model="@customer" OnValidSubmit="@HandleValidSubmit">
<FluentValidationValidator />
<ValidationSummary />
<div class="form-group">
<label for="surname">苗字:</label>
<InputText id="surname" @bind-Value="customer.Surname" class="form-control" />
<ValidationMessage For="@(() => customer.Surname)" />
</div>
<div class="form-group">
<label for="forename">名前:</label>
<InputText id="forename" @bind-Value="customer.Forename" class="form-control" />
<ValidationMessage For="@(() => customer.Forename)" />
</div>
<button type="submit" class="btn btn-primary">保存</button>
</EditForm>
@code {
private Customer customer = new Customer();
private async Task HandleValidSubmit()
{
var validationResult = await CustomerValidator.ValidateAsync(customer);
if (validationResult.IsValid)
{
// 保存処理
Console.WriteLine("顧客情報が保存されました");
}
}
}
// Entity Frameworkとの統合例
public class CustomerService
{
private readonly IValidator<Customer> _validator;
private readonly ApplicationDbContext _context;
public CustomerService(IValidator<Customer> validator, ApplicationDbContext context)
{
_validator = validator;
_context = context;
}
public async Task<Result<Customer>> CreateCustomerAsync(Customer customer)
{
// バリデーション実行
var validationResult = await _validator.ValidateAsync(customer);
if (!validationResult.IsValid)
{
return Result<Customer>.Failure(validationResult.Errors.Select(e => e.ErrorMessage));
}
// データベース保存
try
{
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return Result<Customer>.Success(customer);
}
catch (Exception ex)
{
return Result<Customer>.Failure($"データベースエラー: {ex.Message}");
}
}
}
// 結果クラス
public class Result<T>
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public IEnumerable<string> Errors { get; set; }
public static Result<T> Success(T data) => new Result<T>
{
IsSuccess = true,
Data = data,
Errors = Enumerable.Empty<string>()
};
public static Result<T> Failure(IEnumerable<string> errors) => new Result<T>
{
IsSuccess = false,
Errors = errors
};
public static Result<T> Failure(string error) => Failure(new[] { error });
}
エラーハンドリングとカスタムエラーメッセージ
using FluentValidation;
using FluentValidation.Results;
// 詳細なエラーハンドリング例
public class AdvancedCustomerValidator : AbstractValidator<Customer>
{
public AdvancedCustomerValidator()
{
// カスタムエラーメッセージ
RuleFor(customer => customer.Surname)
.NotEmpty()
.WithMessage("苗字は必須項目です")
.WithErrorCode("SURNAME_REQUIRED")
.WithSeverity(Severity.Error);
RuleFor(customer => customer.Forename)
.NotEmpty()
.WithMessage("名前は必須項目です")
.Length(2, 50)
.WithMessage("名前は{MinLength}文字以上{MaxLength}文字以下で入力してください")
.WithErrorCode("FORENAME_LENGTH");
// 動的エラーメッセージ
RuleFor(customer => customer.Discount)
.Must((customer, discount) => ValidateDiscount(customer, discount))
.WithMessage(customer => $"顧客タイプ'{customer.GetType().Name}'では割引率{customer.Discount}%は無効です")
.WithErrorCode("INVALID_DISCOUNT");
// 複数条件のエラーメッセージ
RuleFor(customer => customer.Address)
.NotEmpty()
.When(customer => customer.HasDiscount)
.WithMessage("割引適用には住所の入力が必要です")
.WithState(customer => new { CustomerType = "Premium", RequiresAddress = true });
// カスタムステート付きバリデーション
RuleFor(customer => customer.Postcode)
.Must(BeValidPostcode)
.WithMessage("郵便番号の形式が正しくありません")
.WithState(customer => new ValidationContext
{
Timestamp = DateTime.Now,
UserId = customer.Id,
ValidationRule = "PostcodeFormat"
});
}
private bool ValidateDiscount(Customer customer, decimal discount)
{
return discount >= 0 && discount <= 0.5m; // 最大50%割引
}
private bool BeValidPostcode(string postcode)
{
return !string.IsNullOrEmpty(postcode) &&
System.Text.RegularExpressions.Regex.IsMatch(postcode, @"^\d{3}-\d{4}$");
}
}
// バリデーション結果の詳細処理
public class ValidationResultProcessor
{
public void ProcessValidationResult(ValidationResult result)
{
if (result.IsValid)
{
Console.WriteLine("バリデーション成功");
return;
}
Console.WriteLine($"バリデーションエラー数: {result.Errors.Count}");
// エラーの分類処理
var errorsByProperty = result.Errors.GroupBy(e => e.PropertyName);
foreach (var propertyErrors in errorsByProperty)
{
Console.WriteLine($"\nプロパティ: {propertyErrors.Key}");
foreach (var error in propertyErrors)
{
Console.WriteLine($" エラーコード: {error.ErrorCode}");
Console.WriteLine($" メッセージ: {error.ErrorMessage}");
Console.WriteLine($" 入力値: {error.AttemptedValue}");
Console.WriteLine($" 重要度: {error.Severity}");
if (error.CustomState != null)
{
Console.WriteLine($" カスタムステート: {error.CustomState}");
}
Console.WriteLine();
}
}
}
public Dictionary<string, List<string>> GetErrorDictionary(ValidationResult result)
{
return result.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(e => e.ErrorMessage).ToList()
);
}
public bool HasCriticalErrors(ValidationResult result)
{
return result.Errors.Any(e => e.Severity == Severity.Error);
}
}
// カスタムバリデーションコンテキスト
public class ValidationContext
{
public DateTime Timestamp { get; set; }
public int UserId { get; set; }
public string ValidationRule { get; set; }
public override string ToString()
{
return $"検証時刻: {Timestamp:yyyy-MM-dd HH:mm:ss}, " +
$"ユーザーID: {UserId}, " +
$"ルール: {ValidationRule}";
}
}
// グローバルバリデーション設定
public static class ValidationConfiguration
{
public static void ConfigureGlobalSettings()
{
// グローバルカスケードモードの設定
ValidatorOptions.Global.DefaultClassLevelCascadeMode = CascadeMode.Continue;
ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
// デフォルト重要度の設定
ValidatorOptions.Global.Severity = Severity.Error;
// カスタム表示名解決の設定
ValidatorOptions.Global.DisplayNameResolver = (type, member, expression) =>
{
if (member != null)
{
// プロパティ名の日本語化
return member.Name switch
{
"Surname" => "苗字",
"Forename" => "名前",
"Address" => "住所",
"Postcode" => "郵便番号",
"Discount" => "割引率",
_ => member.Name
};
}
return null;
};
// カスタムプロパティ名分割の設定
ValidatorOptions.Global.PropertyNameResolver = (type, member, expression) =>
{
if (expression != null)
{
// ネストしたプロパティの日本語表示
return expression.ToString().Replace(".", " の ");
}
return member?.Name;
};
}
}
// 使用例
class Program
{
static void Main()
{
// グローバル設定の適用
ValidationConfiguration.ConfigureGlobalSettings();
var customer = new Customer
{
Id = 1,
Surname = "",
Forename = "A",
Discount = 0.6m,
HasDiscount = true,
Address = "",
Postcode = "invalid"
};
var validator = new AdvancedCustomerValidator();
var result = validator.Validate(customer);
var processor = new ValidationResultProcessor();
processor.ProcessValidationResult(result);
// エラー辞書の取得
var errorDict = processor.GetErrorDictionary(result);
Console.WriteLine("\nエラー辞書:");
foreach (var kvp in errorDict)
{
Console.WriteLine($"{kvp.Key}: {string.Join(", ", kvp.Value)}");
}
// 重大エラーの確認
bool hasCriticalErrors = processor.HasCriticalErrors(result);
Console.WriteLine($"\n重大エラーあり: {hasCriticalErrors}");
}
}
型安全性とパフォーマンス最適化
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;
// 高性能バリデータープール
public class ValidatorPool<T> where T : class
{
private readonly ConcurrentBag<IValidator<T>> _validators;
private readonly Func<IValidator<T>> _validatorFactory;
public ValidatorPool(Func<IValidator<T>> validatorFactory)
{
_validatorFactory = validatorFactory;
_validators = new ConcurrentBag<IValidator<T>>();
}
public IValidator<T> Get()
{
if (_validators.TryTake(out var validator))
{
return validator;
}
return _validatorFactory();
}
public void Return(IValidator<T> validator)
{
_validators.Add(validator);
}
}
// 型安全なバリデーションサービス
public interface IValidationService
{
Task<ValidationResult<T>> ValidateAsync<T>(T instance) where T : class;
ValidationResult<T> Validate<T>(T instance) where T : class;
}
public class ValidationService : IValidationService
{
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<Type, object> _validatorCache;
public ValidationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_validatorCache = new ConcurrentDictionary<Type, object>();
}
public async Task<ValidationResult<T>> ValidateAsync<T>(T instance) where T : class
{
var validator = GetValidator<T>();
var result = await validator.ValidateAsync(instance);
return new ValidationResult<T>
{
Instance = instance,
IsValid = result.IsValid,
Errors = result.Errors.Select(e => new ValidationError
{
PropertyName = e.PropertyName,
ErrorMessage = e.ErrorMessage,
AttemptedValue = e.AttemptedValue,
ErrorCode = e.ErrorCode,
Severity = e.Severity
}).ToList()
};
}
public ValidationResult<T> Validate<T>(T instance) where T : class
{
var validator = GetValidator<T>();
var result = validator.Validate(instance);
return new ValidationResult<T>
{
Instance = instance,
IsValid = result.IsValid,
Errors = result.Errors.Select(e => new ValidationError
{
PropertyName = e.PropertyName,
ErrorMessage = e.ErrorMessage,
AttemptedValue = e.AttemptedValue,
ErrorCode = e.ErrorCode,
Severity = e.Severity
}).ToList()
};
}
private IValidator<T> GetValidator<T>() where T : class
{
return (IValidator<T>)_validatorCache.GetOrAdd(typeof(T), _ =>
_serviceProvider.GetRequiredService<IValidator<T>>());
}
}
// 型安全なバリデーション結果
public class ValidationResult<T> where T : class
{
public T Instance { get; set; }
public bool IsValid { get; set; }
public List<ValidationError> Errors { get; set; } = new();
public bool HasErrorsFor<TProperty>(Expression<Func<T, TProperty>> propertyExpression)
{
var propertyName = GetPropertyName(propertyExpression);
return Errors.Any(e => e.PropertyName == propertyName);
}
public IEnumerable<ValidationError> GetErrorsFor<TProperty>(Expression<Func<T, TProperty>> propertyExpression)
{
var propertyName = GetPropertyName(propertyExpression);
return Errors.Where(e => e.PropertyName == propertyName);
}
private string GetPropertyName<TProperty>(Expression<Func<T, TProperty>> propertyExpression)
{
if (propertyExpression.Body is MemberExpression memberExpression)
{
return memberExpression.Member.Name;
}
throw new ArgumentException("Expression must be a member expression", nameof(propertyExpression));
}
}
public class ValidationError
{
public string PropertyName { get; set; }
public string ErrorMessage { get; set; }
public object AttemptedValue { get; set; }
public string ErrorCode { get; set; }
public Severity Severity { get; set; }
}
// パフォーマンス最適化されたバリデーター基底クラス
public abstract class PerformantValidator<T> : AbstractValidator<T>
{
protected PerformantValidator()
{
// クラスレベルでのカスケード継続(全ルール実行)
ClassLevelCascadeMode = CascadeMode.Continue;
// ルールレベルでの即座停止(個別ルール内では最初の失敗で停止)
RuleLevelCascadeMode = CascadeMode.Stop;
}
// 条件付きバリデーションの最適化
protected void WhenNotNull<TProperty>(Expression<Func<T, TProperty>> expression, Action action)
where TProperty : class
{
When(instance => expression.Compile()(instance) != null, action);
}
// 条件付きバリデーションの最適化(値型用)
protected void WhenHasValue<TProperty>(Expression<Func<T, TProperty?>> expression, Action action)
where TProperty : struct
{
When(instance => expression.Compile()(instance).HasValue, action);
}
}
// 使用例:最適化されたカスタマーバリデーター
public class OptimizedCustomerValidator : PerformantValidator<Customer>
{
public OptimizedCustomerValidator()
{
// 基本ルール(高速)
RuleFor(x => x.Surname).NotEmpty().WithErrorCode("SURNAME_REQUIRED");
RuleFor(x => x.Forename).NotEmpty().WithErrorCode("FORENAME_REQUIRED");
// 条件付きルール(最適化済み)
WhenNotNull(x => x.Address, () =>
{
RuleFor(x => x.Address).Length(10, 200);
});
// 複雑なルール(必要時のみ)
RuleFor(x => x.Postcode)
.Must(BeValidPostcode)
.When(x => !string.IsNullOrEmpty(x.Postcode))
.WithErrorCode("INVALID_POSTCODE");
}
private bool BeValidPostcode(string postcode) =>
System.Text.RegularExpressions.Regex.IsMatch(postcode ?? "", @"^\d{3}-\d{4}$");
}
// ベンチマークテスト
public class ValidationBenchmark
{
private readonly Customer _customer;
private readonly IValidator<Customer> _validator;
private readonly ValidationService _validationService;
public ValidationBenchmark()
{
_customer = new Customer
{
Surname = "田中",
Forename = "太郎",
Address = "東京都新宿区新宿1-1-1",
Postcode = "160-0022"
};
_validator = new OptimizedCustomerValidator();
_validationService = new ValidationService(CreateServiceProvider());
}
public void BenchmarkDirectValidation()
{
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
var result = _validator.Validate(_customer);
}
stopwatch.Stop();
Console.WriteLine($"直接バリデーション (10,000回): {stopwatch.ElapsedMilliseconds}ms");
}
public async Task BenchmarkServiceValidation()
{
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
var result = await _validationService.ValidateAsync(_customer);
}
stopwatch.Stop();
Console.WriteLine($"サービス経由バリデーション (10,000回): {stopwatch.ElapsedMilliseconds}ms");
}
private IServiceProvider CreateServiceProvider()
{
var services = new ServiceCollection();
services.AddScoped<IValidator<Customer>, OptimizedCustomerValidator>();
services.AddScoped<IValidationService, ValidationService>();
return services.BuildServiceProvider();
}
}
// 使用例
class Program
{
static async Task Main()
{
// パフォーマンステスト
var benchmark = new ValidationBenchmark();
benchmark.BenchmarkDirectValidation();
await benchmark.BenchmarkServiceValidation();
// 型安全なバリデーション使用例
var services = new ServiceCollection();
services.AddScoped<IValidator<Customer>, OptimizedCustomerValidator>();
services.AddScoped<IValidationService, ValidationService>();
var serviceProvider = services.BuildServiceProvider();
var validationService = serviceProvider.GetService<IValidationService>();
var customer = new Customer { Surname = "", Forename = "太郎" };
var result = await validationService.ValidateAsync(customer);
if (!result.IsValid)
{
Console.WriteLine("バリデーションエラー:");
foreach (var error in result.Errors)
{
Console.WriteLine($"- {error.PropertyName}: {error.ErrorMessage}");
}
}
// 特定プロパティのエラー確認
if (result.HasErrorsFor(x => x.Surname))
{
var surnameErrors = result.GetErrorsFor(x => x.Surname);
Console.WriteLine($"苗字のエラー数: {surnameErrors.Count()}");
}
}
}