Appearance
值转换 (Value Conversions)
概述
值转换(Value Conversions)允许在实体属性和数据库列之间自定义数据的转换逻辑。这在处理枚举、加密、序列化等场景时非常有用。
核心概念
C# 实体属性 ←→ ValueConverter ←→ 数据库列
(强类型) (转换逻辑) (存储格式)典型应用场景
| 场景 | C# 类型 | 数据库类型 | 转换器 |
|---|---|---|---|
| 枚举字符串 | enum | string | EnumToStringConverter |
| JSON 序列化 | object | string | JsonValueConverter |
| 加密存储 | string | string | 自定义加密转换器 |
| 时间戳 | DateTime | long | DateTimeToBinaryConverter |
| 布尔标志 | bool | int | BoolToZeroOneConverter |
| GUID 压缩 | Guid | byte[] | 自定义转换器 |
内置转换器
枚举转字符串
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\"}"
// 读取: 自动反序列化为 DictionaryGUID 压缩转换器
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[])核心要点
- 优先内置: 使用 EF Core 提供的内置转换器
- 保持简单: 转换器只做格式转换,不包含业务逻辑
- 处理空值: 始终考虑 null 的情况
- 性能意识: 避免在转换器中执行耗时操作
- 线程安全: 确保转换器无状态且可复用