Sérialisation dans Orleans

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

  • Sérialisation d’appels de grain : permet de sérialiser des objets passés à des grains et à partir de grains.
  • Sérialisation du stockage de grains : permet de sérialiser des objets vers des systèmes de stockage et à partir de systèmes de stockage.

La majorité de cet article est consacrée à la sérialisation d’appels de grain via l’infrastructure de sérialisation incluse dans Orleans. La section Sérialiseurs de stockage de grains traite de la sérialisation du stockage de grains.

Utiliser la sérialisation Orleans

Orleans inclut une infrastructure de sérialisation avancée et extensible qui peut être appelée Orleans.Serialization. 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 des performances. D’autres informations sont disponibles dans cette présentation.
  • Haute fidélité : le sérialiseur représente fidèlement la majeure partie du système de type de .NET, notamment la prise en charge des génériques, le polymorphisme, les hiérarchies d’héritage, l’identité d’objet et les graphes cycliques. Les pointeurs ne sont pas pris en charge, car ils ne sont pas portables entre les processus.
  • Flexibilité : le sérialiseur peut être personnalisé pour prendre en charge des bibliothèques tierces en créant des substituts ou en déléguant à des bibliothèques de sérialisation externes 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 les opérations suivantes :
    • Ajout et suppression de membres
    • Définition de sous-classes
    • Augmentation et diminution numériques (par exemple : int en/à partir de long, float en/à partir de double)
    • Renommage de types

La représentation haute fidélité de types est assez rare pour les sérialiseurs. Certains points méritent donc d’être approfondis :

  1. Types dynamiques et polymorphisme arbitraire : Orleans n’applique pas de restrictions sur les types qui peuvent être passés dans les appels de grain et maintiennent la nature dynamique du type de données réel. Cela signifie, par exemple, que si la méthode dans les interfaces de grain est déclarée comme acceptant IDictionary mais qu’au moment de l’exécution l’expéditeur passe SortedDictionary<TKey,TValue>, le récepteur obtient en effet SortedDictionary (bien que l’interface de grain/« contrat statique » n’ait pas spécifié ce comportement).

  2. Conservation de l’identité des objets : si le même objet se voit transmettre plusieurs types dans les arguments d’un appel de grain ou est indirectement pointé plusieurs fois à partir des arguments, Orleans le sérialise une seule fois. Côté récepteur, Orleans restaure correctement toutes les références afin que deux pointeurs vers le même objet pointent toujours vers le même objet après la désérialisation. L’identité des objets est importante à conserver dans des scénarios comme celui qui suit. Imaginez que le grain A envoie un dictionnaire avec 100 entrées au grain B, et que 10 des clés du dictionnaire pointent vers le même objet, obj, du 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 la préservation de l’identité des objets, le dictionnaire du côté B ressemble exactement au dictionnaire du côté A avec ces 10 clés pointant vers un objet obj unique. Notez que, étant donné que les implémentations de code de hachage de chaîne par défaut dans .NET sont lues aléatoirement par processus, l’ordre des valeurs dans les dictionnaires et les jeux de hachage (par exemple) peut ne pas être conservé.

Pour prendre en charge la tolérance de version, le sérialiseur exige que les développeurs soient explicites sur les types et les membres qui sont sérialisés. Nous avons essayé de rendre cela aussi fluide que possible. Vous devez marquer tous les types sérialisables avec Orleans.GenerateSerializerAttribute afin d’indiquer à Orleans de générer le 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 le Orleans.IdAttribute nécessaire aux membres sérialisables de vos types, comme indiqué ici :

An animated image of the available code fix being suggested and applied on the GenerateSerializerAttribute when the containing type doesn't contain IdAttribute's on its members.

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 séparément les couches individuelles de la hiérarchie, ce qui leur permet d’avoir des ID de membres 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 Publication et Book ont tous les deux 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 membres sont limités au niveau de l’héritage, et non au type dans son ensemble. Des membres peuvent être ajoutés et supprimés dans Publication et Book indépendamment, mais une nouvelle classe de base ne peut pas être insérée dans la hiérarchie une fois que l’application a été déployée sans considération particulière.

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 codant son nom complet. Vous pouvez le remplacer en ajoutant un Orleans.AliasAttribute. Cela entraîne la sérialisation de votre type en utilisant un nom qui est résilient au renommage de la classe sous-jacente ou à son déplacement entre des assemblys. 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 d’alias doit inclure le nombre de paramètres génériques précédés d’un backtick (accent grave). 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. Ceci signifie que vous ne pouvez pas changer l’ordre des paramètres pour un type déjà déployé, car cela entraîne une rupture de la compatibilité avec les versions précédentes de votre application (dans le cas d’une mise à niveau propagée), et avec les 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 voulez pas que les paramètres du constructeur principal soient inclus automatiquement en tant que champs sérialisables, vous pouvez utiliser [GenerateSerializer(IncludePrimaryConstructorParameters = false)].

Substituts pour la sérialisation des types étrangers

Parfois, vous pouvez être amené à passer des types entre les grains sur lesquels vous n’avez pas un contrôle total. Dans ce cas, il peut s’avérer difficile d’effectuer une conversion manuelle depuis et vers un type personnalisé dans le code de votre application. Orleans offre une solution à ce genre de situation sous la forme de 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 hors de votre contrôle, défini dans une bibliothèque consommatrice.
  • MyForeignLibraryValueTypeSurrogate est un type de substitution mappé à MyForeignLibraryValueType.
  • RegisterConverterAttribute spécifie que MyForeignLibraryValueTypeSurrogateConverter agit en tant que convertisseur pour effectuer un mappage depuis et vers les deux types. La classe est une implémentation de l’interface IConverter<TValue,TSurrogate>.

Orleans prend en charge la sérialisation des types dans les hiérarchies de types (types qui dérivent d’autres types). Au cas où un type étranger apparaîtrait dans une hiérarchie de types (par exemple en tant que classe de base pour l’un de vos propres types), vous devez implémenter en plus 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 à condition que le développeur suive un ensemble de règles lors de la modification des types. Si le développeur a une bonne connaissance de systèmes tels que les mémoires tampon de protocole Google (Protobuf), ces règles lui 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. La classe de base d’une classe ne peut pas être ajoutée, remplacée par une autre classe ou supprimée.
  • À l’exception de certains types numériques, décrits dans la section Valeurs numériques ci-dessous, les types de champs ne peuvent pas être changés.
  • Il est possible d’ajouter ou de supprimer des champs à n’importe quel point d’une hiérarchie d’héritage.
  • Les ID de champ ne peuvent pas être modifiés.
  • Les ID de champ doivent être uniques pour chaque niveau d’une hiérarchie de types, mais ils peuvent être réutilisés entre des classes de base et des sous-classes. Par exemple, la classe Base peut déclarer un champ avec l’ID 0 et un autre champ peut être déclaré par Sub : Base avec le même ID, 0.

Fonctions numériques

  • La signature d’un champ numérique ne peut pas être modifiée.
    • Les conversions entre int et uint ne sont pas valides.
  • La largeur d’un champ numérique peut être modifiée.
    • Par exemple : les conversions de int en long ou de ulong en ushort sont prises en charge.
    • Les conversions qui diminuent la largeur lèvent une exception si la valeur d’exécution d’un champ provoque un dépassement de capacité.
      • Les conversions de ulong en ushort ne sont prises en charge que si la valeur au moment de l’exécution 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

Par défaut, Orleans favorise la sécurité. Cela inclut la sécurité de certaines classes de bogues d’accès concurrentiel. En particulier, par défaut, Orleans copie immédiatement les objets passés dans des appels de grain. Cette copie est simplifiée par Orleans.Serialization et quand Orleans.CodeGeneration.GenerateSerializerAttribute est appliqué à un type, Orleans génère également des copieurs pour ce type. Orleans évite de copier des types ou des membres individuels qui sont marqués à l’aide de 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 manière identique, car les enregistrements ont des membres de constructeurs principaux en plus des membres classiques. Les deux ne sont donc 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 sans problème. Les deux propriétés de l’exemple suivant ont un ID égal à 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 restreindre les types de membre 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. Ainsi, la restriction d’un champ int à short peut entraîner une exception d’exécution si une valeur de ce genre est rencontrée.
  • Ne changez pas la signature d’un membre de type numérique. Vous ne devez pas changer le type d’un membre de uint en int ou de int en 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 Orleans 7.0, il existe désormais une interface universelle de sérialisation d’état des grains, IGrainStorageSerializer, qui offre 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 qui implique la définition de la propriété IStorageProviderSerializerOptions.GrainStorageSerializer 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 le montre à 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 de grain, ainsi que les objets d’état persistant de grain. Dans le cadre de ce framework, 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/désérialisation plus efficace pour les types qui sont déjà sérialisables dans .NET, Orleans tente également de générer des sérialiseurs pour les types utilisés dans les interfaces de grain qui ne sont pas sérialisables dans .NET. 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 caractéristiques importantes du sérialiseur d’Orleans le distinguent de nombreux autres frameworks de sérialisation tiers : types dynamiques/polymorphisme arbitraire et identité des objets.

  1. Types dynamiques et polymorphisme arbitraire : Orleans n’applique pas de restrictions sur les types qui peuvent être passés dans les appels de grain et maintiennent la nature dynamique du type de données réel. Cela signifie, par exemple, que si la méthode dans les interfaces de grain est déclarée comme acceptant IDictionary mais qu’au moment de l’exécution l’expéditeur passe SortedDictionary<TKey,TValue>, le récepteur obtient en effet SortedDictionary (bien que l’interface de grain/« contrat statique » n’ait pas spécifié ce comportement).

  2. Conservation de l’identité des objets : si le même objet se voit transmettre plusieurs types dans les arguments d’un appel de grain ou est indirectement pointé plusieurs fois à partir des arguments, Orleans le sérialise une seule fois. Côté récepteur, Orleans restaure correctement toutes les références afin que deux pointeurs vers le même objet pointent toujours vers le même objet après la désérialisation. L’identité des objets est importante à conserver dans des scénarios comme celui qui suit. Imaginez que le grain A envoie un dictionnaire avec 100 entrées au grain B, et que 10 des clés du dictionnaire pointent vers le même objet, obj, du 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 la préservation de l’identité des objets, le dictionnaire côté B ressemble exactement au dictionnaire côté A avec ces 10 clés pointant vers un objet obj unique.

Les deux comportements ci-dessus sont fournis par le sérialiseur binaire .NET standard, et il était donc important pour nous de prendre en charge ce comportement standard et familier également dans Orleans.

Sérialiseurs générés

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

  1. Analysez tous les types dans tous les assemblys qui référencent la bibliothèque principale Orleans.
  2. En dehors de ces assemblys : générez des sérialiseurs pour les types qui sont directement référencés dans la signature de classe d’état ou les signatures de méthode d’interfaces de grain ou pour tout type marqué avec SerializableAttribute.
  3. En outre, un projet d’implémentation ou d’interface de grain peut pointer vers des types arbitraires pour la génération de sérialisation en ajoutant des attributs de niveau assembly KnownTypeAttribute ou KnownAssemblyAttribute pour indiquer 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 et, 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). Par conséquent, il est nécessaire de sérialiser juste-à-temps des types qui étaient inattendus ou qui n’ont pas pu avoir de sérialiseurs 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 :

Le sérialiseur de secours peut être configuré à l’aide de la propriété FallbackSerializationProvider sur ClientConfiguration sur le client et GlobalConfiguration sur les silos.

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

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

Le fournisseur de sérialisation de secours peut également être spécifié dans la configuration XML :

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

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

Sérialisation des exceptions

Les exceptions sont sérialisées à l’aide du sérialiseur de secours. À l’aide de la configuration par défaut, BinaryFormatter est le sérialiseur de secours et le modèle ISerializable doit donc être suivi afin de 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 conviennent pas autant pour le deuxième objectif, car ils ne présentent pas une tolérance de version explicite. Il est recommandé aux utilisateurs de configurer un sérialiseur à tolérance de version, tel que les mémoires tampon de protocole pour les données persistantes. Les mémoires tampon de protocole sont prises en charge via Orleans.Serialization.ProtobufSerializer à partir du package NuGet Microsoft.Orleans.OrleansGoogleUtils. Les bonnes pratiques relatives au sérialiseur particulier choisi doivent être utilisées pour garantir la tolérance de version. Des sérialiseurs tiers peuvent être configurés à l’aide de la propriété de configuration SerializationProviders, comme décrit ci-dessus.