- 后端
- 微服务
【免费下载链接】orleans
Cloud Native application framework for .NET
序列化是 .NET Orleans 整体系统设计中至关重要的一环:grain 调用、响应、请求上下文、持久化状态、提醒与流数据都要经过序列化在进程间传递。Orleans 虽然提供了开箱即用的默认序列化方案,但在实际应用中,你经常需要把特定类型委托给Newtonsoft.Json或System.Text.Json等外部序列化器,并且必须理解"注册序列化器"与"授权类型名解析"是两件不同的事。本文以 serialization-configuration.md 为骨架,结合本仓库源码,完整讲解 JSON 序列化器配置、类型名解析安全边界与 grain 存储序列化的实操方案,读完即可在 silo 与客户端两侧落地一套安全、可运行的序列化配置。
序列化配置在 Orleans 中的定位
Orleans 对序列化提供了合理的默认实现:应用类型通过[GenerateSerializer]特性配合 Orleans 源代码生成器(见 Orleans.CodeGenerator)生成高性能的专用序列化器。但并非所有类型都适合由生成的序列化器处理,典型场景包括:
- 多态契约中,基类是抽象的,具体运行时类型要到反序列化时才能确定;
- 类型结构复杂、频繁变动,希望交给成熟的 JSON 库处理;
- 与既有系统对接,必须使用某种既定序列化格式。
针对这些场景,Orleans 在消息传输路径上支持将序列化工作委托给其他序列化器,例如Newtonsoft.Json与System.Text.Json。Orleans.Serialization的源码(Orleans.Serialization.NewtonsoftJson/SerializationHostingExtensions.cs、Orleans.Serialization.SystemTextJson/SerializationHostingExtensions.cs)展示了这两套官方实现的完整模式,你可以照此模式为其他序列化库编写适配器。
需要注意两条路径的区分:
- 主机间数据传输(grain 调用参数、返回值、流数据等):通过
ISerializerBuilder注册 codec 委托给外部序列化器; - grain 持久化存储:最佳实践是使用
IGrainStorageSerializer配置自定义存储序列化器,而不是依赖消息传输层的 codec 选择逻辑。
配置 Orleans 使用Newtonsoft.Json
安装与基本配置
首先在目标项目中引用Microsoft.Orleans.Serialization.NewtonsoftJsonNuGet 包(对应本仓库的 Orleans.Serialization.NewtonsoftJson.csproj)。然后配置序列化器,并明确指定它负责哪些类型。下面这个来自文档片段 TypeNameResolutionExamples.cs 的示例,让Newtonsoft.Json负责Example.Namespace命名空间下的所有类型:
siloBuilder.Services.AddSerializer(serializerBuilder => { serializerBuilder.AddNewtonsoftJsonSerializer( isSupported: type => type.Namespace?.StartsWith("Example.Namespace", StringComparison.Ordinal) is true); });AddNewtonsoftJsonSerializer调用(Orleans.Serialization.SerializationHostingExtensions)为使用Newtonsoft.Json.JsonSerializer的序列化与反序列化添加支持。所有需要处理这些类型的客户端必须做相同的配置——客户端通过IClientBuilder(即UseOrleansClient回调中的 builder)执行同样的AddSerializer调用,否则客户端与 silo 之间的 codec 选择不一致会导致消息无法解析。
谓词与选项的完整形态
从 SerializationHostingExtensions.cs 的实现可以看到,AddNewtonsoftJsonSerializer提供了三个重载,核心参数语义如下:
isSupported(即isSerializable):Func<Type, bool>委托,决定哪些类型由该 codec 负责序列化/反序列化。注册时被包装为DelegateCodecSelector并注册为ICodecSelector单例;isCopyable:可选的第二个谓词,决定哪些类型由该 codec 负责深拷贝。注册时被包装为DelegateCopierSelector并注册为ICopierSelector单例。若不单独指定,默认与序列化谓词一致;configureOptions/jsonSerializerSettings:配置NewtonsoftJsonCodecOptions。NewtonsoftJsonCodecOptions.SerializerSettings(见 NewtonsoftJsonCodecOptions.cs)允许你传入完整的JsonSerializerSettings,同时选项类还提供IsSerializableType与IsCopyableType两个可选的Func<Type, bool?>属性,供 codec 内部二次判定。
因此更完整的配置形态是分别控制序列化与拷贝范围,并自定义JsonSerializerSettings:
siloBuilder.Services.AddSerializer(serializerBuilder => { serializerBuilder.AddNewtonsoftJsonSerializer( isSerializable: type => type.Namespace?.StartsWith("Example.Namespace", StringComparison.Ordinal) is true, isCopyable: type => type.Namespace?.StartsWith("Example.Namespace", StringComparison.Ordinal) is true, options => options.Configure(o => { o.SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.None, NullValueHandling = NullValueHandling.Ignore, }; })); });底层注册机制
在AddNewtonsoftJsonSerializer内部,首次调用时会一次性完成以下注册(幂等,通过ServiceDescriptor判重):
- 注册
NewtonsoftJsonCodec单例; - 通过
AddFromExisting将其同时暴露为IGeneralizedCodec(通用 codec)、IGeneralizedCopier(通用 copier)与ITypeFilter(类型过滤器)三个服务; - 在
TypeManifestOptions.WellKnownTypeAliases中登记NewtonsoftJsonCodec.WellKnownAlias。
这意味着 Newtonsoft 序列化器不仅负责读写字段,还参与值的深拷贝,并在类型解析阶段作为ITypeFilter提供意见——这一点在理解下文"类型名解析授权"时非常关键。
与生成序列化器的优先级
对于标记了[GenerateSerializer]的类型,Orleans优先使用生成的序列化器,而不是Newtonsoft.Json序列化器。换句话说,isSupported谓词只会接管那些没有专用生成序列化器的类型。这是设计上的刻意行为:生成序列化器更高效、更安全,外部 JSON 序列化器只作为补充手段。
配置 Orleans 使用System.Text.Json
安装与基本配置
作为替代方案,可以引用Microsoft.Orleans.Serialization.SystemTextJsonNuGet 包(对应 Orleans.Serialization.SystemTextJson.csproj),然后调用AddJsonSerializer方法配置序列化器。下面的示例来自 TypeNameResolutionExamples.cs,让System.Text.Json负责Example.Namespace命名空间下的所有类型:
siloBuilder.Services.AddSerializer(serializerBuilder => { serializerBuilder.AddJsonSerializer( isSupported: type => type.Namespace?.StartsWith("Example.Namespace", StringComparison.Ordinal) is true); });在 silo 一侧,通过ISiloBuilder(UseOrleans回调)完成上述配置;在客户端一侧,则在IClientBuilder(UseOrleansClient回调)中做同样配置。
选项定制:SerializerOptions / ReaderOptions / WriterOptions
与 Newtonsoft 版本对称,AddJsonSerializer也提供isSupported、isSerializable/isCopyable、configureOptions等多个重载(见 Orleans.Serialization.SystemTextJson/SerializationHostingExtensions.cs)。其底层同样将JsonCodec注册为IGeneralizedCodec、IGeneralizedCopier与ITypeFilter。
JsonCodecOptions.cs 提供了三个可配置的选项对象:
SerializerOptions:JsonSerializerOptions,默认new(),控制属性命名策略、忽略规则、转换器集合等;ReaderOptions:JsonReaderOptions,控制读取时的注释/尾随逗号处理等;WriterOptions:JsonWriterOptions,控制输出缩进、编码等。
例如开启 camelCase 属性命名并忽略 null 值:
siloBuilder.Services.AddSerializer(serializerBuilder => { serializerBuilder.AddJsonSerializer( isSupported: type => type.Namespace?.StartsWith("Example.Namespace", StringComparison.Ordinal) is true, options => options.Configure(o => { o.SerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; })); });授权类型名解析:注册序列化器不等于信任类型
这是本主题中最容易踩坑、也最需要重视的安全边界。注册一个外部序列化器只是选择了由哪个 codec 来处理值,它并不会自动授权 Orleans 去解析该序列化器所能接受的每一个 CLR 类型名。类型名解析是独立的安全边界,且TypeManifestOptions.AllowAllTypes的默认值是false(见 TypeManifestOptions.cs)。
这一点在多态签名下尤其明显。考虑IReadOnlyList<TriggerRule>这样的契约,其中TriggerRule是抽象类,具体值由System.Text.Json处理(文档示例见 HostSnippets.cs):
siloBuilder.Services.AddSerializer(serializerBuilder => { serializerBuilder.AddJsonSerializer( isSupported: type => type.Namespace?.StartsWith("MyApp", StringComparison.Ordinal) == true); serializerBuilder.Configure(options => options.AddAllowedType(typeof(TriggerRule))); });这段代码做了两件事:注册 JSON 序列化器处理MyApp命名空间的类型,同时显式信任应用类型TriggerRule,允许 Orleans 在收到其运行时类型名时解析该类型。
AddAllowedType 与 AllowedTypes
TypeManifestOptions.AddAllowedType(Type)是添加单个类型信任的首选方式。从其实现(TypeManifestOptions.cs)可以看到:
public void AddAllowedType(Type type) { if (type is null) throw new ArgumentNullException(nameof(type)); AllowedTypes.Add(RuntimeTypeNameFormatter.FormatInternalNoCache(type, allowAliases: false)); }它使用 Orleans 的运行时类型名格式化器(RuntimeTypeNameFormatter)生成条目,覆盖构造泛型与嵌套泛型类型的完整形态,且allowAliases: false意味着不使用复合别名,避免后续别名配置影响已登记的条目。
AllowedTypes字符串集合(HashSet<string>,StringComparer.Ordinal大小写敏感比较,见 TypeManifestOptions.cs)为兼容性而保留,其中存放的是 Orleans 格式的运行时类型名(CLR 类型名语法,含命名空间、嵌套、泛型参数、数组与可选的程序集限定名)。优先使用AddAllowedType,而不是手工拼接这些名称——手工拼接极易因格式细节出错,且会在别名配置变化时产生不一致。若确实需要字符串形式,应使用RuntimeTypeNameFormatter.Format(type)生成后再加入集合。
程序集信任:AddAllowedAssembly
如果应用程序集中的所有类型都可信任,可以直接信任整个程序集(示例见 HostSnippets.cs):
siloBuilder.Services.AddSerializer(serializerBuilder => { serializerBuilder.Configure(options => options.AddAllowedAssembly(typeof(TriggerRule).Assembly)); });AddAllowedAssembly通过CachedTypeResolver.GetName(assembly)将程序集名加入AllowedAssemblies集合。程序集信任是逐组件生效的:允许一个泛型类型定义的所在程序集,并不会隐式信任来自其他程序集的泛型实参。也就是说,对于List<Foo>,即使List<T>定义在已信任的程序集中,Foo所在的程序集也必须单独受信任;数组元素类型同理,必须逐个组件检查并受信任。
基于策略的信任:ITypeNameFilter
当信任策略复杂、无法用"允许某个类型/程序集"表达时,可以实现ITypeNameFilter接口,在 Orleans 加载对应类型之前对类型名进行评估。文档示例 TypeNameResolutionExamples.cs 中的过滤器如下:
public sealed class ApplicationTypeNameFilter : ITypeNameFilter { public bool? IsTypeNameAllowed(string typeName, string assemblyName) { if (assemblyName == "MyApp.Contracts" || assemblyName.StartsWith("MyApp.Contracts,", StringComparison.Ordinal)) { return true; } return null; } }通过依赖注入注册过滤器(TypeNameResolutionExamples.cs):
siloBuilder.Services.AddSingleton<ITypeNameFilter, ApplicationTypeNameFilter>();IsTypeNameAllowed的返回值语义是:
true:允许;false:拒绝;null:该过滤器对此名称没有意见。
信任裁决的优先级规则需要准确掌握(这也是 serialization-configuration.md 的核心要点):
- 显式加入
AllowedTypes的类型是权威的,直接放行; - 对于其他名称,任何一个
ITypeNameFilter的拒绝都优先于其他类型名过滤器与程序集信任(denial wins)——也就是说,只要有一个过滤器说"拒绝",即使另一个过滤器同意、或者该程序集已被信任,也会被拒绝; - 当基于名称的检查没有肯定性结果时,
ITypeFilter提供基于已解析Type的回退判定,且在该回退路径中拒绝依然优先; - 这些检查在格式化(写入)与解析(读取)两个方向都会执行,并且覆盖构造泛型的各个组成部分与数组元素类型。
这种"拒绝优先、白名单权威"的设计,保证安全策略不会被多个过滤器的分歧所削弱。
兼容性逃生舱:AllowAllTypes
作为兼容性逃生舱,可以显式关闭该边界(TypeNameResolutionExamples.cs):
siloBuilder.Services.AddSerializer(serializerBuilder => { serializerBuilder.Configure((TypeManifestOptions options) => options.AllowAllTypes = true); });警告:
AllowAllTypes会绕过类型名验证(包括自定义过滤器),允许解析任何可解析的类型。仅在序列化输入完全可信时才可使用。更安全的做法始终是:优先允许单个类型(AddAllowedType)或受信任的程序集(AddAllowedAssembly)。
从安全角度看(完整指导见 serialization security):开放的类型解析面会让输入选择到应用声明消息契约之外的类型上的代码路径。在配置了外部序列化器且输入可被低信任方影响(客户端、流、队列、存储系统)时,务必保持AllowAllTypes = false,并用上述机制收敛类型面。
存储侧的序列化:IGrainStorageSerializer
上述 JSON 委托机制解决的是主机间消息传输的序列化。对于grain 持久化存储,官方建议的最佳实践是使用IGrainStorageSerializer来配置自定义序列化器。这与消息序列化是两条独立的配置路径。
以 Azure Blob 存储为例,AzureBlobStorageGrainStorageProviderBuilder.cs 中的实现展示了存储序列化器的接入方式:
options.GrainStorageSerializer = services.GetRequiredKeyedService<IGrainStorageSerializer>(serializerKey);即存储提供者通过键控服务(keyed service)解析IGrainStorageSerializer,将其注入存储选项。这意味着你可以为不同存储、不同数据分区注册不同的键控序列化器,实现按需选择格式(例如 JSON、二进制等)。Azure Table 存储提供者(AzureTableStorageGrainStorageProviderBuilder.cs)采用同样的模式,DynamoDB、AdoNet 等提供者也一致地支持该接口(参见 DynamoDBStorageOptions.cs、AdoNetGrainStorageOptions.cs 等)。
需要再次强调:存储与消息传输的序列化器配置必须一致地部署在 silo 与客户端两侧。安全文档 serialization.md 明确建议:将IsSupportedType谓词限制在预期的契约范围内,把命名空间前缀选择与显式类型策略配对使用,并在所有处理该契约的 silo 与客户端上配置相同的兼容序列化策略。
最佳实践小结
综合文档与源码,落地一套安全、可维护的序列化配置时,请遵循以下原则:
- 优先使用生成序列化器:应用自有的 grain 契约类型用
[GenerateSerializer]+ 源代码生成器,外部 JSON 序列化器只负责补充场景; - 委托范围收窄:
isSupported/isSerializable谓词尽量用精确的命名空间前缀或类型条件限定,不要无条件委托; - 类型解析边界保持关闭:维持
AllowAllTypes = false;对多态类型用AddAllowedType,对整个可信程序集用AddAllowedAssembly; - 复杂策略用过滤器:实现
ITypeNameFilter(名称级)并按需组合ITypeFilter(类型级)回退,牢记"拒绝优先于同意与程序集信任"; - 客户端与 silo 配置对称:所有处理同一契约的 silo 和客户端必须配置相同的序列化器与类型策略;
- 存储序列化走专用通道:grain 持久化使用
IGrainStorageSerializer按存储提供者配置,而不是依赖消息 codec; - 反序列化后仍要校验数据:成功的反序列化只证明字节符合格式与类型策略,标识符、长度、范围、状态迁移等业务不变量必须在修改 grain 状态或调用依赖之前验证(详见 serialization security)。
参考资源
- 本文依据的主文档:serialization-configuration.md
- 完整可编译示例:TypeNameResolutionExamples.cs 与 HostSnippets.cs
- Newtonsoft.Json 适配实现:Orleans.Serialization.NewtonsoftJson
- System.Text.Json 适配实现:Orleans.Serialization.SystemTextJson
- 类型清单与信任选项:TypeManifestOptions.cs
- 序列化安全完整指南:serialization security
- 后端
- 微服务
【免费下载链接】orleans
Cloud Native application framework for .NET
相关推荐
Fabric.js序列化与反序列化:JSON数据的存储与恢复
Fabric.js序列化与反序列化:JSON数据的存储与恢复 Fabric.js是一个强大的HTML5 Canvas库,它提供了完整的对象序列化和反序列化功能,
前端图形学AutoCLI与autocli.ai云平台:如何发现和共享社区适配器资源
AutoCLI与autocli.ai云平台:如何发现和共享社区适配器资源 AutoCLI是一款功能强大的命令行工具,能够从超过60个网站和平台快速获取信息,同时
网页爬虫CLIRetrofit Gson Converter 完全指南:JSON 序列化与反序列化的配置、原理与实战
Retrofit Gson Converter 完全指南:JSON 序列化与反序列化的配置、原理与实战 导读 retrofit converters/gson
网络API设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考