☰
NHibernate自定义映射类型IUserType深度实战与避坑指南
2026/9/26 17:12:29 网站建设 项目流程
## 1. 什么时候才真正需要自定义映射类型 先说结论:NHibernate 自带的那些映射规则,能覆盖八成以上的常规需求。你定义了一个 `int` 属性,它就映射成数据库里的 `INT`;你定义了一个 `DateTime`,它就对应 `DATETIME` 或 `TIMESTAMP`。真正需要你动手写自定义映射类型(Custom Mapping Type)的场景,通常绕不开下面几类问题。 第一类是**属性类型和数据库列类型天然不对等**。比如你用了 DDD 的“值对象”模式,`Money` 这个类里有 `Amount` 和 `Currency` 两个字段,但你不希望为它单独建一张表,而是想让它以 `DECIMAL` 加 `VARCHAR` 两列的形式直接嵌在订单表里。再比如你有一个 `PhoneNumber` 值对象,里面封装了区号、号码和分机号的校验逻辑,数据库里只存一个字符串。这种“对象是多个字段的封装,表里也是多列,但实体属性只有一个”的错位,NHibernate 默认不知道该怎么处理,必须靠自定义类型把对象和列之间来回翻译。 第二类是**枚举或标记对象的存储格式不理想**。最常见的就是把枚举存成字符串而不是数字。默认情况下 NHibernate 能用 `EnumStringType` 或 `EnumType` 处理一部分,但当你希望“数据库里可以读懂的英文名”和“代码里语义化很强的枚举名”不完全一致时,还是要自己写。 第三类是**需要自定义序列化策略**。比如 `ImageMeta` 这类复杂对象,你想在某个字段里直接存 JSON 文本,但又希望读出来时是强类型对象;或者某个属性在持久化时要加密、要压缩,这些横切的转换逻辑放在实体里会很脏,放在自定义类型里则非常合适。 第四类是**遗留数据库改造**。老数据库里的字段类型和格式往往是历史遗留的,比如日期以 `VARCHAR(14)` 存储,格式是 `yyyyMMddHHmmss`,而新代码里的属性是 `DateTime`。这种“旧格式到新类型”的翻译,正是自定义映射类型的用武之地。 所以,我判断该不该动手写自定义类型的标准就一句话:**看属性和列之间是不是存在“转换成本”**。如果只是名字不同、类型一样,那是 FluentNHibernate 映射配置的事,不需要自定义类型;只要出现“类型结构不一致、格式不一致、存储方式不一致”这三类情况,就得进入 `IUserType` 的领域了。 ## 2. IUserType 接口拆解:每个方法到底在干什么 要写自定义映射类型,先得把 `NHibernate.UserTypes.IUserType` 这个接口吃透。刚接触它的时候容易犯一个毛病:把十个成员全都补齐,却不知道每个方法是给谁调的、什么时候调、调错了有什么后果。我根据自己的经验,按“运行时”“持久化”“缓存”“元数据”四个维度重新给你拆一遍。 ### 2.1 类型的“身份证”:SqlTypes 与 ReturnedType ```csharp public SqlType[] SqlTypes => new[] { SqlTypeFactory.String }; public Type ReturnedType => typeof(PhoneNumber);

SqlTypes告诉 NHibernate 这个属性要映射成哪些数据库列、每列用什么类型。它返回的是数组,意味着一个属性可以对应多列。ReturnedType则是实体属性在 .NET 里的真实类型。

这两个成员决定了 Nhibernate 在生成 Schema、绑定参数、读取结果集时用什么样的类型去打交道。如果你让SqlTypes返回SqlTypeFactory.String却把ReturnedType写成DateTime,那执行查询时肯定崩,因为类型元数据对不上。

这里有个经验:在多列场景下,SqlTypes数组的顺序,必须和你后面NullSafeGet里读dr.GetString(0)、NullSafeGet里设参数的下标顺序保持一致。顺序错了,数据就串列了。

2.2 核心转换逻辑:NullSafeGet 与 NullSafeSet

这两个方法是整个自定义类型的灵魂,名字也和 NHibernate 的“可空值安全性”绑定在一起。

public object NullSafeGet(IDataReader rs, string[] names, object owner) { var phoneStr = (string)NHibernateUtil.String.NullSafeGet(rs, names[0]); return string.IsNullOrEmpty(phoneStr) ? null : PhoneNumber.Parse(phoneStr); } public void NullSafeSet(IDbCommand cmd, object value, int index) { var phone = value as PhoneNumber; var phoneStr = phone?.ToString(); NHibernateUtil.String.NullSafeSet(cmd, phoneStr, index); }

读的时候(Get)从IDataReader里取原始数据库值,加工成ReturnedType的实例;写的时候(Set)把实体属性值反向翻译回数据库参数。务必调用 NHibernate 自带类型(如NHibernateUtil.String)的NullSafeGet/NullSafeSet来处理空值,自己用rs.IsDBNull判断会漏掉很多边界情况,尤其是参数为空时容易怠惰地写死DBNull.Value,但IDataReader的GetString(0)在DBNull时直接抛异常。

2.3 对象的拷贝与拆分:DeepCopy / Assemble / Disassemble

很多教程对这三个方法一笔带过,但在实际项目中它们决定了你的事务能不能安全回滚、二级缓存能不能正常工作。

DeepCopy在 NHibernate 需要克隆属性值时调用。为什么需要克隆?因为如果实体对象在会话里被修改了,NHibernate 要拿“修改后的值”和“快照里的旧值”做 dirty-check。如果自定义类型返回的是同一个可变对象引用,快照和当前值永远相等,脏检查就失效了,update 永远发不出去。

public object DeepCopy(object value) { var phone = value as PhoneNumber; return phone?.Clone(); }

Assemble和Disassemble是为二级缓存准备的。对象从数据库里取出、放入缓存时,需要被“拆解”成可序列化的形式(通常就是拆回原始字段),从缓存取出时再“组装”回对象。如果自定义类型是不可变的(如字符串、值对象),直接返回原对象即可。

public object Disassemble(object value) => value; public object Assemble(object cached, object owner) => cached;

重要提示:如果你的值对象是可变类型(比如包含List<T>),这里不能直接返回原对象,否则缓存里存了同一个引用,多个会话会互相污染。

2.4 生命周期与比较:IsMutable / Replace / Equals / GetHashCode

IsMutable表示对象的可变性。不可变对象在缓存和脏检查时有大量优化空间,所以如果你能保证值对象设计成不可变的,就返回false。

Replace只在不可变对象上有实际意义:NHibernate 做快照比较时,如果对象不可变,它直接拿新对象替换旧对象。

public object Replace(object original, object target, object owner) => original;

Equals和GetHashCode比较关键。NHibernate 判断两个属性值是否相同,不是拿object.Equals直接比,而是调用你这个类型里的Equals。如果你写的是引用相等,那每次从数据库读出来的新对象和实体里的对象永远是“不相等”,会导致每次会话都发一次无意义的 update。所以必须按业务字段重写Equals/GetHashCode。

3. 完整实战:从值对象到自定义映射类型的落地实现

理论拆完,我带你完整地实现一个真实案例。这个案例是我在电商项目里做过的——一个Money值对象,包含金额(decimal)和币种(Currency枚举)。数据库里不额外建表,直接嵌在订单表的MoneyAmount、MoneyCurrency两列里。

3.1 定义值对象

public class Money { public decimal Amount { get; } public Currency Currency { get; } public Money(decimal amount, Currency currency) { if (amount < 0) throw new ArgumentException("金额不能为负数", nameof(amount)); Amount = amount; Currency = currency; } public static Money Parse(string amountStr, string currencyStr) { var amount = decimal.Parse(amountStr, CultureInfo.InvariantCulture); var currency = Enum.Parse<Currency>(currencyStr); return new Money(amount, currency); } public override bool Equals(object obj) { return obj is Money other && Amount == other.Amount && Currency == other.Currency; } public override int GetHashCode() { unchecked { return (Amount.GetHashCode() * 397) ^ Currency.GetHashCode(); } } } public enum Currency { CNY, USD, EUR }

注意这里我特意把Money设计成不可变的:属性只有 get、构造函数在构造时校验。这不是所有教程都强调的点,但对自定义映射类型非常重要,因为不可变对象在IsMutable、DeepCopy、缓存处理等环节都能简化到极致。

3.2 实现 IUserType

这是整个自定义映射类型的核心文件。你会发现实现本身不复杂,难的是把接口里每个细节都想明白。

public class MoneyType : IUserType { public bool IsMutable => false; public Type ReturnedType => typeof(Money); public SqlType[] SqlTypes => new[] { SqlTypeFactory.Decimal, SqlTypeFactory.String }; public object NullSafeGet(IDataReader rs, string[] names, object owner) { if (names.Length != 2) return null; var amount = (decimal)NHibernateUtil.Decimal.NullSafeGet(rs, names[0]); var currencyStr = (string)NHibernateUtil.String.NullSafeGet(rs, names[1]); if (currencyStr == null) return null; return new Money(amount, Enum.Parse<Currency>(currencyStr)); } public void NullSafeSet(IDbCommand cmd, object value, int index) { var money = value as Money; if (money == null) { NHibernateUtil.Decimal.NullSafeSet(cmd, null, index); NHibernateUtil.String.NullSafeSet(cmd, null, index + 1); return; } NHibernateUtil.Decimal.NullSafeSet(cmd, money.Amount, index); NHibernateUtil.String.NullSafeSet(cmd, money.Currency.ToString(), index + 1); } public object DeepCopy(object value) { // Money 不可变,直接返回原对象即可 return value; } public object Replace(object original, object target, object owner) { return original; } public object Assemble(object cached, object owner) { return cached; } public object Disassemble(object value) { return value; } public override bool Equals(object x, object y) { if (ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; return x.Equals(y); } public override int GetHashCode(object x) { return x?.GetHashCode() ?? 0; } }

这版实现里有几个细节值得琢磨:

第一,NullSafeSet的下标处理。我用了index和index + 1。NHibernate 传进来的index是当前属性在 SQL 参数列表中的起始位置,多列情况需要依此后移。如果你写成固定0,多个自定义类型属性并存时参数就会互相覆盖,这是一个坑。

第二,空值分支要“对称”。读的时候我判断currencyStr == null就返回null;写的时候money == null就把两列全部设为null。不对称的话,一个全空的记录读出来就可能解析失败或产生脏数据。

第三,字符串枚举转换。这里选用Enum.Parse<Currency>,并且把数据库里的值按ToString()写入。这样当你在 SQL 客户端看表时,MoneyCurrency列显示的是CNY、USD,而不是0、1。

3.3 在实体中应用自定义类型

public class Order { public virtual int Id { get; set; } // 这是自定义类型的核心应用点 public virtual Money TotalAmount { get; set; } public virtual DateTime CreatedAt { get; set; } }

如果使用 FluentNHibernate,映射配置这样写:

public class OrderMap : ClassMap<Order> { public OrderMap() { Table("Orders"); Id(x => x.Id).GeneratedBy.Identity(); // Key 点:自定义类型映射到两列 Map(x => x.TotalAmount) .CustomType<MoneyType>() .Columns.Clear() .Columns.Add("MoneyAmount", "MoneyCurrency"); } }

CustomType<MoneyType>()是关键入口,它告诉 NHibernate:这个属性不要用默认的类型推断,而是交给MoneyType来处理。后面的Columns.Add把对象属性拆到两列上,顺序跟SqlTypes里的定义一致。

如果你想用 XML 映射,则是这样:

<property name="TotalAmount" type="YourNamespace.MoneyType, YourAssembly"> <column name="MoneyAmount" /> <column name="MoneyCurrency" /> </property>

4. 注册、配置与查询检索的完整链路

写完了自定义类型和实体映射,新的问题来了:NHibernate 怎么知道你的MoneyType是个合法的类型?它在查询、缓存、脏检查这些环节是怎么走通的?

4.1 注册方式:显式声明好过隐式扫描

使用 FluentNHibernate 场景下,最简单的方式是在映射里显式声明CustomType<MoneyType>()。这种方式的优点是非常直接,编译器帮你检查类型存在性,缺点是一个属性写一次,类多了会比较啰嗦。

另一种方式是通过配置注册全局别名。在Configuration里把自定义类型挂到一个字符串名字下:

var config = new Configuration(); config.SetProperty(NHibernate.Cfg.Environment.Dialect, typeof(MsSql2012Dialect).AssemblyQualifiedName); // 注册类型别名 config.SetTypeDef("MoneyType", typeof(MoneyType).AssemblyQualifiedName, null);

注册之后,XML 映射里可以直接写type="MoneyType",而不用再写完整的程序集限定名。这种方式对维护旧项目的 XML 映射文件很友好,尤其是当系统里已经有大量映射文件、不想逐个改CustomType的时候。

4.2 自定义类型与查询:HQL、LINQ、条件查询的边界

很多人刚写完自定义类型,就会兴冲冲地在 HQL 里写:

session.Query<Order>() .Where(o => o.TotalAmount.Amount > 1000) .ToList();

这里我要泼一盆冷水:这个查询基本不会成功,除非你给MoneyType配置了足够明确的“列投影”指示。NHibernate 处理属性内部的子字段访问时,会尝试把Amount翻译成 SQL 列表达式。但它的翻译器并不了解你的值对象内部结构,所以更稳妥的写法是把值对象当作一个整体比较,或者在查询时使用 HQL 明确指定构成列:

// 正确但有限:只能做整体比较 session.CreateQuery("from Order o where o.TotalAmount = :money") .SetParameter("money", new Money(1000, Currency.CNY)) .List<Order>(); // 按具体列查:直接绕开自定义类型,使用原始 SQL 表达式 session.CreateSQLQuery( "select * from Orders where MoneyAmount > :amount and MoneyCurrency = :currency") .AddEntity<Order>() .SetDecimal("amount", 1000) .SetString("currency", "USD") .List<Order>();

这种“值对象内部字段不适合直接参与查询”的局限,其实不是 NHibernate 独有的,Hibernate 也一样。我的建议是:在查询边界上,不要试图用值对象的子字段做精细过滤,能用独立列过滤就用独立列,能整体比较就整体比较。你设计值对象是为了业务封装,不是为了查询优化。

4.3 事务、脏检查与快照:不可变类型带来的红利

你选择IsMutable = false之后,NHibernate 的 dirty-check 机制会有明显区别。常规的可变对象在会话快照里存的是对象的引用,一旦实体属性值被修改,快照和实体对象指向同一个引用,快照检测变得困难,所以 NHibernate 必须在快照里额外做DeepCopy;不可变对象没有这种烦恼,快照里存的就是原引用,因为改不了,所以不会产生“快照被改”的假象。

这让update语句的生成更精准——只有真正出现“旧值和新值不同”时才触发 update,而不像某些实现那样,因为对象引用比较失败而反反复复发更新语句,白白增加数据库压力。

5. 踩过的坑:自定义映射类型里那些“看起来没问题但最终爆炸”的地方

这部分内容不是从文档里抄来的,而是我真实调试过项目、查过源码甚至被生产环境数据坑过之后总结出来的经验。读一遍,能帮你省下至少两三个通宵。

5.1 缓存模式冲突:假设不可变,结果可变

这是最常见的坑。你把自定义类型标成IsMutable = false,但如果属性值对象内部含有List、Dictionary这种可变成员,或者你在DeepCopy里返回了原引用,那一旦二级缓存开启,多个 session 之间就有机会共享同一个可变对象。某个 session 改了它,其它 session 读到的数据就“凭空变化”了,而且这种变化不会触发任何脏检查,也不会发update,完全是内存里的野指针式污染。

解决办法只有一个:诚实地标注可变性。只要值对象内部有任何可变成员,就标true,并在DeepCopy里真正地深拷贝。不要为了贪图那一点性能优化而谎报不可变。

5.2 Equals 和 GetHashCode 实现不当:无数无意义的 update

我早期写自定义类型时,直接用了默认的对象相等比较:return x == y用的是引用比较。结果每次session.Flush()时,NHibernate 都会认为所有实体的属性值都“变了”,因为从数据库读出来的对象和实体里持有的对象不是同一个引用。于是每条记录都被 update 了一遍,即使是完全没改动过的数据,日志里充满了UPDATE ... SET ...。

这种问题很难定位,因为业务逻辑看起来完全正常,只有通过 SQL 日志才能发现大量重复UPDATE。修复的办法就是按值对象的核心字段重写Equals和GetHashCode,让“数值相等”替代“引用相等”。

5.3 多列类型与NullSafeSet下标错位

一个属性映射多列时,NullSafeSet的下标计算很容易错。回顾一下我前面的例子:

NHibernateUtil.Decimal.NullSafeSet(cmd, money.Amount, index); NHibernateUtil.String.NullSafeSet(cmd, money.Currency.ToString(), index + 1);

这里假设SqlTypes数组长度为 2,所以一次NullSafeSet占用index和index + 1。如果你同时有两个这样的自定义类型属性,第二个属性的index起始值就是整个参数列表的下一个位置,如果下标不动,你写进去的值就会复盖第一个属性的参数。

5.4 二级缓存里的Assemble/Disassemble实现不当

如果你不实现这两个方法(或直接返回int之类的基础对象),缓存读写会变得很奇怪。最典型的表现是:第一次查询没问题,第二次查询直接返回缓存的“残缺对象”,属性是null或变成非预期类型。

根因在于Disassemble时你把值对象拆分了,Assemble时没能拼回去。标准做法是:如果值对象不可变,这两个方法直接返回原对象;如果可变,那么Disassemble返回可序列化的基础数据(比如把Money拆成Tuple<decimal, string>),Assemble再拼回来。

5.5 方言差异:不同数据库类型名不同

SqlTypeFactory.Decimal和SqlTypeFactory.String是跨方言的抽象,NHibernate 会根据方言翻译成具体数据库类型。但你如果为了省事直接写SqlTypeFactory.GetString(100)或SqlTypeFactory.GetDecimal(18, 2),遇到 SQLite、Oracle、MySQL 这些不同方言,长度或精度处理差异就可能让 SchemaExport 生成的 DDL 不一致。

我的建议是:能用抽象工厂方法就用抽象工厂方法,需要指定长度/精度时,再额外用WithLength之类的配套方法,尽量减少在自定义类型里写死数据库特性。

6. 进阶玩法:把自定义映射类型用到更复杂的场景

掌握了MoneyType这种基础例子之后,你会发现IUserType的扩展空间其实很大。这里再分享几种我实际用过的进阶场景,供你举一反三。

6.1 JSON 序列化值对象:单列存储复杂结构

如果你的值对象是一个ImageMeta,包含Url、Width、Height、Alt多个字段,但数据库里不想建多列,只想在MetaJson一个NVARCHAR(MAX)列存 JSON,那自定义类型就是天然的解决方案。

public class ImageMetaType : IUserType { public SqlType[] SqlTypes => new[] { SqlTypeFactory.GetString(int.MaxValue) }; public Type ReturnedType => typeof(ImageMeta); public object NullSafeGet(IDataReader rs, string[] names, object owner) { var json = (string)NHibernateUtil.String.NullSafeGet(rs, names[0]); return string.IsNullOrEmpty(json) ? null : JsonConvert.DeserializeObject<ImageMeta>(json); } public void NullSafeSet(IDbCommand cmd, object value, int index) { var meta = value as ImageMeta; var json = meta == null ? null : JsonConvert.SerializeObject(meta); NHibernateUtil.String.NullSafeSet(cmd, json, index); } }

这种玩法的利弊都很明显。好处是表结构清爽,加字段不用改表;坏处是这一列无法在 SQL 里做索引、排序、过滤,查询只能整列匹配或走后端过滤。我一般只在“展示型元数据”上使用,比如图片尺寸、链接信息,核心业务字段绝不塞 JSON。

6.2 加密列:让加密逻辑不出现在业务代码里

假设你有SSN(社会安全号码)或手机号这类需要加密存储的字段。最简单的做法是在 Service 层手动加密解密,但容易漏,业务代码里到处都是加解密的杂音。把它封装进自定义类型则要干净得多:

public class EncryptedStringType : IUserType { private static readonly byte[] Key = Convert.FromBase64String("..."); private static readonly byte[] IV = Convert.FromBase64String("..."); public SqlType[] SqlTypes => new[] { SqlTypeFactory.GetString(512) }; public Type ReturnedType => typeof(string); public object NullSafeGet(IDataReader rs, string[] names, object owner) { var encrypted = (string)NHibernateUtil.String.NullSafeGet(rs, names[0]); return string.IsNullOrEmpty(encrypted) ? null : Decrypt(encrypted); } public void NullSafeSet(IDbCommand cmd, object value, int index) { var plain = value as string; var encrypted = string.IsNullOrEmpty(plain) ? null : Encrypt(plain); NHibernateUtil.String.NullSafeSet(cmd, encrypted, index); } // 参考代码省略 Encrypt / Decrypt 的具体实现 }

这个玩法最大的价值在于:所有读写入口都经过同一个加密转换逻辑,不会出现一条数据在某个入口被加密、在另一个入口却是明文的情况。就算以后更换加密算法,你也只需要改一个类。当然,坑也存在,尤其是查询条件里不能直接用这个字段做where比较,因为数据库里存的是密文。

6.3 时间日期特殊格式:把遗留格式翻译成 DateTime

我处理过一个老系统,数据库里CREATE_TIME列存的是VARCHAR(17),值形如20240117143052123,表示2024-01-17 14:30:52.123。应用层属性是DateTime。直接在数据库那侧能看懂,但代码这边就是一个乱字符串。

用自定义类型最省事:

public class LegacyDateTimeType : IUserType { public SqlType[] SqlTypes => new[] { SqlTypeFactory.GetString(17) }; public Type ReturnedType => typeof(DateTime); public object NullSafeGet(IDataReader rs, string[] names, object owner) { var raw = (string)NHibernateUtil.String.NullSafeGet(rs, names[0]); if (string.IsNullOrEmpty(raw)) return null; return DateTime.ParseExact(raw, "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture); } public void NullSafeSet(IDbCommand cmd, object value, int index) { var dt = value as DateTime?; var raw = dt?.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture); NHibernateUtil.String.NullSafeSet(cmd, raw, index); } }

这样业务层面对这个字段的感知就是正常的DateTime类型,与其它新字段没有差别;只有数据访问层知道它在数据库里是那种奇怪格式。以后如果数据库结构升级、字段格式改成真正的DATETIME,只需要改这个自定义类型,业务层一行都不用动。

7. 性能与设计取舍:不是所有属性都值得自定义类型

最后聊一个更实际的话题。自定义类型写起来并不复杂,但工程上最怕的是“为了炫技而抽象”,把本来简单直接的问题复杂化。我给自己定的取舍标准是:

能用内置类型解决的,绝不上自定义类型。

  • 单字段映射、类型一致:直接Map(x => x.Name)。
  • 枚举本身可以直接用Map(x => x.Status).CustomType<EnumStringType>(),不需要自己写。
  • 多个字段的组合值对象,如果只在查询结果里展示、不参与过滤、不参与脏检查:其实可以考虑 DTO + AutoMapper,而不是实体属性。
  • 如果值对象在多个实体里反复出现,比如Money在订单、退款单、付款单里都有:这种“全局值对象”才是值得写自定义类型的场景,因为一次实现,多处复用。

另外,性能方面要注意一点:NullSafeGet在每次查询加载实体时都会被调用,如果你的转换逻辑里有高开销操作,比如加密、正则、JSON 反序列化,它的执行频率会比你想的高得多。我的经验是:

  • 加密字段性能开销大,适合低频读取的数据,不适合放在每次列表页都全量加载的模型里。
  • JSON 序列化的值对象要小心惰性加载,如果实体是延迟加载的,访问该属性时才会触发一次读取,但如果每次列表也会 select 这个列,开销依然存在。
  • 自定义类型里的常量(加密密钥、正则表达式)要定义成静态只读,避免每次调用都重新初始化。

还有一个小技巧:在查询时如果不需要某个自定义类型字段,可以用Projections.Property或 DTO 投影,绕开实体的完整加载,这样NullSafeGet连触发机会都没有,性能会有可感知的提升。

8. 最后几句实在话

自定义映射类型这个东西,本质上是给 NHibernate 的“类型系统”插了一个自定义的翻译器。它的接口不算复杂,但每个方法的调用时机、返回值的生命周期,都跟 NHibernate 的缓存、脏检查、会话管理深度绑定。我见过太多项目,写了自定义类型却只实现了NullSafeGet和NullSafeSet,其它方法全部返回null,然后某天开启二级缓存后数据莫名丢失,最后花了一整周排查,才发现是Assemble没写对。

所以我的建议是:第一次写自定义类型,务必要把接口所有成员都实现完整,哪怕只是简单地转交原对象。先把正确性跑通,再去做可变性的优化和缓存特性的利用。尤其是不可变值的对象,IsMutable = false+DeepCopy直接返回原对象 +Disassemble/Assemble直接返回原对象,这四个组合在大多数场合都能安全运转。

如果你是在维护老项目,XML 映射文件里可能已经写了不少type="某某Type",这时候看到IUserType的实现,可以拿我上面的检查清单逐条对一遍。毕竟自定义类型的坑都藏在细节里,等生产环境爆出来再修,代价就不是写几行代码的事了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询