Partager via


Sérialisation dans Orleans

Globalement, deux types de sérialisation sont utilisés dans Orleans :

  • Sérialisation des appels de grain : utilisée pour sérialiser les objets passés vers et à partir de grains.
  • Sérialisation du stockage des grains : utilisée pour sérialiser des objets vers et depuis des systèmes de stockage.

Cet article traite principalement de la sérialisation des appels de grain via le framework de sérialisation inclus dans Orleans. La section Des sérialiseurs de stockage des grains décrit la sérialisation du stockage des grains.

Utiliser la sérialisation Orleans

Orleans inclut une infrastructure de sérialisation avancée et extensible appelée Orleans. Sérialisation. L’infrastructure de sérialisation incluse dans Orleans est conçue pour répondre aux objectifs suivants :

  • Hautes performances : le sérialiseur est conçu et optimisé pour les performances. D’autres informations sont disponibles dans cette présentation.
  • Haute fidélité : le sérialiseur représente fidèlement la plupart du système de types de .NET, y compris la prise en charge des génériques, du polymorphisme, des hiérarchies d’héritage, de l’identité d’objet et des graphes cycliques. Les pointeurs ne sont pas pris en charge, car ils ne sont pas portables entre les processus.
  • Flexibilité : vous pouvez personnaliser le sérialiseur pour prendre en charge les bibliothèques tierces en créant des substitutions ou en déléguant vers des bibliothèques de sérialisation externe telles que System.Text.Json, Newtonsoft.Json et Google.Protobuf.
  • Tolérance de version : le sérialiseur permet aux types d’applications d’évoluer au fil du temps, en prenant en charge :
    • Ajout et suppression de membres
    • Sous-classement
    • Élargissement et rétrécissement numériques (par exemple, de int à long, de float à double)
    • Renommage de types

La représentation haute fidélité des types est assez rare pour les sérialiseurs, donc certains points justifient une explication supplémentaire :

  1. Types dynamiques et polymorphisme arbitraire : Orleans n’applique pas de restrictions sur les types passés dans les appels de grain et conserve la nature dynamique du type de données réel. Cela signifie, par exemple, si une méthode d’une interface graine est déclarée à accepter IDictionary, mais au moment de l’exécution, l’expéditeur transmet un SortedDictionary<TKey,TValue>, le récepteur obtient effectivement un SortedDictionary (même si l’interface « contrat statique » /grain n’a pas spécifié ce comportement).

  2. Gestion de l’identité de l’objet : si le même objet est passé plusieurs fois dans les arguments d’un appel de grain ou est indirectement pointé vers plusieurs fois à partir des arguments, Orleans sérialise celui-ci une seule fois. Côté récepteur, Orleans restaure toutes les références correctement afin que deux pointeurs vers le même objet pointent toujours vers le même objet après la désérialisation. La préservation de l’identité d’objet est importante dans les scénarios suivants : Imaginez que le grain A envoie un dictionnaire avec 100 entrées au grain B et 10 clés dans le dictionnaire pointent vers le même objet, objsur le côté A. Si l’identité des objets n’était pas préservée, B recevrait un dictionnaire de 100 entrées avec ces 10 clés pointant vers 10 clones différents d’obj. Avec l’identité d’objet conservée, le dictionnaire côté B ressemble exactement au côté A, avec ces 10 clés pointant vers un seul objet obj. Notez que, étant donné que les implémentations de code de hachage de chaîne par défaut dans .NET sont aléatoires par processus, l’ordre des valeurs dans les dictionnaires et les jeux de hachage (par exemple) peut ne pas être conservé.

Pour permettre la tolérance de version, le sérialiseur vous demande d’être explicite sur les types et les membres qui sont sérialisés. Nous avons essayé de rendre cela aussi indolore que possible. Marquez tous les types sérialisables avec Orleans.GenerateSerializerAttribute pour indiquer Orleans de générer du code de sérialiseur pour votre type. Une fois que vous avez effectué cette opération, vous pouvez utiliser le correctif de code inclus pour ajouter les éléments requis Orleans.IdAttribute aux membres sérialisables sur vos types, comme illustré ici :

Image animée du correctif de code disponible suggéré et appliqué sur GenerateSerializerAttribute quand le type contenant ne contient pas IdAttribute sur ses membres.

Voici un exemple de type sérialisable dans Orleans, illustrant comment appliquer les attributs.

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

Orleans prend en charge l’héritage et sérialise les couches individuelles de la hiérarchie séparément, ce qui leur permet d’avoir des ID de membre distincts.

[GenerateSerializer]
public class Publication
{
    [Id(0)]
    public string Title { get; set; }
}

[GenerateSerializer]
public class Book : Publication
{
    [Id(0)]
    public string ISBN { get; set; }
}

Dans le code précédent, notez que les deux Publication et Book ont des membres avec [Id(0)], même si Book dérive de Publication. Il s'agit de la pratique recommandée dans Orleans, car les identificateurs de membre sont limités au niveau d'héritage, et non au type dans son ensemble. Vous pouvez ajouter et supprimer des membres de Publication et Book indépendamment, mais vous ne pouvez pas insérer une nouvelle classe parente dans la hiérarchie une fois l'application déployée, sauf à envisager des mesures particulières.

Orleans prend également en charge la sérialisation de types avec les membres internal, privateet readonly, comme dans cet exemple de type :

[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}";
}

Par défaut, Orleans sérialise votre type en encodant son nom complet. Vous pouvez le remplacer en ajoutant un Orleans.AliasAttribute. Cela entraîne la sérialisation de votre type à l’aide d’un nom résistant aux changements comme le renommage de la classe sous-jacente ou le déplacement de la classe entre les assemblies. Les alias de type sont délimités à l’échelle globale, et vous ne pouvez pas avoir deux alias avec la même valeur dans une application. Pour les types génériques, la valeur de l’alias doit inclure le nombre de paramètres génériques précédés d’un backtick ; par exemple, MyGenericType<T, U> peut avoir l’alias [Alias("mytype`2")].

Sérialisation des types record

Les membres définis dans le constructeur principal d’un enregistrement ont des ID implicites par défaut. En d’autres termes, Orleans prend en charge la sérialisation des types record. Cela signifie que vous ne pouvez pas modifier l’ordre des paramètres d’un type déjà déployé, car cela interrompt la compatibilité avec les versions précédentes de votre application (dans un scénario de mise à niveau propagée) et avec des instances sérialisées de ce type dans le stockage et les flux. Les membres définis dans le corps d’un type d’enregistrement ne partagent pas d’identités avec les paramètres du constructeur principal.

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

Si vous ne souhaitez pas que les paramètres du constructeur principal soient automatiquement inclus en tant que champs sérialisables, utilisez [GenerateSerializer(IncludePrimaryConstructorParameters = false)].

Substituts pour la sérialisation des types étrangers

Parfois, vous devrez peut-être passer des types entre les grains sur lesquels vous n’avez pas de contrôle total. Dans ce cas, la conversion manuelle vers et à partir d’un type défini sur mesure dans votre code d’application peut être impraticable. Orleans offre une solution pour ces situations : types de substitution. Les substitutions sont sérialisées à la place de leur type cible et disposent de fonctionnalités leur permettant de convertir depuis et vers le type cible. Prenons l’exemple suivant d’un type étranger avec le type de substitution et le convertisseur correspondants :

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

Dans le code précédent :

  • MyForeignLibraryValueType est un type en dehors de votre contrôle, défini dans une bibliothèque consommatrice.
  • MyForeignLibraryValueTypeSurrogate est une correspondance de type substitut vers MyForeignLibraryValueType.
  • RegisterConverterAttribute spécifie que MyForeignLibraryValueTypeSurrogateConverter agit en tant que convertisseur pour mapper entre les deux types. La classe implémente l’interface IConverter<TValue,TSurrogate> .

Orleans prend en charge la sérialisation des types dans les hiérarchies de types (types dérivant d’autres types). Si un type étranger peut apparaître dans une hiérarchie de types (par exemple, en tant que classe de base pour l’un de vos propres types), vous devez également implémenter l’interface Orleans.IPopulator<TValue,TSurrogate> . Prenons l’exemple suivant :

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

Règles de contrôle de version

La tolérance de version est prise en charge si vous suivez un ensemble de règles lors de la modification des types. Si vous êtes familiarisé avec les systèmes comme Google Protocol Buffers (Protobuf), ces règles sont familières.

Types composés (class et struct)

  • L’héritage est pris en charge, mais la modification de la hiérarchie d’héritage d’un objet n’est pas prise en charge. Vous ne pouvez pas ajouter, modifier ou supprimer la classe de base d’une classe.
  • À l’exception de certains types numériques décrits dans la section Numerics ci-dessous, vous ne pouvez pas modifier les types de champs.
  • Vous pouvez ajouter ou supprimer des champs à tout moment dans une hiérarchie d’héritage.
  • Vous ne pouvez pas modifier les ID de champ.
  • Les ID de champ doivent être uniques pour chaque niveau d’une hiérarchie de types, mais peuvent être réutilisés entre les classes de base et les sous-classes. Par exemple, une Base classe peut déclarer un champ avec l’ID 0et une Sub : Base classe peut déclarer un champ différent avec le même ID. 0

Fonctions numériques

  • Vous ne pouvez pas modifier la signature d’un champ numérique.
    • Les conversions entre int et uint ne sont pas valides.
  • Vous pouvez modifier la largeur d’un champ numérique.
    • Par exemple, les conversions depuis int ou longulong vers ushort sont prises en charge.
    • Les conversions qui réduisent la largeur lèvent une exception si la valeur d’exécution du champ provoque un dépassement de capacité.
    • La conversion de ulong à ushort n’est prise en charge que si la valeur du runtime est inférieure à ushort.MaxValue.
    • Les conversions de double en float ne sont prises en charge que si la valeur d’exécution est comprise entre float.MinValue et float.MaxValue.
    • De même pour decimal, qui a une plage plus restreinte que double et float.

Copieurs

Orleans favorise la sécurité par défaut, y compris la sécurité contre certaines classes de bogues liés à la concurrence. En particulier, Orleans copie immédiatement les objets passés dans les appels de grain par défaut. Orleans. La sérialisation facilite cette copie. Lorsque vous appliquez Orleans.CodeGeneration.GenerateSerializerAttribute à un type, Orleans génère également des copieurs pour ce type. Orleans évite de copier des types ou des membres individuels marqués avec ImmutableAttribute. Pour plus d’informations, consultez Sérialisation de types immuables dans Orleans.

Bonnes pratiques de sérialisation

  • Donnez des alias à vos types à l’aide de l’attribut [Alias("my-type")]. Les types avec des alias peuvent être renommés sans rupture de la compatibilité.

  • Ne changez pas un record en class classique, ou vice-versa. Les enregistrements et les classes ne sont pas représentés de façon identique, car les enregistrements ont des membres du constructeur principal en plus des membres réguliers ; par conséquent, les deux ne sont pas interchangeables.

  • N’ajoutez pas de nouveaux types à une hiérarchie de types existante pour un type sérialisable. Vous ne devez pas ajouter de nouvelle classe de base à un type existant. Vous pouvez ajouter sans problème une nouvelle sous-classe à un type existant.

  • Remplacez les utilisations de SerializableAttribute par GenerateSerializerAttribute ainsi que les déclarations IdAttribute correspondantes.

  • Démarrez tous les ID de membre à zéro pour chaque type. Les ID d’une sous-classe et de sa classe de base peuvent se chevaucher en toute sécurité. Les deux propriétés de l’exemple suivant ont des ID égaux à 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; }
    }
    
  • Étendez les types de membre numériques selon les besoins. Vous pouvez étendre sbyte à short à int à long.

    • Vous pouvez affiner les types de membres numériques, mais cela entraîne une exception d’exécution si les valeurs observées ne peuvent pas être représentées correctement par le type restreint. Par exemple, int.MaxValue ne peut pas être représenté par un champ short, de sorte que la réduction d'un champ int à short peut entraîner une exception d'exécution si une telle valeur est rencontrée.
  • Ne changez pas la signature d’un membre de type numérique. Vous ne devez pas modifier le type d’un membre de uint vers int ou de int vers uint, par exemple.

Sérialiseurs de stockage de grains

Orleans inclut un modèle de persistance basé sur un fournisseur pour les grains, accessible via la propriété State ou par injection d’une ou de plusieurs valeurs IPersistentState<TState> dans votre grain. Avant Orleans 7.0, chaque fournisseur avait un mécanisme différent pour configurer la sérialisation. Dans la version 7.0, il existe désormais une interface de sérialiseur d’état à usage général, Orleans, offrant un moyen cohérent de personnaliser la sérialisation d’état pour chaque fournisseur. Les fournisseurs de stockage pris en charge implémentent un modèle impliquant la définition de la IStorageProviderSerializerOptions.GrainStorageSerializer propriété sur la classe d’options du fournisseur, par exemple :

La sérialisation du stockage de grains est actuellement par défaut Newtonsoft.Json pour sérialiser l’état. Vous pouvez la remplacer en modifiant cette propriété au moment de la configuration. L’exemple suivant illustre cela à l’aide de OptionsBuilder<TOptions> :

siloBuilder.AddAzureBlobGrainStorage(
    "MyGrainStorage",
    (OptionsBuilder<AzureBlobStorageOptions> optionsBuilder) =>
    {
        optionsBuilder.Configure<IMySerializer>(
            (options, serializer) => options.GrainStorageSerializer = serializer);
    });

Pour plus d’informations, consultez l’API OptionsBuilder.

Orleans dispose d’un framework de sérialisation avancé et extensible. Orleans sérialise les types de données transmis dans les messages de demande et de réponse liés au grain, ainsi que les objets d'état persistant du grain. Dans le cadre de cette infrastructure, Orleans génère automatiquement du code de sérialisation pour ces types de données. En plus de générer une sérialisation/desérialisation plus efficace pour les types déjà .NET-sérialisables, Orleans tente également de générer des sérialiseurs pour les types utilisés dans les interfaces de grain qui ne sont pas .NET-sérialisables. L’infrastructure inclut également un ensemble de sérialiseurs intégrés efficaces pour les types fréquemment utilisés : listes, dictionnaires, chaînes, primitives, tableaux, etc.

Deux fonctionnalités importantes du sérialiseur le distinguent de nombreux autres frameworks de Orleanssérialisation tiers : les types dynamiques/polymorphisme arbitraire et l’identité d’objet.

  1. Types dynamiques et polymorphisme arbitraire : Orleans n’applique pas de restrictions sur les types passés dans les appels de grain et conserve la nature dynamique du type de données réel. Cela signifie, par exemple, si une méthode d’une interface graine est déclarée à accepter IDictionary, mais au moment de l’exécution, l’expéditeur transmet un SortedDictionary<TKey,TValue>, le récepteur obtient effectivement un SortedDictionary (même si l’interface « contrat statique » /grain n’a pas spécifié ce comportement).

  2. Gestion de l’identité de l’objet : si le même objet est passé plusieurs fois dans les arguments d’un appel de grain ou est indirectement pointé vers plusieurs fois à partir des arguments, Orleans sérialise celui-ci une seule fois. Côté récepteur, Orleans restaure toutes les références correctement afin que deux pointeurs vers le même objet pointent toujours vers le même objet après la désérialisation. La préservation de l’identité d’objet est importante dans les scénarios suivants : Imaginez que le grain A envoie un dictionnaire avec 100 entrées au grain B et 10 clés dans le dictionnaire pointent vers le même objet, objsur le côté A. Si l’identité des objets n’était pas préservée, B recevrait un dictionnaire de 100 entrées avec ces 10 clés pointant vers 10 clones différents d’obj. Avec l’identité d’objet conservée, le dictionnaire côté B ressemble exactement au côté A, avec ces 10 clés pointant vers un seul objet obj.

Le sérialiseur binaire .NET standard fournit les deux comportements ci-dessus. Il était donc important que nous prenions en charge ce comportement Orleans standard et familier.

Sérialiseurs générés

Orleans utilise les règles suivantes pour déterminer les sérialiseurs à générer :

  1. Analysez tous les types dans tous les assembly qui référencent la bibliothèque principale Orleans.
  2. À partir de ces assemblys, générez des sérialiseurs pour les types directement référencés dans les signatures de méthode d’interface de grain ou les signatures de classe d’état, ou pour tout type marqué avec SerializableAttribute.
  3. En outre, une interface de grain ou un projet d’implémentation peut pointer vers des types arbitraires pour la génération de sérialisation en ajoutant des attributs au niveau de l'assembly KnownTypeAttribute ou KnownAssemblyAttribute. Celles-ci indiquent au générateur de code de générer des sérialiseurs pour des types spécifiques ou tous les types éligibles au sein d’un assembly. Pour plus d’informations sur les attributs de niveau assembly, consultez Appliquer des attributs au niveau de l’assembly.

Sérialisation de secours

Orleans prend en charge la transmission de types arbitraires au moment de l’exécution. Par conséquent, le générateur de code intégré ne peut pas déterminer l’ensemble des types qui seront transmis à l’avance. En outre, certains types ne peuvent pas avoir de sérialiseurs générés pour eux, car ils sont inaccessibles (par exemple private, ) ou ont des champs inaccessibles (par exemple, readonly). Ainsi, il est nécessaire de réaliser une sérialisation à la demande des types inattendus ou pour lesquels les sérialiseurs n'ont pas pu être générés à l'avance. Le sérialiseur responsable de ces types est appelé sérialiseur de secours. Orleans est livré avec deux sérialiseurs de secours :

Configurez le sérialiseur de repli en utilisant la propriété qui s'applique à la fois au FallbackSerializationProvider (client) et au ClientConfiguration (silos).

// Client configuration
var clientConfiguration = new ClientConfiguration();
clientConfiguration.FallbackSerializationProvider =
    typeof(FantasticSerializer).GetTypeInfo();

// Global configuration
var globalConfiguration = new GlobalConfiguration();
globalConfiguration.FallbackSerializationProvider =
    typeof(FantasticSerializer).GetTypeInfo();

Vous pouvez également spécifier le fournisseur de sérialisation de secours dans la configuration XML :

<Messaging>
    <FallbackSerializationProvider
        Type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
</Messaging>

BinaryFormatterSerializer est le sérialiseur de secours par défaut.

Avertissement

La sérialisation binaire avec BinaryFormatter peut être dangereuse. Pour plus d’informations, consultez le Guide de la sécurité de BinaryFormatter et le Guide de migration de BinaryFormatter.

Sérialisation des exceptions

Les exceptions sont sérialisées à l’aide du sérialiseur de secours. Avec la configuration par défaut, BinaryFormatter est le sérialiseur de secours. Par conséquent, vous devez suivre le modèle ISerializable pour garantir la sérialisation correcte de toutes les propriétés dans un type d’exception.

Voici un exemple de type d’exception avec une sérialisation correctement implémentée :

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

Bonnes pratiques de sérialisation

La sérialisation sert deux objectifs principaux dans Orleans :

  1. Comme format de transmission des données entre les grains et les clients au moment de l’exécution.
  2. Comme format de stockage pour rendre persistantes des données de longue durée pour une récupération ultérieure.

Les sérialiseurs générés par Orleans conviennent au premier objectif en raison de leur flexibilité, de leurs performances et de leur polyvalence. Ils ne sont pas aussi adaptés à la seconde utilisation, car ils ne sont pas explicitement tolérants à la version. Nous vous recommandons de configurer un sérialiseur à tolérance de version, tel que les mémoires tampons de protocole, pour les données persistantes. Protocol Buffers est pris en charge via Orleans.Serialization.ProtobufSerializer du package Microsoft.Orleans.OrleansGoogleUtils NuGet. Suivez les meilleures pratiques pour votre sérialiseur choisi pour garantir la tolérance de version. Configurez des sérialiseurs tiers à l’aide de la SerializationProviders propriété de configuration, comme décrit ci-dessus.