Guard Clauses

バリデーションライブラリC#.NET防御的プログラミングシンプル軽量拡張可能

GitHub概要

ardalis/GuardClauses

A simple package with guard clause extensions.

スター3,250
ウォッチ33
フォーク281
作成日:2017年9月12日
言語:C#
ライセンス:MIT License

トピックス

clean-architectureclean-codedesign-patternsdotnetdotnet-coreguardguard-clauseguard-clauseshacktoberfestpatternpatterns

スター履歴

ardalis/GuardClauses Star History
データ取得日時: 2025/10/22 04:10

ライブラリ

Guard Clauses

概要

Guard ClausesはC#/.NETのための軽量で拡張可能なガードクローズライブラリです。「Fail-Fastの原則を最小限のコードで実現」を理念に開発され、メソッドやコンストラクタの引数検証を簡潔に記述できます。300万回以上のダウンロード実績を持ち、シンプルなAPIと優れた拡張性により、防御的プログラミングの実践を容易にします。

詳細

Ardalis.GuardClausesは、Steve Smith(@ardalis)とNimbleProによって開発された、ガードクローズのベストプラクティスを実装したライブラリです。Guard.Against.*という統一されたAPIを提供し、メソッドの最初で引数検証を行うことで、不正な入力による予期しない動作を防ぎます。全てのガードメソッドは検証済みの値を返すため、変数への直接代入が可能で、コードの簡潔性を保ちながら堅牢性を向上させます。

主な特徴

  • シンプルなAPI: Guard.Against.*の統一されたインターフェース
  • 値の返却: 検証済みの値を返すことで直接代入が可能
  • 豊富な組み込みガード: Null、Empty、OutOfRange、InvalidFormatなど
  • 拡張可能: IGuardClauseへの拡張メソッドで独自ガードを追加
  • 軽量: 最小限の依存関係で高速動作
  • 型安全: ジェネリクスとExpression Treesを活用

メリット・デメリット

メリット

  • 防御的プログラミングを最小限のコードで実現
  • Fail-Fast原則により早期にエラーを検出
  • 読みやすく保守しやすいコード
  • NuGetで簡単にインストール可能
  • .NET Standard 2.0/2.1、.NET 8対応で幅広い互換性
  • カスタムガードクローズの作成が容易

デメリット

  • ビジネスルール検証には不向き(例外を投げるため)
  • パフォーマンスが重要な高頻度処理では例外のオーバーヘッド
  • 複雑な検証ロジックにはFluentValidationなどが適切
  • インターフェース使用による軽微なボクシングのオーバーヘッド
  • 期待される問題にはバリデーションフレームワークの使用が推奨

参考ページ

書き方の例

インストールと基本セットアップ

# NuGetパッケージのインストール
dotnet add package Ardalis.GuardClauses

# Package Manager Consoleの場合
Install-Package Ardalis.GuardClauses

基本的な使い方

using Ardalis.GuardClauses;

public class Order
{
    public string OrderNumber { get; private set; }
    public string CustomerName { get; private set; }
    public int Quantity { get; private set; }
    public decimal Price { get; private set; }
    public DateTime OrderDate { get; private set; }

    public Order(string orderNumber, string customerName, int quantity, decimal price, DateTime orderDate)
    {
        // Guard句による引数検証と代入
        OrderNumber = Guard.Against.NullOrWhiteSpace(orderNumber, nameof(orderNumber));
        CustomerName = Guard.Against.NullOrWhiteSpace(customerName, nameof(customerName));
        Quantity = Guard.Against.NegativeOrZero(quantity, nameof(quantity));
        Price = Guard.Against.Negative(price, nameof(price));
        OrderDate = Guard.Against.OutOfSQLDateRange(orderDate, nameof(orderDate));
    }
}

// メソッドでの使用例
public class OrderService
{
    public void ProcessOrder(Order order)
    {
        // Nullチェック
        Guard.Against.Null(order, nameof(order));
        
        // ビジネスロジック
        Console.WriteLine($"注文 {order.OrderNumber} を処理中...");
    }
    
    public void UpdateQuantity(Order order, int newQuantity)
    {
        Guard.Against.Null(order, nameof(order));
        Guard.Against.NegativeOrZero(newQuantity, nameof(newQuantity));
        Guard.Against.OutOfRange(newQuantity, nameof(newQuantity), 1, 1000);
        
        // 数量更新ロジック
    }
}

組み込みガードクローズの活用

using Ardalis.GuardClauses;

public class ProductService
{
    // 文字列の検証
    public void CreateProduct(string name, string description, string sku)
    {
        // 空文字チェック
        var productName = Guard.Against.NullOrEmpty(name, nameof(name));
        
        // 空白文字を含む空チェック
        var productDescription = Guard.Against.NullOrWhiteSpace(description, nameof(description));
        
        // 文字列長の検証
        var productSku = Guard.Against.NullOrEmpty(sku, nameof(sku));
        if (productSku.Length != 8)
        {
            throw new ArgumentException("SKUは8文字である必要があります", nameof(sku));
        }
    }
    
    // 数値の範囲検証
    public void SetDiscount(decimal discountPercentage)
    {
        // 0から100の範囲内チェック
        var discount = Guard.Against.OutOfRange(discountPercentage, nameof(discountPercentage), 0, 100);
        
        // ビジネスロジック
        Console.WriteLine($"割引率を{discount}%に設定しました");
    }
    
    // 日付の検証
    public void ScheduleDelivery(DateTime deliveryDate)
    {
        // SQL Server日付範囲チェック
        var validDate = Guard.Against.OutOfSQLDateRange(deliveryDate, nameof(deliveryDate));
        
        // 未来日付チェック(カスタムロジック)
        if (validDate <= DateTime.Now)
        {
            throw new ArgumentException("配送日は未来の日付である必要があります", nameof(deliveryDate));
        }
    }
    
    // コレクションの検証
    public void ProcessItems<T>(IEnumerable<T> items)
    {
        // Nullまたは空のコレクションチェック
        var validItems = Guard.Against.NullOrEmpty(items, nameof(items));
        
        foreach (var item in validItems)
        {
            // 処理
        }
    }
}

高度な検証パターン

using Ardalis.GuardClauses;
using System.Text.RegularExpressions;

public class UserRegistration
{
    public string Email { get; private set; }
    public string Username { get; private set; }
    public int Age { get; private set; }
    public string PostalCode { get; private set; }

    public UserRegistration(string email, string username, int age, string postalCode)
    {
        // Expression検証(無効な条件の場合に例外)
        Email = Guard.Against.Expression(
            x => !IsValidEmail(x), 
            email, 
            nameof(email),
            "有効なメールアドレスを入力してください");
        
        // InvalidFormat検証(正規表現)
        Username = Guard.Against.InvalidFormat(
            username, 
            nameof(username), 
            @"^[a-zA-Z0-9_]{3,20}$",
            "ユーザー名は3-20文字の英数字とアンダースコアのみ使用可能です");
        
        // 範囲検証
        Age = Guard.Against.OutOfRange(age, nameof(age), 18, 120);
        
        // 日本の郵便番号形式検証
        PostalCode = Guard.Against.InvalidFormat(
            postalCode,
            nameof(postalCode),
            @"^\d{3}-\d{4}$",
            "郵便番号は123-4567の形式で入力してください");
    }
    
    private static bool IsValidEmail(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return false;
        
        // 簡易的なメールアドレス検証
        return Regex.IsMatch(email, 
            @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
    }
}

// InvalidInput検証の例(有効な条件でtrueを返す)
public class PaymentService
{
    public void ProcessPayment(decimal amount, string creditCardNumber)
    {
        // 金額の検証
        var validAmount = Guard.Against.InvalidInput(
            amount,
            nameof(amount),
            x => x > 0 && x <= 1000000,
            "支払い金額は0円より大きく100万円以下である必要があります");
        
        // クレジットカード番号の検証
        var validCardNumber = Guard.Against.InvalidInput(
            creditCardNumber,
            nameof(creditCardNumber),
            IsValidCreditCard,
            "有効なクレジットカード番号を入力してください");
        
        // 支払い処理
        Console.WriteLine($"{validAmount}円の支払いを処理中...");
    }
    
    private bool IsValidCreditCard(string cardNumber)
    {
        // Luhnアルゴリズムなどの実装
        return !string.IsNullOrWhiteSpace(cardNumber) && cardNumber.Length == 16;
    }
}

カスタムガードクローズの作成

using Ardalis.GuardClauses;
using System.Runtime.CompilerServices;

// カスタムガードクローズの拡張メソッド(必ずArdalis.GuardClauses名前空間に配置)
namespace Ardalis.GuardClauses
{
    public static class GuardClauseExtensions
    {
        // 日本の電話番号検証
        public static string JapanesePhoneNumber(
            this IGuardClause guardClause,
            string input,
            [CallerArgumentExpression("input")] string? parameterName = null)
        {
            Guard.Against.NullOrWhiteSpace(input, parameterName);
            
            // 日本の電話番号パターン(例: 03-1234-5678, 090-1234-5678)
            var phonePattern = @"^0\d{1,4}-\d{1,4}-\d{4}$";
            
            if (!Regex.IsMatch(input, phonePattern))
            {
                throw new ArgumentException(
                    $"パラメータ {parameterName} は有効な日本の電話番号形式ではありません。",
                    parameterName);
            }
            
            return input;
        }
        
        // 平仮名のみ検証
        public static string Hiragana(
            this IGuardClause guardClause,
            string input,
            [CallerArgumentExpression("input")] string? parameterName = null)
        {
            Guard.Against.NullOrWhiteSpace(input, parameterName);
            
            if (!Regex.IsMatch(input, @"^[\u3040-\u309F\s]+$"))
            {
                throw new ArgumentException(
                    $"パラメータ {parameterName} は平仮名のみで入力してください。",
                    parameterName);
            }
            
            return input;
        }
        
        // 営業日検証
        public static DateTime BusinessDay(
            this IGuardClause guardClause,
            DateTime input,
            [CallerArgumentExpression("input")] string? parameterName = null)
        {
            if (input.DayOfWeek == DayOfWeek.Saturday || 
                input.DayOfWeek == DayOfWeek.Sunday)
            {
                throw new ArgumentException(
                    $"パラメータ {parameterName} は営業日(月-金)である必要があります。",
                    parameterName);
            }
            
            return input;
        }
        
        // 偶数検証
        public static int Even(
            this IGuardClause guardClause,
            int input,
            [CallerArgumentExpression("input")] string? parameterName = null,
            string? message = null)
        {
            if (input % 2 != 0)
            {
                throw new ArgumentException(
                    message ?? $"パラメータ {parameterName} は偶数である必要があります。",
                    parameterName);
            }
            
            return input;
        }
        
        // パスワード強度検証
        public static string StrongPassword(
            this IGuardClause guardClause,
            string input,
            [CallerArgumentExpression("input")] string? parameterName = null)
        {
            Guard.Against.NullOrWhiteSpace(input, parameterName);
            
            var hasNumber = Regex.IsMatch(input, @"[0-9]+");
            var hasUpperChar = Regex.IsMatch(input, @"[A-Z]+");
            var hasLowerChar = Regex.IsMatch(input, @"[a-z]+");
            var hasSpecialChar = Regex.IsMatch(input, @"[!@#$%^&*(),.?""':{}|<>]+");
            var hasMinimum8Chars = input.Length >= 8;
            
            if (!(hasNumber && hasUpperChar && hasLowerChar && hasSpecialChar && hasMinimum8Chars))
            {
                throw new ArgumentException(
                    $"パラメータ {parameterName} は8文字以上で、大文字、小文字、数字、特殊文字を含む必要があります。",
                    parameterName);
            }
            
            return input;
        }
    }
}

// カスタムガードクローズの使用例
public class JapaneseUserService
{
    public void RegisterUser(string name, string phoneNumber, string furigana, string password)
    {
        // カスタムガードの使用
        var validName = Guard.Against.NullOrWhiteSpace(name, nameof(name));
        var validPhone = Guard.Against.JapanesePhoneNumber(phoneNumber, nameof(phoneNumber));
        var validFurigana = Guard.Against.Hiragana(furigana, nameof(furigana));
        var validPassword = Guard.Against.StrongPassword(password, nameof(password));
        
        Console.WriteLine($"ユーザー登録: {validName} ({validFurigana})");
        Console.WriteLine($"電話番号: {validPhone}");
    }
    
    public void ScheduleMeeting(DateTime meetingDate, int attendeeCount)
    {
        // 営業日チェック
        var validDate = Guard.Against.BusinessDay(meetingDate, nameof(meetingDate));
        
        // 偶数人数チェック(ペアワークの場合など)
        var validCount = Guard.Against.Even(attendeeCount, nameof(attendeeCount), 
            "ペアワークのため参加者は偶数人数である必要があります");
        
        Console.WriteLine($"{validDate:yyyy/MM/dd}{validCount}名でミーティングを設定しました");
    }
}

エラーハンドリングとカスタム例外

using Ardalis.GuardClauses;

// カスタム例外を使用するガードクローズ
public static class CustomExceptionGuardExtensions
{
    public static T NotFound<T>(
        this IGuardClause guardClause,
        T? input,
        string key,
        [CallerArgumentExpression("input")] string? parameterName = null)
        where T : class
    {
        if (input is null)
        {
            throw new NotFoundException(key, parameterName ?? "unknown");
        }
        
        return input;
    }
    
    public static decimal WithinBudget(
        this IGuardClause guardClause,
        decimal input,
        decimal maxBudget,
        [CallerArgumentExpression("input")] string? parameterName = null)
    {
        if (input > maxBudget)
        {
            throw new BudgetExceededException(input, maxBudget, parameterName ?? "unknown");
        }
        
        return input;
    }
}

// カスタム例外クラス
public class NotFoundException : Exception
{
    public string Key { get; }
    public string Type { get; }
    
    public NotFoundException(string key, string type) 
        : base($"{type} with key '{key}' was not found.")
    {
        Key = key;
        Type = type;
    }
}

public class BudgetExceededException : Exception
{
    public decimal RequestedAmount { get; }
    public decimal MaxBudget { get; }
    
    public BudgetExceededException(decimal requestedAmount, decimal maxBudget, string parameterName)
        : base($"要求額 {requestedAmount:C} が予算上限 {maxBudget:C} を超えています。({parameterName})")
    {
        RequestedAmount = requestedAmount;
        MaxBudget = maxBudget;
    }
}

// 使用例
public class ExpenseService
{
    private readonly Dictionary<string, Expense> _expenses = new();
    private readonly decimal _maxBudget = 100000m;
    
    public void ApproveExpense(string expenseId, decimal amount)
    {
        // NotFoundガード(カスタム例外)
        var expense = Guard.Against.NotFound(
            _expenses.GetValueOrDefault(expenseId), 
            expenseId);
        
        // 予算内チェック(カスタム例外)
        var validAmount = Guard.Against.WithinBudget(
            amount, 
            _maxBudget);
        
        expense.Approve(validAmount);
    }
}

public class Expense
{
    public string Id { get; set; }
    public decimal Amount { get; set; }
    public bool IsApproved { get; set; }
    
    public void Approve(decimal amount)
    {
        Amount = amount;
        IsApproved = true;
    }
}

パフォーマンスを考慮した使用方法

using Ardalis.GuardClauses;
using System.Diagnostics;

public class PerformanceOptimizedService
{
    // 高頻度で呼ばれるメソッドでの最適化
    public void ProcessHighFrequencyData(string data)
    {
        // Guard句は必要最小限に
        if (string.IsNullOrWhiteSpace(data))
        {
            throw new ArgumentException("Data cannot be null or empty", nameof(data));
        }
        
        // 以降の処理...
    }
    
    // 条件付きガードの使用
    public void ProcessConditionally(Order? order, bool validateStrict)
    {
        if (validateStrict)
        {
            // 厳密な検証が必要な場合のみGuard句を使用
            Guard.Against.Null(order, nameof(order));
            Guard.Against.NullOrWhiteSpace(order!.OrderNumber, nameof(order.OrderNumber));
            Guard.Against.OutOfRange(order.Quantity, nameof(order.Quantity), 1, 1000);
        }
        else if (order == null)
        {
            // 最小限のチェックのみ
            throw new ArgumentNullException(nameof(order));
        }
        
        // ビジネスロジック
    }
    
    // デバッグビルドでのみ有効なガード
    [Conditional("DEBUG")]
    private void DebugGuard<T>(T value, string parameterName) where T : class
    {
        Guard.Against.Null(value, parameterName);
    }
    
    public void ProcessWithDebugGuards(Customer customer)
    {
        // デバッグ時のみ検証
        DebugGuard(customer, nameof(customer));
        
        // 本番環境でも必要な検証
        if (customer == null)
        {
            throw new ArgumentNullException(nameof(customer));
        }
        
        // 処理続行
    }
}

// ベンチマーク比較
public class GuardClauseBenchmark
{
    private readonly string _testString = "test";
    private readonly int _testNumber = 50;
    
    public void CompareValidationApproaches()
    {
        const int iterations = 1000000;
        
        // Guard句使用
        var sw1 = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            try
            {
                var result = Guard.Against.NullOrWhiteSpace(_testString, nameof(_testString));
            }
            catch { }
        }
        sw1.Stop();
        
        // 手動チェック
        var sw2 = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            if (string.IsNullOrWhiteSpace(_testString))
            {
                throw new ArgumentException("Value cannot be null or whitespace", nameof(_testString));
            }
        }
        sw2.Stop();
        
        Console.WriteLine($"Guard句: {sw1.ElapsedMilliseconds}ms");
        Console.WriteLine($"手動チェック: {sw2.ElapsedMilliseconds}ms");
    }
}

ASP.NET Coreとの統合

using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    
    public ProductsController(IProductService productService)
    {
        _productService = Guard.Against.Null(productService, nameof(productService));
    }
    
    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        try
        {
            // IDの検証
            var validId = Guard.Against.NegativeOrZero(id, nameof(id));
            
            var product = await _productService.GetByIdAsync(validId);
            
            // NotFoundガード
            var validProduct = Guard.Against.Null(product, nameof(product));
            
            return Ok(validProduct);
        }
        catch (ArgumentException ex)
        {
            return BadRequest(new { error = ex.Message });
        }
        catch (NotFoundException ex)
        {
            return NotFound(new { error = ex.Message });
        }
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] CreateProductDto dto)
    {
        try
        {
            // DTOの検証
            Guard.Against.Null(dto, nameof(dto));
            Guard.Against.NullOrWhiteSpace(dto.Name, nameof(dto.Name));
            Guard.Against.Negative(dto.Price, nameof(dto.Price));
            Guard.Against.OutOfRange(dto.Stock, nameof(dto.Stock), 0, 10000);
            
            var product = await _productService.CreateAsync(dto);
            
            return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
        }
        catch (ArgumentException ex)
        {
            return BadRequest(new 
            { 
                error = ex.Message,
                parameter = ex.ParamName 
            });
        }
    }
}

// ミドルウェアでの使用
public class RequestValidationMiddleware
{
    private readonly RequestDelegate _next;
    
    public RequestValidationMiddleware(RequestDelegate next)
    {
        _next = Guard.Against.Null(next, nameof(next));
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        // リクエストヘッダーの検証
        var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
        
        try
        {
            Guard.Against.NullOrWhiteSpace(apiKey, "X-API-Key");
            Guard.Against.InvalidFormat(apiKey, "X-API-Key", @"^[A-Za-z0-9]{32}$");
            
            await _next(context);
        }
        catch (ArgumentException ex)
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync($"Invalid API Key: {ex.Message}");
        }
    }
}

// DTOクラス
public class CreateProductDto
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

テストでの活用

using Xunit;
using Ardalis.GuardClauses;

public class OrderTests
{
    [Fact]
    public void Constructor_WithNullOrderNumber_ThrowsArgumentNullException()
    {
        // Arrange
        string? orderNumber = null;
        var customerName = "山田太郎";
        var quantity = 10;
        var price = 1000m;
        var orderDate = DateTime.Now;
        
        // Act & Assert
        var exception = Assert.Throws<ArgumentNullException>(() =>
            new Order(orderNumber!, customerName, quantity, price, orderDate));
        
        Assert.Equal("orderNumber", exception.ParamName);
    }
    
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(-100)]
    public void Constructor_WithInvalidQuantity_ThrowsArgumentException(int invalidQuantity)
    {
        // Arrange
        var orderNumber = "ORD-001";
        var customerName = "山田太郎";
        var price = 1000m;
        var orderDate = DateTime.Now;
        
        // Act & Assert
        var exception = Assert.Throws<ArgumentException>(() =>
            new Order(orderNumber, customerName, invalidQuantity, price, orderDate));
        
        Assert.Equal("quantity", exception.ParamName);
    }
    
    [Fact]
    public void CustomGuard_JapanesePhoneNumber_ValidatesCorrectly()
    {
        // Arrange
        var validPhone = "03-1234-5678";
        var invalidPhone = "123-456-7890";
        
        // Act & Assert
        var result = Guard.Against.JapanesePhoneNumber(validPhone);
        Assert.Equal(validPhone, result);
        
        Assert.Throws<ArgumentException>(() => 
            Guard.Against.JapanesePhoneNumber(invalidPhone));
    }
}

// モックを使用したテスト
public class ProductServiceTests
{
    [Fact]
    public void Constructor_WithNullRepository_ThrowsArgumentNullException()
    {
        // Arrange
        IProductRepository? repository = null;
        
        // Act & Assert
        Assert.Throws<ArgumentNullException>(() => 
            new ProductService(repository!));
    }
}

実践的な使用パターン

using Ardalis.GuardClauses;

// ドメインエンティティでの使用
public class DomainEntity
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime? ModifiedAt { get; private set; }
    
    protected DomainEntity(Guid id, string name)
    {
        Id = Guard.Against.Default(id, nameof(id));
        Name = Guard.Against.NullOrWhiteSpace(name, nameof(name));
        CreatedAt = DateTime.UtcNow;
    }
    
    public void UpdateName(string newName)
    {
        Name = Guard.Against.NullOrWhiteSpace(newName, nameof(newName));
        ModifiedAt = DateTime.UtcNow;
    }
}

// リポジトリパターンでの使用
public class Repository<T> where T : DomainEntity
{
    private readonly Dictionary<Guid, T> _entities = new();
    
    public T GetById(Guid id)
    {
        var validId = Guard.Against.Default(id, nameof(id));
        
        var entity = _entities.GetValueOrDefault(validId);
        return Guard.Against.Null(entity, nameof(entity));
    }
    
    public void Add(T entity)
    {
        var validEntity = Guard.Against.Null(entity, nameof(entity));
        
        if (_entities.ContainsKey(validEntity.Id))
        {
            throw new InvalidOperationException($"Entity with ID {validEntity.Id} already exists");
        }
        
        _entities.Add(validEntity.Id, validEntity);
    }
}

// サービスレイヤーでの使用
public class BusinessService
{
    private readonly ILogger<BusinessService> _logger;
    
    public BusinessService(ILogger<BusinessService> logger)
    {
        _logger = Guard.Against.Null(logger, nameof(logger));
    }
    
    public async Task<decimal> CalculateTotalPrice(
        decimal unitPrice, 
        int quantity, 
        decimal? discountRate = null)
    {
        // 基本的な検証
        var validPrice = Guard.Against.Negative(unitPrice, nameof(unitPrice));
        var validQuantity = Guard.Against.NegativeOrZero(quantity, nameof(quantity));
        
        var total = validPrice * validQuantity;
        
        if (discountRate.HasValue)
        {
            // 割引率の検証
            var validDiscount = Guard.Against.OutOfRange(
                discountRate.Value, 
                nameof(discountRate), 
                0, 
                1);
            
            total *= (1 - validDiscount);
        }
        
        _logger.LogInformation(
            "価格計算完了: 単価={UnitPrice}, 数量={Quantity}, 合計={Total}", 
            validPrice, 
            validQuantity, 
            total);
        
        return total;
    }
}

// 設定クラスでの使用
public class AppSettings
{
    public string ConnectionString { get; }
    public int MaxRetryCount { get; }
    public TimeSpan Timeout { get; }
    
    public AppSettings(IConfiguration configuration)
    {
        Guard.Against.Null(configuration, nameof(configuration));
        
        ConnectionString = Guard.Against.NullOrWhiteSpace(
            configuration.GetConnectionString("DefaultConnection"),
            "DefaultConnection");
        
        MaxRetryCount = Guard.Against.OutOfRange(
            configuration.GetValue<int>("MaxRetryCount"),
            "MaxRetryCount",
            1,
            10);
        
        var timeoutSeconds = configuration.GetValue<int>("TimeoutSeconds");
        Timeout = TimeSpan.FromSeconds(
            Guard.Against.OutOfRange(timeoutSeconds, "TimeoutSeconds", 1, 300));
    }
}