Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Existem basicamente dois tipos de serialização usados em Orleans:
- Serialização de chamadas de grãos: 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 maior parte deste artigo concentra-se na serialização de chamadas de grain através 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.
Utilizar Orleans serialização
Orleans Inclui uma estrutura de serialização avançada e extensível conhecida como Orleans. Serialização. A estrutura de serialização incluída em Orleans foi 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 maior parte do sistema de tipos do .NET, incluindo suporte para genéricos, polimorfismo, hierarquias de herança, identidade de objeto e estruturas cíclicas. Os ponteiros não são suportados, uma vez que não são portáteis entre processos.
- Flexibilidade: Você pode personalizar o serializador para oferecer suporte a 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
para / delong
,float
para / dedouble
) - Renomeando tipos
A representação de tipos de fidelidade elevada é razoavelmente incomum para ferramentas de serialização, por isso alguns pontos merecem mais explicações:
Tipos dinâmicos e polimorfismo arbitrário: Orleans não impõe restrições aos tipos passados em chamadas de grão e mantém a natureza dinâmica do tipo de dados real. Isso significa, por exemplo, que se um método em uma interface grain é declarado para aceitar IDictionary, mas no tempo de execução o remetente passa um SortedDictionary<TKey,TValue>, o recetor realmente recebe um
SortedDictionary
(mesmo que o "contrato estático"/interface grain não tenha especificado esse comportamento).Manutenção da identidade do objeto: Se o mesmo objeto for passado várias vezes nos argumentos de uma chamada do grain ou for apontado indiretamente mais de uma vez a partir dos argumentos, Orleans serializa-o apenas uma vez. No lado do recetor, Orleans restaura todas as referências corretamente para que dois ponteiros para o mesmo objeto ainda apontem para o mesmo objeto após a desserialização. Preservar a identidade do objeto é importante em cenários como os seguintes: Imagine que o grão A envia um dicionário com 100 entradas para o grão B, e 10 teclas no dicionário apontam para o mesmo objeto,
obj
no 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 você seja explícito sobre quais tipos e membros são serializados. Tentamos tornar isso o mais indolor possível. Marque todos os tipos serializáveis com Orleans.GenerateSerializerAttribute para instruir Orleans a gerar código serializador para seu tipo. Depois de fazer isso, pode usar a correção de código incluída para adicionar o elemento Orleans.IdAttribute necessário aos membros serializáveis nos seus tipos, conforme ilustrado 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 serializa as camadas individuais na hierarquia separadamente, permitindo que elas tenham IDs de membros distintos.
[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
derive de Publication
. Essa é a prática recomendada em Orleans porque os identificadores de membro abrangem o nível de herança e não o tipo como um todo. Você pode adicionar e remover membros de Publication
e Book
independentemente, mas não pode inserir uma nova classe base na hierarquia uma vez que o aplicativo esteja implantado 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 serializa seu tipo codificando seu nome completo. Você pode substituir isso adicionando um Orleans.AliasAttribute. Isso faz com que o seu tipo seja serializado usando um nome resiliente a renomeações da classe subjacente ou à sua movimentação 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 do alias deve incluir o número de parâmetros genéricos precedidos por um acento grave (`); por exemplo, MyGenericType<T, U>
poderia ter o alias [Alias("mytype`2")]
.
Tipos de serialização record
Os membros definidos no construtor principal 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, pois isso quebra a compatibilidade com versões anteriores do seu aplicativo (em um cenário de 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, use [GenerateSerializer(IncludePrimaryConstructorParameters = false)]
.
Substitutos para serialização de tipos externos
Às vezes, você pode precisar passar tipos entre grãos sobre os quais você não tem controle total. Nesses casos, a conversão manual de e para um tipo personalizado no código do aplicativo pode ser impraticável. Orleans oferece uma solução para estas situações: tipos substitutos. Os substitutos são serializados no lugar do tipo de destino e têm a funcionalidade de converter para e a partir do 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:
-
MyForeignLibraryValueType
é um tipo fora do seu controle, definido em uma biblioteca de consumo. -
MyForeignLibraryValueTypeSurrogate
é um mapeamento de tipo substituto paraMyForeignLibraryValueType
. -
RegisterConverterAttribute Especifica que
MyForeignLibraryValueTypeSurrogateConverter
atua como um conversor para mapear entre os dois tipos. A classe implementa a IConverter<TValue,TSurrogate> interface.
Orleans suporta a serialização de tipos em hierarquias de tipo (tipos derivados de outros tipos). Se um tipo estrangeiro pode 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 você siga um conjunto de regras ao modificar tipos. Se você 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. Não é possível adicionar, alterar ou remover a classe base de uma classe.
- Com exceção de alguns tipos numéricos descritos na seção Numéricos abaixo, não é possível alterar os tipos de campo.
- Você pode adicionar ou remover campos a qualquer momento em uma hierarquia de herança.
- Não é possível alterar IDs de campo.
- 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, uma
Base
classe pode declarar um campo com ID0
e umaSub : Base
classe pode declarar um campo diferente com a mesma ID,0
.
Números
- Não é possível alterar a assinatura de um campo numérico.
- As conversões entre
int
&uint
são inválidas.
- As conversões entre
- Você pode alterar a largura de um campo numérico.
- Por exemplo, conversões de
int
paralong
ouulong
paraushort
são suportadas. - Conversões que reduzem a largura geram uma exceção se o valor de tempo de execução do campo causar um estouro.
- A conversão de
ulong
paraushort
só é suportada se o valor em tempo de execução for menor queushort.MaxValue
. - As conversões de
double
parafloat
só são suportadas se o valor em tempo de execução estiver entrefloat.MinValue
efloat.MaxValue
. - Da mesma forma para
decimal
, que tem um intervalo mais estreito do quedouble
efloat
.
- Por exemplo, conversões de
Copiadoras
Orleans promove a segurança por padrão, incluindo a segurança de algumas classes de bugs de simultaneidade. Em particular, Orleans copia imediatamente objetos passados em chamadas de grão por padrão. Orleans. A serialização facilita essa cópia. Quando se aplica Orleans.CodeGeneration.GenerateSerializerAttribute a um tipo, Orleans gera também cópias para esse tipo. Orleans Evita copiar tipos ou membros individuais marcados com 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 de um
record
para umclass
regular ou vice-versa. Registros e classes não são representados de forma idêntica, uma vez que os registros têm membros construtores primários, além de membros regulares; 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 identificadores de membro a partir de zero para cada tipo. IDs numa subclasse e na sua classe base podem sobrepor-se 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
, paraint
e paralong
.- Você pode restringir tipos de membros numéricos, mas isso resulta 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 tipos de membros numéricos, mas isso resulta 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
ouint
parauint
, por exemplo.
Serializadores de armazenamento de grãos
Orleans inclui um modelo de persistência suportado pelo fornecedor para grains, acessível através da propriedade State ou injetando um ou mais valores IPersistentState<TState> no seu grain. 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, IGrainStorageSerializeroferecendo 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 do armazenamento de grãos tem como predefinição Newtonsoft.Json
para serializar o estado. 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 serialização/desserialização mais eficiente para tipos já .NET-serializáveis, Orleans também tenta gerar serializadores para tipos usados em interfaces "grain" que não são .NET-serializáveis. 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 serializador de Orleans o diferenciam de muitos outros frameworks 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 passados em chamadas de grão e mantém a natureza dinâmica do tipo de dados real. Isso significa, por exemplo, que se um método em uma interface grain é declarado para aceitar IDictionary, mas no tempo de execução o remetente passa um SortedDictionary<TKey,TValue>, o recetor realmente recebe um
SortedDictionary
(mesmo que o "contrato estático"/interface grain não tenha especificado esse comportamento).Manutenção da identidade do objeto: Se o mesmo objeto for passado várias vezes nos argumentos de uma chamada do grain ou for apontado indiretamente mais de uma vez a partir dos argumentos, Orleans serializa-o apenas uma vez. No lado do recetor, Orleans restaura todas as referências corretamente para que dois ponteiros para o mesmo objeto ainda apontem para o mesmo objeto após a desserialização. Preservar a identidade do objeto é importante em cenários como os seguintes: Imagine que o grão A envia um dicionário com 100 entradas para o grão B, e 10 teclas no dicionário apontam para o mesmo objeto,
obj
no 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
.
O serializador binário .NET padrão fornece os dois comportamentos acima, por isso era importante para nós oferecer suporte a esse padrão e comportamento familiar no Orleans também.
Serializadores gerados
Orleans usa as seguintes regras para decidir quais serializadores gerar:
- Analise todos os tipos em todos os assemblies que fazem referência à biblioteca principal Orleans .
- A partir desses assemblies, gere serializadores para tipos referenciados diretamente nas assinaturas de métodos de interfaces de grão ou assinaturas de classes 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 gerar serialização ao adicionar atributos de nível de montagem KnownTypeAttribute ou KnownAssemblyAttribute. Eles instruem o gerador de código a 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 alternativa
Orleans suporta a transmissão de tipos arbitrários em tempo de execução. Portanto, o gerador de código interno 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
). Assim, há uma necessidade de serialização just-in-time de tipos que foram surpreendentes ou não poderiam ter serializadores gerados antecipadamente. 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 utilizam o framework de serialização de Orleans 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.
Configure o serializador de fallback usando a propriedade FallbackSerializationProvider tanto no ClientConfiguration (cliente) como no 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 o provedor de serialização de fallback na configuração XML:
<Messaging>
<FallbackSerializationProvider
Type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
</Messaging>
O BinaryFormatterSerializer é o serializador de fallback padrão.
Advertência
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. Com a configuração padrão, BinaryFormatter
é o serializador de fallback. Portanto, você deve seguir o padrão ISerializable 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. Recomendamos configurar um serializador tolerante à versão, como buffers de protocolo, para dados persistentes. Buffers de Protocolo são suportados via Orleans.Serialization.ProtobufSerializer
do Microsoft.Orleans.OrleansGoogleUtils pacote NuGet. Siga as práticas recomendadas para o serializador escolhido para garantir a tolerância de versão. Configure serializadores de terceiros usando a propriedade de configuração SerializationProviders
conforme descrito acima.