Orleans 中的序列化自訂
Orleans 的其中一個重要層面是自訂序列化的支援,這是將物件或資料結構轉換為可儲存或傳輸的格式,並在稍後重新建構的程序。 這可讓開發人員在系統不同部分之間傳送資料時,控制資料的編碼和解碼方式。 序列化自訂可用於最佳化效能、互通性和安全性。
序列化提供者
Orleans 提供兩個序列化程式實作:
若要設定其中一個套件,請參閱 Orleans 中的序列化組態。
自訂序列化程式實作
若要建立自訂序列化程式實作,會涉及幾個常見的步驟。 您必須實作數個介面,然後向 Orleans 執行階段註冊序列化程式。 下列各節會更詳細地說明這些步驟。
首先,實作下列 Orleans 序列化介面:
- IGeneralizedCodec:支援多個類型的轉碼器。
- IGeneralizedCopier:提供複製多個類型物件的功能。
- ITypeFilter:允許負載類型和參與序列化和還原序列化的功能。
請考慮自訂序列化程式實作的下列範例:
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();
}
在上述範例實作中:
- 系統會明確實作每個介面,以避免與方法名稱解析衝突。
- 每個方法都會擲回 NotImplementedException 以表示方法未實作。 您必須實作每個方法,以提供所需的功能。
下一個步驟是向 Orleans 執行階段註冊序列化程式。 這通常是透過擴充 ISerializerBuilder 和公開自訂 AddCustomSerializer
擴充方法來達成。 下列範例示範一般模式:
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;
}
}
其他考量事項是公開可接受自訂實作專用自訂序列化選項的多載。 這些選項可以與產生器中的註冊一起設定。 這些選項可能是插入自訂序列化程式實作中的相依性。
Orleans 支援使用提供者模型與協力廠商序列化程式整合。 這需要本文自訂序列化一節中所述 IExternalSerializer 類型的實作。 某些常見序列化程式的整合會與 Orleans 一起維護,例如:
- 通訊協定緩衝區: Orleans.Serialization.ProtobufSerializer 來自 Microsoft.Orleans.OrleansGoogleUtils NuGet 套件。
- Bon:Orleans.Serialization.BondSerializer 來自 Microsoft.Orleans.Serialization.Bond NuGet 套件。
- Newtonsoft.Json:Orleans.Serialization.OrleansJsonSerializer 來自核心 Orleans 程式庫。
IExternalSerializer
的自訂實作如下一節所述。
自訂外部序列化程式
除了自動序列化產生之外,應用程式程式碼還可以為其選擇的類型提供自訂序列化。 Orleans 建議您針對大部分的應用程式類型使用自動序列化產生,並且只有在您認為手動編碼序列化程式可以提升效能時,才在罕見的情況下撰寫自訂序列化程式。 此附註描述如何執行此操作,並識別一些可能實用的特定案例。
應用程式有三種方式可自訂序列化:
- 將序列化方法新增至您的類型,並以適當的屬性 (CopierMethodAttribute、SerializerMethodAttribute、DeserializerMethodAttribute) 標示。 這個方法較適合您應用程式擁有的類型,也就是您可以新增方法的類型。
- 在設定期間實作
IExternalSerializer
並予以註冊。 這個方法適用於整合外部序列化程式庫。 - 使用其中 3 個序列化方法,以及上述的相同屬性,撰寫以
[Serializer(typeof(YourType))]
標註的個別靜態類別。 這個方法適用於應用程式未擁有的類型,例如,在應用程式無法控制的其他程式庫中定義的類型。
下列各節會詳細說明這些序列化方法。
自訂序列化簡介
Orleans 序列化會分三個階段進行:
- 物件會立即深層複製,以確保隔離。
- 在進行傳輸之前,物件會序列化為訊息位元組資料流。
- 傳遞至目標啟用時,系統會從接收的位元組資料流重新建立物件 (還原序列化)。
訊息中可能傳送的資料類型,也就是可能傳遞為方法引數或傳回值的類型,且必須有執行這三個步驟的相關聯常式。 我們將這些常式統稱為資料類型的序列化程式。
類型的複製器是獨立的,而序列化程式和還原序列化程式則是一組可一起運作的配對。 您可以只提供自訂複製器,或只提供自訂序列化程式和自訂還原序列化程式,也可以提供這三者的自訂實作。
序列化程式會在定址接收器啟動,以及在每次載入組件時註冊每個支援的資料類型。 需要註冊才能使用類型的自訂序列化程式常式。 序列化程式選取項目是以要複製或序列化物件的動態類型為基礎。 基於這個理由,不需要為抽象類別或介面建立序列化程式,因為系統永遠不會使用。
撰寫自訂序列化程式的時機
手動製作序列化程式常式很少會比產生的版本執行得更好。 如果您想要撰寫一個,您應該先考慮下列選項:
如果您的資料類型中有不需要序列化或複製的欄位或屬性,您可以使用 NonSerializedAttribute 來進行標記。 這會導致產生的程式碼在複製和序列化時跳過這些欄位。 盡可能使用 ImmutableAttribute 和 Immutable<T> 以避免複製不可變的資料。 如需詳細資訊,請參閱最佳化複製。 如果您避免使用標準泛型集合類型,請勿這麼做。 Orleans 執行階段包含泛型集合的自訂序列化程式,這些集合會使用集合的語意來最佳化複製、序列化和還原序列化。 這些集合在序列化位元組資料流中也有特殊的「縮寫」標記法,因而產生更高的效能優勢。 例如,
Dictionary<string, string>
的速度會比List<Tuple<string, string>>
快。最常見的情況是,自訂序列化程式可以提供明顯的效能提升,也就是當資料類型中編碼的重要語意資訊無法單純透過複製欄位值來使用時。 例如,疏鬆填入的陣列通常可透過將陣列視為索引/值組的集合來更有效率地序列化,即使應用程式將資料保留為完全實現的陣列以加快作業速度也是如此。
撰寫自訂序列化程式之前要實作的重點是確定產生的序列化程式會減損您的效能。 程式碼剖析將在這裡有所幫助,但更有價值的是以不同的序列化負載執行應用程式的端對端壓力測試,以量測系統層級的影響,而非序列化的微影響。 例如,建置不會對於粒紋方法傳遞或產生任何參數的測試版本,只要在任一端使用 canned 值,就會放大序列化和複製對系統效能的影響。
將序列化方法新增至類型
所有序列化程式常式都應該實作為其運作所在的靜態成員或結構。 這裡顯示的名稱並非必要專案;註冊是以個別屬性的存在為基礎,而非根據方法名稱。 請注意,序列化程式方法不一定是公用的。
除非您實作這三個序列化常式,否則您應該將類型標示為 SerializableAttribute,以便為您產生遺漏的方法。
複製器
複製器方法會標示為 Orleans.CodeGeneration.CopierMethodAttribute:
[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
// ...
}
複製器通常是要寫入的最簡單序列化程式常式。 其接受物件,保證與複製器定義的類型相同,且必須傳回物件語意上等同的複本。
如果複製物件時需要複製子物件,最好的方法是使用 SerializationManager.DeepCopyInner 常式:
var fooCopy = SerializationManager.DeepCopyInner(foo, context);
重要
請務必使用 SerializationManager.DeepCopyInner 而非 SerializationManager.DeepCopy,以維護完整複製作業的物件識別內容。
維護物件識別
複製常式的重要責任是維護物件識別。 Orleans 執行階段會為此目的提供協助程式類別。 在「手動」(非透過呼叫 DeepCopyInner
) 複製子物件之前,請檢查其是否已參考,如下所示:
var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
// Actually make a copy of foo
context.RecordObject(foo, fooCopy);
}
最後一行是呼叫 RecordObject,這是必要操作,以便未來可能參考到相同的物件,因為 foo
參考將會由 CheckObjectWhileCopying 正確地找到。
注意
這應該只針對類別執行個體完成,而「非」struct
執行個體或 .NET 基本類型,例如 string
、Uri
和 enum
。
如果您使用 DeepCopyInner
來複製子物件,則系統會為您處理物件身分識別。
序列化程式
序列化方法會標幟為 Orleans.CodeGeneration.SerializerMethodAttribute:
[SerializerMethod]
static private void Serialize(
object input,
ISerializationContext context,
Type expected)
{
// ...
}
如同複製器,傳遞至序列化程式的「輸入」物件保證為定義類型的執行個體。 系統可能會忽略「expected」類型;其為以資料項目的相關編譯時間類型資訊為基礎,並用於較高層級,以形成位元組資料流中的類型首碼。
若要序列化子物件,請使用 SerializationManager.SerializeInner 常式:
SerializationManager.SerializeInner(foo, context, typeof(FooType));
如果 foo 沒有特定的預期類型,您可以針對預期的類型傳遞 null。
BinaryTokenStreamWriter 類別提供各種不同的方法,可將資料寫入位元組資料流。 類別的執行個體可以透過 context.StreamWriter
屬性來取得。 如需文件,請參閱類別。
還原序列化程式
還原序列化方法會標幟為 Orleans.CodeGeneration.DeserializerMethodAttribute:
[DeserializerMethod]
static private object Deserialize(
Type expected,
IDeserializationContext context)
{
//...
}
系統可能會忽略「expected」類型;其為以資料項目的相關編譯時間類型資訊為基礎,並用於較高層級,以形成位元組資料流中的類型首碼。 要建立物件的實際類型一律是定義還原序列化程式的類別類型。
若要還原序列化子物件,請使用 SerializationManager.DeserializeInner 常式:
var foo = SerializationManager.DeserializeInner(typeof(FooType), context);
或者:
var foo = SerializationManager.DeserializeInner<FooType>(context);
如果 foo 沒有特定的預期類型,請使用非泛型 DeserializeInner
變體,並針對預期的類型傳遞 null
。
BinaryTokenStreamReader 類別提供各種不同的方法,可從位元組資料流讀取資料。 類別的執行個體可以透過 context.StreamReader
屬性來取得。 如需文件,請參閱類別。
撰寫序列化程式提供者
在此方法中,您會在用戶端和 GlobalConfiguration 定址接收器上的 ClientConfiguration 實作 Orleans.Serialization.IExternalSerializer 並將其新增至 SerializationProviderOptions.SerializationProviders 屬性。 如需設定的相關資訊,請參閱序列化提供者。
IExternalSerializer
的實作會遵循先前針對序列化所描述的模式,以及 Orleans 用來判斷序列化程式是否支援指定類型的 Initialize
方法與 IsSupportedType
方法。 這是介面定義:
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);
}
撰寫個別類型的序列化程式
在此方法中,您會撰寫使用屬性 [SerializerAttribute(typeof(TargetType))]
標註的新類別,其中 TargetType
是要序列化的類型,以及實作 3 個序列化常式。 如何撰寫這些常式的規則與實作 IExternalSerializer
時相同。 Orleans 會使用 [SerializerAttribute(typeof(TargetType))]
來判斷這個類別是 TargetType
的序列化程式,且如果此屬性能夠序列化多個類型,則可以在相同的類別上多次指定這個屬性。 以下是此類類別的範例:
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;
}
}
序列化泛型型別
[Serializer(typeof(TargetType))]
的 TargetType
參數可以是開放式泛型型別,例如 MyGenericType<T>
。 在此情況下,序列化程式類別必須與目標型別具有相同的泛型參數。 Orleans 將會在執行階段針對每個序列化的具體 MyGenericType<T>
類型建立具體版本的序列化程式,例如,每個 MyGenericType<int>
和 MyGenericType<string>
各一個。
撰寫序列化程式和還原序列化程式的提示
撰寫序列化程式/還原序列化程式配對的最簡單方式通常是藉由建構位元組陣列並將陣列長度寫入資料流,後面接著陣列本身,然後反轉程序來還原序列化。 如果陣列是固定長度,您可以從資料流中省略。 當您的資料類型可以精簡表示,而且沒有可能重複的子物件 (因此您不需要擔心物件識別) 時,這非常有用。
另一種方法是 Orleans 執行階段所採用針對字典之類集合的方法,適用於具有重要且複雜內部結構的類別:使用執行個體方法來存取物件的語意內容、序列化該內容,以及藉由設定語意內容而非複雜內部狀態來還原序列化。 在此方法中,內建物件是使用 SerializeInner 撰寫,並使用 DeserializeInner 讀取。 在此情況下,通常也會撰寫自訂複製器。
如果您撰寫自訂序列化程式,且看起來像是類別中每個欄位的 SerializeInner 呼叫序列,則不需要該類別的自訂序列化程式。