protobuf-net 序列化前必须标记 [ProtoContract]
和 [ProtoMember]
不加这些特性,
Serializer.Serialize会静默失败或抛出
InvalidOperationException:“No parameterless constructor defined”——哪怕类有默认构造函数。Protobuf-net 不依赖反射自动发现字段,它只序列化显式标注的成员。
常见错误是只给类加
[ProtoContract],却忘了给每个要序列化的字段/属性加
[ProtoMember(n)],其中
n是唯一整数序号(不能重复,不能跳空,建议从 1 开始):
[ProtoContract]
public class Person
{
[ProtoMember(1)] public string Name { get; set; }
[ProtoMember(2)] public int Age { get; set; }
[ProtoMember(3)] public DateTime BirthDate { get; set; }
}
序号一旦发布就不要改,否则旧数据反序列化失败
可读写属性、自动属性、私有字段都支持,但字段需加 private+
[ProtoMember]才生效 不支持
dynamic、匿名类型、未标记的嵌套类
用 Serializer.Serialize
和 Serializer.Deserialize
处理流
这是最轻量、最常用的 API,直接操作
Stream,无 JSON 或 XML 中间层,性能接近原生二进制。注意:它不处理编码、长度前缀、分帧,需要自己封装协议头。
典型用法:
var person = new Person { Name = "Alice", Age = 30 };
using var stream = new MemoryStream();
Serializer.Serialize(stream, person); // 写入
stream.Position = 0;
var deserialized = Serializer.Deserialize<Person>(stream); // 读回
务必重置 stream.Position,否则
Deserialize从末尾开始读,返回默认值 避免对同一
Stream多次复用而不清空或重置,容易因位置错乱导致反序列化失败 如果要网络传输,推荐配合
SerializeWithLengthPrefix/
DeserializeWithLengthPrefix自动处理变长消息边界
泛型类型和集合要显式告知运行时类型
Protobuf-net 默认不支持开放泛型(如
List<t></t>)或未实例化的泛型定义。遇到
NotSupportedException: Type is not expected, and no contract can be inferred就是这个原因。
解决方式有两种:
在类定义中用[ProtoInclude]预注册子类型(适合继承场景) 对泛型集合,在序列化前调用
RuntimeTypeModel.Default.Add(typeof(List<person>), true)</person>显式注册 更稳妥的做法:使用具体封闭类型(如
List<person></person>)并确保其元素类型已标记
[ProtoContract]
例如,以下写法会失败:
[ProtoContract]
public class Container
{
[ProtoMember(1)] public List<object> Items; // ❌ object 无法推断具体类型
}
应改为:
[ProtoContract]
public class Container
{
[ProtoMember(1)] public List<Person> People; // ✅ 类型明确且已注册
}
避免在 ASP.NET Core 中直接用 Serializer
替换 JSON 输出
有人试图在
Startup.ConfigureServices里把
JsonSerializerOptions换成 protobuf-net 的逻辑,这是行不通的。ASP.NET Core 的
IMvcBuilder.AddJsonOptions只接受
JsonConverter,不兼容 protobuf-net 的二进制流。
若想全局启用 Protobuf 响应,正确路径是:
实现自定义OutputFormatter(继承
OutputFormatter),重写
CanWriteResult和
WriteResponseBody注册时指定
SupportedMediaTypes.Add("application/x-protobuf")
客户端请求头需带 Accept: application/x-protobuf
别忽略 MIME 类型协商——Protobuf 不是浏览器原生支持的格式,纯前端调用基本不可行,主要用在后端服务间通信(gRPC 除外,那是另一套体系)。
真正容易被忽略的是线程安全性:
RuntimeTypeModel.Default是全局单例,首次访问会触发类型扫描和模型构建,后续并发调用是安全的;但如果你手动创建了多个
RuntimeTypeModel实例,又没做好初始化同步,可能引发竞态或重复注册异常。
