Serialization configuration in Orleans
Serialization configuration in Orleans is a crucial part of the overall system design. While Orleans provides reasonable defaults, you can configure serialization to suit your app’s needs. For sending data between hosts, Orleans.Serialization supports delegating to other serializers, such as Newtonsoft.Json and System.Text.Json. You can add support for other serializers by following the pattern set by those implementations. For grain storage, it’s best to use IGrainStorageSerializer to configure a custom serializer.
Configure Orleans to use Newtonsoft.Json
Section titled “Configure Orleans to use Newtonsoft.Json”To configure Orleans to serialize certain types using Newtonsoft.Json, first reference the Microsoft.Orleans.Serialization.NewtonsoftJson NuGet package. Then, configure the serializer, specifying which types it will be responsible for. In the following example, we specify that the Newtonsoft.Json serializer is responsible for all types in the Example.Namespace namespace.
siloBuilder.Services.AddSerializer(serializerBuilder =>{ serializerBuilder.AddNewtonsoftJsonSerializer( isSupported: type => type.Namespace.StartsWith("Example.Namespace"));});In the preceding example, the call to AddNewtonsoftJsonSerializer adds support for serializing and deserializing values using Newtonsoft.Json.JsonSerializer. You must perform similar configuration on all clients that need to handle those types.
For types marked with GenerateSerializerAttribute, Orleans prefers the generated serializer over the Newtonsoft.Json serializer.
Configure Orleans to use System.Text.Json
Section titled “Configure Orleans to use System.Text.Json”Alternatively, to configure Orleans to use System.Text.Json to serialize your types, reference the Microsoft.Orleans.Serialization.SystemTextJson NuGet package. Then, configure the serializer, specifying which types it will be responsible for. In the following example, we specify that the System.Text.Json serializer is responsible for all types in the Example.Namespace namespace.
- Install the Microsoft.Orleans.Serialization.SystemTextJson NuGet package.
- Configure the serializer using the AddJsonSerializer method.
Consider the following example when interacting with the ISiloBuilder:
siloBuilder.Services.AddSerializer(serializerBuilder =>{ serializerBuilder.AddJsonSerializer( isSupported: type => type.Namespace.StartsWith("Example.Namespace"));});Authorize type-name resolution
Section titled “Authorize type-name resolution”Registering an external serializer selects which codec can handle a value. It doesn’t, by itself, authorize Orleans to resolve every CLR type name accepted by that serializer. Type-name resolution is a separate security boundary, and TypeManifestOptions.AllowAllTypes defaults to false.
This distinction is especially visible for polymorphic signatures such as IReadOnlyList<TriggerRule>, where TriggerRule is abstract and values are handled by System.Text.Json. Register the JSON serializer and explicitly trust the application type:
siloBuilder.Services.AddSerializer(serializerBuilder =>{ serializerBuilder.AddJsonSerializer( isSupported: type => type.Namespace?.StartsWith("MyApp") == true); serializerBuilder.Configure(options => options.AddAllowedType(typeof(TriggerRule)));});AddAllowedType uses Orleans’ runtime type-name formatter, including for constructed and nested generic types. The AllowedTypes string set remains supported for compatibility and contains Orleans-formatted runtime type names. Prefer AddAllowedType instead of constructing those names manually.
If every type in an application assembly is trusted, allow the assembly instead:
siloBuilder.Services.AddSerializer(serializerBuilder =>{ serializerBuilder.Configure(options => options.AddAllowedAssembly(typeof(TriggerRule).Assembly));});Assembly trust applies component by component. Allowing a generic type definition’s assembly doesn’t implicitly trust generic arguments from other assemblies.
For policy-based trust, register ITypeNameFilter to evaluate names before Orleans loads the corresponding type:
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; }}Register the filter with dependency injection:
siloBuilder.Services.AddSingleton<ITypeNameFilter, ApplicationTypeNameFilter>();A filter returns true to allow, false to deny, or null when it has no opinion. Types explicitly added to AllowedTypes are authoritative. For other names, a denial from any ITypeNameFilter takes precedence over other type-name filters and assembly trust. ITypeFilter provides a resolved-Type fallback when name-based checks have no affirmative result; a denial wins within that fallback. Both formatting and parsing apply these checks, including to constructed generic components and array element types.
As a compatibility escape hatch, you can disable the boundary:
siloBuilder.Services.AddSerializer(serializerBuilder =>{ serializerBuilder.Configure((TypeManifestOptions options) => options.AllowAllTypes = true);});For the full trust-boundary and data-validation guidance, see serialization security.
