Partilhar via


Personalização da serialização em Orleans

Um aspeto importante é Orleans seu suporte para personalização da serialização, que é o processo de converter um objeto ou estrutura de dados em um formato que pode ser armazenado ou transmitido e reconstruído posteriormente. Isso permite que os desenvolvedores controlem como os dados são codificados e decodificados quando são enviados entre diferentes partes do sistema. A personalização da serialização pode ser útil para otimizar o desempenho, a interoperabilidade e a segurança.

Provedores de serialização

Orleans Fornece duas implementações de serializador:

Para configurar qualquer um desses pacotes, consulte Configuração de serialização em Orleans.

Implementação personalizada do serializador

Para criar uma implementação de serializador personalizado, há algumas etapas comuns envolvidas. Você tem que implementar várias interfaces e, em seguida, registrar seu serializador com o Orleans tempo de execução. As seções a seguir descrevem as etapas com mais detalhes.

Comece implementando as seguintes Orleans interfaces de serialização:

  • IGeneralizedCodec: Um codec que suporta vários tipos.
  • IGeneralizedCopier: Fornece funcionalidade para copiar objetos de vários tipos.
  • ITypeFilter: Funcionalidade para permitir que os tipos sejam carregados e participem da serialização e desserialização.

Considere o seguinte exemplo de uma implementação de serializador personalizado:

internal sealed class CustomOrleansSerializer :
    IGeneralizedCodec, IGeneralizedCopier, ITypeFilter
{
    void IFieldCodec.WriteField<TBufferWriter>(
        ref Writer<TBufferWriter> writer, 
        uint fieldIdDelta,
        Type expectedType,
        object value) =>
        throw new NotImplementedException();

    object IFieldCodec.ReadValue<TInput>(
        ref Reader<TInput> reader, Field field) =>
        throw new NotImplementedException();

    bool IGeneralizedCodec.IsSupportedType(Type type) =>
        throw new NotImplementedException();

    object IDeepCopier.DeepCopy(object input, CopyContext context) =>
        throw new NotImplementedException();

    bool IGeneralizedCopier.IsSupportedType(Type type) =>
        throw new NotImplementedException();
}

No exemplo anterior de implementação:

  • Cada interface é explicitamente implementada para evitar conflitos com a resolução de nomes de método.
  • Cada método lança um NotImplementedException para indicar que o método não está implementado. Você precisará implementar cada método para fornecer a funcionalidade desejada.

A próxima etapa é registrar seu serializador com o Orleans tempo de execução. Isso geralmente é conseguido estendendo ISerializerBuilder e expondo um método de extensão personalizado AddCustomSerializer . O exemplo a seguir demonstra o padrão típico:

using Microsoft.Extensions.DependencyInjection;
using Orleans.Serialization;
using Orleans.Serialization.Serializers;
using Orleans.Serialization.Cloning;

public static class SerializationHostingExtensions
{
    public static ISerializerBuilder AddCustomSerializer(
        this ISerializerBuilder builder)
    {
        var services = builder.Services;

        services.AddSingleton<CustomOrleansSerializer>();
        services.AddSingleton<IGeneralizedCodec, CustomOrleansSerializer>();
        services.AddSingleton<IGeneralizedCopier, CustomOrleansSerializer>();
        services.AddSingleton<ITypeFilter, CustomOrleansSerializer>();

        return builder;
    }
}

Considerações adicionais seriam expor uma sobrecarga que aceita opções de serialização personalizadas específicas para sua implementação personalizada. Estas opções podem ser configuradas juntamente com o registo no construtor. Essas opções podem ser de dependência injetada em sua implementação de serializador personalizado.

Orleans Suporta integração com serializadores de terceiros usando um modelo de provedor. Isso requer uma implementação do IExternalSerializer tipo descrito na seção de serialização personalizada deste artigo. As integrações para alguns serializadores comuns são mantidas ao lado do , por Orleansexemplo:

A implementação personalizada do IExternalSerializer é descrita na seção a seguir.

Serializadores externos personalizados

Além da geração automática de serialização, o código do aplicativo pode fornecer serialização personalizada para os tipos escolhidos. Orleans recomenda usar a geração de serialização automática para a maioria dos tipos de aplicativo e escrever apenas serializadores personalizados em casos raros quando você acredita que é possível obter melhor desempenho codificando serializadores manualmente. Esta nota descreve como fazê-lo e identifica alguns casos específicos em que pode ser útil.

Há três maneiras pelas quais os aplicativos podem personalizar a serialização:

  1. Adicione métodos de serialização ao seu tipo e marque-os com atributos apropriados (CopierMethodAttribute, SerializerMethodAttribute, DeserializerMethodAttribute). Esse método é preferível para tipos que seu aplicativo possui, ou seja, os tipos aos quais você pode adicionar novos métodos.
  2. Implemente-o IExternalSerializer e registre-o durante o tempo de configuração. Esse método é útil para integrar uma biblioteca de serialização externa.
  3. Escreva uma classe estática separada anotada com um [Serializer(typeof(YourType))] com os 3 métodos de serialização e os mesmos atributos acima. Esse método é útil para tipos que o aplicativo não possui, por exemplo, tipos definidos em outras bibliotecas sobre as quais seu aplicativo não tem controle.

Cada um desses métodos de serialização é detalhado nas seções a seguir.

Introdução à serialização personalizada

Orleans A serialização acontece em três etapas:

  • Os objetos são imediatamente copiados profundamente para garantir o isolamento.
  • Antes de serem colocados no fio, os objetos são serializados para um fluxo de bytes de mensagem.
  • Quando entregues à ativação de destino, os objetos são recriados (desserializados) a partir do fluxo de bytes recebido.

Os tipos de dados que podem ser enviados em mensagens, ou seja, tipos que podem ser passados como argumentos de método ou valores de retorno, devem ter rotinas associadas que executem essas três etapas. Nós nos referimos a essas rotinas coletivamente como os serializadores para um tipo de dados.

A copiadora de um tipo fica sozinha, enquanto o serializador e o desserializador são um par que trabalham juntos. Você pode fornecer apenas uma copiadora personalizada, ou apenas um serializador personalizado e um desserializador personalizado, ou você pode fornecer implementações personalizadas de todos os três.

Os serializadores são registrados para cada tipo de dados suportado na inicialização do silo e sempre que um assembly é carregado. O registro é necessário para rotinas de serializador personalizadas para um tipo a ser usado. A seleção do serializador é baseada no tipo dinâmico do objeto a ser copiado ou serializado. Por esse motivo, não há necessidade de criar serializadores para classes abstratas ou interfaces, porque eles nunca serão usados.

Quando escrever um serializador personalizado

Uma rotina de serializador artesanal raramente terá um desempenho melhor do que as versões geradas. Se você está tentado a escrever um, você deve primeiro considerar as seguintes opções:

  • Se houver campos ou propriedades em seus tipos de dados que não precisem ser serializados ou copiados, você poderá marcá-los com o NonSerializedAttribute. Isso fará com que o código gerado ignore esses campos ao copiar e serializar. Use ImmutableAttribute e, Immutable<T> sempre que possível, evite copiar dados imutáveis. Para obter mais informações, consulte Otimizar cópia. Se você estiver evitando usar os tipos de coleção genéricos padrão, não faça. O Orleans tempo de execução contém serializadores personalizados para as coleções genéricas que usam a semântica das coleções para otimizar a cópia, serialização e desserialização. Essas coleções também têm representações especiais "abreviadas" no fluxo de bytes serializado, resultando em ainda mais vantagens de desempenho. Por exemplo, um Dictionary<string, string> será mais rápido do que um List<Tuple<string, string>>.

  • O caso mais comum em que um serializador personalizado pode fornecer um ganho de desempenho percetível é quando há informações semânticas significativas codificadas no tipo de dados que não estão disponíveis simplesmente copiando valores de campo. Por exemplo, matrizes que são escassamente preenchidas podem muitas vezes ser serializadas de forma mais eficiente tratando a matriz como uma coleção de pares índice/valor, mesmo que o aplicativo mantenha os dados como uma matriz totalmente realizada para velocidade de operação.

  • Uma coisa importante a fazer antes de escrever um serializador personalizado é certificar-se de que o serializador gerado está prejudicando seu desempenho. A criação de perfil ajudará um pouco aqui, mas ainda mais valioso é a execução de testes de estresse de ponta a ponta do seu aplicativo com cargas de serialização variáveis para avaliar o impacto no nível do sistema, em vez do microimpacto da serialização. Por exemplo, a criação de uma versão de teste que não passe parâmetros ou resulte de métodos de grão, simplesmente usando valores enlatados em ambas as extremidades, ampliará o impacto da serialização e da cópia no desempenho do sistema.

Adicionar métodos de serialização a um tipo

Todas as rotinas do serializador devem ser implementadas como membros estáticos da classe ou estrutura em que operam. Os nomes mostrados aqui não são obrigatórios; O registro é baseado na presença dos respetivos atributos, não em nomes de métodos. Observe que os métodos do serializador não precisam ser públicos.

A menos que você implemente todas as três rotinas de serialização, você deve marcar seu tipo com o SerializableAttribute para que os métodos ausentes sejam gerados para você.

Copiadora

Os métodos da copiadora são sinalizados com o Orleans.CodeGeneration.CopierMethodAttribute:

[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
    // ...
}

As copiadoras são geralmente as rotinas de serializador mais simples de escrever. Eles usam um objeto, garantido como sendo do mesmo tipo em que a copiadora está definida, e devem retornar uma cópia semanticamente equivalente do objeto.

Se, como parte da cópia do objeto, um subobjeto precisar ser copiado, a melhor maneira de fazer isso é usar a SerializationManager.DeepCopyInner rotina:

var fooCopy = SerializationManager.DeepCopyInner(foo, context);

Importante

É importante usar SerializationManager.DeepCopyInner, em vez de , para manter o contexto de identidade do objeto para a operação de SerializationManager.DeepCopycópia completa.

Manter a identidade do objeto

Uma responsabilidade importante de uma rotina de cópia é manter a identidade do objeto. O Orleans tempo de execução fornece uma classe auxiliar para essa finalidade. Antes de copiar um subobjeto "à mão" (não chamando DeepCopyInner), verifique se ele já foi referenciado da seguinte forma:

var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
    // Actually make a copy of foo
    context.RecordObject(foo, fooCopy);
}

A última linha é a chamada para , que é necessária para RecordObjectque possíveis referências futuras ao mesmo objeto que foo as referências sejam encontradas corretamente pela CheckObjectWhileCopying.

Nota

Isso só deve ser feito para instâncias de classe, não struct instâncias ou primitivos do .NET, como string, Urie enum.

Se você usar DeepCopyInner para copiar subobjetos, a identidade do objeto será tratada para você.

Serializador

Os métodos de serialização são sinalizados com o Orleans.CodeGeneration.SerializerMethodAttribute:

[SerializerMethod]
static private void Serialize(
    object input,
    ISerializationContext context,
    Type expected)
{
    // ...
}

Tal como acontece com as copiadoras, o objeto "input" passado para um serializador é garantido como uma instância do tipo definidor. O tipo "esperado" pode ser ignorado; Ele é baseado em informações de tipo em tempo de compilação sobre o item de dados e é usado em um nível mais alto para formar o prefixo de tipo no fluxo de bytes.

Para serializar subobjetos, use a SerializationManager.SerializeInner rotina:

SerializationManager.SerializeInner(foo, context, typeof(FooType));

Se não houver nenhum tipo esperado específico para foo, então você pode passar null para o tipo esperado.

A BinaryTokenStreamWriter classe fornece uma ampla variedade de métodos para gravar dados no fluxo de bytes. Uma instância da classe pode ser obtida através da context.StreamWriter propriedade. Consulte a classe para obter a documentação.

Desserializador

Os métodos de desserialização são sinalizados com o Orleans.CodeGeneration.DeserializerMethodAttribute:

[DeserializerMethod]
static private object Deserialize(
    Type expected,
    IDeserializationContext context)
{
    //...
}

O tipo "esperado" pode ser ignorado; Ele é baseado em informações de tipo em tempo de compilação sobre o item de dados e é usado em um nível mais alto para formar o prefixo de tipo no fluxo de bytes. O tipo real do objeto a ser criado será sempre o tipo de classe na qual o desserializador é definido.

Para desserializar subobjetos, use a SerializationManager.DeserializeInner rotina:

var foo = SerializationManager.DeserializeInner(typeof(FooType), context);

Ou, alternativamente:

var foo = SerializationManager.DeserializeInner<FooType>(context);

Se não houver nenhum tipo esperado específico para foo, use a variante não genérica DeserializeInner e passe null para o tipo esperado.

A BinaryTokenStreamReader classe fornece uma ampla variedade de métodos para ler dados do fluxo de bytes. Uma instância da classe pode ser obtida através da context.StreamReader propriedade. Consulte a classe para obter a documentação.

Escrever um provedor de serializador

Nesse método, você implementa Orleans.Serialization.IExternalSerializer e adiciona à SerializationProviderOptions.SerializationProviders propriedade no ClientConfiguration cliente e GlobalConfiguration nos silos. Para obter informações sobre configuração, consulte Provedores de serialização.

Implementações de IExternalSerializer segue o padrão descrito anteriormente para serialização com a adição de um Initialize método e um IsSupportedType método que Orleans usa para determinar se o serializador suporta um determinado tipo. Esta é a definição da interface:

public interface IExternalSerializer
{
    /// <summary>
    /// Initializes the external serializer. Called once when the serialization manager creates
    /// an instance of this type
    /// </summary>
    void Initialize(Logger logger);

    /// <summary>
    /// Informs the serialization manager whether this serializer supports the type for serialization.
    /// </summary>
    /// <param name="itemType">The type of the item to be serialized</param>
    /// <returns>A value indicating whether the item can be serialized.</returns>
    bool IsSupportedType(Type itemType);

    /// <summary>
    /// Tries to create a copy of source.
    /// </summary>
    /// <param name="source">The item to create a copy of</param>
    /// <param name="context">The context in which the object is being copied.</param>
    /// <returns>The copy</returns>
    object DeepCopy(object source, ICopyContext context);

    /// <summary>
    /// Tries to serialize an item.
    /// </summary>
    /// <param name="item">The instance of the object being serialized</param>
    /// <param name="context">The context in which the object is being serialized.</param>
    /// <param name="expectedType">The type that the deserializer will expect</param>
    void Serialize(object item, ISerializationContext context, Type expectedType);

    /// <summary>
    /// Tries to deserialize an item.
    /// </summary>
    /// <param name="context">The context in which the object is being deserialized.</param>
    /// <param name="expectedType">The type that should be deserialized</param>
    /// <returns>The deserialized object</returns>
    object Deserialize(Type expectedType, IDeserializationContext context);
}

Escrever um serializador para um tipo individual

Neste método, você escreve uma nova classe anotada com um atributo [SerializerAttribute(typeof(TargetType))], onde TargetType é o tipo que está sendo serializado e implementa as 3 rotinas de serialização. As regras de como escrever essas rotinas são idênticas àquelas ao implementar o IExternalSerializer. Orleans usa o [SerializerAttribute(typeof(TargetType))] para determinar que essa classe é um serializador para TargetType e esse atributo pode ser especificado várias vezes na mesma classe se for capaz de serializar vários tipos. Abaixo está um exemplo para tal classe:

public class User
{
    public User BestFriend { get; set; }
    public string NickName { get; set; }
    public int FavoriteNumber { get; set; }
    public DateTimeOffset BirthDate { get; set; }
}

[Orleans.CodeGeneration.SerializerAttribute(typeof(User))]
internal class UserSerializer
{
    [CopierMethod]
    public static object DeepCopier(
        object original, ICopyContext context)
    {
        var input = (User)original;
        var result = new User();

        // Record 'result' as a copy of 'input'. Doing this
        // immediately after construction allows for data
        // structures that have cyclic references or duplicate
        // references. For example, imagine that 'input.BestFriend'
        // is set to 'input'. In that case, failing to record
        // the copy before trying to copy the 'BestFriend' field
        // would result in infinite recursion.
        context.RecordCopy(original, result);

        // Deep-copy each of the fields.
        result.BestFriend =
            (User)context.SerializationManager.DeepCopy(input.BestFriend);

        // strings in .NET are immutable, so they can be shallow-copied.
        result.NickName = input.NickName;
        // ints are primitive value types, so they can be shallow-copied.
        result.FavoriteNumber = input.FavoriteNumber;
        result.BirthDate =
            (DateTimeOffset)context.SerializationManager.DeepCopy(input.BirthDate);

        return result;
    }

    [SerializerMethod]
    public static void Serializer(
        object untypedInput, ISerializationContext context, Type expected)
    {
        var input = (User) untypedInput;

        // Serialize each field.
        SerializationManager.SerializeInner(input.BestFriend, context);
        SerializationManager.SerializeInner(input.NickName, context);
        SerializationManager.SerializeInner(input.FavoriteNumber, context);
        SerializationManager.SerializeInner(input.BirthDate, context);
    }

    [DeserializerMethod]
    public static object Deserializer(
        Type expected, IDeserializationContext context)
    {
        var result = new User();

        // Record 'result' immediately after constructing it.
        // As with the deep copier, this
        // allows for cyclic references and de-duplication.
        context.RecordObject(result);

        // Deserialize each field in the order that they were serialized.
        result.BestFriend =
            SerializationManager.DeserializeInner<User>(context);
        result.NickName =
            SerializationManager.DeserializeInner<string>(context);
        result.FavoriteNumber =
            SerializationManager.DeserializeInner<int>(context);
        result.BirthDate =
            SerializationManager.DeserializeInner<DateTimeOffset>(context);

        return result;
    }
}

Serializar tipos genéricos

O TargetType parâmetro de [Serializer(typeof(TargetType))] pode ser um tipo aberto-genérico, por exemplo, MyGenericType<T>. Nesse caso, a classe serializer deve ter os mesmos parâmetros genéricos que o tipo de destino. Orleans criará uma versão concreta do serializador em tempo de execução para cada tipo de concreto MyGenericType<T> que é serializado, por exemplo, um para cada um dos MyGenericType<int> e MyGenericType<string>.

Dicas para escrever serializadores e desserializadores

Muitas vezes, a maneira mais simples de escrever um par serializador/desserializador é serializar construindo uma matriz de bytes e gravando o comprimento da matriz no fluxo, seguido pela própria matriz e, em seguida, desserializar invertendo o processo. Se a matriz for de comprimento fixo, você poderá omiti-la do fluxo. Isso funciona bem quando você tem um tipo de dados que pode representar compactamente e que não tem subobjetos que podem ser duplicados (para que você não precise se preocupar com a identidade do objeto).

Outra abordagem, que é a abordagem que o Orleans tempo de execução adota para coleções como dicionários, funciona bem para classes com estrutura interna significativa e complexa: use métodos de instância para acessar o conteúdo semântico do objeto, serialize esse conteúdo e desserialize definindo o conteúdo semântico em vez do estado interno complexo. Nessa abordagem, os objetos internos são escritos usando SerializeInner e lidos usando DeserializeInner. Neste caso, é comum escrever uma copiadora personalizada também.

Se você escrever um serializador personalizado e ele acabar parecendo uma sequência de chamadas para SerializeInner para cada campo na classe, você não precisará de um serializador personalizado para essa classe.

Consulte também