Skip to content

值转换 Value Conversions ​

目录 ​


什么是值转换 ​

概念理解 ​

值转换(Value Conversion) 允许将实体属性的 CLR 类型转换为不同的数据库类型存储,实现领域模型与数据库结构的解耦。

csharp
// 领域模型: 使用强类型
public class Order
{
    public OrderId Id { get; set; }          // 值对象
    public Money TotalAmount { get; set; }   // 金额对象
    public PhoneNumber Phone { get; set; }   // 电话号码
    public Address ShippingAddress { get; set; } // 地址对象
}

// 数据库存储: 使用基本类型
// Orders 表:
// - Id: INT
// - TotalAmount: DECIMAL(18,2)
// - Phone: NVARCHAR(20)
// - ShippingAddress_Street: NVARCHAR(200)
// - ShippingAddress_City: NVARCHAR(100)

// 值转换器: 自动双向转换
// OrderId (CLR) ↔ INT (Database)
// Money (CLR) ↔ DECIMAL (Database)
// PhoneNumber (CLR) ↔ STRING (Database)

工作原理 ​

mermaid
graph LR
    A[实体属性] --> B[值转换器]
    B --> C[数据库列]
    C --> B
    B --> A
    
    style A fill:#e1f5ff
    style C fill:#fff4e1
    style B fill:#d4edda

转换流程:

  1. 保存时: CLR 类型 → 转换 → 数据库类型 → 写入
  2. 读取时: 数据库类型 → 转换 → CLR 类型 → 赋值

为什么需要值转换? ​

问题: 领域模型与数据库不匹配 ​

csharp
// ❌ 没有值转换: 领域模型被数据库污染
public class Order
{
    // 应该用值对象,但为了数据库妥协为 int
    public int OrderId { get; set; }
    
    // 应该用 decimal,但要手动处理货币
    public decimal Amount { get; set; }
    public string Currency { get; set; }
    
    // 应该用 PhoneNumber 类型,但只能用 string
    public string Phone { get; set; }
    
    // 地址应该是值对象,但只能拆分为多个字段
    public string Street { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

// 问题:
// - 类型安全性差
// - 缺少验证逻辑
// - 代码重复
// - 不符合 DDD 原则

解决: 使用值转换 ​

csharp
// ✅ 使用值转换: 领域模型纯净
public class Order
{
    public OrderId Id { get; set; }           // 强类型 ID
    public Money TotalAmount { get; set; }    // 包含货币
    public PhoneNumber Phone { get; set; }    // 带验证
    public Address ShippingAddress { get; set; } // 值对象
}

// 配置转换
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .Property(o => o.Id)
        .HasConversion(
            v => v.Value,              // OrderId → int
            v => new OrderId(v)        // int → OrderId
        );
    
    modelBuilder.Entity<Order>()
        .Property(o => o.TotalAmount)
        .HasConversion(
            v => v.Amount,             // Money → decimal
            v => new Money(v, "USD")   // decimal → Money
        );
    
    modelBuilder.Entity<Order>()
        .Property(o => o.Phone)
        .HasConversion(
            v => v.Number,             // PhoneNumber → string
            v => new PhoneNumber(v)    // string → PhoneNumber
        );
}

内置值转换器 ​

EF Core 提供的转换器 ​

EF Core 提供了多种内置转换器:

1. Enum ↔ String ​

csharp
public enum OrderStatus
{
    Pending = 0,
    Processing = 1,
    Shipped = 2,
    Delivered = 3,
    Cancelled = 4
}

public class Order
{
    public int Id { get; set; }
    public OrderStatus Status { get; set; }
}

// 默认: Enum → Int
// Status 列: 0, 1, 2, 3, 4

// 转换为字符串
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .Property(o => o.Status)
        .HasConversion<string>();  // ← 简单!
}

// Status 列: "Pending", "Processing", "Shipped"

// 优点:
// - 可读性好
// - 重构安全(重命名 Enum 成员不会破坏数据)
// - 调试方便

2. DateTime ↔ Utc ​

csharp
public class Event
{
    public int Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}

// 确保始终存储 UTC 时间
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Event>()
        .Property(e => e.CreatedAt)
        .HasConversion(
            v => v.ToUniversalTime(),     // DateTime → UTC
            v => DateTime.SpecifyKind(v, DateTimeKind.Utc)  // UTC → DateTime
        );
    
    modelBuilder.Entity<Event>()
        .Property(e => e.UpdatedAt)
        .HasConversion(
            v => v.HasValue ? v.Value.ToUniversalTime() : (DateTime?)null,
            v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : (DateTime?)null
        );
}

// 或者使用 .NET 8+ 简化语法
modelBuilder.Entity<Event>()
    .Property(e => e.CreatedAt)
    .HasConversion<UtcValueConverter>();

3. Bool ↔ String ​

csharp
public class User
{
    public bool IsActive { get; set; }
}

// 存储为 "Y"/"N"(兼容旧系统)
modelBuilder.Entity<User>()
    .Property(u => u.IsActive)
    .HasConversion(
        v => v ? "Y" : "N",
        v => v == "Y"
    );

4. String ↔ Trimmed ​

csharp
public class Product
{
    public string Name { get; set; }
    public string Description { get; set; }
}

// 自动去除首尾空格
modelBuilder.Entity<Product>()
    .Property(p => p.Name)
    .HasConversion(
        v => v?.Trim(),
        v => v?.Trim()
    );

5. Number Formatting ​

csharp
public class Product
{
    public decimal Price { get; set; }
}

// 格式化为固定小数位
modelBuilder.Entity<Product>()
    .Property(p => p.Price)
    .HasConversion(
        v => Math.Round(v, 2),
        v => Math.Round(v, 2)
    );

完整内置转换器列表 ​

转换器用途示例
ToStringConversion()Any → StringEnum → String
ToNumberConversion()String → Number"123" → 123
ToBoolConversion()Any → Bool"Y" → true
ToDateTimeConversion()Any → DateTimeTimestamp → DateTime
UtcValueConverterDateTime → UTCLocal → Utc
TrimmingStringConverterString → Trimmed" abc " → "abc"

自定义值转换器 ​

基础自定义转换器 ​

示例 1: 强类型 ID ​

csharp
// 值对象: 订单 ID
public readonly struct OrderId : IEquatable<OrderId>
{
    public int Value { get; }
    
    public OrderId(int value)
    {
        if (value <= 0)
            throw new ArgumentException("Order ID must be positive");
        
        Value = value;
    }
    
    public bool Equals(OrderId other) => Value == other.Value;
    public override bool Equals(object obj) => obj is OrderId other && Equals(other);
    public override int GetHashCode() => Value.GetHashCode();
    public override string ToString() => $"ORD-{Value}";
    
    public static implicit operator int(OrderId id) => id.Value;
    public static implicit operator OrderId(int value) => new OrderId(value);
}

// 实体
public class Order
{
    public OrderId Id { get; set; }
    public DateTime OrderDate { get; set; }
}

// 转换器
public class OrderIdConverter : ValueConverter<OrderId, int>
{
    public OrderIdConverter() : base(
        v => v.Value,              // OrderId → int
        v => new OrderId(v),       // int → OrderId
        new ConverterMappingHints(valueComparer: new OrderIdComparer())
    )
    {
    }
}

// 比较器
public class OrderIdComparer : ValueComparer<OrderId>
{
    public OrderIdComparer() : base(
        (v1, v2) => v1.Equals(v2),
        v => v.GetHashCode(),
        v => v
    )
    {
    }
}

// 配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .Property(o => o.Id)
        .HasConversion(new OrderIdConverter());
    
    // .NET 8+ 简化语法
    modelBuilder.Entity<Order>()
        .Property(o => o.Id)
        .HasConversion<int>(
            v => v.Value,
            v => new OrderId(v)
        );
}

示例 2: 金额对象 ​

csharp
// 值对象: 金额
public class Money : IEquatable<Money>
{
    public decimal Amount { get; }
    public string Currency { get; }
    
    public Money(decimal amount, string currency = "CNY")
    {
        Amount = Math.Round(amount, 2);
        Currency = currency;
    }
    
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Cannot add different currencies");
        
        return new Money(Amount + other.Amount, Currency);
    }
    
    public bool Equals(Money other) 
        => Amount == other.Amount && Currency == other.Currency;
    
    public override string ToString() => $"{Currency} {Amount:F2}";
}

// 实体
public class Order
{
    public int Id { get; set; }
    public Money TotalAmount { get; set; }
}

// 转换器(存储为 JSON)
public class MoneyJsonConverter : ValueConverter<Money, string>
{
    public MoneyJsonConverter() : base(
        v => JsonSerializer.Serialize(new { v.Amount, v.Currency }, null),
        v => 
        {
            var json = JsonSerializer.Deserialize<Dictionary<string, object>>(v, null);
            return new Money(
                Convert.ToDecimal(json["Amount"]),
                Convert.ToString(json["Currency"])
            );
        }
    )
    {
    }
}

// 配置
modelBuilder.Entity<Order>()
    .Property(o => o.TotalAmount)
    .HasConversion(new MoneyJsonConverter())
    .HasColumnType("nvarchar(100)");

// 数据库存储: {"Amount":199.99,"Currency":"CNY"}

示例 3: 电话号码 ​

csharp
// 值对象: 电话号码
public class PhoneNumber : IEquatable<PhoneNumber>
{
    private static readonly Regex PhoneRegex = new(@"^\+?[\d\s\-()]{7,20}$");
    
    public string Number { get; }
    
    public PhoneNumber(string number)
    {
        if (string.IsNullOrWhiteSpace(number))
            throw new ArgumentException("Phone number cannot be empty");
        
        // 标准化格式
        Number = new string(number.Where(c => char.IsDigit(c) || c == '+').ToArray());
        
        if (!PhoneRegex.IsMatch(Number))
            throw new ArgumentException($"Invalid phone number: {number}");
    }
    
    public string Format()
    {
        // +8613800138000 → +86 138-0013-8000
        if (Number.StartsWith("+86") && Number.Length == 14)
        {
            return $"+86 {Number.Substring(3, 3)}-{Number.Substring(6, 4)}-{Number.Substring(10)}";
        }
        return Number;
    }
    
    public bool Equals(PhoneNumber other) => Number == other?.Number;
    public override string ToString() => Format();
}

// 转换器
modelBuilder.Entity<Customer>()
    .Property(c => c.Phone)
    .HasConversion(
        v => v.Number,
        v => new PhoneNumber(v)
    );

复杂对象转换 ​

示例 4: 地址值对象 ​

csharp
// 值对象: 地址
public class Address
{
    public string Street { get; }
    public string City { get; }
    public string Province { get; }
    public string ZipCode { get; }
    public string Country { get; }
    
    public Address(string street, string city, string province, string zipCode, string country = "CN")
    {
        Street = street;
        City = city;
        Province = province;
        ZipCode = zipCode;
        Country = country;
    }
    
    public string FullAddress => $"{Province}{City}{Street} ({ZipCode})";
}

// 方式 1: JSON 序列化(推荐)
modelBuilder.Entity<Order>()
    .Property(o => o.ShippingAddress)
    .HasConversion(
        v => JsonSerializer.Serialize(v, null),
        v => JsonSerializer.Deserialize<Address>(v, null)
    )
    .HasColumnType("nvarchar(500)");

// 数据库存储: {"Street":"长安街1号","City":"北京","Province":"北京","ZipCode":"100000","Country":"CN"}

// 方式 2: 拆分为多列(传统方式)
modelBuilder.Entity<Order>()
    .OwnsOne(o => o.ShippingAddress, a =>
    {
        a.Property(addr => addr.Street).HasColumnName("ShippingStreet");
        a.Property(addr => addr.City).HasColumnName("ShippingCity");
        a.Property(addr => addr.Province).HasColumnName("ShippingProvince");
        a.Property(addr => addr.ZipCode).HasColumnName("ShippingZipCode");
        a.Property(addr => addr.Country).HasColumnName("ShippingCountry");
    });

示例 5: 集合转换 ​

csharp
// 标签列表
public class Product
{
    public int Id { get; set; }
    public List<string> Tags { get; set; }
}

// 存储为逗号分隔的字符串
modelBuilder.Entity<Product>()
    .Property(p => p.Tags)
    .HasConversion(
        v => string.Join(",", v),
        v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
    )
    .HasColumnType("nvarchar(500)");

// 数据库存储: "Electronics,Gadgets,New"

// 或者存储为 JSON(支持复杂对象)
modelBuilder.Entity<Product>()
    .Property(p => p.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, null),
        v => JsonSerializer.Deserialize<List<string>>(v, null)
    )
    .HasColumnType("nvarchar(max)");

// 数据库存储: ["Electronics","Gadgets","New"]

高级应用场景 ​

场景 1: 加密存储 ​

csharp
// 敏感数据加密
public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public EncryptedString IdNumber { get; set; }  // 身份证号
}

public class EncryptedString
{
    private readonly string _encryptedValue;
    
    public EncryptedString(string plainText)
    {
        _encryptedValue = Encrypt(plainText);
    }
    
    public string Decrypt()
    {
        return Decrypt(_encryptedValue);
    }
    
    private static string Encrypt(string plainText)
    {
        // AES 加密逻辑
        using var aes = Aes.Create();
        // ... 加密实现
        return Convert.ToBase64String(encryptedBytes);
    }
    
    private static string Decrypt(string encryptedText)
    {
        // AES 解密逻辑
        return Encoding.UTF8.GetString(decryptedBytes);
    }
}

// 转换器
modelBuilder.Entity<User>()
    .Property(u => u.IdNumber)
    .HasConversion(
        v => v.ToString(),  // 调用 ToString() 返回加密字符串
        v => new EncryptedString(v)
    );

场景 2: 审计信息 ​

csharp
// 审计元数据
public class AuditableEntity
{
    public int Id { get; set; }
    public AuditInfo AuditInfo { get; set; }
}

public class AuditInfo
{
    public DateTime CreatedAt { get; }
    public string CreatedBy { get; }
    public DateTime? UpdatedAt { get; }
    public string UpdatedBy { get; }
    
    public AuditInfo(DateTime createdAt, string createdBy)
    {
        CreatedAt = createdAt;
        CreatedBy = createdBy;
    }
}

// JSON 存储
modelBuilder.Entity<AuditableEntity>()
    .Property(e => e.AuditInfo)
    .HasConversion(
        v => JsonSerializer.Serialize(v, null),
        v => JsonSerializer.Deserialize<AuditInfo>(v, null)
    );

场景 3: 多语言支持 ​

csharp
// 多语言文本
public class Product
{
    public int Id { get; set; }
    public LocalizedString Name { get; set; }
}

public class LocalizedString
{
    public Dictionary<string, string> Translations { get; }
    
    public LocalizedString()
    {
        Translations = new Dictionary<string, string>();
    }
    
    public string this[string language]
    {
        get => Translations.TryGetValue(language, out var value) ? value : null;
        set => Translations[language] = value;
    }
}

// JSON 存储
modelBuilder.Entity<Product>()
    .Property(p => p.Name)
    .HasConversion(
        v => JsonSerializer.Serialize(v.Translations, null),
        v => 
        {
            var translations = JsonSerializer.Deserialize<Dictionary<string, string>>(v, null);
            var localized = new LocalizedString();
            foreach (var kvp in translations)
                localized[kvp.Key] = kvp.Value;
            return localized;
        }
    );

// 数据库存储: {"en":"Laptop","zh":"笔记本电脑","ja":"ノートパソコン"}

.NET 8/9/10 新特性 ​

.NET 8: 简化的转换器语法 ​

csharp
// .NET 8 之前
modelBuilder.Entity<Order>()
    .Property(o => o.Status)
    .HasConversion(
        new EnumToStringConverter<OrderStatus>(),
        new ValueComparer<OrderStatus>(...)
    );

// .NET 8 简化
modelBuilder.Entity<Order>()
    .Property(o => o.Status)
    .HasConversion<string>();  // ← 自动推断!

// 自定义转换也更简洁
modelBuilder.Entity<Order>()
    .Property(o => o.Id)
    .HasConversion<int>(
        v => v.Value,
        v => new OrderId(v)
    );

.NET 9: 泛型改进 ​

csharp
// .NET 9 支持更好的泛型推断
modelBuilder.Entity<Order>()
    .Property(o => o.TotalAmount)
    .HasConversion<Money, string>(
        v => JsonSerializer.Serialize(v, null),
        v => JsonSerializer.Deserialize<Money>(v, null)
    );

.NET 10: 智能转换(路线图) ​

预计特性:

  • 基于 AI 的自动转换建议
  • 更强大的复合类型支持
  • 运行时动态转换
  • 转换性能优化

最佳实践与陷阱 ​

最佳实践 ​

1. 优先使用值对象 ​

csharp
// ✅ 推荐: 使用值对象 + 转换
public class Order
{
    public OrderId Id { get; set; }
    public Money Amount { get; set; }
    public PhoneNumber Phone { get; set; }
}

// ❌ 避免: 使用原始类型
public class Order
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
    public string Phone { get; set; }
}

2. 实现 ValueComparer ​

csharp
// ✅ 正确: 提供比较器以支持变更跟踪
modelBuilder.Entity<Order>()
    .Property(o => o.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, null),
        v => JsonSerializer.Deserialize<List<string>>(v, null),
        new ValueComparer<List<string>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()
        )
    );

// ❌ 错误: 没有比较器,变更跟踪失效

3. 选择合适的存储格式 ​

csharp
// 简单枚举 → String
.HasConversion<string>()

// 复杂对象 → JSON
.HasConversion(
    v => JsonSerializer.Serialize(v, null),
    v => JsonSerializer.Deserialize<T>(v, null)
)

// 结构化数据 → 多列
.OwnsOne(x => x.Address)

常见陷阱 ​

陷阱 1: 忘记比较器 ​

csharp
// ❌ 错误: 集合属性没有比较器
modelBuilder.Entity<Product>()
    .Property(p => p.Tags)
    .HasConversion(...);  // 变更跟踪不工作!

// ✅ 正确: 添加比较器
modelBuilder.Entity<Product>()
    .Property(p => p.Tags)
    .HasConversion(..., new ValueComparer<List<string>>(...));

陷阱 2: 过度使用 JSON ​

csharp
// ❌ 错误: 频繁查询的字段存为 JSON
modelBuilder.Entity<Order>()
    .Property(o => o.CustomerName)  // 经常 WHERE CustomerName LIKE ...
    .HasConversion(...);  // JSON 中无法高效查询

// ✅ 正确: 查询字段单独列,其他存 JSON
modelBuilder.Entity<Order>()
    .Property(o => o.CustomerName);  // 单独列
    
modelBuilder.Entity<Order>()
    .Property(o => o.Metadata);  // 不常查询的元数据存 JSON

陷阱 3: 性能问题 ​

csharp
// ⚠️ 注意: 复杂的 JSON 序列化可能成为瓶颈
// 对于高频访问数据,考虑拆分为多列

// ❌ 慢: 每次查询都序列化/反序列化
.HasConversion(
    v => ComplexSerialization(v),
    v => ComplexDeserialization(v)
)

// ✅ 快: 简单的列映射
.OwnsOne(x => x.Address, a =>
{
    a.Property(addr => addr.Street);
    a.Property(addr => addr.City);
})

总结 ​

核心要点 ​

  1. 值转换作用: 解耦领域模型与数据库结构
  2. 内置转换器: Enum、DateTime、Bool 等常用类型
  3. 自定义转换: 支持任意复杂类型
  4. JSON 存储: 适合复杂但不常查询的对象
  5. 比较器: 必须实现以支持变更跟踪

转换策略选择 ​

类型推荐策略示例
EnumStringHasConversion<string>()
值对象(简单)自定义转换OrderId ↔ int
值对象(复杂)JSON 序列化Address → JSON
集合JSON 数组List<string> → JSON
敏感数据加密转换EncryptedString
结构化数据拆分为多列OwnsOne

性能对比 ​

访问模式          | 推荐方式      | 原因
-----------------|--------------|------------------
频繁查询         | 单独列        | 可直接 WHERE
偶尔查询         | JSON         | 节省空间
复杂对象         | JSON         | 简化开发
简单值对象       | 自定义转换    | 性能最优

下一步 ​

基于 MIT 许可发布