Serialización en Orleans
En términos generales, hay dos tipos de serialización que se utilizan en Orleans:
- Serialización de llamadas de grano: se usa para serializar objetos que se pasan desde y hacia granos.
- Serialización de almacenamiento de granos: se usa para serializar objetivos desde y hacia sistemas de almacenamiento.
La mayoría de este artículo se dedica a la serialización de llamadas de grano a través del marco de serialización que se incluye en Orleans. En la sección Serializadores de almacenamiento de granos, se analiza la serialización de almacenamiento de granos.
Uso de la serialización Orleans
Orleans incluye un marco de serialización avanzado y extensible que se puede denominar Orleans.Serialization. El marco de serialización que se incluye en Orleans está diseñado para cumplir con estos objetivos:
- Alto rendimiento: el serializador está diseñado y optimizado para mejorar el rendimiento. Puede encontrar más detalles en esta presentación.
- Alta fidelidad: el serializador representa fielmente la mayor parte del sistema de tipos .NET, incluida la compatibilidad con polimorfismo paramétrico, polimorfismo, jerarquías heredadas, identidad de objetos y grafos cíclicos. No se admiten punteros, ya que no se pueden portar entre los distintos procesos.
- Flexibilidad: el serializador se puede personalizar para admitir bibliotecas de terceros mediante la creación de suplentes o la delegación en bibliotecas de serialización externas, como System.Text.Json, Newtonsoft.Json y Google.Protobuf.
- Tolerancia a versiones: el serializador permite que los tipos de aplicación evolucionen con el tiempo, lo que admite:
- Incorporación y eliminación de miembros
- Subclasificación
- Ampliación y restricción numéricas (por ejemplo,
int
hacia/desdelong
,float
hacia/desdedouble
) - Cambio de nombre de tipos
La representación de alta fidelidad de tipos es bastante inusual para los serializadores, por lo que algunos puntos requieren una mayor elaboración:
Tipos dinámicos y polimorfismo arbitrario: Orleans no impone ninguna restricción a los tipos que se pueden pasar en llamadas de grano y mantiene la naturaleza dinámica del tipo de datos real. Esto significa, por ejemplo, que si el método de las interfaces de grano se declara para aceptar IDictionary, pero en tiempo de ejecución el remitente pasa SortedDictionary<TKey,TValue>, el receptor obtendrá
SortedDictionary
(aunque el "contrato estático" o la interfaz de grano no especificaron este comportamiento).Mantenimiento de la identidad del objeto: si al mismo objeto se le pasan varios tipos en los argumentos de una llamada de grano o si se apunta a él indirectamente más de una vez desde los argumentos, Orleans lo serializará solo una vez. En el lado receptor, Orleans restaurará todas las referencias correctamente para que dos punteros al mismo objeto sigan apuntando también al mismo objeto después de la deserialización. Es importante conservar la identidad del objeto en escenarios como los siguientes. Imagine que el grano A envía un diccionario con 100 entradas al grano B, y 10 de las claves del diccionario apuntan al mismo objeto,
obj
, en el lado de A. Sin conservar la identidad del objeto, B recibiría un diccionario de 100 entradas con esas 10 claves que apuntan a 10 clones diferentes deobj
. Con la identidad de objeto conservada, el diccionario del lado de B es exactamente igual al lado de A con esas 10 claves que apuntan a un objetoobj
único. Tenga en cuenta que, dado que las implementaciones de código hash de cadena predeterminadas en .NET son aleatorias por proceso, es posible que no se conserve la ordenación de valores en diccionarios y conjuntos hash (por ejemplo).
Para admitir la tolerancia a versiones, el serializador requiere que los desarrolladores sean explícitos sobre qué tipos y miembros se serializan. Hemos intentado hacer esto lo más sencillo posible. Debe marcar todos los tipos serializables con Orleans.GenerateSerializerAttribute para indicar a Orleans que genere código serializador para el tipo. Una vez que haya hecho esto, puede usar la corrección de código incluida para agregar el Orleans.IdAttribute necesario a los miembros serializables en sus tipos, como se muestra aquí:
Este es un ejemplo de un tipo serializable en Orleans que muestra cómo aplicar los atributos.
[GenerateSerializer]
public class Employee
{
[Id(0)]
public string Name { get; set; }
}
Orleans admite la herencia y serializará de manera independiente las capas individuales de la jerarquía, lo que les permitirá tener identificadores de miembros diferentes.
[GenerateSerializer]
public class Publication
{
[Id(0)]
public string Title { get; set; }
}
[GenerateSerializer]
public class Book : Publication
{
[Id(0)]
public string ISBN { get; set; }
}
En el código anterior, tenga en cuenta que tanto Publication
como Book
tienen miembros con [Id(0)]
, aun cuando Book
deriva de Publication
. Este es el procedimiento recomendado en Orleans, porque los identificadores de miembros tienen como ámbito el nivel de herencia y no el tipo en su conjunto. Los miembros se pueden agregar y quitar de Publication
y Book
de manera independiente, pero no se puede insertar una clase base en la jerarquía una vez implementada la aplicación sin una consideración especial.
Orleans también admite la serialización de tipos con internal
, private
, y readonly
miembros, como en este tipo de ejemplo:
[GenerateSerializer]
public struct MyCustomStruct
{
public MyCustom(int intProperty, int intField)
{
IntProperty = intProperty;
_intField = intField;
}
[Id(0)]
public int IntProperty { get; }
[Id(1)] private readonly int _intField;
public int GetIntField() => _intField;
public override string ToString() => $"{nameof(_intField)}: {_intField}, {nameof(IntProperty)}: {IntProperty}";
}
De forma predeterminada, Orleans serializará el tipo codificando su nombre completo. Puede invalidar esto agregando un Orleans.AliasAttribute. Si lo hace, el tipo se serializa con un nombre que es resistente al cambio de nombre de la clase subyacente o al traslado entre ensamblados. Los alias de tipo tienen un ámbito global y no se pueden tener dos alias con el mismo valor en una aplicación. En el caso de los tipos genéricos, el valor de alias debe incluir el número de parámetros genéricos precedidos de una comilla simple, por ejemplo, MyGenericType<T, U>
podría tener el alias [Alias("mytype`2")]
.
Serialización de tipos de record
Los miembros definidos en el constructor principal de un registro tienen identificadores implícitos de forma predeterminada. En otras palabras, Orleans admite la serialización de tipos de record
. Esto significa que no puede cambiar el orden de los parámetros de un tipo ya implementado, ya que esto interrumpe la compatibilidad con versiones anteriores de la aplicación (en el caso de una actualización gradual) y con instancias serializadas de ese tipo en almacenamiento y secuencias. Los miembros definidos en el cuerpo de un tipo de registro no comparten identidades con los parámetros del constructor principal.
[GenerateSerializer]
public record MyRecord(string A, string B)
{
// ID 0 won't clash with A in primary constructor as they don't share identities
[Id(0)]
public string C { get; init; }
}
Si no desea que los parámetros del constructor principal se incluyan automáticamente como campos serializables, puede usar [GenerateSerializer(IncludePrimaryConstructorParameters = false)]
.
Sustitutos para serializar tipos externos
A veces, es posible que tenga que pasar tipos entre granos sobre los que no tiene control total. En estos casos, puede resultar poco práctico convertir manualmente hacia y desde algún tipo definido de forma personalizada en su aplicación. Orleans ofrece una solución para estas situaciones en forma de tipos suplentes. Los suplentes se serializan en lugar de su tipo de destino y tienen funcionalidad para convertir hacia y desde el tipo de destino. Tenga en cuenta el ejemplo siguiente de un tipo externo y su correspondiente suplente y convertidor:
// This is the foreign type, which you do not have control over.
public struct MyForeignLibraryValueType
{
public MyForeignLibraryValueType(int num, string str, DateTimeOffset dto)
{
Num = num;
String = str;
DateTimeOffset = dto;
}
public int Num { get; }
public string String { get; }
public DateTimeOffset DateTimeOffset { get; }
}
// This is the surrogate which will act as a stand-in for the foreign type.
// Surrogates should use plain fields instead of properties for better performance.
[GenerateSerializer]
public struct MyForeignLibraryValueTypeSurrogate
{
[Id(0)]
public int Num;
[Id(1)]
public string String;
[Id(2)]
public DateTimeOffset DateTimeOffset;
}
// This is a converter that converts between the surrogate and the foreign type.
[RegisterConverter]
public sealed class MyForeignLibraryValueTypeSurrogateConverter :
IConverter<MyForeignLibraryValueType, MyForeignLibraryValueTypeSurrogate>
{
public MyForeignLibraryValueType ConvertFromSurrogate(
in MyForeignLibraryValueTypeSurrogate surrogate) =>
new(surrogate.Num, surrogate.String, surrogate.DateTimeOffset);
public MyForeignLibraryValueTypeSurrogate ConvertToSurrogate(
in MyForeignLibraryValueType value) =>
new()
{
Num = value.Num,
String = value.String,
DateTimeOffset = value.DateTimeOffset
};
}
En el código anterior:
MyForeignLibraryValueType
es un tipo fuera del control, definido en una biblioteca de consumo.MyForeignLibraryValueTypeSurrogate
es un tipo suplente que se asigna aMyForeignLibraryValueType
.- RegisterConverterAttribute especifica que
MyForeignLibraryValueTypeSurrogateConverter
actúa como convertidor para asignar a y desde los dos tipos. La clase es una implementación de la interfaz IConverter<TValue,TSurrogate>.
Orleans admite la serialización de tipos en jerarquías de tipos (tipos que derivan de otros tipos). En caso de que un tipo externo aparezca en una jerarquía de tipos (por ejemplo, como la clase base para uno de sus propios tipos), también debe implementar adicionalmente la interfaz Orleans.IPopulator<TValue,TSurrogate>. Considere el ejemplo siguiente:
// The foreign type is not sealed, allowing other types to inherit from it.
public class MyForeignLibraryType
{
public MyForeignLibraryType() { }
public MyForeignLibraryType(int num, string str, DateTimeOffset dto)
{
Num = num;
String = str;
DateTimeOffset = dto;
}
public int Num { get; set; }
public string String { get; set; }
public DateTimeOffset DateTimeOffset { get; set; }
}
// The surrogate is defined as it was in the previous example.
[GenerateSerializer]
public struct MyForeignLibraryTypeSurrogate
{
[Id(0)]
public int Num;
[Id(1)]
public string String;
[Id(2)]
public DateTimeOffset DateTimeOffset;
}
// Implement the IConverter and IPopulator interfaces on the converter.
[RegisterConverter]
public sealed class MyForeignLibraryTypeSurrogateConverter :
IConverter<MyForeignLibraryType, MyForeignLibraryTypeSurrogate>,
IPopulator<MyForeignLibraryType, MyForeignLibraryTypeSurrogate>
{
public MyForeignLibraryType ConvertFromSurrogate(
in MyForeignLibraryTypeSurrogate surrogate) =>
new(surrogate.Num, surrogate.String, surrogate.DateTimeOffset);
public MyForeignLibraryTypeSurrogate ConvertToSurrogate(
in MyForeignLibraryType value) =>
new()
{
Num = value.Num,
String = value.String,
DateTimeOffset = value.DateTimeOffset
};
public void Populate(
in MyForeignLibraryTypeSurrogate surrogate, MyForeignLibraryType value)
{
value.Num = surrogate.Num;
value.String = surrogate.String;
value.DateTimeOffset = surrogate.DateTimeOffset;
}
}
// Application types can inherit from the foreign type, assuming they're not sealed
// since Orleans knows how to serialize it.
[GenerateSerializer]
public sealed class DerivedFromMyForeignLibraryType : MyForeignLibraryType
{
public DerivedFromMyForeignLibraryType() { }
public DerivedFromMyForeignLibraryType(
int intValue, int num, string str, DateTimeOffset dto) : base(num, str, dto)
{
IntValue = intValue;
}
[Id(0)]
public int IntValue { get; set; }
}
Reglas de control de versiones
Se admite la tolerancia a versiones siempre que el desarrollador siga un conjunto de reglas al modificar tipos. Si el desarrollador ha trabajado con sistemas como los búferes de protocolo de Google (Protobuf), estas reglas le resultarán conocidas.
Tipos compuestos (class
& struct
)
- Se admite la herencia, pero no se admite la modificación de la jerarquía de herencia de un objeto. La clase base de una clase no se puede agregar, quitar ni cambiar a otra clase.
- Los tipos de campo no se pueden cambiar, a excepción de algunos tipos numéricos que se describen a continuación en la sección Numéricos.
- Los campos se pueden agregar o quitar en cualquier punto de una jerarquía de herencia.
- No se pueden cambiar los identificadores de campo.
- Los identificadores de campo deben ser únicos para cada nivel de una jerarquía de tipos, pero se pueden reutilizar entre clases base y subclases. Por ejemplo, la clase
Base
puede declarar un campo con identificador0
y un campo diferente se puede declarar medianteSub : Base
con el mismo identificador,0
.
Valores numéricos
- No se puede cambiar el tipo signed/unsigned de un campo numérico.
- Las conversiones entre
int
&uint
no son válidas.
- Las conversiones entre
- Se puede cambiar el ancho de un campo numérico.
- Por ejemplo, se admiten las conversiones de
int
along
o deulong
aushort
. - Las conversiones que limitan el ancho generan excepciones cuando el valor de tiempo de ejecución de un campo provoca un desbordamiento.
- Las conversiones de
ulong
aushort
solo se admiten si el valor en tiempo de ejecución es inferior aushort.MaxValue
. - Las conversiones de
double
afloat
solo se admiten si el valor en tiempo de ejecución está entrefloat.MinValue
tfloat.MaxValue
. - Del mismo modo para
decimal
, que tiene un intervalo más restringido quedouble
yfloat
.
- Las conversiones de
- Por ejemplo, se admiten las conversiones de
Copiadores
Orleans promueve la seguridad de manera predeterminada. Esto incluye la seguridad de algunas clases de errores de simultaneidad. En concreto, Orleans copiará inmediatamente los objetos pasados en las llamadas de grano de manera predeterminada. Orleans.Serialization facilita esta copia y, cuando Orleans.CodeGeneration.GenerateSerializerAttribute se aplica a un tipo, Orleans también generará copiadores para ese tipo. Orleans evitará copiar tipos o miembros individuales marcados con ImmutableAttribute. Para más detalles, consulte Serialización de tipos inmutables en Orleans.
Procedimientos recomendados de serialización
✅Asigne los alias de tipos mediante el atributo
[Alias("my-type")]
. Los tipos con alias se pueden renombrar sin interrumpir la compatibilidad.❌No cambie un valor
record
a un valorclass
normal o viceversa. Los registros y las clases no se representan de forma idéntica, ya que los registros tienen miembros constructores principales además de miembros normales y, por lo tanto, ambos no son intercambiables.❌No agregue nuevos tipos a una jerarquía de tipos existente para un tipo serializable. No debe agregar una nueva clase base a un tipo existente. Puede agregar de forma segura una nueva subclase a un tipo existente.
✅Reemplace las utilizaciones de SerializableAttribute por GenerateSerializerAttribute y las declaraciones IdAttribute correspondientes.
✅Inicie todos los identificadores de miembro en cero para cada tipo. Los identificadores de una subclase y de su clase base se pueden superponer de forma segura. Ambas propiedades del ejemplo siguiente tienen identificadores iguales a
0
.[GenerateSerializer] public sealed class MyBaseClass { [Id(0)] public int MyBaseInt { get; set; } } [GenerateSerializer] public sealed class MySubClass : MyBaseClass { [Id(0)] public int MyBaseInt { get; set; } }
✅Amplíe los tipos de miembros numéricos según sea necesario. Puede ampliar
sbyte
ashort
aint
along
.- Puede restringir los tipos de miembros numéricos, pero dará lugar a una excepción en tiempo de ejecución si los valores observados no se pueden representar correctamente mediante el tipo restringido. Por ejemplo,
int.MaxValue
no se puede representar mediante un camposhort
, por lo que restringir un campoint
ashort
puede dar lugar a una excepción en tiempo de ejecución si se encontró ese valor.
- Puede restringir los tipos de miembros numéricos, pero dará lugar a una excepción en tiempo de ejecución si los valores observados no se pueden representar correctamente mediante el tipo restringido. Por ejemplo,
❌No cambie el signo de un miembro de tipo numérico. No debe cambiar el tipo de un miembro de
uint
aint
o deint
auint
, por ejemplo.
Serializadores de almacenamiento de granos
Orleans incluye un modelo de persistencia respaldado por el proveedor para los granos, al que se accede a través de la propiedad State o insertando uno o varios valores IPersistentState<TState> en el grano. Antes de Orleans 7.0, cada proveedor tenía un mecanismo diferente para configurar la serialización. Ahora, en Orleans 7.0, hay una interfaz de serializador de estado de uso general, IGrainStorageSerializer, que ofrece una manera coherente de personalizar la serialización de estado para cada proveedor. Los proveedores de almacenamiento admitidos implementan un patrón que implica establecer la propiedad IStorageProviderSerializerOptions.GrainStorageSerializer en la clase de opciones del proveedor, por ejemplo:
- DynamoDBStorageOptions.GrainStorageSerializer
- AzureBlobStorageOptions.GrainStorageSerializer
- AzureTableStorageOptions.GrainStorageSerializer
- GrainStorageSerializer
Actualmente, la serialización de almacenamiento de granos tiene Newtonsoft.Json
como valor predeterminado para serializar el estado. Puede reemplazarlo modificando esa propiedad en el momento de la configuración. En el ejemplo siguiente se muestra esto con OptionsBuilder<TOptions>:
siloBuilder.AddAzureBlobGrainStorage(
"MyGrainStorage",
(OptionsBuilder<AzureBlobStorageOptions> optionsBuilder) =>
{
optionsBuilder.Configure<IMySerializer>(
(options, serializer) => options.GrainStorageSerializer = serializer);
});
Para obtener más información, consulte OptionsBuilder API.
Orleans tiene un marco de serialización avanzado y extensible. Orleans serializa los tipos de datos que se pasan en los mensajes de solicitud y respuesta de los granos, así como los objetos de estado persistente de los granos. Como parte de este marco, Orleans genera automáticamente código de serialización para estos tipos de datos. Además de generar una serialización y deserialización más eficaces para los tipos que ya son serializables en .NET, Orleans también intenta generar serializadores para los tipos que se usan en interfaces de grano que no son serializables en .NET. El marco también incluye un conjunto de serializadores integrados eficaces para tipos que se usan con frecuencia: listas, diccionarios, cadenas, primitivos, matrices, etc.
Dos características importantes del serializador de Orleans lo diferencian de muchos otros marcos de serialización de terceros: los tipos dinámicos y el polimorfismo arbitrario, por un lado, y la identidad de objeto, por otro.
Tipos dinámicos y polimorfismo arbitrario: Orleans no impone ninguna restricción a los tipos que se pueden pasar en llamadas de grano y mantiene la naturaleza dinámica del tipo de datos real. Esto significa, por ejemplo, que si el método de las interfaces de grano se declara para aceptar IDictionary, pero en tiempo de ejecución el remitente pasa SortedDictionary<TKey,TValue>, el receptor obtendrá
SortedDictionary
(aunque el "contrato estático" o la interfaz de grano no especificaron este comportamiento).Mantenimiento de la identidad del objeto: si al mismo objeto se le pasan varios tipos en los argumentos de una llamada de grano o si se apunta a él indirectamente más de una vez desde los argumentos, Orleans lo serializará solo una vez. En el lado receptor, Orleans restaurará todas las referencias correctamente para que dos punteros al mismo objeto sigan apuntando también al mismo objeto después de la deserialización. Es importante conservar la identidad del objeto en escenarios como los siguientes. Imagine que el grano A envía un diccionario con 100 entradas al grano B, y 10 de las claves del diccionario apuntan al mismo objeto, obj, en el lado de A. Si no se conserva la identidad del objeto, B recibiría un diccionario con 100 entradas en el que esas 10 claves apuntan a 10 clones diferentes de "obj". Si se conserva la identidad del objeto, el diccionario del lado de B tiene un aspecto idéntico al del lado de A, y esas 10 claves apuntan a un solo objeto "obj".
El serializador binario estándar .NET proporciona los dos comportamientos anteriores y, por este motivo, era importante para nosotros admitir también en Orleans este comportamiento estándar y familiar.
Serializadores generados
Orleans usa las reglas siguientes para decidir qué serializadores se generan. Las reglas son:
- Examine todos los tipos de todos los ensamblados que hacen referencia a la biblioteca principal de Orleans.
- Céntrese en estos ensamblados y genere serializadores para tipos a los que se haga referencia directamente en signaturas de método de interfaz de grano o signaturas de clase de estado o para cualquier tipo marcado como SerializableAttribute.
- Además, una interfaz de grano o un proyecto de implementación pueden apuntar a tipos arbitrarios para la generación de serialización mediante la adición de atributos de nivel de ensamblado KnownTypeAttribute o KnownAssemblyAttribute, con el fin de indicar al generador de código que genere serializadores para tipos específicos o para todos los tipos aptos de un ensamblado. Para obtener más información sobre los atributos de nivel de ensamblado, consulte Aplicación de atributos en el nivel de ensamblado.
Serialización de reserva
Orleans admite la transmisión de tipos arbitrarios en tiempo de ejecución y, por lo tanto, el generador de código integrado no puede determinar con antelación el conjunto completo de tipos que se transmitirán. Además, no se pueden generar serializadores para algunos tipos porque no son accesibles (por ejemplo, private
) o tienen campos inaccesibles (por ejemplo, readonly
). Por lo tanto, se requiere una serialización Just-In-Time de los tipos que eran inesperados o para los que no se podían generar serializadores con antelación. El serializador responsable de estos tipos se denomina serializador de reserva. Orleans incluye dos serializadores de reserva:
- Orleans.Serialization.BinaryFormatterSerializer, que usa BinaryFormatter de .NET; y
- Orleans.Serialization.ILBasedSerializer, que emite instrucciones CIL en tiempo de ejecución a fin de crear serializadores que aprovechan el marco de serialización de Orleans para serializar cada campo. Esto significa que, si un tipo inaccesible
MyPrivateType
contiene un campoMyType
que tiene un serializador personalizado, dicho serializador personalizado se usará para serializarlo.
El serializador de reserva puede configurarse mediante la propiedad FallbackSerializationProvider tanto en ClientConfiguration en el cliente como en GlobalConfiguration en los silos.
// Client configuration
var clientConfiguration = new ClientConfiguration();
clientConfiguration.FallbackSerializationProvider =
typeof(FantasticSerializer).GetTypeInfo();
// Global configuration
var globalConfiguration = new GlobalConfiguration();
globalConfiguration.FallbackSerializationProvider =
typeof(FantasticSerializer).GetTypeInfo();
Como alternativa, el proveedor de serialización de reserva puede especificarse en la configuración XML:
<Messaging>
<FallbackSerializationProvider
Type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
</Messaging>
BinaryFormatterSerializer es el serializador de reserva predeterminado.
Advertencia
La serialización binaria con BinaryFormatter
puede ser peligrosa. Para más información, consulte la Guía de seguridad de BinaryFormatter y la Guía de migración de BinaryFormatter.
Serialización de excepciones
Las excepciones se serializan mediante el serializador de reserva. Con la configuración predeterminada, BinaryFormatter
es el serializador de reserva y, por tanto, debe seguirse el patrón ISerializable para garantizar la serialización correcta de todas las propiedades en un tipo de excepción.
Este es un ejemplo de un tipo de excepción con la serialización implementada correctamente:
[Serializable]
public class MyCustomException : Exception
{
public string MyProperty { get; }
public MyCustomException(string myProperty, string message)
: base(message)
{
MyProperty = myProperty;
}
public MyCustomException(string transactionId, string message, Exception innerException)
: base(message, innerException)
{
MyProperty = transactionId;
}
// Note: This is the constructor called by BinaryFormatter during deserialization
public MyCustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
MyProperty = info.GetString(nameof(MyProperty));
}
// Note: This method is called by BinaryFormatter during serialization
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(MyProperty), MyProperty);
}
}
Procedimientos recomendados de serialización
La serialización sirve para dos propósitos principales en Orleans:
- Como formato de conexión para transmitir datos entre granos y clientes en tiempo de ejecución.
- Como formato de almacenamiento para conservar datos de larga duración para su recuperación posterior.
Los serializadores que genera Orleans son adecuados para el primer propósito debido a su flexibilidad, rendimiento y versatilidad. No son tan adecuados para el segundo propósito, ya que no son explícitamente tolerantes a versiones. Se recomienda que los usuarios configuren un serializador tolerante a versiones, como Protocol Buffers para datos persistentes. Los búferes de protocolo son compatibles a través de Orleans.Serialization.ProtobufSerializer
desde el paquete NuGet Microsoft.Orleans.OrleansGoogleUtils. Deben seguirse los procedimientos recomendados del serializador concreto que se elija a fin de garantizar la tolerancia a versiones. Los serializadores de terceros pueden configurarse mediante la propiedad de configuración SerializationProviders
como se ha descrito anteriormente.