唯一稳定轻量的运行时加载方案是用 Google.Protobuf.DynamicMessage + FileDescriptor:读取.proto文本→ParseFrom→Add到DescriptorPool→FindMessageTypeByName→Create DynamicMessage→SetField。

运行时加载 .proto 文件必须用 protobuf-net.Grpc 或 Google.Protobuf.Reflection?
不能直接“编译 .proto 生成 C# 类”——C# 没有内置的 proto 编译器运行时。你看到的 protoc 生成代码,是构建时行为,不是运行时能力。
真正可行的路径只有两条:一是用 Google.Protobuf.Reflection 解析 .proto 得到 FileDescriptor,靠反射动态构造消息;二是用 protobuf-net.Grpc 的 ProtoBuf.Serializer.NonGeneric + 运行时类型注册,但前提是类型已存在(不帮你从 schema 创建新 class)。
常见错误现象:System.TypeLoadException: Could not load type 'MyProto.Msg' —— 因为没生成类,也没注册运行时类型。
- 如果你只是想序列化/反序列化未知结构的数据,用
Google.Protobuf.DynamicMessage+FileDescriptor - 如果你需要强类型访问字段,必须提前生成 C# 类(通过
protoc --csharp_out=.),或用 Roslyn 在运行时编译生成的代码(极重,且需信任源 proto) -
protobuf-net不支持从 .proto 动态建类型,它只支持已有 .NET 类加[ProtoContract]标记
用 Google.Protobuf.DynamicMessage 解析未知 .proto 的最小闭环
这是唯一稳定、轻量、无需代码生成的方案。核心是:读取 .proto → 调用 DescriptorPool.BuildFromBytes() → 构造 DynamicMessage 实例。
注意点:.proto 文件必须是文本格式,且不含 import 依赖(或你得手动加载所有依赖文件并合并字节流)。
- 先用
File.ReadAllText("schema.proto")读入内容,再用Google.Protobuf.DescriptorProtos.FileDescriptorProto.ParseFrom()解析成描述对象 - 调用
Google.Protobuf.Reflection.DescriptorPool.Default.Add()注册该描述,否则后续无法查找 message 类型名 - 用
DescriptorPool.Default.FindMessageTypeByName("my.package.MyMsg")获取MessageDescriptor,再 newDynamicMessage(descriptor) - 填充字段用
dynamicMessage.SetField(descriptor.FindFieldByName("field_name"), value),不能用属性访问
示例片段:
var protoText = File.ReadAllText("msg.proto");
var fileDesc = DescriptorProtos.FileDescriptorProto.ParseFrom(
Google.Protobuf.WellKnownTypes.SourceContext.Parser.ParseFrom(
Encoding.UTF8.GetBytes(protoText)
).ToString().ToByteArray()
);
DescriptorPool.Default.Add(fileDesc); // 实际需处理嵌套依赖
var msgDesc = DescriptorPool.Default.FindMessageTypeByName("example.Person");
var msg = DynamicMessage.Create(msgDesc);为什么别轻易尝试 Roslyn 运行时编译生成的 C# 代码?
有人会想:既然 protoc 能输出 .cs,那我 runtime 调用 protoc → 读取输出 → 用 CSharpCompilation 编译 → AssemblyLoadContext.Load() —— 理论可行,但实际踩坑密集。
典型问题:FileNotFoundException 找不到 Google.Protobuf 引用;生成的类依赖特定命名空间和 partial 类结构;protoc --csharp_out 输出受 --csharp_opt=base_namespace=... 影响,不一致就绑定失败。
- 必须显式添加对
Google.Protobuf.dll和System.Memory.dll等运行时引用,且版本要和项目完全一致 - 生成的代码含
partial类和静态Parser字段,Roslyn 编译后无法被常规反射识别为“可序列化的 protobuf 类” - 每次编译都产生新 Assembly,长期运行导致
AssemblyLoadContext内存泄漏,除非你精细控制卸载 - Windows 上
protoc.exe调用还涉及路径、权限、编码(BOM)问题
真正适合动态 schema 的场景其实很窄
多数所谓“运行时加载 proto”的需求,本质是配置驱动的消息路由或调试工具,而非业务主流程。这时候硬上动态类型反而增加不可靠性。
比如网关解析上游 proto payload 并转发:用 DynamicMessage 完全够用;但如果你要在业务逻辑里反复调 msg.Name、做字段校验、参与 EF Core 映射——那就该回归生成代码 + CI 集成 protoc 步骤。
容易被忽略的一点:DynamicMessage 序列化后的二进制和原生生成类完全兼容(因为共用同一 descriptor),但性能低约 3–5 倍(反射开销),且 IDE 无智能提示、编译期零检查。
事情说清了就结束。










