首页
/ 革新.NET价值对象生成:从原始类型到领域模型的新范式

革新.NET价值对象生成:从原始类型到领域模型的新范式

2026-04-19 09:34:41作者:龚格成

在现代软件开发中,我们经常面临一个隐藏的危机:原始类型滥用。当int同时代表用户ID、订单号和数量,当string既存储邮箱又保存密码时,编译时无法捕获的逻辑错误、混乱的业务语义和重复的验证代码便会如影随形。Vogen(Value Object Generator)作为.NET生态中的创新工具,通过源代码生成技术,将基础类型转化为具有业务含义的价值对象💡:代表业务实体的强类型封装,为领域驱动设计提供了轻量级解决方案。

Vogen项目封面

🔥 价值解析:为何原始类型正在破坏你的代码

想象这样一个场景:开发人员在修复订单金额计算bug时,误将CustomerId(本质是int)作为Amount(同样是int)传入支付接口。编译器对此毫无反应,但生产环境中这会导致用户账户被错误扣款。这就是典型的原始类型痴迷症——用通用数据类型表示特定业务概念,导致代码可读性降低、错误难以捕获。

Vogen通过以下核心价值解决这些痛点:

  • 编译时类型安全:将int CustomerId转换为强类型CustomerId,使类型不匹配的错误在编译阶段而非运行时暴露
  • 自动验证逻辑:在价值对象创建时嵌入业务规则验证,确保数据从源头符合领域约束
  • 语义化代码表达:让代码直接反映业务概念,CalculateTotal(OrderId id, Money amount)CalculateTotal(int id, decimal amount)更具可读性
  • 减少样板代码:自动生成相等性检查、字符串转换、JSON序列化等重复代码

[!TIP] 价值对象模式特别适合表示:用户ID、订单号、金额、邮箱地址、电话号码等具有明确业务规则的基础类型数据。

💡 应用指南:5分钟上手Vogen流程

步骤1:获取Vogen

通过NuGet将Vogen集成到你的.NET项目中:

# 安装核心包
dotnet add package Vogen

# 如需JSON序列化支持(可选)
dotnet add package Vogen.JsonConverters

步骤2:定义第一个价值对象

创建一个表示"正整数订单号"的价值对象:

// OrderId.cs
using Vogen;

// 声明价值对象,基础类型为int
[ValueObject(typeof(int))]
public partial struct OrderId
{
    // 定义验证规则:订单号必须大于0
    private static Validation Validate(int value)
    {
        return value > 0 
            ? Validation.Ok 
            : Validation.Invalid("订单号必须是正整数");
    }
}

步骤3:使用价值对象

在业务逻辑中直接使用强类型:

// 订单服务
public class OrderService
{
    public void ProcessOrder(OrderId orderId, Money amount)
    {
        // 编译时确保参数类型正确
        Console.WriteLine($"处理订单 {orderId},金额 {amount}");
    }
}

// 使用示例
var validOrderId = OrderId.From(12345); // 成功创建
var invalidOrderId = OrderId.From(-1);  // 抛出ValidationException

// 编译错误:无法将int直接赋值给OrderId
// OrderId wrongId = 123; 

// 正确用法
var orderService = new OrderService();
orderService.ProcessOrder(validOrderId, Money.From(99.9m));

步骤4:集成到Web API

Vogen自动支持ASP.NET Core模型绑定,在控制器中直接接收价值对象:

[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet("{orderId}")]
    public IActionResult GetOrder(OrderId orderId)
    {
        // orderId已通过验证并确保有效
        return Ok($"Order details for {orderId}");
    }
}

Swagger中价值对象参数展示

⚠️ 领域建模实战:三个业务场景案例

案例1:电商系统中的用户标识体系

问题:用户ID、商品ID、订单ID都使用string类型,导致在传递参数时容易混淆,且无法确保ID格式正确。

解决方案:为每种ID创建专用价值对象:

// 用户ID(GUID格式)
[ValueObject(typeof(string))]
public partial struct UserId
{
    private static Validation Validate(string value)
    {
        return Guid.TryParse(value, out _) 
            ? Validation.Ok 
            : Validation.Invalid("用户ID必须是有效的GUID");
    }
}

// 商品SKU(特定格式:字母+数字,8位)
[ValueObject(typeof(string))]
public partial struct ProductSku
{
    private static Validation Validate(string value)
    {
        return Regex.IsMatch(value, @"^[A-Z0-9]{8}$")
            ? Validation.Ok
            : Validation.Invalid("SKU必须是8位字母或数字");
    }
}

效果AddToCart(UserId userId, ProductSku sku)这样的方法签名明确表达业务意图,编译器会阻止错误的参数顺序或类型。

案例2:金融系统中的货币处理

问题:使用decimal表示金额时,无法区分不同货币类型,且容易出现精度错误和无效值(如负数金额)。

解决方案:创建带货币类型的Money价值对象:

[ValueObject(typeof(decimal))]
public partial struct Money
{
    public Currency Currency { get; } // 货币类型属性
    
    // 自定义工厂方法,包含货币信息
    public static Money From(decimal amount, Currency currency)
    {
        Validate(amount); // 调用验证方法
        return new Money(amount, currency);
    }
    
    private static Validation Validate(decimal value)
    {
        return value >= 0 
            ? Validation.Ok 
            : Validation.Invalid("金额不能为负数");
    }
    
    // 金额比较方法
    public bool IsGreaterThan(Money other)
    {
        if (Currency != other.Currency)
            throw new ArgumentException("不能比较不同货币的金额");
            
        return Value > other.Value;
    }
}

// 货币枚举
public enum Currency { CNY, USD, EUR }

效果:确保金额非负,自动处理货币单位,避免不同货币间的直接比较,减少财务计算错误。

案例3:医疗系统中的患者数据验证

问题:患者信息(如病历号、年龄、血压值)使用基本类型存储,缺乏统一验证,导致无效数据进入系统。

解决方案:为关键医疗数据创建带业务规则的价值对象:

// 年龄(0-150岁)
[ValueObject(typeof(int))]
public partial struct Age
{
    private static Validation Validate(int value)
    {
        return value >= 0 && value <= 150
            ? Validation.Ok
            : Validation.Invalid("年龄必须在0-150之间");
    }
}

// 血压值(收缩压90-180 mmHg)
[ValueObject(typeof(int))]
public partial struct SystolicBloodPressure
{
    private static Validation Validate(int value)
    {
        return value >= 90 && value <= 180
            ? Validation.Ok
            : Validation.Invalid("收缩压必须在90-180 mmHg之间");
    }
    
    // 血压状态判断
    public BloodPressureStatus GetStatus()
    {
        if (Value < 120) return BloodPressureStatus.Normal;
        if (Value < 130) return BloodPressureStatus.Elevated;
        if (Value < 140) return BloodPressureStatus.HypertensionStage1;
        return BloodPressureStatus.HypertensionStage2;
    }
}

效果:确保医疗数据符合业务规则,封装领域知识(如血压状态判断),提高系统可靠性。

常见问题速查表

问题场景 解决方案
如何处理可为空的价值对象? 使用[ValueObject(AllowNulls = true)]特性,并在验证方法中处理null情况
价值对象如何支持JSON序列化? 安装Vogen.JsonConverters包,自动生成System.Text.Json转换器
能否继承价值对象? 不建议。价值对象应设计为不可变且无继承层次,可通过组合实现复杂概念
如何比较两个价值对象? Vogen自动生成==和!=运算符及Equals方法,基于基础值比较
能否在EF Core中使用价值对象? 可以,通过[ValueObject(Conversion.EfCoreValueConverter)]生成EF Core转换器
价值对象支持哪些基础类型? 支持所有.NET基本类型(int、string、decimal等)及DateTime、Guid等常见结构体

通过Vogen,我们可以将枯燥的原始类型转变为富有业务含义的领域对象,在编译时捕获错误,减少重复代码,并使代码更接近业务语言。这种转变不仅提升了代码质量,更建立了从代码到业务的直接映射,为领域驱动设计提供了实用工具支持。

Vogen项目口号

登录后查看全文
热门项目推荐
相关项目推荐