Compartilhar via


Serialização em Orleans

Em geral, há dois tipos de serialização usados em Orleans:

  • Serialização de chamadas de grains: usada para serializar objetos passados de e para grains.
  • Serialização de armazenamento de grãos: utilizada para serializar objetos em e de sistemas de armazenamento.

A maior parte deste artigo se concentra na serialização de chamada de grãos por meio da estrutura de serialização incluída em Orleans. A seção Serializadores de armazenamento de grãos discute a serialização do armazenamento de grãos.

Usar a serialização Orleans

Orleans inclui uma estrutura de serialização avançada e extensível conhecida como Orleans. Serialização. A estrutura de serialização incluída no Orleans foi projetada para atender às seguintes metas:

  • Alto desempenho: o serializador foi 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 grafos cíclicos. Não há suporte para ponteiros, pois eles não são portáteis entre processos.
  • Flexibilidade: você pode personalizar o serializador para dar suporte a bibliotecas de terceiros criando substitutos ou delegando para bibliotecas de serialização externas, como System.Text.Json, Newtonsoft.Json e Google.Protobuf.
  • Tolerância a versão: o serializador permite que os tipos de aplicativo evoluam ao longo do tempo, dando suporte a:
    • Adicionar e remover membros
    • Subclassificação
    • Ampliação e estreitamento numérico (por exemplo, int para/de long, float para/de double)
    • Renomear tipos

A representação de alta fidelidade de tipos é bastante incomum para serializadores, portanto, alguns pontos merecem uma explicação adicional.

  1. Tipos dinâmicos e polimorfismo arbitrário: Orleans não impõe restrições aos tipos passados em chamadas granuladas e mantém a natureza dinâmica do tipo de dados real. Isso significa, por exemplo, se um método em uma interface de grãos for declarado para aceitar IDictionary, mas em runtime o remetente passa um SortedDictionary<TKey,TValue>, o receptor realmente obtém um SortedDictionary (mesmo que a interface "contrato estático"/granulação não tenha especificado esse comportamento).

  2. Mantendo a identidade do objeto: se o mesmo objeto for passado várias vezes nos argumentos de uma grain call ou for indiretamente apontado mais de uma vez a partir dos argumentos, Orleans o serializa apenas uma vez. No lado do receptor, 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. A preservação da identidade do objeto é importante em cenários como o seguinte: Imagine que o grão A envia um dicionário com 100 entradas para o grão B, e 10 chaves 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 de obj. Com a identidade do objeto preservada, o dicionário do lado de B é exatamente parecido com o do lado de A, com essas 10 chaves apontando para um único objeto obj. 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 dar suporte à tolerância de versão, o serializador exige 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 de serialização para o seu tipo. Depois de fazer isso, você pode usar a correção de código incluída para adicionar os membros serializáveis necessários Orleans.IdAttribute aos seus tipos, conforme demonstrado aqui:

Uma imagem animada da correção de código disponível que está sendo sugerida e aplicada no GenerateSerializerAttribute quando o tipo recipiente não contém IdAttribute em seus membros.

Aqui, temos um exemplo de tipo serializável no Orleans, demonstrando como aplicar os atributos.

[GenerateSerializer]
public class Employee
{
    [Id(0)]
    public string Name { get; set; }
}

Orleans dá suporte à herança e serializa 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 derivam de Publication. Essa é a prática recomendada em Orleans, pois os identificadores de membro têm como escopo o nível de herança, não o tipo como um todo. Você pode adicionar e remover membros de Publication e Book de forma independente, mas não pode inserir uma nova classe base na hierarquia depois que o aplicativo é implantado sem consideração especial.

Orleans também dá suporte a serialização de tipos com membros internal, private e readonly, 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 seu tipo seja serializado usando um nome resiliente à renomeação 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 backtick; por exemplo, MyGenericType<T, U> poderia ter o alias [Alias("mytype`2")].

Serializando tipos record

Os membros definidos no construtor primário de um registro têm IDs implícitas por padrão. Em outras palavras, o Orleans dá suporte a tipos de serialização record. Isso significa que você não pode alterar a ordem de parâmetro para um tipo já implantado, pois isso interrompe a compatibilidade com versões anteriores do seu aplicativo (em um cenário de atualização sem interrupção) e com instâncias serializadas desse tipo no armazenamento e fluxos. Os membros definidos no corpo de um tipo de registro não compartilham identidades com os parâmetros do construtor primário.

[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 do construtor primário sejam incluídos automaticamente como campos serializáveis, use [GenerateSerializer(IncludePrimaryConstructorParameters = false)].

Substitutos para serializar tipos estranhos

Às vezes, talvez seja necessário 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 definido sob medida no código do aplicativo pode ser impraticável. Orleans oferece uma solução para essas situações: tipos alternativos. 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 estranho 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 consumidora.
  • MyForeignLibraryValueTypeSurrogate é um mapeamento de tipo alternativo para MyForeignLibraryValueType.
  • RegisterConverterAttribute especifica que MyForeignLibraryValueTypeSurrogateConverter atua como um conversor para mapear entre os dois tipos. A classe implementa a IConverter<TValue,TSurrogate> interface.

Orleans dá suporte à serialização de tipos em hierarquias de tipo (tipos derivados de outros tipos). Se um tipo estrangeiro puder aparecer em uma hierarquia de tipos (por exemplo, como a classe base para um de seus tipos), você deverá implementar a interface Orleans.IPopulator<TValue,TSurrogate>. 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

Há suporte para tolerância de versão, desde que você siga um conjunto de regras ao modificar tipos. Se você estiver familiarizado com sistemas como Os Buffers de Protocolo do Google (Protobuf), essas regras serão familiares.

Tipos compostos (class & struct)

  • A herança tem suporte, mas não há suporte para a modificação da hierarquia de herança de um objeto. Você não pode 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, você não pode alterar os tipos de campo.
  • Você pode adicionar ou remover campos em qualquer ponto de uma hierarquia de herança.
  • Não é possível alterar as IDs de campo.
  • As IDs de campo devem ser exclusivas para cada nível em uma hierarquia de tipos, mas podem ser reutilizados entre classes base e subclasses. Por exemplo, uma Base classe pode declarar um campo com ID 0e uma Sub : Base classe pode declarar um campo diferente com a mesma ID 0.

Numéricos

  • Você não pode alterar a assinatura de um campo numérico.
    • As conversões entre int e uint são inválidas.
  • Você pode alterar a largura de um campo numérico.
    • Por exemplo, há suporte para conversões de int para long ou de ulong para ushort.
    • As conversões que restringem a largura geram uma exceção se o valor do campo em tempo de execução causar um estouro.
    • A conversão de ulong para ushort só terá suporte se o valor do runtime for menor que ushort.MaxValue.
    • As conversões de double para float só terá suporte se o valor do runtime estiver entre float.MinValue e float.MaxValue.
    • Da mesma forma para decimal, que tem um intervalo mais estreito que double e float.

Copiadoras

Orleans promove a segurança por padrão, incluindo a segurança contra algumas classes de bugs de concorrência. Particularmente, Orleans copia imediatamente, por padrão, objetos passados em chamadas de grain. Orleans. A serialização facilita essa cópia. Quando você aplica Orleans.CodeGeneration.GenerateSerializerAttribute a um tipo, Orleans também gera copiadores 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.

Melhores práticas serialização

  • Forneça seus aliases de tipos usando o atributo [Alias("my-type")]. Tipos com aliases podem ser renomeados sem interromper a compatibilidade.

  • Não altere um record para um class regular ou vice-versa. Registros e classes não são representados de forma idêntica, pois os registros têm membros do construtor primário, além de membros regulares; portanto, os dois não são intercambiáveis.

  • Não adicione novos tipos a uma hierarquia de tipo existente para um tipo serializável. Você não deve adicionar uma nova classe base a um tipo existente. Você não deve adicionar uma nova subclasse a um tipo existente.

  • Substitua os usos de SerializableAttribute por GenerateSerializerAttribute e declarações correspondentes IdAttribute.

  • Inicie todas as 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 membro numéricos conforme necessário. Você pode ampliar sbyte para short para int para long.

    • Você pode restringir tipos de membro numéricos, mas isso resultará em uma exceção de runtime se os valores observados não puderem ser representados corretamente pelo tipo restrito. Por exemplo, um campo int.MaxValue não pode ser representado por um campo short, então restringir um campo int para short pode resultar em uma exceção em tempo de execução se tal valor for encontrado.
  • Não altere a assinatura de um membro de tipo numérico. Você não deve alterar o tipo de um membro de uint ou intint para uint, por exemplo.

Serializadores de armazenamento de granularidade

O Orleans inclui um modelo de persistência com suporte do provedor para granularidades, acessado por meio da propriedade State ou injetando um ou mais valores IPersistentState<TState> na sua granularidade. Antes do 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 serialização de estado de grão de uso geral, IGrainStorageSerializer oferecendo uma maneira consistente de personalizar a serialização de estado para cada provedor. Os provedores de armazenamento com suporte implementam um padrão que envolve a definição da IStorageProviderSerializerOptions.GrainStorageSerializer propriedade na classe de opções do provedor, por exemplo:

No momento, a serialização de armazenamento de granularidade usa como padrã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 OptionsBuilder API.

O Orleans tem uma estrutura de serialização avançada e extensível. Orleans serializa os tipos de dados passados em mensagens de solicitação e resposta de grãos, bem como objetos de estado persistente de granulaçã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á serializáveis pelo .NET, Orleans também tenta gerar serializadores para tipos usados em interfaces de grain que não são serializáveis pelo .NET. A estrutura também inclui um conjunto de serializadores internos eficientes para tipos usados com frequência: listas, dicionários, cadeias de caracteres, primitivos, matrizes etc.

Dois recursos importantes do Orleansserializador o diferenciam de muitas outras estruturas de serialização de terceiros: tipos dinâmicos/polimorfismo arbitrário e identidade de objeto.

  1. Tipos dinâmicos e polimorfismo arbitrário: Orleans não impõe restrições aos tipos passados em chamadas granuladas e mantém a natureza dinâmica do tipo de dados real. Isso significa, por exemplo, se um método em uma interface de grãos for declarado para aceitar IDictionary, mas em runtime o remetente passa um SortedDictionary<TKey,TValue>, o receptor realmente obtém um SortedDictionary (mesmo que a interface "contrato estático"/granulação não tenha especificado esse comportamento).

  2. Mantendo a identidade do objeto: se o mesmo objeto for passado várias vezes nos argumentos de uma grain call ou for indiretamente apontado mais de uma vez a partir dos argumentos, Orleans o serializa apenas uma vez. No lado do receptor, 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. A preservação da identidade do objeto é importante em cenários como o seguinte: Imagine que o grão A envia um dicionário com 100 entradas para o grão B, e 10 chaves 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 de obj. Com a identidade do objeto preservada, o dicionário do lado de B é exatamente parecido com o do lado de A, com essas 10 chaves apontando para um único objeto obj.

O serializador binário padrão do .NET fornece os dois comportamentos acima, portanto, era importante dar suporte da mesma forma a esse comportamento padrão e familiar em Orleans também.

Serializadores gerados

Orleans usa as seguintes regras para decidir quais serializadores gerar:

  1. Examine todos os tipos em todos os assemblies que fazem referência à biblioteca principal Orleans .
  2. Nesses assemblies, gere serializadores para tipos diretamente referenciados em assinaturas de método de interface de grãos ou assinaturas de classe de estado ou para qualquer tipo marcado com SerializableAttribute.
  3. Além disso, um projeto de interface ou implementação de grain pode apontar para tipos arbitrários para geração de serialização, adicionando atributos no nível do assembly KnownTypeAttribute ou KnownAssemblyAttribute. Eles instruem o gerador de código para gerar serializadores para tipos específicos ou todos os tipos elegíveis em um assembly. Para obter mais informações sobre atributos no nível do assembly, consulte Aplicar atributos no nível do assembly.

Serialização de fallback

Orleans dá suporte à transmissão de tipos arbitrários em runtime. Portanto, o gerador de código interno não pode determinar todo o conjunto de tipos que serão transmitidos antecipadamente. Além disso, determinados tipos não podem ter serializadores gerados para eles porque são inacessíveis (por exemplo, private) ou têm campos inacessíveis (por exemplo, readonly). Portanto, há a necessidade de serialização sob demanda de tipos que eram inesperados ou não podiam ter serializadores gerados com antecedência. O serializador responsável por esses tipos é chamado de serializador de fallback. O Orleans é fornecido com dois serializadores de fallback:

Configure o serializador de fallback usando a propriedade FallbackSerializationProvider em ambos os ClientConfiguration (cliente) e 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.

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ção

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);
    }
}

Melhores práticas serialização

A serialização atende a duas finalidades primárias no Orleans:

  1. Como um formato de conexão para transmitir dados entre granularidades e clientes em runtime.
  2. Como um formato de armazenamento para manter dados de longa duração para recuperação posterior.

Os serializadores gerados pelo Orleans são adequados para a primeira finalidade devido à flexibilidade, desempenho e versatilidade. Eles não são tão adequados para o segundo propósito, pois não são explicitamente tolerantes à versão. Recomendamos configurar um serializador tolerante a versão, como buffers de protocolo, para dados persistentes. Os buffers de protocolo são suportados por meio de Orleans.Serialization.ProtobufSerializer a partir do pacote NuGet Microsoft.Orleans.OrleansGoogleUtils. Siga as práticas recomendadas para o serializador escolhido para garantir a tolerância à versão. Configure serializadores de terceiros usando a SerializationProviders propriedade de configuração, conforme descrito acima.