سازگار نمودن DataContract ها با نسخه های بعدی!
  در این مقاله به بررسی موضوع سازگار نمودن DataContract ها با نسخه های بعدی و قبلی در هنگام طراحی سرویس های WCF می پردازیم
   WCF
   ۲۲۷۶۲
   این مقاله حاوی فایل ضمیمه نمی باشد
   مرتضی صحراگرد
   ۱۳۹۰/۹/۲۵
ارسال لینک صفحه برای دوستان ارسال لینک صفحه برای دوستان  اضافه کردن به علاقه مندیها اضافه کردن به علاقه مندیها   نسخه قابل چاپ نسخه قابل چاپ

 

تذکر:

آشنایی با مفاهیم پایه تکنولوژی WCF جهت درک صحیح مطالب این مقاله الزامی می باشد.

مقدمه:

هنگام طراحی برنامه های توزیع شده (Distributed) معمولا طراحان با ملاحظاتی روبرو هستند که کمتر در برنامه هایی از انواع دیگر مشاهده می شود. از جمله این ملاحظات می توان به مسئله سازگار نمودن DataContract با نسخه های قبلی و بعدی نام برد. برای درک بهتر مسئله یک مثال مطرح می کنیم.

فرض کنید یک DataContract به نام User به شکل زیر طراحی نموده ایم.

// Old Version
[DataContract]
public class User
{
    [DataMember]
    public string FirstName { get; set; }
    [DataMember]
    public string LastName { get; set; }

}

کلاس User دارای دو خصوصیت نام و نام خانوادگی می باشد. پس از مدتی نیاز می شود که خصوصیت دیگری به نام آدرس نیز به این کلاس اضافه کنیم. بنابراین نسخه جدید این کلاس به مشابه زیر می شود.

// New Version
[DataContract]
public class User
{
    [DataMember]
    public string FirstName { get; set; }
    [DataMember]
    public string LastName { get; set; }

    //New member
    [DataMember]
    public string Address { get; set; }
}

خوب، مشکل از همین نقطه آغاز می شود. کلاینت هایی که تاکنون از این سرویس استفاده نموده اند، نسخه جدید کلاس User را نمی شناسند و در نتیجه خصوصیت Address نیز برای آن ها ناشناس باقی می ماند. پس راه حل این مشکل چیست؟

مشکل جالب دیگری نیز ممکن است به وجود آید. فرض کنید که از این پس کلاینت های جدید دیگری از سرویس شروع به استفاده کنند. درنتیجه کلاینت های جدید، نسخه جدید کلاس User را می شناسند. اکنون اگر وظیفه این سرویس رد و بدل نمودن کلاس User بین کلاینت ها باشد، هنگام انتقال کلاس User از کلاینت های جدید به قدیم و برعکس چه اتفاقاتی خواهد افتاد؟ (به این مسئله Versioning Round-Trip گفته می شود)

معمولا در پروژه های کوچک با دامنه هایی نه چندان وسیع، می توان این مشکل را با اطلاع دادن به کلاینت های قدیمی، جهت بروز رسانی رفرنس یا API رفع نمود ولی همانگونه که مطلع هستید این کار همیشه امکان پذیر نیست. در این مقاله قصد داریم در مورد اینگونه مشکلات بحث کنیم

آغاز:

DataContract ها در سرویس های WCF به طور پیشفرض Version-Tolerant می باشند. بدین معنی که اگر در نسخه های مختلف مربوط به یک Data Contract اختلافی در خصوصیات وجود داشت، به طور پیشفرض هم سرویس و هم کلاینت این تغییرات را به شکلی که در ادامه توضیح خواهیم داد، نادیده می گیرند. این عمل توسط کلاس DataContractSerializer انجام می شود. همانطور که از نام این کلاس پیداست، وظیفه آن سریالایز و دی سریالایز نمودن DataContract ها می باشد.

اگر نسخه جدید کلاس User از سمت سرویس به کلاینتی که نسخه قدیمی را می شناسد ارسال شود، به طور پیشفرض کلاس DataContractSerializer خصوصیت Address را حذف نموده و کلاس User را تبدیل به نسخه قدیمی می نماید و تحویل کلاینت می دهد و در نتیجه مشکلی به وجود نمی آید (به جز اینکه حتی روح کلاینت نیز از وجود فیلد Address خبر دار نیست و طبیعتا امکان استفاده از آن را نیز ندارد).

اما اگر نسخه قدیمی کلاس User به سمت سرویس یا کلاینتی که نسخه جدید را می شناسد ارسال شود، کلاس DataContractSerializer خصوصیات جدید را به شکل اتوماتیک ایجاد نموده و به نسخه قدیمی اضافه می نماید. مقدار این خصوصیات جدید اگر از نوع Value Type باشند، با مقدار پیشفرض خود مقدار دهی می شوند و اگر  Reference Type باشند با مقدار null.

در این حالت این امکان وجود دارد که متوجه شویم که نسخه ی ارسال شده قدیمی است و قبل از اینکه کلاس User تحویل سرویس شود می توانیم خصوصیات ارسال شده را با مقادیر پیشفرض مورد نظر خود پر کنیم. این عمل را می توانیم از طریق رویداد OnDeserializing به شکل زیر انجام دهیم.

// New Version
[DataContract]
public class User
{
    [DataMember]
    public string FirstName;
    [DataMember]
    public string LastName;

    //New Member
    [DataMember]
    public string Address;

    [OnDeserializing]
    
void OnDeserializing(StreamingContext context)
    {
        
// If Address is missing
        
if (Address == null)
        {
            Address =
"Some default address";
        }
    }

}

در قطعه کد بالا در رویداد OnDeserializing کنترل نموده ایم که آیا فیلد Address برابر null است یا خیر. و از این طریق می توانیم مقادیر پیشفرض را جایگزین نماییم.

مجبور نمودن کلاینت ها برای استفاده از نسخه جدید:

ممکن است که تصمیم بگیریم خصوصیات جدید را برای تمامی کلاینت ها اجباری کنیم. برای انجام این کار می توانید خصوصیت IsRequired کلاس DataMemeber را برابر true کنیم. مقدار این خصوصیت به طور پیشفرض false است.

// New Version
[DataContract]
public class User
{
    [DataMember]
    public string FirstName { get; set; }
    [DataMember]
    public string LastName { get; set; }

    //New member
    [DataMember(IsRequired = true)]
    public string Address { get; set; }

}

در این حالت اگر نسخه ی ارسال شده به سرویس (یا کلاینت)قدیمی باشد (یعنی خصوصیت Address وجود نداشته باشد) یک خطا از نوع NetDispatcherFaultException ایجاد شده و ادامه عملیات متوقف می شود و در نتیجه کلاینت ها مجبور هستند که خود را بروز رسانی کنند تا امکان استفاده از سرویس را داشته باشند.

اما اگر عکس این عمل اتفاق بیفتد یعنی نسخه جدید کلاس User به سرویس یا کلاینتی ارسال شود که نسخه قدیمی را می شناسد، هیچ اتفاق خاصی نمی افتد و طبق روال فیلد Address حذف شده و عملیات ادامه پیدا می کند.

بررسی وضعیت Versioning Round-Trip:

در ابتدای مقاله وضعیتی به نام Versioning Round-Trip را معرفی کردیم. در این وضعیت کلاس User بین سرویس ها و کلاینت های مختلف رد و بدل می شود یا اینکه این کلاس بین سرویس و کلاینت که نسخه های مختلفی را می شناسند، مرتبا رد و بدل می شود.

بدین ترتیب هنگامی که سرویس یا کلاینتی که نسخه قدیمی را می شناسد، کلاس User را دریافت می کند، خصوصیت Address را حذف می کند و هنگامی که مجددا آن را برای کلاینت که نسخه جدید را می شناسد، ارسال می کند، دیگر کلاینت به خصوصیت Address دسترسی ندارد! (زیرا قبلا حذف شده است) در شکل زیر این موضوع را مشاهده می کنید.


برای حل این مشکل از اینترفیسی به نام IExtensibleDataObjectاستفاده می شود. این اینترفیس را در قسمت زیر ملاحظه می کنید.

public interface IExtensibleDataObject
{
    ExtensionDataObject ExtensionData { get; set; }
}

در قطعه کد زیر ملاحظه می کنید که کلاس User این اینترفیس را پیاده سازی نموده است.

// New Version
[DataContract]
public class User : IExtensibleDataObject
{
    [DataMember]
    public string FirstName;
    [DataMember]
    public string LastName;

    //New Member
    [DataMember]
    public string Address;


    #region IExtensibleDataObject Members

    
public ExtensionDataObject ExtensionData { get; set; }

    #endregion
}

با پیاده سازی اینترفیس IExtensibleDataObjectمشکل Versioning Round-Trip حل می شود. بدین شکل که در صورتی که سرویس یا کلاینتی که نسخه قدیمی را می شناسد، نسخه جدید را دریافت کند، خصوصیاتی را که نمی شناسد (در اینجا خصوصیت Address) را در خصوصیت ExtensionData ذخیره می کند و هنگامی که قرار باشد کلاس User برای سرویس یا کلاینتی که نسخه جدید را می شناسند، ارسال شود، مجددا DataContractSerializer اطلاعات خصوصیات جدید را از خصوصیت ExtensionData بیرون کشیده و کلاس User جدید را بازسازی می نماید. و بدین ترتیب خصوصیات جدید و مقادیر آن ها همواره در دسترس خواهند بود.

نکته جالب اینجاست که این عملیات صرفا با پیاده سازی اینترفیس IExtensibleDataObject انجام می پذیرد و نیازی به هیچگونه کار اضافی از طرف برنامه نویسی نمی باشد. بررسی محتویات خصوصیت ExtensionData توسط خود DataContractSerializer انجام می شود و تاکنون راهکاری برای تعامل با این خصوصیت توسط برنامه نویس ارائه نشده است (شاید به این دلیل که نیازی برای انجام این کار ندیده اند!)

آخرین نکته ای که قصد داریم امروز در مورد آن بحث کنیم، خصوصیت IgnoreExtensionDataObject از کلاس ServiceBehavior می باشد. فرض کنید که قصد داریم برای کلاس سرویس خاصی قابلیت استفاده از اینترفیس IExtensibleDataObject را از کار بیندازیم. یعنی کلاس User که از متدهای این سرویس به سمت کلاینت ها ارسال می شود، همان نسخه User ای باشد که سرویس می شناسد و دیگر هیچگونه اطلاعاتی در خصوصیت ExtensionData ذخیره نمی شود.

برای انجام این کار کافیست که خصوصیت IgnoreExtensionDataObject که مقدار پیشفرض آن false است را به true تغییر دهیم. در اینصورت هنگام استفاده از این سرویس اگر نسخه قدیمی کلاس User به متدهای این سرویس ارسال شود، نقش اینترفیس IExtensibleDataObject بی اثر می شود و خروجی این سرویس نسخه ی User خود آن می شود (قدیمی یا جدید فرقی نمی کند)

[ServiceContract()]
public interface IUserManager
{
     [OperationContract]
     User GetUserAfterModification(User user);
}

[
ServiceBehavior(IgnoreExtensionDataObject = true)]
public class UserManager : IUserManager
{

    #region IUserManager Members

    public User GetUserAfterModification(User user)
    {
        // Implementation Here
        // Return User

    }

    #endregion
}

 

برگفته از :  Programming WCF Services: Mastering WCF and the Azure AppFabric Service Bus