.NET 10 + DDD 領域驗證實戰:構建堅不可摧的領域模型核心法則
驗證(Domain Validation)是在 .NET 10 中使用整潔架構(Clean Architecture)和領域驅動設計(Domain-Driven Design, DDD)原則構建健壯、可維護應用程序的基石。它確保業務規則和領域不變條件(invariants)得到一致地強制執行,同時保持清晰的關注點分離(separation of concerns),并防止無效狀態破壞您的領域模型。
理解領域驗證基礎
領域驗證與輸入驗證(input validation)有著根本性的不同。輸入驗證確保數據在應用程序邊界處滿足基本格式要求,而領域驗證則強制執行定義領域對象有效性的業務規則和不變條件。在 DDD 中,領域實體(domain entities)應該始終是有效的實體——絕不應存在實體可以處于無效狀態的情況。
“始終有效的領域模型”(Always-Valid Domain Model)原則指出,領域對象應該保護自己,避免變成無效狀態。這種方法提供了幾個關鍵優勢:
? 消除防御性編程(Defensive Programming):一旦創建,您可以信任領域對象處于有效狀態,無需進行持續的驗證檢查
? 集中化業務邏輯:所有驗證規則都存在于領域對象本身
? 降低維護負擔:消除了代碼庫中分散的驗證檢查
兩種主要的驗證方法
1. 基于異常的驗證(Exception-Based Validation)
傳統方法使用異常來指示驗證失敗:
public sealedclassEmail : ValueObject
{
privatestaticreadonly Regex EmailRegex = new(
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
publicstring Value { get; }
private Email(string value)
{
Value = value;
}
public static Email Create(string value)
{
if (string.IsNullOrWhiteSpace(value))
thrownew DomainException("Email cannot be empty");
if (value.Length > 255)
thrownew DomainException("Email cannot exceed 255 characters");
if (!EmailRegex.IsMatch(value))
thrownew DomainException("Invalid email format");
returnnew Email(value.ToLowerInvariant());
}
}優勢:
? 通過立即終止操作清晰指示失敗
? 對大多數開發者來說很熟悉
? 堆棧跟蹤有助于調試
劣勢:
? 異常創建帶來的性能開銷
? 難以收集多個驗證錯誤
? 異常處理的復雜性
2. 結果模式驗證(Result Pattern Validation)
結果模式(Result pattern)提供了一種函數式的錯誤處理方法:
public sealedclassResult<T>
{
privatereadonly T? _value;
privatereadonly Error? _error;
private Result(T value)
{
_value = value;
_error = null;
IsSuccess = true;
}
private Result(Error error)
{
_value = default;
_error = error;
IsSuccess = false;
}
publicbool IsSuccess { get; }
publicbool IsFailure => !IsSuccess;
public T Value => IsSuccess
? _value!
: thrownew InvalidOperationException("Cannot access value of failed result");
public Error Error => IsFailure
? _error!
: thrownew InvalidOperationException("Cannot access error of successful result");
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(Error error) => new(error);
}優勢:
? 顯式錯誤處理:調用者必須顯式處理成功/失敗情況
? 提高性能:避免異常開銷
? 更易測試:比測試拋出異常的代碼更容易
? 收集多個錯誤:可以聚合驗證錯誤
劣勢:
? 冗長:相比異常需要編寫更多代碼
? 堆棧跟蹤傳播:必須標記調用鏈中的所有方法以返回 Result 對象
用于保護不變條件的守衛子句(Guard Clauses)
守衛子句提供了一種優雅的方式來強制執行驗證規則,同時保持代碼的整潔和可讀性:
public staticclassGuard
{
public static void NotNull<T>(T value,
[CallerArgumentExpression(nameof(value))] string? paramName = null)
{
if (valueisnull)
thrownew ArgumentNullException(paramName);
}
public static void NotEmpty(string value,
[CallerArgumentExpression(nameof(value))] string? paramName = null)
{
if (string.IsNullOrWhiteSpace(value))
thrownew DomainException($"{paramName} cannot be empty");
}
public static void GreaterThan<T>(T value, T minimum,
[CallerArgumentExpression(nameof(value))] string? paramName = null)
where T : IComparable<T>
{
if (value.CompareTo(minimum) <= 0)
thrownew DomainException($"{paramName} must be greater than {minimum}");
}
}在領域實體中的用法:
public sealedclassProduct : Entity<ProductId>
{
publicstring Name { get; privateset; }
public Money Price { get; privateset; }
publicint StockQuantity { get; privateset; }
public Product(string name, Money price, int stockQuantity)
: base(new ProductId(Guid.NewGuid()))
{
Guard.NotEmpty(name, nameof(name));
Guard.NotNull(price, nameof(price));
Guard.GreaterThan(stockQuantity, -1, nameof(stockQuantity));
Name = name;
Price = price;
StockQuantity = stockQuantity;
}
}領域錯誤目錄(Domain Error Catalogs)
創建集中化的錯誤目錄以提高可維護性:
public staticclassCustomerErrors
{
publicstaticreadonly Error NameRequired = new("Customer.NameRequired", "Customer name is required");
publicstaticreadonly Error NameTooLong = new("Customer.NameTooLong", "Customer name cannot exceed 100 characters");
publicstaticreadonly Error EmailRequired = new("Customer.EmailRequired", "Customer email is required");
publicstaticreadonly Error EmailInvalid = new("Customer.EmailInvalid", "Customer email format is invalid");
publicstaticreadonly Error NotFound = new("Customer.NotFound", "Customer not found");
}
public sealed record Error(string Code, string Message); // 錯誤記錄類型聚合驗證與不變條件(Aggregate Validation and Invariants)
聚合(Aggregates)充當一致性邊界(consistency boundaries),必須強制執行其內部實體之間的不變條件:
public sealedclassOrder : AggregateRoot<OrderId>
{
privatereadonly List<OrderItem> _items = new();
public CustomerId CustomerId { get; privateset; }
public Money TotalAmount { get; privateset; }
public OrderStatus Status { get; privateset; }
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public static Result<Order> Create(CustomerId customerId, List<OrderItem> items)
{
// 業務規則:訂單必須至少包含一個項目
if (!items.Any())
return Result<Order>.Failure(OrderErrors.EmptyOrder);
// 業務規則:訂單金額不能超過最大值
var totalAmount = items.Sum(item => item.Price.Amount * item.Quantity);
if (totalAmount > 10000)
return Result<Order>.Failure(OrderErrors.ExceedsMaximumValue);
var order = new Order(customerId, new Money(totalAmount, "USD"));
foreach (var item in items)
{
order._items.Add(item);
}
return Result<Order>.Success(order);
}
}與 .NET 10 中 FluentValidation 的集成
雖然領域驗證應位于領域層(domain layer),但 FluentValidation 在應用層(application layer)對其進行了補充:
public sealedclassCreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Customer name is required")
.MaximumLength(100)
.WithMessage("Customer name cannot exceed 100 characters");
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage("Customer email is required")
.EmailAddress()
.WithMessage("Customer email format is invalid");
}
}結合兩種方法的應用層處理程序:
public sealedclassCreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, Result<CustomerId>>
{
privatereadonly ICustomerRepository _customerRepository;
privatereadonly IUnitOfWork _unitOfWork;
publicasync Task<Result<CustomerId>> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
// 通過工廠方法進行領域驗證
var customerResult = Customer.Create(request.Name, request.Email);
if (customerResult.IsFailure)
return Result<CustomerId>.Failure(customerResult.Error);
_customerRepository.Add(customerResult.Value);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result<CustomerId>.Success(customerResult.Value.Id);
}
}領域驗證的最佳實踐
選擇正確的驗證策略
在以下情況下使用異常:
? 驗證失敗代表編程錯誤
? 需要立即終止無效操作
? 預期發生單一驗證失敗
在以下情況下使用結果模式:
? 需要收集多個驗證錯誤
? 希望進行顯式錯誤處理
? 性能至關重要
正確分層驗證
? 輸入驗證(Input Validation)(應用層):
格式驗證
必填字段檢查
基本數據類型驗證
? 業務驗證(Business Validation)(領域層):
業務規則強制執行
不變條件保護
跨實體驗證
使驗證顯式化
使用業務利益相關者可以理解的清晰、描述性的錯誤消息和代碼。避免層之間的驗證重復——依靠領域對象來維護其自身的有效性。
.NET 10 的特定增強功能
.NET 10 帶來了幾項與領域驗證相關的改進:
? 增強的性能:運行時優化有利于驗證密集的場景
? 改進的 LINQ:新的 CountBy 和 AggregateBy 方法簡化了驗證聚合
? 更好的錯誤處理:增強的異常處理和結果處理
? 安全性改進:強化的驗證框架和輸入處理
在 .NET 10 中,結合整潔架構和 DDD 的領域驗證為構建可維護、業務導向的應用程序提供了堅實的基礎。通過在領域層使用守衛子句和結果模式等適當模式實施驗證,同時保持清晰的關注點分離,您可以創建既技術上合理又與業務需求保持一致的系統。關鍵是為您的特定用例選擇正確的驗證策略,并確保業務規則在您的領域模型中得到一致的強制執行。





























