C# Binary Serialization

Bu makaleyi okumaya başlamadan önce C# Serialization ve DeSerialization makalemi okumanızı tavsiye ederim.

Binary serileştirme tip sistem güvenilirliğini korumaktadır (preserves type system fidelity). Yani nesnelerin serileştirme sırasında tip bilgilerini sakladığından tip sistem güvenilirliğide korunmuş olur. Örneğin MyClass sınıfı tipinde bir nesne serileştirilirken, ters-serileştirme sırasında MyClass sınıfı tipindeki nesnenin tekrar oluşturulacağından emindir.

Tip sistem güvenirliği (type system fidelity) mekanizması serileştirme ve ters-serileştirmede işleminde aynı sistem tipinin kullanılması için katkıda bulunur. Tip sistem güvenirliğinden dolayı binary serileştirme nesneleri istemci (client) ve sunucu (server) arasında göndermek için kullanışlıdır. XML serileştirme tip sistem güvenirliğini desteklememektedir.

Binary serileştirme işlemi sırasında, nesnenin public ve private tüm özellikleri, tipin ait olduğu sınıfı ve hangi assembly içerisinde yer aldığı bilgileri byte stream’e dönüştürülerek bu verileri içerecek bir stream’e yazılır. Ters-serileştirme işlemi sırasında, nesnenin bire bir aynısı tip bilgilerine bakılarak tekrar oluşturulur.

Basit Serileştirme

Serileştirme ve ters-serileştirme işlemlerini gerçekleştiren sınıfa formatter denilmektedir. Formatter serileştirme ve ters-serileştirme yapmak için System.Runtime.Serialization.IFormatter interface’i ile genişletilmiştir.

Bir tip tasarlanırken, geliştirici bu tipin serileştirilebilir olup olmayacağı ile ilgili bir karar vermelidir. Eğer serileştirtilebilir bir tasarım yapıyor ise aşağıdaki noktaları göz önünde bulundurmalıdır :

  • Varsayılan olarak tipler serileştirilemezlerdir. Bir tipi serileştirebilmek için en kolay yol [Serializable] etiketi ile işaretlemektir.
  • [Serializable] etiketi sadece referans tipleri, değer tipleri, enumerator’ler ve delegate’lere uygulanabilmektedir.
  • Bir tip ile genişletilmiş veya bir tipten türetilmiş bir nesne serileştirilecek ise genişletilen ve türetilen tipler [Serializable] etiketi ile işaretlenmelidir.
  • Eğer türeyen sınıf [Serializable] etiketi ile işaretlenmiş ise, tüm ana sınıflar [Serializable] etiketi ile işaretlenmelidir. Eğer ana sınıflardan herhangi biri [Serializable] etiketi ile işaretlenmez ise aşağıdaki hata alınır :
    Serialization Exception
  • Aynı şekilde bir tip [Serializable] etiketi ile işaretlenmiş ve bir yada birden fazla özelliği serileştirilebilir olmasına rağmen referans ettiği tip serileştirilebilir değil ise aynı hata alınır.
  • Enumeration ve delegate tipleri her zaman serileştirilebilir olduğundan [Serializable] etiketi ile işaretlemeye gerek yoktur.

Örnek 1 :

Aşağıdaki örnek kodda basit bir serileştirme ve ters-serileştirme’nin nasıl yapılacağı anlatılmaktadır :

[Serializable()]
public class MyClass
{
    private string sPrivate;
    public string sPublic;

    public MyClass(string arg1, string arg2)
    {
        sPrivate = arg1;
        sPublic = arg2;
    }
}
private void btnBasicBinarySerialization_Click(object sender, EventArgs e)
{
    SerializeObject();
    DeSerializeObject();
}
private void SerializeObject()
{
    // Serileştirilebilir tipin instance'ı oluşturuluyor.
    MyClass ob = new MyClass("Private veri", "Public veri");

    // Serileştirilecek nesneyi taşıyacak stream oluşturuluyor.
    // Bu stream System.IO.Stream soyut sınıfından türemiş herhangi
    // bir stream (MemoryStream, FileStream, NetworkStream) olabilir.
    // Örneğimizde FileStream kullanarak serileştirme işlemi yapacağız.
    Stream stream = new FileStream("SerializedFile.dat", FileMode.Create, FileAccess.Write, FileShare.Write);

    // Nesneyi serileştirmek için BinaryFormatter hazırlanıp hazırlanıp,
    // serileştirme gerçekleştiriliyor.
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    formatter.Serialize(stream, ob);

    // Stream kapatılıyor
    stream.Close();
}
private void DeSerializeObject()
{
    // Dosyadan okunup stream'e yazılıyor
    Stream stream = new FileStream("SerializedFile.dat", FileMode.Open, FileAccess.Read, FileShare.Read);

    // BinaryFormatter oluşturuluyor.
    // Serileştirme ve ters- serileştirmede aynı formatter ile işlem yapmamız
    // gerektiğini unutmamak gerekir
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

    // Nesne ters-serileştirilip serileştirildiğindeki durumu elde ediliyor.
    // Burada dikkat edilmesi gereken nokta tipin
    // yüklenicisi (constructor)'nin çağırılmayışıdır.
    MyClass ob = (MyClass)formatter.Deserialize(stream);
    MessageBox.Show(ob.sPublic);

    // Stream kapatılıyor
    stream.Close();
}

MyClass tipindeki nesnemizin serileştirilmiş durumunu barındıran SerializedFile.dat dosyasını incelediğimizde aşağıdaki gibi bir çıktı ile karşılaşırız :

 ÿÿÿÿ          GSerializationEx1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
SerializationEx1.MyBasicClass  sPrivatesPublic      Private veri   Public veri

Dosya içeriğinde dikkat edileceği üzere MyClass sınıfının private üyesi olan sPrivate özelliği serileştirilmiştir. Bu açıdan bakıldığında binary serileştirme ile XML serileştirme arasındaki fark XML serileştirme sadece public alanları serileştirmesidir. Dikkat edilmesi gereken önemli bir etkende ters-serileştirmede sınıf yükleniyicilerinin (constructors)’ın çalıştırılmayışıdır.

Eğer MyClass sınıfı ana (base) bir sınıftan türemiş ve ana sınıf [Serializable] etiketi ile işaretlenmemiş olsaydı SerializationException alınacaktır.

Örnek 2 :

Tüm nesneler BinaryFormatter ile serileştirilip sonrasında ters-serileştirilebilir. Buda .Net içerisinde nesne serileştirip/ters-serileştirmek için ideal bir seçim olmaktadır. Eğer taşınabilirlik (portability ) gerektiğinde binary formatter yerine SoapFormatter kullanarak işlemlerinizi gerçekleştirebilirsiniz. SoapFormatter aşağıdaki fark dışında BinaryFormatter ile aynı aynı şekilde kullanılabilmektedir.

SoapFormatter için yapılacak değişiklik:

// Aşağıdaki satır ;
System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

// Bu satır ile değiştirilmelidir.
System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();

SoapFormatter ile serileştirilmiş nesne aşağıdaki gibi bir çıktı verecektir :

<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:MyClass id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/SerializationEx1/SerializationEx1%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<sPrivate id="ref-3">Private soap veri</sPrivate>
<sPublic id="ref-4">Public soap veri</sPublic>
</a1:MyClass>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Örnek 3 :

Serileştirilmiş nesne çeşitli lokasyonlara gönderilebilir ; örneğin uygulamanın çalıştığı aynı process’e, aynı istemci yada sunucuda çalışan farklı process’e, faklı istemci yada sunucuda çalışan farklı bir process’e vb. Böyle belirli durumlar için nesne hangi durumda ters-serileştirileceğini bilmek isteyecektir. Örneğin bir nesne bir semaphore tarafından kullanılmakta ve kullanıldığı zamanın bir anında serileştileştirilmiş olsun bu nesneyi ters-serileştirip o andaki halini elde etmek için bu işlemi aynı process içerisinde yapılmalıdır.

(Multi thread programlama yapılırken ortak kullanılan (paylaşılan) nesnelerin assembly seviyesinde istenmiyen işlemlerde kullanılmasını önlemek için kullanılan yönteme Semaphore denilmektedir.)

Eğer nesne semaphore ismi ile birlikte serileştirilmiş ise ters-serileştirme işlemide aynı istemci/sunucu üzerinde ise yapılabilir. Eğer nesne farklı bir istemci yada sunucu üzerinde ters-serileştiriliyor ise exception alınır. Diğer bir değişle nesne nerede ters-serileştirileceğini bilmediği için exception üretecektir.

Böyle bir durumu aşmak için StreamingContext yapısından faydalanarak StreamingContext.State özelliğini değiştirerek nesnenin nerede serileştirilip nerede ters-serileştirileceğini belirtebilmekteyiz. Aşağıdaki örnekte serileştirilecek nesnenin o anki kopyası (clone)’u oluşturuyoruz. StreamingContextStates enumaration’ından faydalanarak farklı durumlara özel işlemlerinizi gerçekleştirebilirsiniz.

(StreamingContextStates’in alabileceği değerler ; All, Clone, CrossAppDomain, CrossMachine, CrossProcess, File, Other, Persistance ve Remoting dir.)

private object DeepCloneForSerialization(object obSource)
{
    // Serileştirilecek nesneyi taşıyacak stream oluşturuluyor.
    Stream stream = new MemoryStream();

    // BinaryFormatter oluşturuluyor.
    BinaryFormatter formatter = new BinaryFormatter();

    // Yukarıdaki açıklamalara istinaden nesne ile nasıl çalışalacı belirtiliyor.
    formatter.Context = new StreamingContext(StreamingContextStates.Clone);

    // Nesne memory stream'e serileştiriliyor.
    formatter.Serialize(stream, obSource);

    // Stream pozisyonu başa alınıyor.
    stream.Position = 0;

    stream.Close();

    // Parametre olarak gelen nesnenin kopyası (clone'u) farklı bir nesneye atanıyor.
    object obTarget = formatter.Deserialize(stream);

    return obTarget;
}

Örnek 4 :

Birden fazla nesneyi tek bir stream üzerinde serileştirebilirsiniz. Çok basit bir işlemle bu durum bize kullanışlı bir kullanım sağlamaktadır :

private Stream SerializeMultipleObjects()
{
    // Serileştirilecek nesneleri taşıyacak stream oluşturuluyor.
    Stream stream = new MemoryStream();

    // BinaryFormatter oluşturuluyor.
    BinaryFormatter formatter = new BinaryFormatter();

    // Birden fazla nesne aynı stream üzerine serileştiriliyor.
    formatter.Serialize( stream, objOrders );
    formatter.Serialize( stream, objProducts );
    formatter.Serialize( stream, objCustomers );

    return stream;
}

private void DeSerializeMultipleObject(Stream stream)
{
    // BinaryFormatter oluşturuluyor.
    BinaryFormatter formatter = new BinaryFormatter();

    // Nesneler ters-serileştiriliyor.
    Orders     objOrders    = (Orders)formatter.Deserialize(stream);
    Products   objProducts  = (Products)formatter.Deserialize(stream);
    Customers  objCustomers = (Customer)formatter,Deserialize(stream)
}
Basit serileştirme örneklerini buraya tıklayarak indirebilirsiniz.

Formatlayıcılar (Formatters)

Formatlayıcılar bir nesneyi nasıl serileştirebileceğini nesneye referans eden meta veriyi bilmektedirler. Serialize metodu reflection kullanarak nesnenin tüm özelliklerini ve tiplerini anlamaktadır. Eğer serileştirilecek nesnenin özelliklerden herhangi biri başka bir nesneye referans ediyorsa Serialize metodu aynı şekilde bu nesneyi nasıl serileştireceğini bilmektedir.

Formatlayıcılar akıllı bir algoritmaya sahiptirler. Bir stream üzerinde birden fazla nesneyi serileştirebilirler. Örneğin serileştirilmeye çalışılan 2 nesne tamamen aynı özelliklere sahip ise (iki nesnede aynı yere referans ediyorsa) formatlayıcı bunu anlar ve sadece bir nesne serileştirir. Aynı şekilde formatlayıcı ters-serileşirme yaparken stream içeriğini inceleyerek içeriğinde yer alan tüm nesnelerin instance’larını oluşturur; nesneler ve özellikleri serileştirildiği zamanki değerlere sahip olmuş olur. (Burada dikkat edilmesi gereken nokta ters-serileştirme işleminde nesne yükleyicisinin çalışıtırılmıyor oluşudur.)

Formatlayıcılar ve Assembly İsimleri

Serileştirirken..

Formatlayıcı bir nesneyi serileştirirken tipin tam adını (MyNamespace.MySubNamespace.MyClass vb.) ve tipin hangi assembly içerisinde (MyAssembly.dll vb.) yer aldığını stream’e yazar. Varsayılan olarak BinaryFormatter ve SoapFormatter çıktı olarak tipin yer aldığı assembly kimlik bilgilerinin (name, version, public key, and culture) çıktısını üretir. Eğer tüm bu bilgiler size gereksiz geliyorsa sadece nesnenin hangi assembly içerisinde yer aldğını öğrenmek istiyorsanız formatlayıcının AssemblyFormat özelliğini FormatterAssemblyStyle.Simple olarak değiştirmeniz yeterli olacaktır. (Varsayılan olarak bu özellik FormatterAssemblyStyle.Full seçilidir. )

Ters-Serileştirilirken..

Formatlayıcılar bir nesneyi ters-serileştireceği zaman ilk önce assembly kimliklerine ve sonrasında çalışılan AppDomain’de o assembly’nin yüklü olduğundan emin olmak için bakarlar. Formatlayıcı AssemblyFormat özelliğine bağlı olarak yüklenen assembly’nin hangi değerlere sahip olarak yüklendiğini bilir :

  • FormatterAssemblyStyle.Full

    Bu enum değeri ile yüklenen assembly System.Reflection.Assembly.Load metodu kullanılarak yüklenir ve ilk önce yüklenecek assembly’i GAC’de arar bulamazsa uygulamanın çalıştığı dizine bakar. Eğer assembly’i bulamaz ise exception üretir ve ters-serileştirme yapılamaz.

  • FormatterAssemblyStyle.Simple

    Bu enum değeri ile yüklenen assembly ise System.Reflection.Assembly.LoadWithPartialName metodu kullanılarak yüklenir ve ilk önce yüklenecek assembly’i uygulama dizininde arar bulamazsa GAC’e bakarak aynı assembly’nin en yüksek versiyonlu olanını kullanır.

Assembly yüklendikten sonra formatlayıcı serileştirilen nesnenin tipinin assembly içerisindeki bir tip ile uyuşup uyuşmadığını kontrol eder. Eğer uyuşan bir tip yok ise exception üretir ve başka nesne ters-serileştirilmez. Eğer uyuşan bir tip bulunur ise intance’ı oluşturulur (constructor’ı çağırılmaz) ve tüm özellikleri stream’ yazıldığı andaki değerler ile yüklenir.

Formatlayıcılar ve Dinamik Yüklenen Assembly’ler

Bazı uygulamalar Assembly.LoadFrom metodunu kullanarak assembly’leri yüklerler ve assembly içerisindeki tanımlı tiplerden kullanacakları nesneleri oluştururlar. Bu şekildeki nesnelerde sorunsuz bir şekildede serileştirilebilmektedir. Ancak bu nesneler ters-serileştirilirken, formatlayıcı Assembly.LoadFrom metodu yerine Assembly.Load veya Assembly.LoadWithPartialName metodlarından birini kullanarak assembly’i yüklemeye çalışır. Çoğu durumda da yüklemeye zamanında hata alınır.

Bu duruma çözüm olarak, nesne ters-serileştirilmeden önce System.ResolveEventHandler delegesinin imzası ile uyuşan bir metod oluşturulur ve bu metod System.AppDomain.AssemblyResolve event’ı ile birlikte kayıt edilir. Formatlayıcı assembly’i yüklerken hata aldığında ise CLR oluşturmuş olduğunuz System.ResolveEventHandler metoduma yükleyemediği assembly’nin kimlik bilgilerini gönderir ve bu bilgi sayesinde Assembly.LoadFrom metodu kullanılarak ilgili assembly yüklenebilir.

Örnek bir kullanımı aşağıdaki gibi olacaktır :

// Ters-serileştirme çalıştırılmadan önce çalıştırılacak ve kaydedilecek metod
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

// Delege ile imzası uyuşan metod
// Dikkat edileceği üzere çalışılan AppDomain içerisindeki assembly'ler yüklenmektedir.
// Eğer assembly'leriniz farklı bir dizinde ise Assembly.LoadFrom ile bunu belirtebilirsiniz.
System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    System.Reflection.Assembly ayResult = null;
    string sShortAssemblyName = args.Name.Split(',')[0];
    System.Reflection.Assembly[] ayAssemblies = AppDomain.CurrentDomain.GetAssemblies();
    foreach (System.Reflection.Assembly ayAssembly in ayAssemblies)
    {
        if (sShortAssemblyName == ayAssembly.FullName.Split(',')[0])
        {
            ayResult = ayAssembly;
            break;
        }
    }
    return ayResult;
}

İkinci bir çözüm olarak uygulamanızın config dosyasına codebase yada probing elementi ile uygulamanızın kullanacağı assembly’leri içeren klasörleri belirmek olacaktır.

Formatlayıcılar ve Arka Plan

Bu bölümde formatlayıcıların nesneleri nasıl serileştirip/ters-serileştirdiğinin detayını inceleyeceğiz. Bu bilgiler serileştirme işlemlerinin nasıl çalıştığını anlamanıza yardımcı olacaktır.

Aşağıdaki adımlar ile formatlayıcının [Serializable] etiketi uygulanmış olan tiplerin nesnelerini nasıl serileştirdiğini gözlemlemiş olacağız :

  • Formatlayıcı System.Runtime.Serialization namespace’i içerisinde yer alan FormatterServices.GetSerializableMemebers metodu kullanarak serileştirilecek tipin serileştirilebilir tüm alanlarını MemberInfo nesne dizisi şeklinde alır. (MemberInfo nesnesine ayrıca serileştirme zamanında erişilebilmektedir.)
  • Serileştirilen nesne ve MemberInfo dizisi System.Runtime.Serialization namespace’i içerisinde yer alan FormatterServices.GetObjectData metoduna parametre olarak gönderilir, bu nesne dizisi içerisinde yer alan her bir dizi elemanı serileştirilen bir alanı ifade eder.
  • Formatlayıcı assembly’nin kimlik bilgilerini ve tipin tam adını stream’e yazar.
  • Son olarak formatlayıcı dizi üzerinde gezinerek her dizi elemanı için adını ve değerini stream’e yazar.

Aşağıdaki adımlar ile formatlayıcının [Serializable] etiketi uygulanmış olan tiplerin nesnelerini nasıl ters-serileştirdiğini gözlemlemiş olacağız :

  • Formatlayıcı System.Runtime.Serialization namespace’i içerisinde yer alan FormatterServices.GetUninitializedObject metodunu çalıştırarak ters-serileştirilecek yeni nesne için memory’de yer açar fakat constructor’ı çalıştırılmaz.
  • Formatlayıcı MemberInfo dizisini oluşturarak gerekli bilgileri yükler.
  • Formatlayıcı verileri içeren stream’de yer alan nesne dizisini oluşturur ve değerleri yükler.
  • Yeni oluşturulmuş nesne, MemberInfo dizisi ile serileştirilmiş nesnenin özellik ve değerlerini içeren dizi FormatterServices.PopulateObectMembers metoduna parametre olarak gönderilir.

Seçici Serileştirme

Genel olarak çoğu tipin serileştirilebilir olması önerilen bir durumdur buda kullanıcılara bir çok esneklik sağlamaktadır. Fakat nesne serileştirilirken public, internal, protected veya private olan tüm özellikleri saklanmaktadır. Serileştirme yaparken bazı hassas, gizli yada serileştirildiğinde gereksiz olan özelliklerin serileştirilmesini istemeyebilirsiniz.

Seçici Serileştirme, hangi alanlanların serileştirip hangilerinin serileştirilmeyeceği anlamına gelir. Örneğin nesne üzerinde yer alan çalışılan thread’in kimlik bilgilerini tutan bir özellik nesne ters-serileştirildiğinde işinize yarayacak bir bilgi olmayacaktır çünkü o anda o thread canlı olmayacaktır. Seçici serileştirme [Serializable] etiketi ile işaretlenmiş sınıf içerisinde bulunan serileştirilmek istenmeyen özelliği [NonSerialized] olarak işaretleyerek yapılmaktadır.

Örnek bir kullanım aşağıdaki gibi olacaktır :

[Serializable()]
public class Circle
{
    // Bu özellik serileştirilecektir.
    private double dRadius;

    // Bu özelliğin serileştirilmesin diye işaretlenmiştir.
    [NonSerialized] private double dArea;

    public Circle( doubel dR )
    {
        dRadius = dR;
        dArea = Math.PI * dRadius * dRadius;
    }
}

// Herhangi bir yerde Circle nesnesi oluşturuluyor.
Circle cir = new Circle(  8.5 );

Nesne oluşturulduğu zaman yarıçapı (radius) ve alan (area) özellikleri değerleri yüklenmiş olmaktadır. Nesne serileştirildiği zaman sadece dRadius özelliğinin değeri serileştirilecektir. dArea özelliği hesaplanan bir özellik olduğu için serileştirilmesine gerek yoktur. Fakat Circle nesnesi ters-serileştirildiğinde dRadius değeri 8.5 olarak yüklenecektir ve dArea değeri serileştirilmediği için 0 değerini alacaktır. Böyle bir durumu çözmek için aşağıdaki şekilde çözüm uygulamamız gerekmektedir :

[Serializable()]
public class Circle : IDeserializationCallback
{
    // Bu özellik serileştirilecektir.
    private double dRadius;

    // Bu özelliğin serileştirilmesin diye işaretlenmiştir.
    [NonSerialized] private double dArea;

    public Circle(double dR)
    {
        dRadius = dR;
        dArea = Math.PI * dRadius * dRadius;
    }

    // IDeserializationCallback uygulanışı
    public void IDeserializationCallback.OnDeserialization(object oSender)
    {
        dArea = Math.PI * dRadius * dRadius;
    }
}

Ters-serileştirme işleminde formatlayıcılar ters-serileştirilen tipin IDeserializationCallback interface’i ile genişletilip genişletilmediğini kontrol eder. Eğer bu interface i ile genişletilmiş tip bulur ise kendi iç listesine ekler. Tüm nesneler serileştirildikten sonra formatter bu liste üzerinde dolaşarak her nesne için OnDeserialization metodunu çağırır. OnDeserialization metodunda hesaplama vb. ek işiniz var ise nesne tamamen ters-serileştirilmeden halledebilirsiniz.

Özel (Custom) Serileştirme

[Serializable] etiketi ile işaretlenen sınıflar serileştirilebilirlerdir. [Serializable] etiketi ile işaretlenen sınıf varsayılan serileştirme işlemlerine tabi tutulup [NonSerialized] etiketi ile işaretlenmiş özellikleri dışındaki tüm özellikleri serileştirilir. ISerializable interface’i sayesinde bir nesnenin serileştirme yada ters-serileştirme işlemlerini kontrol edip müdahele edebiliriz. Diğer bir değişle [Serializable] etiketi ile işaretlenmiş bir sınıf serileştirme zamanında ISerializable interface’i ile genişletilmiş gibi davranmaktadır. Eğer bir sınıf ISerializable interface’i ile genişletilip [Serializable] etiketi ile işaretlenmez ise aşağıdaki gibi bir hata alınır:

Serialization Exception

Serileştirme işlemini kontrol ve müdahale etmek için ISerializable interface’i ile genişletme yapılıp özel bir yükleyici (constructor) ve GetObjectData() isimli bir metod oluşturulur :

  • GetObjectData metodu nesne serileştirilirken çalışacak metotdur.
  • Oluşturacağımız özel yükleyici (constructor) ise nesne ters-serileştirilirken çalışacaktır.

Örnek 1:

Aşağıdaki kod ile ISerializable interface’i ile nasıl bir genişletme işlemi yapıldığı gösterilmektedir:

[Serializable]
public class MySerializableClass : System.Runtime.Serialization.ISerializable
{
    // Özellikler
    private string sPrivate = String.Empty;
    public string sPublic = String.Empty;
    public SomeClass obMyClass = null;

    // Sınıf yükleyicisi (constructor)
    public MySerializableClass(string s1, string s2)
    {
        sPrivate = s1;
        sPublic = s2;
        obMyClass = new SomeClass(123);
    }

    /* ISerializable */

    // ISerializable ile özel ters-serileştirme için gelen zorunlu yükleyici(constructor).
    // Bu yükleyiciyi uygulamak önemlidir eğer uygulanmaz ise ters-serileştirme sırasında
    // hata alınır. Serileştirilecek sınıf sealed olmadığı sürece bu yükleyiciyi protected
    // yapmak iyi bir fikirdir çünkü türeyen sınıflar bu yükleyiciyi çağırabilir ancak 
    // başka bir şekilde yükleyici dışarıdan direk olarak çağırılamaz durumdadır.
    protected MySerializableClass(SerializationInfo info, StreamingContext ctx)
    {
        sPrivate = info.GetString("s1");
        sPublic = info.GetString("s2");
        // diğer nesne ters-serileştiriliyor.
        obMyClass = (SomeClass)info.GetValue("ob", typeof(SomeClass));       
    }

    // ISerializable ile özel serileştirme için gelen zorunlu metod.
    // Bu metoda serileştirilecek bilgiler isim/değer olarak ekleniyor. 
    // Hangi bilgilerin serileştirileceğine karar verildiği nokta. 
    // Unutmamak gerekirki her değişken ismi bir defa kullanılmalıdır.
    public virtual void GetObjectData(SerializationInfo info, StreamingContext ctx)
    {
        info.AddValue("s1", sPrivate);
        info.AddValue("s2", sPublic);
        // diğer nesne serileştiriliyor.
        info.AddValue("ob", obMyClass);       
    }
}

[Serializable]
public class SomeClass
{
    private int counter;

    public SomeClass(int cnter)
    {
        counter = cnter;
    }
}


private void btnCustomSerialization_Click(object sender, EventArgs e)
{
    // Serileştirme için hazırlık.
    MySerializableClass ob = new MySerializableClass("Public Veri", "Private Veri");
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream = new FileStream("CustomBinaryTest.dat", FileMode.Create, FileAccess.Write, FileShare.Write);

    // Nesne serileştiriliyor.
    formatter.Serialize(stream, ob);
    stream.Close();

    // Ters-serileştirme için hazırlık.
    System.Runtime.Serialization.IFormatter formatter2 = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream2 = new FileStream("CustomBinaryTest.dat", FileMode.Open, FileAccess.Read, FileShare.Read);

    // Nesne ters serileştiriliyor.
    MySerializableClass ob2 = (MySerializableClass)formatter2.Deserialize(stream2);
}

Formatlayıcı bir nesneyi serileştirirken nesne içerisindeki her tipe bakar. Eğer bu nesnenin tipi ISerializable interface’i ile genişletilmiş ise formatlayıcı bu nesnenin serileştirilecek özellik ve değer bilgilerini içeren yeni bir SerializationInfo nesnesi oluşturur ve serileştirilen nesneye parametre olarak gönderir. SerializationInfo nesnesi oluşturulduğunda formatlayıcı GetObjectData metoduna SerializationInfo nesnesini parametre olarak vererek çağırır. GetObjectData metodu SerializationInfo nesnesine bakarak serileştirilen nesnenin hangi özelliklerinin serileştirilip hangilerinin serileştirilmeyeceğinin ayarlandığı noktadır. GetObjectData metodu içerisinde serileştirilecek bilgiler AddValue metodu kullanılarak serileştirilen nesneye eklenir.

Serileştirilecek bilgiler her tip için mutlaka AddValue metodu eklenmelidir. Eğer serileştirilen nesnenin özelliklerinden biri ISerializeble ile genişletilmiş bir tip ise (örnek 1 deki SomeClass gibi) GetObjectData metodu içerisinde o nesne çağırılmaz. Bunun yerine AddValue kullanılarak nesne bilgisi eklenir. Çünkü formatlayıcı ISerializable ile genişletilmiş nesneyi belirleyerek sizin yerinize GetObjectData metodunu çağıracaktır.

Ters-serileştirme işleminde formatlayıcı stream’den nesne bilgilerini FormatterServices.GetUninitializedObject metodunu çağırarak çıkarır ve bellekte bu bilgiler için yer ayırır. Nesnenin çıkarılan her bir özelliğine ilk başta null yada 0 değeri atanır. Sonrasında formatlayıcı ISerializable ile genişletilmiş bir özellik olup olmadığına bakar eğer var ise o tipin GetObjectData metodunu ile uyuşan özel yükleyicisini (constructor) çalıştırarak nesne tamamen ters-serileştirilmiş olur.

Örnek 2 :

Bir sınıftan türemiş olan sınıfların ana sınıfları ISerializable ile genişletilebilir. Eğer türeyen sınıflar özel serileştirmeye ihtiyaç duymuyorsa sadece [Serializable] etiketi ile işaretlenerek serileştirilebilir yapılabilir. Fakat türeyen sınıflarda özel serileştirilecek (ISerializable ile genişletilecek) ise aşağıdaki durumları göz önünde bulundurmalısınız :

  • Ana sınıf ve türeyen sınıfın GetObjectData metodu ve özel yükleyicisi (constructor) olmalıdır.
  • Türeyen sınıftaki GetObjectData metodu uygulanışında ilk önce ana sınıftaki GetObjectData çağırılmalıdır.
  • Türeyen sınıftaki özel yükleyici (constructor) uygulanışında ilk önce ana sınıftaki özel yükleyici (constructor) çağırılmalıdır.
[Serializable]
public class MyDerivedSerializableClass : MySerializableClass
{
    public int nNum;

    // ISerializable ile özel ters-serileştirme için gelen zorunlu yükleyici(constructor).
    // Ana sınıftaki zorunlu yükleyicinin (constructor) çağırılmasına dikkat edilmelidir.
    public MyDerivedSerializableClass(string s1, string s2) : base(s1, s2) { }

    protected MyDerivedSerializableClass(SerializationInfo info, StreamingContext ctx)
        : base(info, ctx)
    {
        nNum = info.GetInt32("n1");
    }

    // ISerializable ile özel serileştirme için gelen zorunlu metod.
    // Ana sınıftaki zorunlu GetObjectData metodunun çağırılmasına dikkat edilmelidir.
    public override void GetObjectData(SerializationInfo info, StreamingContext ctx)
    {
        base.GetObjectData(info, ctx);
        info.AddValue("n1", nNum);
    }
}

private void btnDerivedSerializable_Click(object sender, EventArgs e)
{
    // Serileştirme için hazırlık.
    MyDerivedSerializableClass ob = new MyDerivedSerializableClass("Public Veri", "Private Veri");
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream = new FileStream("CustomDerivedTest.dat", FileMode.Create, FileAccess.Write, FileShare.Write);

    // Nesne serileştiriliyor.
    formatter.Serialize(stream, ob);
    stream.Close();

    // Ters-serileştirme için hazırlık.
    System.Runtime.Serialization.IFormatter formatter2 = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream2 = new FileStream("CustomDerivedTest.dat", FileMode.Open, FileAccess.Read, FileShare.Read);

    // Nesne ters serileştiriliyor.
    MyDerivedSerializableClass ob2 = (MyDerivedSerializableClass)formatter2.Deserialize(stream2);
}
Özel (Custom) serileştirme örneklerini buraya tıklayarak indirebilirsiniz.

Delegeler ve Serileştirme

Tüm delegeler serileştirilebilir sınıflar olarak derlenir. Yani bir nesne serileştirilirken bir delege içeriyorsa, delegenin invocation listeside serileştirilebilirdir. Aslında bu delegeleri içeren nesneleri serileştirmek biraz uğraştırıcıdır çünkü delegenin iç invocation listesindeki nesneler serileştirilebilir olmayabilir. Sonuç olarak delege içeren nesneleri serileştirirken bazen exception alabilirsiniz. Hatta delegeyi içeren nesne delegenin durumunu bilmeyebilir. Örneğin kendisine üye olan abone olaylarını yöneten bir delegemiz olduğunu düşünelim bu delegeye kayıt olan aboneler uygulama çalıştığı sürece değişebilmektedir bu durumda delegeyi ve üye olan aboneleri [NonSerialized] etiketini kullanarak serileştirilemez olarak işaretlememiz gerekir :

[Serializable]
public class MyClass
{
    [NonSerialized]
    public delegate void delAlaram( DateTime dt );

    // Olaylar (Events)  için serileştirilmemesini sağlamak için delegelerden farklı 
    // olarak field:NonSerialized] etiketi ile işaretleme yapılır.
    // Delegeler için [NonSerialized] etiketi kullanılabilir.

    field:NonSerialized]
    public event delAlaram evtAlarm;
}

Binary Formatlayıcı ve Serileştirme Olayları

.Net Framework 2.0 ile birlikte serileştirme olayları (events) gelmiştir. Yani bir sınıf tasarladığınızda Serializing, Serialized, Deserializing, Deserialized serileştirme/ters-serileştirme olaylarını etiket kullanarak yakalayabilmektesiniz.

Serialization ve Deserialization

OnSerializing, OnSerialized, OnDeserializing ve OnDeserialized olayları sadece Binary serileştirmede çalışır. Soap formatında yapılan serileştirmede sadece Serialization olayları çalışır.

Serileştirme yada ters-serileştirme olaylarını yakalayabilmek için kullanabileceğiniz etiketlere örnek kullanım :

[Serializable]
public class MyClass
{
    [OnSerializing]
    void OnSerializing(StreamingContext context)
    {  }

    [OnSerialized]
    void OnSerialized(StreamingContext context)
    {  }

    [OnDeserializing]
    void OnDeserializing(StreamingContext context)
    {  }

    [OnDeserialized]
    void OnDeserialized(StreamingContext context)
    {  }
}

Kaynak ;

Serialization (C# and Visual Basic)
AppDomain.AssemblyResolve Event
NonSerializedAttribute Class

0 thoughts on “C# Binary Serialization”

Bir cevap yazın