Skip to content

值转换 (Value Conversions) ​

概述 ​

值转换(Value Conversions)允许在实体属性和数据库列之间自定义数据的转换逻辑。这在处理枚举、加密、序列化等场景时非常有用。

核心概念 ​

C# 实体属性 ←→ ValueConverter ←→ 数据库列
   (强类型)       (转换逻辑)      (存储格式)

典型应用场景 ​

场景C# 类型数据库类型转换器
枚举字符串enumstringEnumToStringConverter
JSON 序列化objectstringJsonValueConverter
加密存储stringstring自定义加密转换器
时间戳DateTimelongDateTimeToBinaryConverter
布尔标志boolintBoolToZeroOneConverter
GUID 压缩Guidbyte[]自定义转换器

内置转换器 ​

枚举转字符串 ​

csharp
public enum OrderStatus
{
    Pending,
    Paid,
    Shipped,
    Delivered,
    Cancelled
}

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

// 配置
modelBuilder.Entity<Order>(entity =>
{
    entity.Property(o => o.Status)
          .HasConversion<string>();  // 存储为 "Pending", "Paid" 等
});

// 生成的 SQL:
// [Status] NVARCHAR(20) NOT NULL

// 使用
var order = new Order { Status = OrderStatus.Paid };
context.Orders.Add(order);
await context.SaveChangesAsync();

// 数据库中存储: Status = "Paid"

枚举转数字 ​

csharp
modelBuilder.Entity<Order>(entity =>
{
    entity.Property(o => o.Status)
          .HasConversion<int>();  // 存储为 0, 1, 2, 3...
});

// 数据库中存储: Status = 1

布尔转整数 ​

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

// 配置: true → 1, false → 0
modelBuilder.Entity<User>(entity =>
{
    entity.Property(u => u.IsActive)
          .HasConversion<int>();
});

// 或者使用预定义转换器
entity.Property(u => u.IsActive)
      .HasConversion(new BoolToZeroOneConverter<int>());

DateTime 转二进制 ​

csharp
public class Event
{
    public int Id { get; set; }
    public DateTime Timestamp { get; set; }
}

// 配置: DateTime → long (更紧凑的存储)
modelBuilder.Entity<Event>(entity =>
{
    entity.Property(e => e.Timestamp)
          .HasConversion<long>();
});

// 或使用专用转换器
entity.Property(e => e.Timestamp)
      .HasConversion(new DateTimeToBinaryConverter());

自定义转换器 ​

基础示例: 大小写转换 ​

csharp
// StringToUpperConverter.cs
public class StringToUpperConverter : ValueConverter<string, string>
{
    public StringToUpperConverter()
        : base(
            v => v.ToUpper(),           // C# → DB
            v => v.ToLower())           // DB → C#
    {
    }
}

// 使用
modelBuilder.Entity<Product>(entity =>
{
    entity.Property(p => p.Name)
          .HasConversion(new StringToUpperConverter());
});

// 保存: "laptop" → 数据库存储 "LAPTOP"
// 读取: 数据库 "LAPTOP" → C# "laptop"

加密转换器 ​

csharp
// EncryptedStringConverter.cs
public class EncryptedStringConverter : ValueConverter<string, string>
{
    private readonly string _encryptionKey;

    public EncryptedStringConverter(string encryptionKey)
        : base(
            v => Encrypt(v, encryptionKey),     // C# → DB (加密)
            v => Decrypt(v, encryptionKey))     // DB → C# (解密)
    {
        _encryptionKey = encryptionKey;
    }

    private static string Encrypt(string plainText, string key)
    {
        // 简化的加密示例(生产环境使用 AES)
        var bytes = Encoding.UTF8.GetBytes(plainText);
        var encrypted = ProtectedData.Protect(
            bytes, 
            Encoding.UTF8.GetBytes(key), 
            DataProtectionScope.CurrentUser);
        
        return Convert.ToBase64String(encrypted);
    }

    private static string Decrypt(string encryptedText, string key)
    {
        var bytes = Convert.FromBase64String(encryptedText);
        var decrypted = ProtectedData.Unprotect(
            bytes, 
            Encoding.UTF8.GetBytes(key), 
            DataProtectionScope.CurrentUser);
        
        return Encoding.UTF8.GetString(decrypted);
    }
}

// 使用
public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string Ssn { get; set; }  // 社会安全号(敏感数据)
}

modelBuilder.Entity<User>(entity =>
{
    entity.Property(u => u.Ssn)
          .HasConversion(new EncryptedStringConverter("my-secret-key"));
});

// 保存: "123-45-6789" → 数据库存储加密字符串
// 读取: 自动解密为 "123-45-6789"

JSON 序列化转换器 ​

csharp
// JsonConverter.cs
public class JsonValueConverter<T> : ValueConverter<T, string>
{
    public JsonValueConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
            v => JsonSerializer.Deserialize<T>(v, (JsonSerializerOptions?)null) 
                 ?? throw new InvalidOperationException($"Failed to deserialize {typeof(T).Name}"))
    {
    }
}

// 使用
public class Product
{
    public int Id { get; set; }
    public Dictionary<string, string> Attributes { get; set; } = new();
}

modelBuilder.Entity<Product>(entity =>
{
    entity.Property(p => p.Attributes)
          .HasColumnType("nvarchar(max)")
          .HasConversion(new JsonValueConverter<Dictionary<string, string>>());
});

// 保存: 
// C#: new Dictionary<string, string> { ["Color"] = "Red", ["Size"] = "XL" }
// DB: "{\"Color\":\"Red\",\"Size\":\"XL\"}"

// 读取: 自动反序列化为 Dictionary

GUID 压缩转换器 ​

csharp
// GuidToBytesConverter.cs
public class GuidToBytesConverter : ValueConverter<Guid, byte[]>
{
    public GuidToBytesConverter()
        : base(
            guid => guid.ToByteArray(),           // Guid → byte[16]
            bytes => new Guid(bytes))             // byte[16] → Guid
    {
    }
}

// 使用
public class Document
{
    public int Id { get; set; }
    public Guid ExternalId { get; set; }
}

modelBuilder.Entity<Document>(entity =>
{
    entity.Property(d => d.ExternalId)
          .HasColumnType("binary(16)")
          .HasConversion(new GuidToBytesConverter());
});

// 优势: binary(16) 比 uniqueidentifier(36) 节省 20 字节

复杂类型转换 ​

值对象(Value Object) ​

csharp
// Money.cs - 值对象
public record Money(decimal Amount, string Currency);

// Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Money Price { get; set; } = new(0, "USD");
}

// MoneyConverter.cs
public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter()
        : base(
            money => $"{money.Amount}:{money.Currency}",  // 序列化
            str =>                                         // 反序列化
            {
                var parts = str.Split(':');
                return new Money(
                    decimal.Parse(parts[0]), 
                    parts[1]);
            })
    {
    }
}

// 配置
modelBuilder.Entity<Product>(entity =>
{
    entity.Property(p => p.Price)
          .HasColumnType("varchar(50)")
          .HasConversion(new MoneyConverter());
});

// 数据库中存储: "99.99:USD"

地址值对象 ​

csharp
// Address.cs
public record Address(
    string Street,
    string City,
    string State,
    string ZipCode,
    string Country);

// Customer.cs
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address ShippingAddress { get; set; } = new("", "", "", "", "");
}

// AddressJsonConverter.cs
public class AddressJsonConverter : ValueConverter<Address, string>
{
    public AddressJsonConverter()
        : base(
            addr => JsonSerializer.Serialize(addr),
            json => JsonSerializer.Deserialize<Address>(json) 
                    ?? new Address("", "", "", "", ""))
    {
    }
}

// 配置
modelBuilder.Entity<Customer>(entity =>
{
    entity.Property(c => c.ShippingAddress)
          .HasColumnType("nvarchar(max)")
          .HasConversion(new AddressJsonConverter());
});

全局值转换 ​

统一配置所有字符串 ​

csharp
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    
    // 全局配置: 所有字符串去除前后空格
    options.ConfigureConventions(conventions =>
    {
        conventions.Properties<string>()
                 .HaveConversion<StringTrimmingConverter>();
    });
});

// StringTrimmingConverter.cs
public class StringTrimmingConverter : ValueConverter<string, string>
{
    public StringTrimmingConverter()
        : base(
            v => v?.Trim(),      // C# → DB (去除空格)
            v => v?.Trim())      // DB → C# (去除空格)
    {
    }
}

统一时区转换 ​

csharp
// UtcDateTimeConverter.cs
public class UtcDateTimeConverter : ValueConverter<DateTime, DateTime>
{
    public UtcDateTimeConverter()
        : base(
            dt => dt.ToUniversalTime(),   // C# → DB (转 UTC)
            dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc))  // DB → C# (标记为 UTC)
    {
    }
}

// 全局应用
conventions.Properties<DateTime>()
           .HaveConversion<UtcDateTimeConverter>();

条件转换 ​

根据值选择转换器 ​

csharp
// NullableStringConverter.cs
public class NullableStringConverter : ValueConverter<string?, string?>
{
    public NullableStringConverter()
        : base(
            v => NormalizeValue(v),   // C# → DB
            v => NormalizeValue(v))   // DB → C#
    {
    }

    private static string? NormalizeValue(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return null;  // 空字符串转为 NULL
        
        return value.Trim();
    }
}

// 使用
modelBuilder.Entity<Product>(entity =>
{
    entity.Property(p => p.Description)
          .HasConversion(new NullableStringConverter());
});

// 保存: "" 或 "   " → 数据库存储 NULL
// 读取: NULL → C# null

性能优化 ​

预编译转换器 ​

csharp
// ✅ 推荐: 静态实例复用
public static class Converters
{
    public static readonly JsonValueConverter<Dictionary<string, string>> 
        DictConverter = new();
    
    public static readonly EncryptedStringConverter 
        SsnConverter = new("production-key");
}

// 使用
entity.Property(p => p.Attributes)
      .HasConversion(Converters.DictConverter);

// ❌ 避免: 每次创建新实例
entity.Property(p => p.Attributes)
      .HasConversion(new JsonValueConverter<Dictionary<string, string>>());

批量转换优化 ​

csharp
// 对于大量数据,考虑在数据库层面转换
// 而不是在 EF Core 层面

// ❌ EF Core 逐行转换(慢)
var products = await context.Products.ToListAsync();
foreach (var product in products)
{
    product.Name = product.Name.ToUpper();  // C# 转换
}

// ✅ 数据库批量转换(快)
await context.Database.ExecuteSqlRawAsync(
    "UPDATE Products SET Name = UPPER(Name)");

调试与测试 ​

查看转换后的 SQL ​

csharp
var product = new Product 
{ 
    Name = "Laptop",
    Price = new Money(999.99m, "USD")
};

context.Products.Add(product);

// 查看生成的 SQL
var sql = context.SaveChanges();

// 输出:
// INSERT INTO Products (Name, Price) 
// VALUES ('Laptop', '999.99:USD')

单元测试转换器 ​

csharp
[Fact]
public void MoneyConverter_Should_Serialize_And_Deserialize()
{
    var converter = new MoneyConverter();
    var originalMoney = new Money(99.99m, "USD");
    
    // 序列化
    var serialized = converter.ConvertToProvider(originalMoney);
    Assert.Equal("99.99:USD", serialized);
    
    // 反序列化
    var deserialized = converter.ConvertFromProvider(serialized);
    Assert.Equal(originalMoney, deserialized);
}

[Fact]
public void EncryptedConverter_Should_Protect_Data()
{
    var converter = new EncryptedStringConverter("test-key");
    var plainText = "sensitive-data";
    
    // 加密
    var encrypted = converter.ConvertToProvider(plainText);
    Assert.NotEqual(plainText, encrypted);
    
    // 解密
    var decrypted = converter.ConvertFromProvider(encrypted);
    Assert.Equal(plainText, decrypted);
}

常见问题与解决方案 ​

问题 1: 转换失败 ​

csharp
// ❌ 错误: 无效的数据格式
entity.Property(p => p.Attributes)
      .HasConversion(new JsonValueConverter<Dictionary<string, string>>());

// 数据库中有损坏的 JSON: "{invalid json}"
// 💥 JsonException 异常

// ✅ 修复: 添加错误处理
public class SafeJsonConverter<T> : ValueConverter<T, string>
{
    public SafeJsonConverter()
        : base(
            v => SerializeSafely(v),
            v => DeserializeSafely(v))
    {
    }

    private static string SerializeSafely(T value)
    {
        try
        {
            return JsonSerializer.Serialize(value);
        }
        catch
        {
            return "{}";  // 默认值
        }
    }

    private static T DeserializeSafely(string json)
    {
        try
        {
            return JsonSerializer.Deserialize<T>(json) 
                   ?? Activator.CreateInstance<T>();
        }
        catch
        {
            return Activator.CreateInstance<T>();  // 默认实例
        }
    }
}

问题 2: 查询无法翻译 ​

csharp
// ❌ 错误: 转换后的字段无法在 WHERE 中使用
var products = await context.Products
    .Where(p => p.Price.Amount > 100)  // 💥 无法翻译!
    .ToListAsync();

// ✅ 修复: 使用原始 SQL 或调整查询
var products = await context.Products
    .Where(p => EF.Functions.Like(p.Price, "%:USD"))  // 基于字符串匹配
    .ToListAsync();

// 或更好的方案: 拆分列
public class Product
{
    public decimal PriceAmount { get; set; }
    public string PriceCurrency { get; set; }
}

问题 3: 性能瓶颈 ​

csharp
// ❌ 复杂转换影响性能
entity.Property(p => p.LargeJsonData)
      .HasConversion(new ComplexJsonConverter());  // 大 JSON 序列化慢

// ✅ 优化方案:
// 1. 使用数据库原生 JSON 类型(PostgreSQL JSONB, SQL Server JSON)
// 2. 延迟加载
// 3. 缓存转换结果

最佳实践 ​

✅ 推荐做法 ​

1. 优先使用内置转换器 ​

csharp
// ✅ 简洁
.HasConversion<string>()

// ❌ 冗余
.HasConversion(new EnumToStringConverter<MyEnum>())

2. 转换器无状态且线程安全 ​

csharp
// ✅ 无状态
public class MyConverter : ValueConverter<string, string>
{
    public MyConverter()
        : base(v => v.ToUpper(), v => v.ToLower())
    {
    }
}

// ❌ 有状态(线程不安全)
public class BadConverter : ValueConverter<string, string>
{
    private int _counter;  // ⚠️ 共享状态
    
    public BadConverter()
        : base(v => v + _counter++, v => v)
    {
    }
}

3. 文档化转换逻辑 ​

csharp
/// <summary>
/// 将 Money 值对象序列化为 "amount:currency" 格式
/// 示例: Money(99.99, "USD") → "99.99:USD"
/// </summary>
public class MoneyConverter : ValueConverter<Money, string>
{
    // ...
}

❌ 避免的错误 ​

1. 不要在转换器中执行业务逻辑 ​

csharp
// ❌ 错误
public class BadConverter : ValueConverter<decimal, decimal>
{
    public BadConverter()
        : base(
            v => ApplyDiscount(v),  // ⚠️ 业务逻辑不应在这里
            v => v)
    {
    }
    
    private decimal ApplyDiscount(decimal price)
    {
        // 复杂的折扣计算...
    }
}

// ✅ 正确: 转换器只做格式转换
public class GoodConverter : ValueConverter<decimal, decimal>
{
    public GoodConverter()
        : base(v => Math.Round(v, 2), v => v)  // 仅格式化
    {
    }
}

2. 不要忽略空值处理 ​

csharp
// ❌ 可能 NullReferenceException
public class BadConverter : ValueConverter<string, string>
{
    public BadConverter()
        : base(v => v.ToUpper(), v => v.ToLower())  // ⚠️ v 可能为 null
    {
    }
}

// ✅ 正确处理 null
public class GoodConverter : ValueConverter<string?, string?>
{
    public GoodConverter()
        : base(
            v => v?.ToUpper(), 
            v => v?.ToLower())
    {
    }
}

总结 ​

转换器选择决策树 ​

需要值转换?
│
├─ 枚举?
│  ├─ 可读性优先 → HasConversion<string>()
│  └─ 性能优先 → HasConversion<int>()
│
├─ 复杂对象?
│  ├─ 简单结构 → 自定义序列化格式
│  └─ 复杂结构 → JSON 序列化
│
├─ 敏感数据?
│  └─ 自定义加密转换器
│
├─ 格式标准化?
│  └─ 大小写/ trimming 转换器
│
└─ 数据库类型不匹配?
   └─ 类型映射转换器(DateTime↔long, Guid↔byte[])

核心要点 ​

  1. 优先内置: 使用 EF Core 提供的内置转换器
  2. 保持简单: 转换器只做格式转换,不包含业务逻辑
  3. 处理空值: 始终考虑 null 的情况
  4. 性能意识: 避免在转换器中执行耗时操作
  5. 线程安全: 确保转换器无状态且可复用

基于 MIT 许可发布