---
URL: "/zh-CN/blog/2026-06/DynamicObject.html"
LLMS_URL: "/zh-CN/blog/2026-06/DynamicObject.md"
lastUpdated: true
commentabled: true
recommended: true
title: "C# 动态对象实战"
description: "用 DynamicObject 打造你的\"万能插件架构\""
date:

pageClass: "blog-page-class"
cover: "/covers/dotnet.svg"
---

---
lastUpdated: true
commentabled: true
recommended: true
title: C# 动态对象实战
description: 用 DynamicObject 打造你的"万能插件架构"
date: 2026-06-12 10:15:00
pageClass: blog-page-class
cover: /covers/dotnet.svg
---

## 🤔 你有没有遇到过这种情况？ ##

系统上线半年，产品经理走过来说："能不能加个新功能，不重新部署？"

你盯着那一堆 `if-else` 和硬编码的类型判断，心里默默叹了口气。每次新增一个插件，就要改一遍核心代码，重新编译、测试、部署——整个流程走下来少则半天，多则两三天。更头疼的是，插件之间的耦合像一团乱麻，改了 A 影响 B，改了 B 又牵连 C。

根据一些中大型项目的实际统计，插件扩展相关的改动占据了迭代周期中约 30%~40% 的维护成本，而其中大部分时间并不是在写新逻辑，而是在"拆线头"。

读完这篇文章，你将掌握：

- DynamicObject 的底层机制与适用边界

- 如何用动态对象构建一套零侵入、热插拔的插件分发架构

- 三个渐进式的落地方案，从简单 Demo 到生产可用

## 🔍 问题深度剖析：静态类型的"天花板" ##

咱们先把问题说清楚。C# 是强类型语言，这是优势，但在插件化场景下，它也是一堵墙。

### 传统插件架构的三大痛点 ###

#### 接口版本爆炸 ####

最常见的做法是定义一个 `IPlugin` 接口，所有插件实现它。听起来很优雅，但现实是：随着业务演进，接口要加方法，旧插件要跟着改，要么用 `default interface method` 打补丁，要么版本号一路飙升——`IPlugin`、`IPlugin2`、`IPluginV3`……

#### 类型强耦合 ####

插件宿主（Host）需要知道插件的具体类型才能调用，这意味着宿主程序集必须引用插件程序集，或者通过反射做大量的 Type.GetMethod + MethodInfo.Invoke，性能和可读性都不理想。

#### 元数据扩展困难 ####

每个插件可能携带不同的配置参数，比如插件 A 需要 Timeout，插件 B 需要 RetryCount。用静态类型来描述这些差异，要么搞一个巨大的配置类把所有字段都塞进去，要么用 `Dictionary<string, object>` 凑合——后者其实已经在向动态迈步了。

这些问题的根源在于：静态类型系统要求在编译期确定所有契约，而插件化的本质是运行期的动态扩展。两者存在结构性矛盾。

## 💡 核心要点提炼：DynamicObject 是什么，能做什么 ##

### 底层机制 ###

`DynamicObject` 是 `System.Dynamic` 命名空间下的一个抽象类，它配合 C# 的 `dynamic` 关键字工作。当你用 dynamic 变量调用一个方法或访问一个属性时，编译器不做类型检查，而是在运行时通过 DLR（Dynamic Language Runtime） 分发调用。

DynamicObject 提供了一系列可重写的虚方法，让你拦截这些运行时调用：
| 可重写方法 | 触发时机 |
| --- | --- |
| TryGetMember | 读取属性时|
| TrySetMember |设置属性时|
| TryInvokeMember |调用方法时|
| TryInvoke |直接调用对象时|
| TryBinaryOperation |二元运算时|

关键理解：DynamicObject 不是反射的替代品，它是一个行为代理层。你可以在这一层做任何事——转发调用、记录日志、做权限校验、动态路由到不同的实现。

### 适用边界 ###

动态对象不是银弹，用错了反而是灾难。它适合的场景是：

- 插件/脚本宿主，需要在运行时动态分发调用

- DSL（领域特定语言）的构建

- 跨语言互操作（如与 Python、JavaScript 引擎交互）

- 配置/元数据的动态访问层

不适合的场景：核心业务逻辑、高频热路径（动态分发有额外开销）、需要 IDE 强类型提示的协作代码。

## 🛠️ 解决方案设计 ##

下面咱们用三个渐进式方案，从原理验证到生产落地，一步步把架构搭起来。

### 方案一：动态属性包——插件元数据的灵活容器 ###

应用场景：每个插件携带不同的配置参数，宿主需要统一读写，但不想为每种插件单独定义配置类。

这是最简单的起点，用 DynamicObject 包装一个字典，让它看起来像一个"真实对象"。

```cs
using System.Dynamic;
using System.Collections.Generic;

/// <summary>
/// 动态属性包：用于存储插件的任意元数据
/// </summary>
public class DynamicPropertyBag : DynamicObject
{
    private readonly Dictionary<string, object?> _store = new();
 
    // 拦截属性读取：bag.Timeout
    public override bool TryGetMember(GetMemberBinder binder, out object? result)
    {
        return _store.TryGetValue(binder.Name, out result);
    }
 
    // 拦截属性写入：bag.Timeout = 3000
    public override bool TrySetMember(SetMemberBinder binder, object? value)
    {
        _store[binder.Name] = value;
        return true;
    }
 
    // 支持枚举所有动态属性名
    public override IEnumerable<string> GetDynamicMemberNames() => _store.Keys;
}
```

> 踩坑预警：TryGetMember 返回 `false` 时，DLR 会抛出 `RuntimeBinderException`，而不是返回 null。如果你希望访问不存在的属性时得到 null 而非异常，把 TryGetMember 改成始终返回 `true`，并在 `result` 为空时赋 null：

### 方案二：动态方法分发器——插件调用的统一入口 ###

应用场景：宿主需要调用插件的方法，但插件的方法签名各不相同，无法用统一接口约束。

这是架构的核心层。我在一个工业数据采集项目中用过类似的设计——不同厂商的设备驱动暴露的接口千奇百怪，用这一层统一包装后，上层代码完全感知不到差异。

配合一个简单的插件注册中心：

性能说明（测试环境：.NET 10，Intel i7-12700，Release 模式）：

- 首次调用（含反射查找）：约 0.8ms

- 缓存后的后续调用：约 0.05ms（与直接反射调用相当）

- 相比直接方法调用（~0.001ms）：有约 50 倍开销，仅适合非热路径

### 方案三：完整的万能插件架构——动态加载 + 生命周期管理 ###

应用场景：生产级插件系统，支持运行时热加载 DLL，插件无需实现固定接口。

这是完整版，把前两个方案整合起来，加上 `AssemblyLoadContext` 实现真正的热插拔。

## 🎯 三个"一句话洞察" ##

动态不是逃避类型系统，而是在类型系统无法触达的边界处优雅地接管。

插件架构的核心不是技术选型，而是把"变化"和"稳定"分离到不同的层次。

DynamicObject 最大的价值不是"能做什么"，而是"把什么横切关注点挡在了调用路径上"。

## 📌 可复用代码模板速查 ##

- 模板 1：带默认值的安全动态属性包（防止访问不存在属性时抛异常）
- 模板 2：带调用日志的插件代理基类（直接继承扩展）

## 🏁 总结与学习路径 ##

这篇文章覆盖了三个核心落地点：用 `DynamicPropertyBag` 解决插件元数据的灵活存储，用 `DynamicPluginProxy` 统一方法分发并内置横切关注点，再用 `HotPluginHost` + `AssemblyLoadContext` 实现真正的运行时热插拔。

三个方案是递进关系，实际项目中不必一步到位——从方案一开始，感受动态对象的边界，再逐步引入方案二和三。

如果你想继续深挖这个方向，推荐的学习路径是：`DynamicObject` → `ExpandoObject`（内置实现，适合简单场景）→ `AssemblyLoadContext`（.NET Core 的插件隔离机制）→ MEF（Managed Extensibility Framework，微软官方插件框架）→ `Roslyn` 脚本 API（真正的运行时代码编译与执行）。

## 💬 互动话题 ##

你在项目里有没有遇到过"插件越加越乱"的情况？当时是怎么处理的——是用反射、接口版本管理，还是别的方案？欢迎在评论区聊聊你的实践经验，说不定能碰出新思路。

另外一个小挑战：如果要给 `DynamicPluginProxy` 加上异步方法支持（即插件方法返回 `Task` 或 `Task<T>`），你会怎么改 TryInvokeMember？可以把思路写在评论里。
