Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En términos generales, hay dos tipos de serialización que se utilizan en Orleans:
- Serialización de llamadas a granos: se utiliza para serializar objetos pasados hacia y desde los granos.
- Serialización de almacenamiento específico: se usa para serializar objetos hacia y desde sistemas de almacenamiento.
La mayoría de este artículo se centra en la serialización de llamadas de granos a través del marco de serialización incluido en Orleans. En la sección Serializadores de almacenamiento de grano se describe la serialización de almacenamiento de grano.
Uso de la serialización Orleans
Orleans incluye un marco de serialización avanzado y extensible denominado Orleans. Serialización. 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 el rendimiento. Puede encontrar más detalles en esta presentación.
- Alta fidelidad: el serializador representa fielmente la mayoría del sistema de tipos de .NET, incluida la compatibilidad con genéricos, polimorfismo, jerarquías de herencia, identidad de objeto y grafos cíclicos. No se admiten punteros, ya que no son portátiles entre procesos.
- Flexibilidad: puede personalizar el serializador 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 de versiones: el serializador permite que los tipos de aplicación evolucionen con el tiempo, admitiendo:
- Incorporación y eliminación de miembros
- Creación de subclases
- Ampliación numérica y restricción (por ejemplo,
inthacia/delong,floata /desdedouble) - Cambio de nombre de tipos
La representación de alta fidelidad de los tipos es bastante poco común para los serializadores, por lo que algunos puntos requieren más explicación.
Tipos dinámicos y polimorfismo arbitrario: Orleans no aplica restricciones en los tipos pasados en llamadas de grano y mantiene la naturaleza dinámica del tipo de datos real. Esto significa, por ejemplo, que si se declara un método en una interfaz de grano para aceptar IDictionary, pero en tiempo de ejecución el remitente pasa un SortedDictionary<TKey,TValue>, el receptor efectivamente recibe un
SortedDictionary(aunque la interfaz de grano/el "contrato estático" no especificó este comportamiento).Mantener la identidad del objeto: si el mismo objeto se pasa varias veces en los argumentos de una llamada de grano o apunta indirectamente a más de una vez desde los argumentos, Orleans lo serializa solo una vez. En el lado receptor, Orleans restaura todas las referencias correctamente para que dos punteros al mismo objeto sigan apuntando al mismo objeto después de la deserialización. Conservar la identidad de los objetos es importante en escenarios como los siguientes: Imagínese que el grain A envía un diccionario con 100 entradas a grain B, y 10 claves en el 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 similar al de A, con esas 10 claves que apuntan a un único objetoobj. 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 sea explícito sobre qué tipos y miembros se serializan. Hemos intentado hacer esto tan indoloro como sea posible. Marque todos los tipos serializables con Orleans.GenerateSerializerAttribute para indicar Orleans que genere código de serializador para el tipo. Una vez hecho esto, puede usar la corrección de código incluida para añadir el Orleans.IdAttribute requerido a los miembros serializables de 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 serializa las capas individuales de la jerarquía por separado, lo que les permite tener identificadores de miembro distintos.
[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)], aunque Book deriva de Publication. Esta es la práctica recomendada en Orleans porque los identificadores de miembro se limitan al nivel de herencia, no al tipo en su conjunto. Puede agregar y quitar miembros de Publication y Book de forma independiente, pero no puede insertar una nueva clase base en la jerarquía una vez que la aplicación esté implementada sin requerir consideraciones especiales.
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 serializa el tipo mediante la codificación de su nombre completo. Puede invalidar esto agregando un Orleans.AliasAttribute. Al hacerlo, se logra que el tipo se serialice utilizando un nombre resistente al cambio de nombre de la clase subyacente o al moverla 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 por una comilla invertida; 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 se 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 un escenario de actualización gradual) y con instancias serializadas de ese tipo en el almacenamiento y las 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, use [GenerateSerializer(IncludePrimaryConstructorParameters = false)].
Sustitutos para serializar tipos externos
A veces, es posible que tengas que pasar tipos entre granos sobre los que no tienes control total. En estos casos, la conversión manual desde y hacia un tipo definido por el usuario en el código de la aplicación podría ser poco práctica. Orleans ofrece una solución para estas situaciones: 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:
-
MyForeignLibraryValueTypees un tipo fuera de tu control, definido en una biblioteca consumidora. -
MyForeignLibraryValueTypeSurrogatees un mapeo de tipo sustituto aMyForeignLibraryValueType. -
RegisterConverterAttribute especifica que
MyForeignLibraryValueTypeSurrogateConverteractúa como convertidor para realizar una correspondencia entre los dos tipos. La clase implementa la IConverter<TValue,TSurrogate> interfaz .
Orleans admite la serialización de tipos en jerarquías de tipos (tipos derivados de otros tipos). Si un tipo externo puede aparecer en una jerarquía de tipos (por ejemplo, como clase base para uno de sus propios tipos), debe implementar además la Orleans.IPopulator<TValue,TSurrogate> interfaz . 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 de versiones siempre que siga un conjunto de reglas al modificar tipos. Si está familiarizado con sistemas como búferes de protocolo de Google (Protobuf), estas reglas será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. No se puede agregar, cambiar ni quitar la clase base de una clase.
- A excepción de algunos tipos numéricos descritos en la sección Numerics siguiente, no se pueden cambiar los tipos de campo.
- Puede agregar o quitar campos 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, una
Baseclase puede declarar un campo con el identificador0y unaSub : Baseclase puede declarar un campo diferente con el mismo identificador,0.
Valores numéricos
- No se puede cambiar el signo de un campo numérico.
- Las conversiones entre
int&uintno son válidas.
- Las conversiones entre
- Puede cambiar el ancho de un campo numérico.
- Por ejemplo, se admiten conversiones de
inta olongulongaushort. - Las conversiones que limitan el ancho producen una excepción si el valor en tiempo de ejecución del campo provoca un desbordamiento.
- La conversión de
ulongaushortsolo se admite si el valor en el tiempo de ejecución es menor queushort.MaxValue. - Las conversiones de
doubleafloatsolo se admiten si el valor en tiempo de ejecución está entrefloat.MinValuetfloat.MaxValue. - Del mismo modo para
decimal, que tiene un intervalo más restringido quedoubleyfloat.
- Por ejemplo, se admiten conversiones de
Copiadores
Orleans promueve la seguridad de forma predeterminada, incluida la seguridad de algunas clases de errores de simultaneidad. En concreto, Orleans copia inmediatamente los objetos pasados en llamadas de grano de forma predeterminada. Orleans. La serialización facilita esta copia. Cuando se aplica Orleans.CodeGeneration.GenerateSerializerAttribute a un tipo, Orleans también genera copiadores para ese tipo. Orleans evita 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
recorda un valorclassnormal o viceversa. Los registros y las clases no se representan de forma idéntica, ya que los registros tienen miembros de constructor primario además de miembros regulares; por lo tanto, los dos 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.
✅ Asegúrese de iniciar todos los identificadores de miembro en cero para cada tipo. Los identificadores de una subclase y su clase base pueden superponerse 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
sbyteashortaintalong.- Puede restringir los tipos de miembros numéricos, pero da como resultado una excepción en tiempo de ejecución si los valores observados no se pueden representar correctamente mediante el tipo restringido. Por ejemplo,
int.MaxValueno se puede representar mediante un camposhort, por lo que restringir un campointashortpuede causar una excepción en tiempo de ejecución si se encuentra ese valor.
- Puede restringir los tipos de miembros numéricos, pero da como resultado 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
uintaintointauint, 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. En Orleans la versión 7.0, ahora hay una interfaz de serializador de estado de propósito 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 IStorageProviderSerializerOptions.GrainStorageSerializer propiedad 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 este uso de 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 pasados en mensajes de solicitud y respuesta de grains, así como en objetos de estado persistente de grains. 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/deserialización más eficiente para los tipos que ya son .NET serializable, Orleans también intenta generar serializadores para tipos utilizados en interfaces de grain que no son .NET serializable. 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 Orleansserializador lo diferencian de muchos otros marcos de serialización de terceros: tipos dinámicos/polimorfismo arbitrario e identidad de objeto.
Tipos dinámicos y polimorfismo arbitrario: Orleans no aplica restricciones en los tipos pasados en llamadas de grano y mantiene la naturaleza dinámica del tipo de datos real. Esto significa, por ejemplo, que si se declara un método en una interfaz de grano para aceptar IDictionary, pero en tiempo de ejecución el remitente pasa un SortedDictionary<TKey,TValue>, el receptor efectivamente recibe un
SortedDictionary(aunque la interfaz de grano/el "contrato estático" no especificó este comportamiento).Mantener la identidad del objeto: si el mismo objeto se pasa varias veces en los argumentos de una llamada de grano o apunta indirectamente a más de una vez desde los argumentos, Orleans lo serializa solo una vez. En el lado receptor, Orleans restaura todas las referencias correctamente para que dos punteros al mismo objeto sigan apuntando al mismo objeto después de la deserialización. Conservar la identidad de los objetos es importante en escenarios como los siguientes: Imagínese que el grain A envía un diccionario con 100 entradas a grain B, y 10 claves en el 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 similar al de A, con esas 10 claves que apuntan a un único objetoobj.
El serializador binario estándar de .NET proporciona los dos comportamientos anteriores, por lo que era importante que admitamos también este comportamiento Orleans estándar y familiar.
Serializadores generados
Orleans utiliza las siguientes reglas para decidir qué serializadores generar.
- Escanear todos los tipos en todos los ensamblados que hacen referencia a la biblioteca principal Orleans.
- A partir de esos ensamblados, genere serializadores para tipos a los que se hace referencia directamente en firmas de método de interfaz de grano o firmas de clase de estado, o para cualquier tipo marcado con SerializableAttribute.
- Además, un proyecto de implementación o interfaz de granos puede apuntar a tipos arbitrarios para la generación de serialización mediante la adición de atributos a nivel de ensamblado KnownTypeAttribute o KnownAssemblyAttribute. Estos indican al generador de código que genere serializadores para tipos específicos o para todos los tipos aptos dentro 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. Por lo tanto, el generador de código integrado no puede determinar todo el conjunto de tipos que se transmitirán con antelación. Además, algunos tipos no pueden tener serializadores generados para ellos porque son inaccesibles (por ejemplo, private) o tienen campos inaccesibles (por ejemplo, readonly). Por lo tanto, hay una necesidad de serialización justo a tiempo de tipos inesperados o que no pudieron tener serializadores generados 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
MyPrivateTypecontiene un campoMyTypeque tiene un serializador personalizado, dicho serializador personalizado se usará para serializarlo.
Configure el serializador de reserva usando la propiedad FallbackSerializationProvider en ClientConfiguration (cliente) y GlobalConfiguration (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, especifique el proveedor de serialización de respaldo 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. Por lo tanto, debe seguir el patrón ISerializable para garantizar la serialización correcta de todas las propiedades de 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 tolerantes explícitamente a versiones. Se recomienda configurar un serializador tolerante a versiones, como búferes de protocolo, para datos persistentes. Los búferes de protocolo son compatibles a través de Orleans.Serialization.ProtobufSerializer desde el paquete NuGet Microsoft.Orleans.OrleansGoogleUtils. Siga los procedimientos recomendados para el serializador elegido para garantizar la tolerancia a las versiones. Configurar serializadores de terceros utilizando la propiedad de configuración SerializationProviders tal como se describe más arriba.