نکته : آشنایی با مفاهیم پایه WCF برای فهم بهتر مفاهیم توصیه میشود.
امروزه استفاده از WCF در پروژههای SOA بسیار فراگیر شده است. کمتر کسی
است که در مورد قدرت تکنولوژی WCF نشنیده باشد یا از این تکنولوژی در پروژههای خود استفاده نکرده باشد. WCF مدل برنامه نویسی یکپارچه مایکروسافت
برای ساخت نرمافزارهای سرویس گرا است و برای توسعه دهندگان امکانی را
فراهم میکند که راهکارهایی امن، و مبتنی بر تراکنش را تولید نمایند که
قابلیت استفاده در بین پلتفرمهای مختلف را دارند. قبل از WCF توسعه
دهندگان پروژههای نرم افزاری برای تولید پروژههای توزیع شده باید شرایط
موجود برای تولید و توسعه را در نظر میگرفتند. برای مثال اگر استفاده
کننده از سرویس در داخل سازمان و بر پایه دات نت تهیه شده بود از .net
remoting استفاده میکردند و اگر استفاده کننده سرویس از خارج سازمان یا
مثلا بر پایه تکنولوژی J2EE بود از Web Serviceها استفاده میشد. با ظهور
WCF این تکنولوژی با هم تجمیع شدند(بهتر بگم تبدیل به یک تکنولوژی واحد
شدند) و دیگر خبری از net remoting یا web serviceها نیست.
WCF با تمام قدرت و امکاناتی که داراست
دارای نقاط ضعفی هم میباشد که البته این معایب (یا محدودیت) بیشتر جهت سازگار سازی سرویسهای نوشته شده با سیستمها و پروتکلهای مختلف است.
برای انتقال دادهها از طریق WCF بین سیستمهای مختلف باید دادههای مورد
نظر حتما سریالایز شوند که مثال هایی از این دست رو در همین سایت میتونید
مطالعه کنید:
(
^ ) و (
^ ) و (
^ )
با توجه به این که دادهها سریالایز میشوند، در نتیجه امکان انقال داده
هایی که از نوع object هستند در WCF وجود ندارد. بلکه نوع داده باید
صراحتا ذکر شود و این نوع باید قابیلت سریالایز شدن را دارا باشد.برای مثال
شما نمیتونید متدی داشته باشید که پارامتر ورودی آن از نوع delegate باشد
یا کلاسی باشد که صفت [Serializable] در بالای اون قرار نداشته باشد یا
کلاسی باشد که صفت DataContract برای خود کلاس و صفت DataMember برای خاصیتهای اون تعریف نشده باشد. حالا سوال مهم این است اگر متدی داشته باشیم که
پارامتر ورودی آن حتما باید از نوع delegate باشد چه باید کرد؟
برای تشریح بهتر مسئله یک مثال میزنم؟
سرویسی داریم برای اطلاعات کتاب ها. قصد داریم متدی بنوسیم که پارامتر
ورودی آن از نوع Lambda Expression است تا Query مورد نظر کاربر از سمت
کلاینت به سمت سرور دریافت کند و خروجی مورد نظر را با توجه به Query ورودی
به کلاینت برگشت دهد.( متدی متداول در اکثر پروژه ها). به صورت زیر عمل میکنیم.
*ابتدا یک Blank Solution ایجاد کنید.
*یک ClassLibrary به نام Model ایجاد کنید و کلاسی به نام Book در آن بسازید .(همانطور که
میبینید کلاس مورد نظر سریالایز شده است):
[DataContract]
public class Book
{
[DataMember]
public int Code { get; set; }
[DataMember]
public string Title { get; set; }
}
* یک WCF Service Application ایجاد کنید
یک Contract برای ارتباط بین سرور و کلاینت میسازیم:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.ServiceModel;
namespace WcfLambdaExpression
{
[ServiceContract]
public interface IBookService
{
[OperationContract]
IEnumerable<Book> GetByExpression( Expression<Func<Book, bool>> expression );
}
}
متد GetByExpression دارای پارامتر ورودی expression است که نوع آن نیز Lambda Expression میباشد. حال یک سرویس ایجاد میکنیم:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace WcfLambdaExpression
{
public class BookService : IBookService
{
public BookService()
{
ListOfBook = new List<Book>();
}
public List<Book> ListOfBook
{
get;
private set;
}
public IEnumerable<Book> GetByExpression( Expression<Func<Book, bool>> expression )
{
ListOfBook.AddRange( new Book[]
{
new Book(){Code = 1 , Title = "Book1"},
new Book(){Code = 2 , Title = "Book2"},
new Book(){Code = 3 , Title = "Book3"},
new Book(){Code = 4 , Title = "Book4"},
new Book(){Code = 5 , Title = "Book5"},
} );
return ListOfBook.AsQueryable().Where( expression );
}
}
}
بعد از Build پروژه همه چیز سمت سرور آماده است. یک پروژه دیگر از نوع
Console ایجاد کنید و از روش AddServiceReference سعی کنید که سرویس مورد
نظر را به پروژه اضافه کنید. در هنگام Add Service Reference برای اینکه سرویس سمت سرور و کلاینت هر دو با یک مدل کار کنند باید از یک Reference assembly استفاده کنند و کافی است از قسمت Advanced گزینه Reuse types in referenced assemblies را تیک بزنید و assemblyهای مورد نظر را انتخاب کنید.( در این پروژه باید Model و System.Xml.Linq را انتخاب کنید)
به طور حتم با خطا روبرو خواهید شد. دلیل آن هم
این است که امکان سریالایز کردن برای پارامتر ورودی expression میسر نیست.
خطای مربوطه به شکل زیر خواهد بود:
Type 'System.Linq.Expressions.Expression`1[System.Func`2[WcfLambdaExpression.Book,System.Boolean]]' cannot be serialized.
Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute.
If the type is a collection, consider marking it with the CollectionDataContractAttribute.
See the Microsoft .NET Framework documentation for other supported types
حال چه باید کرد؟
روشهای زیادی برای بر طرف کردن این محدودیت وجود دارد. اما در این پست روشی رو که خودم از اون استفاده میکنم رو براتون شرح میدهم.
در این روش باید از XElement استفاده شود که در فضای نام System.Linq.Xml
قرار دارد. یعنی آرگومان ورودی سمت کلاینت باید به فرمت Xml سریالایز شود و
سمت سرور دوباره دی سریالایز شده و تبدیل به یک Lambda Expression شود.
اما سریالایز کردن Lambda Expression واقعا کاری سخت و طاقت فرساست . با
توجه به این که در اکثر پروژهها این متدها به صورت Generic نوشته میشوند. برای حل این مسئله بعد از مدتی جستجو، کلاسی رو پیدا کردم که این کار
رو برام انجام میداد. بعد از مطالعه دقیق و مشاهده روش کار کلاس، تغییرات
مورد نظرم رو اعمال کردم و الان در اکثر پروژه هام دارم از این کلاس
استفاده میکنم.
یک مثال از روش استفاده :
برای اینکه از این کلاس در هر دو پروژه (سرور و کلاینت) استفاده میکنیم
باید یک Class Library جدید به نام Common بسازید و یک ارجاع از اون رو به
هر دو پروژه سمت سرور و کلاینت بدید.
سرویس و Contract بالا رو به صورت زیر باز نویسی کنید.
[ServiceContract]
public interface IBookService
{
[OperationContract]
IEnumerable<Book> GetByExpression( XElement expression );
}
و سرویس :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Xml.Linq;
namespace WcfLambdaExpression
{
public class BookService : IBookService
{
public BookService()
{
ListOfBook = new List<Book>();
}
public List<Book> ListOfBook
{
get;
private set;
}
public IEnumerable<Book> GetByExpression( XElement expression )
{
ListOfBook.AddRange( new Book[]
{
new Book(){Code = 1 , Title = "Book1"},
new Book(){Code = 2 , Title = "Book2"},
new Book(){Code = 3 , Title = "Book3"},
new Book(){Code = 4 , Title = "Book4"},
new Book(){Code = 5 , Title = "Book5"},
} );
Common.ExpressionSerializer serializer = new Common.ExpressionSerializer();
return ListOfBook.AsQueryable().Where( serializer.Deserialize( expression ) as Expression<Func<Book, bool>> );
}
}
بعد از Build پروژه از روش Add Service Reference استفاده کنید و میبینید
که بدون هیچ گونه مشکلی سرویس مورد نظر به پروژه Console اضافه شد. برای
استفاده سمت کلاینت به صورت زیر عمل کنید.
using System;
using System.Linq.Expressions;
using TestExpression.MyBookService;
namespace TestExpression
{
class Program
{
static void Main( string[] args )
{
BookServiceClient bookService = new BookServiceClient();
Expression<Func<Book, bool>> expression = x => x.Code > 2 && x.Code < 5;
Common.ExpressionSerializer serializer = new Common.ExpressionSerializer();
bookService.GetByExpression( serializer.Serialize( expression ) );
}
}
}
بعد از اجرای پروژه، در سمت سرور خروجیهای زیر رو مشاهده میکنیم.
خروجی هم به صورت زیر خواهد بود:
دریافت سورس کامل
Expression-Serialization