Guard Clauses
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
スター履歴
データ取得日時: 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));
}
}