.NET 10 + DDD 领域验证实战:构建坚不可摧的领域模型核心法则

作者:微信公众号:【架构师老卢】
7-29 8:15
14

好的,这是翻译后的技术文章,保留了原始代码块、格式和C#语言标识,并添加了一个吸引人的标题:

领域验证(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 sealed class Email : ValueObject
{
    private static readonly Regex EmailRegex = new(
        @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public string Value { get; }

    private Email(string value)
    {
        Value = value;
    }

    public static Email Create(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new DomainException("Email cannot be empty");

        if (value.Length > 255)
            throw new DomainException("Email cannot exceed 255 characters");

        if (!EmailRegex.IsMatch(value))
            throw new DomainException("Invalid email format");

        return new Email(value.ToLowerInvariant());
    }
}

优势:

  • 通过立即终止操作清晰指示失败
  • 对大多数开发者来说很熟悉
  • 堆栈跟踪有助于调试

劣势:

  • 异常创建带来的性能开销
  • 难以收集多个验证错误
  • 异常处理的复杂性

2. 结果模式验证(Result Pattern Validation) 结果模式(Result pattern)提供了一种函数式的错误处理方法:

public sealed class Result<T>
{
    private readonly T? _value;
    private readonly Error? _error;

    private Result(T value)
    {
        _value = value;
        _error = null;
        IsSuccess = true;
    }

    private Result(Error error)
    {
        _value = default;
        _error = error;
        IsSuccess = false;
    }

    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;

    public T Value => IsSuccess
        ? _value!
        : throw new InvalidOperationException("Cannot access value of failed result");

    public Error Error => IsFailure
        ? _error!
        : throw new 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 static class Guard
{
    public static void NotNull<T>(T value,
        [CallerArgumentExpression(nameof(value))] string? paramName = null)
    {
        if (value is null)
            throw new ArgumentNullException(paramName);
    }

    public static void NotEmpty(string value,
        [CallerArgumentExpression(nameof(value))] string? paramName = null)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new 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)
            throw new DomainException($"{paramName} must be greater than {minimum}");
    }
}

在领域实体中的用法:

public sealed class Product : Entity<ProductId>
{
    public string Name { get; private set; }
    public Money Price { get; private set; }
    public int StockQuantity { get; private set; }

    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 static class CustomerErrors
{
    public static readonly Error NameRequired = new("Customer.NameRequired", "Customer name is required");
    public static readonly Error NameTooLong = new("Customer.NameTooLong", "Customer name cannot exceed 100 characters");
    public static readonly Error EmailRequired = new("Customer.EmailRequired", "Customer email is required");
    public static readonly Error EmailInvalid = new("Customer.EmailInvalid", "Customer email format is invalid");
    public static readonly Error NotFound = new("Customer.NotFound", "Customer not found");
}

public sealed record Error(string Code, string Message); // 错误记录类型

聚合验证与不变条件(Aggregate Validation and Invariants) 聚合(Aggregates)充当一致性边界(consistency boundaries),必须强制执行其内部实体之间的不变条件:

public sealed class Order : AggregateRoot<OrderId>
{
    private readonly List<OrderItem> _items = new();

    public CustomerId CustomerId { get; private set; }
    public Money TotalAmount { get; private set; }
    public OrderStatus Status { get; private set; }

    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 sealed class CreateCustomerCommandValidator : 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 sealed class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, Result<CustomerId>>
{
    private readonly ICustomerRepository _customerRepository;
    private readonly IUnitOfWork _unitOfWork;

    public async 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:新的 CountByAggregateBy 方法简化了验证聚合
  • 更好的错误处理:增强的异常处理和结果处理
  • 安全性改进:强化的验证框架和输入处理

在 .NET 10 中,结合整洁架构和 DDD 的领域验证为构建可维护、业务导向的应用程序提供了坚实的基础。通过在领域层使用守卫子句和结果模式等适当模式实施验证,同时保持清晰的关注点分离,您可以创建既技术上合理又与业务需求保持一致的系统。关键是为您的特定用例选择正确的验证策略,并确保业务规则在您的领域模型中得到一致的强制执行。

相关留言评论
昵称:
邮箱:
阅读排行