Partilhar via


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/ longpara, float para/de double)
    • Renomeando tipos

A representação de tipos de alta fidelidade é bastante incomum para serializadores, então alguns pontos merecem mais elaboração:

  1. 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).

  2. 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, objdo 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 essas 10 teclas 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 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:

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

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, privatee 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 para MyForeignLibraryValueType.
  • 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 id 0 e um campo diferente pode ser declarado por Sub : 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.
  • A largura de um campo numérico pode ser alterada.
    • Por exemplo: conversões de int para long ou ulong para ushort 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 de ulong execução for menor que ushort.MaxValue.
      • As conversões de para float só são suportadas se o valor de tempo de double execução estiver entre float.MinValue e float.MaxValue.
      • Da mesma forma para decimal, que tem um intervalo mais estreito do que ambos e double float.

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

  • 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 regular class 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 para short int longpara .

    • 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 um short campo, portanto, restringir um int campo para short pode resultar em uma exceção de 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 para int ou de um int para , uintpor 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:

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.

  1. 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).

  2. 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:

  1. Analise todos os tipos em todos os assemblies que fazem referência à biblioteca principal Orleans .
  2. 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.
  3. 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:

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:

  1. Como um formato de fio para transmitir dados entre grãos e clientes em tempo de execução.
  2. 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.