تذکر:
آشنایی با مفاهیم پایه تکنولوژی 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