ExpandoObject 是基于字典的独立动态容器,非强类型对象补丁工具;必须用 dynamic 声明才能使用点语法,IDE 不提示、无重构支持;默认 JSON 序列化易丢字段,需手动配置转换器;不支持反射获取属性、不能扩展已有对象、无线程安全与索引器支持;性能低、仅适用于结构不确定的临时场景。

ExpandoObject 不是“给任意对象加属性”的工具,它本身就是一个独立的、基于字典的动态容器;用错场景或误以为它能替代强类型模型,后续会踩一堆反射、序列化、线程安全和 IDE 支持相关的坑。
必须用 dynamic 声明才能用点语法访问
直接声明为 ExpandoObject 类型时,.Name 这类写法编译失败——它没有预定义成员。IDE 也不提示,rename 更不会生效。
-
dynamic obj = new ExpandoObject(); obj.Name = "Alice";✅ 编译通过,运行时可读写 -
ExpandoObject obj = new ExpandoObject(); obj.Name = "Alice";❌ 编译错误 -
IDictionary<string object> dict = (IDictionary<string object>)obj; dict["Name"] = "Alice";</string></string>✅ 安全但失去点语法便利
默认 JSON 序列化会丢字段,得手动配转换器
System.Text.Json 和 Newtonsoft.Json 默认都把 ExpandoObject 当作普通字典处理,但输出格式不同:前者默认序列化为空对象或报错,后者默认输出为 JSON 对象(如 {"Name":"Alice"}),但一旦启用了 CamelCasePropertyNamesContractResolver 等配置,仍可能丢字段。
-
System.Text.Json:必须显式注册ExpandoObjectConverter:options.Converters.Add(new ExpandoObjectConverter()); -
Newtonsoft.Json:默认支持,但若用了自定义ContractResolver,也得加ExpandoObjectConverter - 任何依赖反射的序列化/绑定(如 ASP.NET Core Model Binding、EF Core 映射)完全识别不了这些“属性”,
GetProperties()返回空数组
不能给已有对象“打补丁”,只能新建再复制
想对一个已存在的 Person p = new Person(); 实例加个 p.Notes?做不到。ExpandoObject 不继承、不代理、不 hook 原对象,它就是个全新实例。
- 你只能新建
dynamic obj = new ExpandoObject();,再手动把p的字段值复制过去 - 它不支持索引器(
obj[variableKey]直接报错)、方法重载、事件以外的其他成员(比如属性 setter 逻辑、验证逻辑) - 多线程写入同一实例无锁保护,需自行同步
性能比强类型对象低 3–5 倍,别在高频循环里用
点语法访问本质是 DLR 动态绑定,每次都要查字典 + 运行时解析,远不如字段直取快。调试器里看着像属性,实际是键值对。
- 适合场景非常窄:解析结构不确定的 JSON、桥接 IronPython 等 DLR 语言、原型阶段临时组装数据
- 不适合长期维护的核心模型、需要校验/绑定/ORM 映射的业务实体
- 一旦逻辑稳定,立刻换成
class或record—— 否则重构成本和隐性 bug 会指数级上升
真正容易被忽略的是:它看起来像对象,用起来像字典,但既不是对象也不是字典。所有依赖“真实类型信息”的地方(反射、序列化、验证、IDE 支持、线程安全)都会掉链子。别把它当万能胶,它只是个临时中转站。


















