















Exception.Data 的引入我们经常面临这样的业务挑战:一个复杂且冗长的业务流程,例如销售订单的生效和库存处理,其中任何环节都可能因为各种原因中断并抛出异常。当异常发生时,仅仅抛出一个通用的错误信息是远远不够的。用户需要的是具体、有上下文的错误提示,比如“单据 XXX 的第 3 行,商品 A 库存不足,需要 5 个,现在只有 1 个。”
传统的异常处理方式,如在底层服务抛出简单的“库存不足”异常,然后由上层业务逻辑捕获并包装成新的、包含更多上下文信息的异常(比如 OrderProcessingException 内部包含 InsufficientStockException),会导致所谓的“洋葱式”异常。这种层层嵌套的异常链虽然能保留因果关系,但:
try-catch-rethrow 代码。为了解决这些问题,.NET 的 Exception.Data 属性应运而生。它是一个 IDictionary<object, object> 类型的属性,允许你在异常抛出时,附加任意的、非结构化的上下文数据到原始异常对象上,而无需创建新的异常或进行异常包装。
Exception.Data 设计的优点Data 字典中添加额外信息,而不需要捕获、创建新异常、再重新抛出。这大大简化了异常处理链,使异常对象保持简洁。Data 中添加信息,异常对象的类型始终是最初抛出的那个(例如 InvalidOperationException 或 OutOfMemoryException),这对于基于异常类型进行错误分类、监控和报警至关重要。Data 属性是一个键值对字典,可以存储任何类型的对象。这使得它能够适应各种不可预测的、需要附加的上下文信息,例如单据 ID、行号、商品编码、用户 ID 等。Data 字典中的信息会自动跟随,上层代码可以轻松访问这些附加数据,用于生成详细的错误报告或日志。Exception.Data 设计的缺点与挑战IDictionary<object, object> 的设计意味着你可以在其中放入任何类型的数据,且编译器无法在编译时检查键或值的类型。这在大型团队或复杂项目中可能导致:
InvalidCastException。Data 中存储了大量或结构复杂的非标准化数据,调试时需要手动检查字典内容,增加了理解异常上下文的难度。Data 无法像自定义异常属性那样提供强业务语义。例如,一个 InsufficientStockException 应该直接有 RequestedQuantity 和 AvailableQuantity 这样的属性,而不是将它们作为键值对放入 Data 中。当信息具有明确的业务含义和结构时,自定义异常类型带有特定属性是更好的选择。Data 中的任意对象可能导致序列化问题,特别是当其中包含非可序列化对象时。InsufficientStockException),并在其中包含强类型的业务属性。这样能提供更好的类型安全、可读性和可维护性。Data 适用于那些不值得或无法用自定义属性表示的临时性、动态性上下文信息。InnerException (异常链): InnerException 用于表达异常的因果关系,即“这个异常是因为那个异常引起的”。例如,数据库连接失败导致的数据保存失败,应通过 SqlException 作为 InnerException 附加到 DataSaveException 上。Data 关注的是上下文信息,而非直接的因果关系。两者是互补的,而不是替代关系。Result<T, E> 或 Either 这样的返回类型,而不是抛出异常。这种模式将成功值或错误信息(一个包含详细错误代码、消息和上下文的结构化对象)作为函数返回值,避免了异常处理的开销,并强制调用者处理所有可能的错误路径。这种方式比 Exception.Data 提供更强的类型安全和结构化错误信息。在我的经验中,对于可预期的、需要明确处理的业务错误,Result 模式往往比异常更好,因为它将错误变成了常规的返回值。在大型 ERP 产品和不同编程语言中,处理异常和传递上下文信息的方式各有侧重:
大型 ERP 产品(SAP, Oracle EBS, MS Dynamics 365):
cx_root 的子类),这些类可以定义自己的属性来承载业务上下文。此外,SAP 有强大的消息管理机制,通过消息号和消息变量来构建动态、国际化的错误消息,上下文信息通过变量绑定到消息模板中。较少见到通用的 IDictionary 附加到所有异常上。try-catch-throw)。虽然它与 .NET CLR 有互操作性,能够访问 .NET 异常的 Data 属性,但 X++ 自身在业务层面更倾向于通过明确的错误消息和日志来记录上下文。插件开发中,开发人员可能会利用 .NET 异常的 Data 属性。其他编程语言:
Throwable 类有 getCause() 方法用于实现异常链。此外,一些库如 Apache Commons Lang 提供了 ContextedException,它允许你添加任意键值对的上下文信息,与 Exception.Data 的理念非常相似。这表明在 Java 社区也有类似的需求。__cause__ 和 __context__ 用于异常链。value, error)来处理错误。当需要传递上下文时,常见的做法是使用 fmt.Errorf("%w", err) 进行错误包装(error wrapping),通过字符串或自定义结构体添加上下文信息。Go 社区强调错误是值,可以像其他值一样处理和传递,这使得错误信息可以非常结构化。Result<T, E> 枚举来表示成功或失败。对于错误上下文,常用的 anyhow 和 thiserror 等 Crate 提供了强大的功能:anyhow::Error 允许动态添加上下文信息(类似于 Data 但更安全),而 thiserror 则允许你定义带有结构化字段的自定义错误枚举,强制类型安全。从实际的商业开发场景来看,Exception.Data 是一个实用但需要谨慎使用的工具。
何时使用 Exception.Data:
Data 是一个快捷方便的选择。例如,一个临时校验失败的字段名、一个尝试操作的临时标识符等。Data 可以用于在开发和测试阶段,向异常中注入诊断信息,方便快速定位问题。何时避免或谨慎使用 Exception.Data:
Data 的非结构化特性将带来挑战。此时,自定义异常属性或专门的错误 DTO 会更好。Data 中的所有对象都是可序列化的,并且考虑接收方如何理解和解析这些非类型化的数据。通常,在这种场景下,更推荐定义清晰的错误 DTO 来作为服务契约的一部分。最佳实践建议:
Exception.Data,请在团队内部建立严格的键命名约定(例如,使用 ModuleName.PropertyName),并明确每种异常可能在 Data 中包含哪些键以及它们的值类型。Data 添加信息后,立即将其添加到日志中。在最终处理异常时,确保 Data 中的所有重要信息都被提取并记录下来,以便于问题追溯。Data 应该包含解决问题所需的最少且最关键的上下文信息。避免将大量不相关的数据倾倒进去。Exception.Data 并非万能药,它应与自定义异常、InnerException 和 Result 模式结合使用,形成一个全面且健壮的异常处理策略。Exception.Data 是 .NET 框架提供的一个灵活的工具,它填补了自定义异常和异常链之间的空白,允许在不改变异常类型和不增加层级的情况下附加任意上下文。然而,它的非类型化特性也要求我们在设计和使用时保持高度的纪律性和约定,才能真正发挥其价值,而不是成为“技术债”的源头。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。