EF Core 8:基记录类型的Properties<T>.HaveConversion<TValueConverter>()未使用提供的值转换器
最近在ASP.NET Core 8.0 Web API项目里用EF Core实现强类型ID,之前我都是给每个实体单独写强类型ID结构体和对应的ValueConverter,比如Role的实现是这样的:
// Role.cs public record struct RoleId(int Value); public class RoleIdValueConverter : ValueConverter<RoleId, int> { public RoleIdValueConverter() : base(id => id.Value, v => new(v)) {} } public class Role { public RoleId Id { get; set; } // 其他属性... }
后来想着优化一下,把强类型ID抽象成一个基记录类型,比如StronglyTypedId<T>,让所有具体的强类型ID都继承它,然后想用Properties<StronglyTypedId<int>>().HaveConversion<StronglyTypedIdConverter<int>>()这种方式批量配置转换器,结果发现完全没生效——EF Core根本没把我提供的转换器应用到实体的强类型ID属性上。
折腾了半天终于搞懂原因了:EF Core的Properties<T>()只会匹配编译时类型恰好是T的属性。我的Role实体里Id属性的类型是RoleId,而不是基类StronglyTypedId<int>,所以这个批量配置根本不会命中这些属性。
给你几个可行的解决方法:
方法一:用反射批量注册所有强类型ID转换器
如果项目里强类型ID很多,手动一个个配置太麻烦,可以写个扩展方法,通过反射自动找到所有继承自基类的强类型ID,然后批量注册转换器:
首先定义基类和通用转换器:
// 基类强类型ID public abstract record struct StronglyTypedId<TValue>(TValue Value) where TValue : struct; // 通用的强类型ID转换器 public class StronglyTypedIdConverter<TValue> : ValueConverter<StronglyTypedId<TValue>, TValue> where TValue : struct { public StronglyTypedIdConverter() : base(id => id.Value, value => (StronglyTypedId<TValue>)Activator.CreateInstance(typeof(StronglyTypedId<TValue>), value)) { } }
然后写ModelBuilder的扩展方法:
public static class ModelBuilderExtensions { public static ModelBuilder AddStronglyTypedIdConverters(this ModelBuilder modelBuilder) { // 找到所有继承自StronglyTypedId<T>的record struct类型 var stronglyTypedIdTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(a => a.GetTypes()) .Where(t => t.IsValueType && t.IsRecord && t.BaseType != null && t.BaseType.IsGenericType && t.BaseType.GetGenericTypeDefinition() == typeof(StronglyTypedId<>)); foreach (var idType in stronglyTypedIdTypes) { // 获取强类型ID包装的原始值类型(比如int、Guid) var valueType = idType.BaseType.GetGenericArguments()[0]; // 创建对应的转换器实例 var converterType = typeof(StronglyTypedIdConverter<>).MakeGenericType(valueType); var converter = Activator.CreateInstance(converterType); // 为所有该类型的属性应用转换器 foreach (var property in modelBuilder.Properties(idType)) { var hasConversionMethod = typeof(PropertyBuilder) .GetMethod("HasConversion", new[] { typeof(ValueConverter) }); hasConversionMethod?.Invoke(property, new[] { converter }); } } return modelBuilder; } }
最后在DbContext的OnModelCreating里调用这个扩展方法就行:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.AddStronglyTypedIdConverters(); // 其他实体配置... }
方法二:手动为每个具体强类型ID配置转换器
如果项目里强类型ID不多,直接手动给每个类型配置反而更简单,避免反射的复杂性:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Properties<RoleId>().HaveConversion<RoleIdValueConverter>(); modelBuilder.Properties<UserId>().HaveConversion<UserIdValueConverter>(); // 其他强类型ID依次配置... }
方法三:用EF Core 8原生的强类型ID简化配置
其实EF Core 8已经内置了对简单强类型ID的支持,如果你的转换器只是简单的“拆包/包装”值类型,完全不用手动写ValueConverter,直接在实体配置里指定转换逻辑就行:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Role>() .Property(r => r.Id) .HasConversion(id => id.Value, value => new RoleId(value)); }
这种方式更直观,也不需要额外写转换器类,适合大多数简单的强类型ID场景。
备注:内容来源于stack exchange,提问作者N.T. Dang

