Serialização em Orleans
Existem basicamente dois tipos de serialização usados em Orleans:
- Serialização de chamada de grão - usada para serializar objetos passados de e para grãos.
- Serialização de armazenamento de grãos - usada para serializar objetos de e para sistemas de armazenamento.
A maioria deste artigo é dedicada à serialização de chamadas de grão por meio da estrutura de serialização incluída no Orleans. A seção Serializadores de armazenamento de grãos discute a serialização de armazenamento de grãos .
Usar Orleans serialização
Orleans Inclui uma estrutura de serialização avançada e extensível que pode ser referida como Orleans. Serialização. A estrutura de serialização incluída foi Orleans projetada para atender às seguintes metas:
- Alto desempenho - O serializador é projetado e otimizado para desempenho. Mais detalhes estão disponíveis nesta apresentação.
- Alta fidelidade - O serializador representa fielmente a maioria dos . Sistema de tipos NET, incluindo suporte para genéricos, polimorfismo, hierarquias de herança, identidade de objeto e gráficos cíclicos. Os ponteiros não são suportados, uma vez que não são portáteis entre processos.
- Flexibilidade - O serializador pode ser personalizado para suportar bibliotecas de terceiros criando substitutos ou delegando a bibliotecas de serialização externas, como System.Text.Json, Newtonsoft.Json e Google.Protobuf.
- Tolerância de versão - O serializador permite que os tipos de aplicativos evoluam ao longo do tempo, suportando:
- Adicionar e remover membros
- Subclassificação
- Alargamento e estreitamento numéricos (por exemplo:
int
de/long
para,float
para/dedouble
) - Renomeando tipos
A representação de tipos de alta fidelidade é bastante incomum para serializadores, então alguns pontos merecem mais elaboração:
Tipos dinâmicos e polimorfismo arbitrário: Orleans não impõe restrições aos tipos que podem ser passados em chamadas de grão e mantém a natureza dinâmica do tipo de dados real. Isso significa, por exemplo, que se o método nas interfaces de grão for declarado para aceitar IDictionary , mas em tempo de execução, o remetente passa SortedDictionary<TKey,TValue>, o recetor realmente obterá
SortedDictionary
(embora o "contrato estático"/interface de grão não tenha especificado esse comportamento).Mantendo a identidade do objeto: Se o mesmo objeto for passado vários tipos nos argumentos de uma chamada de grão ou for apontado indiretamente mais de uma vez a partir dos argumentos, Orleans serializá-lo-á apenas uma vez. No lado do recetor, Orleans irá restaurar todas as referências corretamente para que dois ponteiros para o mesmo objeto ainda apontem para o mesmo objeto após a desserialização também. A identidade do objeto é importante preservar em cenários como os seguintes. Imagine que o grão A está enviando um dicionário com 100 entradas para o grão B, e 10 das chaves no dicionário apontam para o mesmo objeto,
obj
do lado de A. Sem preservar a identidade do objeto, B receberia um dicionário de 100 entradas com essas 10 chaves apontando para 10 clones diferentes deobj
. Com a identidade do objeto preservada, o dicionário do lado de B se parece exatamente com o lado de A, com essas 10 teclas apontando para um único objetoobj
. Observe que, como as implementações de código hash de cadeia de caracteres padrão no .NET são aleatórias por processo, a ordenação de valores em dicionários e conjuntos de hash (por exemplo) pode não ser preservada.
Para oferecer suporte à tolerância de versão, o serializador requer que os desenvolvedores sejam explícitos sobre quais tipos e membros são serializados. Tentámos que isto fosse o mais indolor possível. Você deve marcar todos os tipos serializáveis com Orleans.GenerateSerializerAttribute para instruir Orleans a gerar código serializador para seu tipo. Depois de fazer isso, você pode usar a correção de código incluída para adicionar o necessário Orleans.IdAttribute aos membros serializáveis em seus tipos, conforme demonstrado aqui:
Aqui está um exemplo de um tipo serializável em Orleans, demonstrando como aplicar os atributos.
[GenerateSerializer]
public class Employee
{
[Id(0)]
public string Name { get; set; }
}
Orleans Suporta herança e serializará as camadas individuais na hierarquia separadamente, permitindo que elas tenham IDs de membro distintas.
[GenerateSerializer]
public class Publication
{
[Id(0)]
public string Title { get; set; }
}
[GenerateSerializer]
public class Book : Publication
{
[Id(0)]
public string ISBN { get; set; }
}
No código anterior, observe que ambos Publication
e Book
têm membros com [Id(0)]
mesmo que Book
deriva de Publication
. Essa é a prática recomendada porque Orleans os identificadores de membros têm como escopo o nível de herança, não o tipo como um todo. Os membros podem ser adicionados e removidos de forma Book
independente, mas uma nova classe base não pode ser inserida na hierarquia depois que o aplicativo tiver sido implantado Publication
sem consideração especial.
Orleans também suporta tipos de serialização com internal
, private
e readonly
membros, como neste tipo de exemplo:
[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}";
}
Por padrão, Orleans serializará seu tipo codificando seu nome completo. Você pode substituir isso adicionando um Orleans.AliasAttributearquivo . Isso resultará em seu tipo sendo serializado usando um nome que seja resiliente para renomear a classe subjacente ou movê-la entre assemblies. Os aliases de tipo têm escopo global e você não pode ter dois aliases com o mesmo valor em um aplicativo. Para tipos genéricos, o valor de alias deve incluir o número de parâmetros genéricos precedidos por um backtick, por exemplo, MyGenericType<T, U>
pode ter o alias [Alias("mytype`2")]
.
Tipos de serialização record
Os membros definidos no construtor primário de um registro têm ids implícitas por padrão. Em outras palavras, Orleans suporta tipos de serialização record
. Isso significa que não é possível alterar a ordem dos parâmetros para um tipo já implantado, uma vez que isso quebra a compatibilidade com versões anteriores do seu aplicativo (no caso de uma atualização contínua) e com instâncias serializadas desse tipo em armazenamento e fluxos. Os membros definidos no corpo de um tipo de registro não compartilham identidades com os parâmetros primários do construtor.
[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; }
}
Se você não quiser que os parâmetros primários do construtor sejam incluídos automaticamente como campos serializáveis, você pode usar [GenerateSerializer(IncludePrimaryConstructorParameters = false)]
o .
Substitutos para serialização de tipos estrangeiros
Às vezes, você pode precisar passar tipos entre grãos sobre os quais você não tem controle total. Nesses casos, pode ser impraticável converter de e para algum tipo personalizado no código do aplicativo manualmente. Orleans oferece uma solução para estas situações sob a forma de tipos substitutos. Os substitutos são serializados no lugar de seu tipo de destino e têm funcionalidade para converter de e para o tipo de destino. Considere o seguinte exemplo de um tipo estrangeiro e um substituto e conversor correspondentes:
// 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
};
}
No código anterior:
- O
MyForeignLibraryValueType
é um tipo fora do seu controle, definido em uma biblioteca de consumo. - O
MyForeignLibraryValueTypeSurrogate
é um tipo substituto que mapeia paraMyForeignLibraryValueType
. - O RegisterConverterAttribute especifica que o
MyForeignLibraryValueTypeSurrogateConverter
atua como um conversor para mapear de e para os dois tipos. A classe é uma implementação da IConverter<TValue,TSurrogate> interface.
Orleans suporta a serialização de tipos em hierarquias de tipo (tipos que derivam de outros tipos). No caso de um tipo estrangeiro aparecer em uma hierarquia de tipo (por exemplo, como a classe base para um de seus próprios tipos), você deve implementar adicionalmente a Orleans.IPopulator<TValue,TSurrogate> interface. Considere o seguinte exemplo:
// 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; }
}
Regras de controle de versão
A tolerância de versão é suportada, desde que o desenvolvedor siga um conjunto de regras ao modificar tipos. Se o desenvolvedor estiver familiarizado com sistemas como o Google Protocol Buffers (Protobuf), essas regras estarão familiarizadas.
Tipos compostos (class
& struct
)
- Há suporte para herança, mas não há suporte para modificar a hierarquia de herança de um objeto. A classe base de uma classe não pode ser adicionada, alterada para outra classe ou removida.
- Com exceção de alguns tipos numéricos, descritos na seção Numéricos abaixo, os tipos de campo não podem ser alterados.
- Os campos podem ser adicionados ou removidos a qualquer momento de uma hierarquia de herança.
- As ids de campo não podem ser alteradas.
- As ids de campo devem ser exclusivas para cada nível em uma hierarquia de tipo, mas podem ser reutilizadas entre classes base e subclasses. Por exemplo,
Base
classe pode declarar um campo com id0
e um campo diferente pode ser declarado porSub : Base
com o mesmo id,0
.
Numéricos
- A assinatura de um campo numérico não pode ser alterada.
- As conversões entre
int
&uint
são inválidas.
- As conversões entre
- A largura de um campo numérico pode ser alterada.
- Por exemplo: conversões de
int
paralong
ouulong
paraushort
são suportadas. - As conversões que reduzem a largura serão lançadas se o valor de tempo de execução de um campo causar um estouro.
- A conversão de para
ushort
só é suportada se o valor em tempo deulong
execução for menor queushort.MaxValue
. - As conversões de para
float
só são suportadas se o valor de tempo dedouble
execução estiver entrefloat.MinValue
efloat.MaxValue
. - Da mesma forma para
decimal
, que tem um intervalo mais estreito do que ambos edouble
float
.
- A conversão de para
- Por exemplo: conversões de
Copiadoras
Orleans promove a segurança por defeito. Isso inclui a segurança de algumas classes de bugs de simultaneidade. Em particular, Orleans copiará imediatamente objetos passados em chamadas de grão por padrão. Esta cópia é facilitada pelo Orleans. A serialização e, quando Orleans.CodeGeneration.GenerateSerializerAttribute aplicada a um tipo, Orleans também gerará copiadoras para esse tipo. Orleans evitará copiar tipos ou membros individuais marcados usando o ImmutableAttribute. Para obter mais detalhes, consulte Serialização de tipos imutáveis em Orleans.
Práticas recomendadas de serialização
✅Dê aos seus tipos aliases usando o
[Alias("my-type")]
atributo. Os tipos com aliases podem ser renomeados sem quebrar a compatibilidade.❌Não mude a
record
para regularclass
ou vice-versa. Registros e classes não são representados de maneira idêntica, uma vez que os registros têm membros construtores primários, além de membros regulares e, portanto, os dois não são intercambiáveis.❌Não adicione novos tipos a uma hierarquia de tipos existente para um tipo serializável. Você não deve adicionar uma nova classe base a um tipo existente. Você pode adicionar com segurança uma nova subclasse a um tipo existente.
✅Substitua os usos de SerializableAttribute por GenerateSerializerAttribute e declarações correspondentes IdAttribute .
✅Inicie todos os IDs de membro em zero para cada tipo. Ids em uma subclasse e sua classe base podem se sobrepor com segurança. Ambas as propriedades no exemplo a seguir têm ids iguais 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; } }
✅Amplie os tipos de membros numéricos conforme necessário. Você pode ampliar
sbyte
parashort
int
long
para .- Você pode restringir os tipos de membros numéricos, mas isso resultará em uma exceção de tempo de execução se os valores observados não puderem ser representados corretamente pelo tipo restrito. Por exemplo,
int.MaxValue
não pode ser representado por umshort
campo, portanto, restringir umint
campo parashort
pode resultar em uma exceção de tempo de execução se tal valor for encontrado.
- Você pode restringir os tipos de membros numéricos, mas isso resultará em uma exceção de tempo de execução se os valores observados não puderem ser representados corretamente pelo tipo restrito. Por exemplo,
❌Não altere a assinatura de um membro de tipo numérico. Você não deve alterar o tipo de um membro de
uint
paraint
ou de umint
para ,uint
por exemplo.
Serializadores de armazenamento de grãos
Orleans Inclui um modelo de persistência apoiado pelo provedor para grãos, acessado por meio da State propriedade ou injetando um ou mais IPersistentState<TState> valores em seu grão. Antes da Orleans 7.0, cada provedor tinha um mecanismo diferente para configurar a serialização. Na Orleans versão 7.0, agora há uma interface de serializador de estado de grão de uso geral, IGrainStorageSerializerque oferece uma maneira consistente de personalizar a serialização de estado para cada provedor. Os provedores de armazenamento suportados implementam um padrão que envolve a definição da IStorageProviderSerializerOptions.GrainStorageSerializer propriedade na classe de opções do provedor, por exemplo:
- DynamoDBStorageOptions.GrainStorageSerializer
- AzureBlobStorageOptions.GrainStorageSerializer
- AzureTableStorageOptions.GrainStorageSerializer
- GrainStorageSerializer
Atualmente, a serialização de armazenamento de grãos tem como padrão o Newtonsoft.Json
estado de serialização. Você pode substituir isso modificando essa propriedade no momento da configuração. O exemplo a seguir demonstra isso, usando OptionsBuilder<TOptions>:
siloBuilder.AddAzureBlobGrainStorage(
"MyGrainStorage",
(OptionsBuilder<AzureBlobStorageOptions> optionsBuilder) =>
{
optionsBuilder.Configure<IMySerializer>(
(options, serializer) => options.GrainStorageSerializer = serializer);
});
Para obter mais informações, consulte API do OptionsBuilder.
Orleans tem uma estrutura de serialização avançada e extensível. Orleans Serializa tipos de dados passados em mensagens de solicitação e resposta de grão, bem como objetos de estado persistente de grão. Como parte dessa estrutura, Orleans gera automaticamente o código de serialização para esses tipos de dados. Além de gerar uma serialização/desserialização mais eficiente para tipos que já são . NET-serializable, Orleans também tenta gerar serializadores para tipos usados em interfaces de grão que não são . NET-serializável. A estrutura também inclui um conjunto de serializadores internos eficientes para tipos usados com freqüência: listas, dicionários, strings, primitivos, matrizes, etc.
Duas características importantes do Orleansserializador do o diferenciam de muitas outras estruturas de serialização de terceiros: tipos dinâmicos/polimorfismo arbitrário e identidade de objeto.
Tipos dinâmicos e polimorfismo arbitrário: Orleans não impõe restrições aos tipos que podem ser passados em chamadas de grão e mantém a natureza dinâmica do tipo de dados real. Isso significa, por exemplo, que se o método nas interfaces de grão for declarado para aceitar IDictionary , mas em tempo de execução, o remetente passa SortedDictionary<TKey,TValue>, o recetor realmente obterá
SortedDictionary
(embora o "contrato estático"/interface de grão não tenha especificado esse comportamento).Mantendo a identidade do objeto: Se o mesmo objeto for passado vários tipos nos argumentos de uma chamada de grão ou for apontado indiretamente mais de uma vez a partir dos argumentos, Orleans serializá-lo-á apenas uma vez. No lado do recetor, Orleans irá restaurar todas as referências corretamente para que dois ponteiros para o mesmo objeto ainda apontem para o mesmo objeto após a desserialização também. A identidade do objeto é importante preservar em cenários como os seguintes. Imagine que o grão A está enviando um dicionário com 100 entradas para o grão B, e 10 das chaves no dicionário apontam para o mesmo objeto, obj, do lado de A. Sem preservar a identidade do objeto, B receberia um dicionário de 100 entradas com essas 10 chaves apontando para 10 clones diferentes de obj. Com a identidade do objeto preservada, o dicionário do lado de B se parece exatamente com o lado de A, com aquelas 10 teclas apontando para um único objeto obj.
Os dois comportamentos acima são fornecidos pelo serializador binário .NET padrão e, portanto, era importante para nós oferecer suporte a esse padrão e comportamento familiar também Orleans .
Serializadores gerados
Orleans usa as regras a seguir para decidir quais serializadores gerar. As regras são:
- Analise todos os tipos em todos os assemblies que fazem referência à biblioteca principal Orleans .
- Fora desses assemblies: gere serializadores para tipos que são diretamente referenciados em assinaturas de método de interfaces de grão ou assinatura de classe de estado ou para qualquer tipo marcado com SerializableAttribute.
- Além disso, uma interface de grão ou projeto de implementação pode apontar para tipos arbitrários para geração de serialização adicionando um KnownTypeAttribute ou KnownAssemblyAttribute atributos de nível de assembly para dizer ao gerador de código para gerar serializadores para tipos específicos ou todos os tipos elegíveis dentro de um assembly. Para obter mais informações sobre atributos de nível de assembly, consulte Aplicar atributos no nível de assembly.
Serialização de fallback
Orleans suporta a transmissão de tipos arbitrários em tempo de execução e, portanto, o gerador de código embutido não pode determinar todo o conjunto de tipos que serão transmitidos com antecedência. Além disso, certos tipos não podem ter serializadores gerados para eles porque eles são inacessíveis (por exemplo, private
) ou têm campos inacessíveis (por exemplo, readonly
). Portanto, há uma necessidade de serialização just-in-time de tipos que foram inesperados ou não poderiam ter serializadores gerados antes do tempo. O serializador responsável por esses tipos é chamado de serializador de fallback. Orleans Vem com dois serializadores de fallback:
- Orleans.Serialization.BinaryFormatterSerializer, que utiliza . NET's BinaryFormatter;
- Orleans.Serialization.ILBasedSerializer, que emite instruções CIL em tempo de execução para criar serializadores que aproveitam Orleansa estrutura de serialização ' para serializar cada campo. Isso significa que, se um tipo
MyPrivateType
inacessível contiver um campoMyType
que tenha um serializador personalizado, esse serializador personalizado será usado para serializá-lo.
O serializador de fallback pode ser configurado usando a FallbackSerializationProvider propriedade no ClientConfiguration cliente e GlobalConfiguration nos 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, o provedor de serialização de fallback pode ser especificado na configuração XML:
<Messaging>
<FallbackSerializationProvider
Type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
</Messaging>
O BinaryFormatterSerializer é o serializador de fallback padrão.
Aviso
A serialização binária com BinaryFormatter
pode ser perigosa. Para obter mais informações, consulte o guia de segurança BinaryFormatter e o guia de migração BinaryFormatter.
Serialização de exceções
As exceções são serializadas usando o serializador de fallback. Usando a configuração padrão, é o serializador de fallback e, portanto, BinaryFormatter
o padrão ISerializable deve ser seguido para garantir a serialização correta de todas as propriedades em um tipo de exceção.
Aqui está um exemplo de um tipo de exceção com serialização implementada corretamente:
[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);
}
}
Práticas recomendadas de serialização
A serialização serve a dois propósitos principais em Orleans:
- Como um formato de fio para transmitir dados entre grãos e clientes em tempo de execução.
- Como um formato de armazenamento para persistência de dados de longa duração para recuperação posterior.
Os serializadores gerados por Orleans são adequados para o primeiro propósito devido à sua flexibilidade, desempenho e versatilidade. Eles não são tão adequados para o segundo propósito, uma vez que não são explicitamente tolerantes à versão. É recomendável que os usuários configurem um serializador tolerante à versão, como buffers de protocolo, para dados persistentes . Buffers de protocolo é suportado via Orleans.Serialization.ProtobufSerializer
da Microsoft.Orleans. Pacote NuGet OrleansGoogleUtils . As práticas recomendadas para o serializador específico de escolha devem ser usadas para garantir a tolerância de versão. Os serializadores de terceiros podem ser configurados usando a SerializationProviders
propriedade configuration conforme descrito acima.