شروع کار با ASP.NET Web API 2
- برای استفاده از سرویس: در همان لایه سرویس برنامه از آن استفاده کنید. مباحث و مفاهیم تزریق وابستگیهای آن تفاوتی با حالت استفاده از یک دیتابیس یا یک WebClient ندارد و یکی است.
- استفاده از WCF Proxy
- استفاده از ChannelFactory
قصد دارم طی یک مقایسه کوتاه این دو روش را بررسی کنیم:
WCF Proxy:
Proxy در واقع یک کلاس CLR است که به عنوان نماینده یک اینترفیس که از نوع Service Contract است مورد استفاده قرار میگیرد. یا به زبان ساده تر، یک Proxy در واقع نماینده Service Contract ای که سمت سرور پیاده سازی شده است در سمت کلاینت خواهد بود. Proxy تمام متد یا Operation Contractهای سمت سرور را داراست به همراه یک سری متدها و خواص دیگر برای مدیریت چرخه طول عمر سرویس، هم چنین اطلاعات مربوط به وضعیت سرویس و نحوه اتصال آن به سرور. ساخت Proxy به دو روش امکان پذیر است:
- با استفاده از امکانات AddServiceReference موجود در Visual Studio. کافیست از پنجره معروف زیر با استفاده از یک URL سرویس مورد نظر را به پروژه سمت کلاینت خود اضافه نمایید
همچنین میتوانید از قسمت Advanced نیز برای تنظیمات خاص مورد نظر خود(مثل تولید کد برای متدهای Async یا تعیین نوع Collectionها در هنگام انتقال داده و ...) استفاده نمایید.
- با استفاده از SvcUtil.exe . کاربرد svcutil.exe در موارد Metadata Export، Service Validtation، XmlSerialization Type Generator و Metadata Download و ... خلاصه میشود. با استفاده از Vs.Net Command Promptو svcutil میتوان به سرویس مورد نظر دسترسی داشت.مثال
svcutil.exe /language:vb /out:generatedProxy.vb /config:app.config http://localhost:8000/ServiceModelSamples/service
ChannelFactory:
ChannelFactory یک کلاس تعبیه شده در دات نت میباشد که به وسیله یک اینترفیس که به عنوان تعاریف سرویس سمت سرور است یک نمونه از سرویس مورد نظر را برای ما خواهد ساخت. اما به خاظر داشته باشید از این روش زمانی میتوان استفاده کرد که دسترسی کامل به سمت سرور و کلاینت داشته باشید.
برای آشنایی با نحوه پیاده سازی به این روش نیز میتوانید از این مقاله کمک بگیرید.
مثال:
public static TChannel CreateChannel() { IBookService service; var endPointAddress = new EndpointAddress( "http://localhost:7000/service.svc" ); var httpBinding = new BasicHttpBinding(); ChannelFactory<TChannel> factory = new ChannelFactory<TChannel>( httpBinding, endPointAddress ); instance= factory.CreateChannel(); return instance; }
در نتیجه برای ساخت یک سرویس به روش ChannelFactory باید مراحل زیر را طی نمایید:
- یک نمونه از WCF Binding بسازید
- یک نمونه از کلاس EndPointAddress به همراه آدرس سرویس مورد نظر در سمت سرور بسازید(البته میتوان این مرحله را نادیده گرفت و آدرس سرویس را مستقیما به نمونه ChannelFactory به عنوان پارامتر پاس داد)
- یک نمونه از کلاس ChannelFactory یا استفاده از EndPointAddress بسازید
- با استفاده از ChannelFactory یک نمونه از Channel مورد نظر را فراخوانی نمایید(فراخوانی متد CreateChannel)
تفاوتهای دو روش
Proxy | ChannelFactory |
فقط نیاز به یک URL برای ساخت سرویس مورد نظر دارد. بقیه مراحل توسط ابزارهای مرتبط انجام خواهد شد | شما نیاز به دسترسی مستقیم به اسمبلی حاوی Service Contract پروژه خود دارید. |
استفاده از این روش بسیار آسان و ساده است | پیاده سازی آن پیچیدگی بیشتر دارد |
فهم مفاهیم این روش بسیار راحت است | نیاز به دانش اولیه از مفاهیم WCF برای پیاده سازی دارد |
زمانی که میزان تغییرات در کلاسهای مدل و Entityها زیاد باشد این روش بسیار موثر است.(مدیریت تغییرات در WCF) | زمانی که اطمینان دارید که مدل و entityها پروژه زیاد تغییر نخواهند کرد و از طرفی نیاز به کد نویسی کمتر در سمت کلاینت دارید، این روش موثرتر خواهد بود |
فقط به اینترفیس هایی که دارای ServiceContractAttribute هستند دسترسی خواهیم داشت. | به تمام اینترفیسهای تعریف شده در بخش Contracts دسترسی داریم. |
فقط به متدهای که دارای OperationContractAttribute هستند دسترسی خواهیم داشت. | به تمام متدهای عمومی سرویس دسترسی داریم. |
آیا میتوان از روش AddServiceReference تعبیه شده در Vs.Net، برای ساخت ChannelFactory استفاده کرد؟
بله! کافیست هنگام ساخت سرویس، در پنجره AddServiceReference از قسمت Advanced وارد برگه تنظیمات شوید. سپس تیک مربوط به قسمت های Reused Type in referenced assemblies و Reused Types in specified referenced assemblies را بزنید. بعد از لیست پایین، اسمبلیهای مربوط به Domain Model و هم چنین Contractهای سمت سرور را انتخاب نمایید. در این حالت شما از روش Channel Factory برای ساخت سرویس WCF استفاده کرده اید.
واژهی استثناء یا exception کوتاه شدهی عبارت exceptional event است. در واقع exception یک نوع رویداد است که در طول اجرای برنامه رخ میدهد و در نتیجه، جریان عادی برنامه را مختل میکند. زمانیکه خطایی درون یک متد رخ دهد، یک شیء (exception object) حاوی اطلاعاتی دربارهی خطا ایجاد خواهد شد. به فرآیند ایجاد یک exception object و تحویل دادن آن به سیستم runtime، اصطلاحاً throwing an exception یا صدور استثناء گفته میشود که در ادامه به آن خواهیم پرداخت.
بعد از اینکه یک متد استثناءایی را صادر میکند، سیستم runtime سعی در یافتن روشی برای مدیریت آن خواهد کرد.
خوب اکنون که با مفهوم استثناء آشنا شدید اجازه دهید دو سناریو را با هم بررسی کنیم.
- سناریوی اول:
فرض کنید یک فایل XML از پیش تعریف شده (برای مثال یک لیست از محصولات) قرار است در کنار برنامهی شما باشد و باید این لیست را درون برنامهی خود نمایش دهید. در این حالت برای خواندن این فایل انتظار دارید که فایل وجود داشته باشد. اگر این فایل وجود نداشته باشد برنامهی شما با اشکال روبرو خواهد شد.
- سناریوی دوم:
فرض کنید یک فایل XML از آخرین محصولات مشاهده شده توسط کاربران را به صورت cache در برنامهتان دارید. در این حالت در اولین بار اجرای برنامه توسط کاربر انتظار داریم که این فایل موجود نباشد و اگر فایل وجود نداشته باشد به سادگی میتوانیم فایل مربوط را ایجاده کرده و محصولاتی را که توسط کاربر مشاهده شده، درون این فایل اضافه کنیم.
در واقع استثناءها بستگی به حالتهای مختلفی دارد. در مثال اول وجود فایل حیاتی است ولی در حالت دوم بدون وجود فایل نیز برنامه میتواند به کار خود ادامه داده و فایل مورد نظر را از نو ایجاد کند.
استثناها مربوط به زمانی هستند که این احتمال وجود داشته باشد که برنامه طبق انتظار پیش نرود.
برای حالت اول کد زیر را داریم:
public IEnumerable<Product> GetProducts() { using (var stream = File.Read(Path.Combine(Environment.CurrentDirectory, "products.xml"))) { var serializer = new XmlSerializer(); return (IEnumerable<Product>)serializer.Deserialize(stream); } }
در مثال دوم میدانیم که ممکن است فایل از قبل موجود نباشد. بنابراین میتوانیم موجود بودن فایل را با یک شرط بررسی کنیم:
public IEnumerable<Product> GetCachedProducts() { var fullPath = Path.Combine(Environment.CurrentDirectory, "ProductCache.xml"); if (!File.Exists(fullPath)) return new Product[0]; using (var stream = File.Read(fullPath)) { var serializer = new XmlSerializer(); return (IEnumerable<Product>)serializer.Deserialize(stream); } }
چه زمانی باید استثناءها را مدیریت کنیم؟
زمانیکه بتوان متدهایی که خروجی مورد انتظار را بر میگردانند ایجاد کرد.
اجازه دهید دوباره از مثالهای فوق استفاده کنیم:
IEnumerable<Product> GetProducts()
IEnumerable<Product> GetCachedProducts()
در واقع استثناها حالتهایی هستند که غیرقابل پیشبینی هستند. این حالتها میتوانند یک خطای منطقی از طرف برنامهنویس و یا چیزی خارج کنترل برنامهنویس باشند (مانند خطاهای سیستمعامل، شبکه، دیسک). یعنی در بیشتر مواقع این نوع خطاها را نمیتوان مدیریت کرد.
اگر میخواهید استثناءها را catch کرده و آنها را لاگ کنید در بالاترین لایه اینکار را انجام دهید.
چه استثناءهایی باید مدیریت شوند و کدامها خیر؟
مدیریت صحیح استثناءها میتواند خیلی مفید باشد. همانطور که عنوان شد یک استثناء زمانی رخ میدهد که یک حالت استثناء در برنامه اتفاق بیفتد. این مورد را بخاطر داشته باشید، زیرا به شما یادآوری میکند که در همه جا نیازی به استفاده از try/catch نیست. در اینجا ذکر این نکته خیلی مهم است:
تنها استثناءهایی را catch کنید که بتوانید برای آن راهحلی ارائه دهید.
به عنوان مثال اگر در لایهی دسترسی به داده، خطایی رخ دهد و استثناءی SqlException صادر شود، میتوانیم آن را catch کرده و درون یک استثناء عمومیتر قرار دهیم:
public class UserRepository : IUserRepository { public IList<User> Search(string value) { try { return CreateConnectionAndACommandAndReturnAList("WHERE value=@value", Parameter.New("value", value)); } catch (SqlException err) { var msg = String.Format("Ohh no! Failed to search after users with '{0}' as search string", value); throw new DataSourceException(msg, err); } } }
اگر مطمئن نیستید که تمام استثناءها توسط شما مدیریت شدهاند، میتوانید در حالتهای زیر، دیگر استثناءها را مدیریت کنید:
ASP.NET: میتوانید Aplication_Error را پیادهسازی کنید. در اینجا فرصت خواهید داشت تا تمامی خطاهای مدیریت نشده را هندل کنید.
WinForms: استفاده از رویدادهای Application.ThreadException و AppDomain.CurrentDomain.UnhandledException
WCF: پیادهسازی اینترفیس IErrorHandler
ASMX: ایجاد یک Soap Extension سفارشی
ASP.NET WebAPI
چه زمانهایی باید یک استثناء صادر شود؟
صادر کردن یک استثناء به تنهایی کار سادهایی است. تنها کافی است throw را همراه شیء exception (exception object) فراخوانی کنیم. اما سوال اینجاست که چه زمانی باید یک استثناء را صادر کنیم؟ چه دادههایی را باید به استثناء اضافه کنیم؟ در ادامه به این سوالات خواهیم پرداخت.
همانطور که عنوان گردید استثناءها زمانی باید صادر شوند که یک استثناء اتفاق بیفتد.
اعتبارسنجی آرگومانها
سادهترین مثال، آرگومانهای مورد انتظار یک متد است:
public void PrintName(string name) { Console.WriteLine(name); }
مشکل فوق را میتوانیم با صدور استثنای ArgumentNullException رفع کنیم:
public void PrintName(string name) { if (name == null) throw new ArgumentNullException("name"); Console.WriteLine(name); }
public void PrintName(string name) { if (name == null) throw new ArgumentNullException("name"); if (name.Length < 5 || name.Length > 10) throw new ArgumentOutOfRangeException("name", name, "Name must be between 5 or 10 characters long"); if (name.Any(x => !char.IsAlphaNumeric(x)) throw new ArgumentOutOfRangeException("name", name, "May only contain alpha numerics"); Console.WriteLine(name); }
حالت دیگر صدور استثناء، زمانی است که متدی خروجی مورد انتظارمان را نتواند تحویل دهد. یک مثال بحثبرانگیز متدی با امضای زیر است:
public User GetUser(int id) { }
با استفاده از بررسی null کدهایی شبیه به این را در همه جا خواهیم داشت:
var user = datasource.GetUser(userId); if (user == null) throw new InvalidOperationException("Failed to find user: " + userId); // actual logic here
public User GetUser(int id) { if (id <= 0) throw new ArgumentOutOfRangeException("id", id, "Valid ids are from 1 and above. Do you have a parsing error somewhere?"); var user = db.Execute<User>("WHERE Id = ?", id); if (user == null) throw new EntityNotFoundException("Failed to find user with id " + id); return user; }
خطاهای متداول حین کار با استثناءها
- صدور مجدد استثناء و از بین بردن stacktrace
کد زیر را در نظر بگیرید:
try { FutileAttemptToResist(); } catch (BorgException err) { _myDearLog.Error("I'm in da cube! Ohh no!", err); throw err; }
- اضافه نکردن اطلاعات استثناء اصلی به استثناء جدید
یکی دیگر از خطاهای رایج اضافه نکردن استثناء اصلی حین صدور استثناء جدید است:
try { GreaseTinMan(); } catch (InvalidOperationException err) { throw new TooScaredLion("The Lion was not in the m00d", err); //<---- استثناء اصلی بهتر است به استثناء جدید پاس داده شود }
- ارائه ندادن context information
در هنگام صدور یک استثناء بهتر است اطلاعات دقیقی را به آن ارسال کنیم تا دیباگ کردن آن به راحتی انجام شود. به عنوان مثال کد زیر را در نظر داشته باشید:
try { socket.Connect("somethingawful.com", 80); } catch (SocketException err) { throw new InvalidOperationException("Socket failed", err); }
void IncreaseStatusForUser(int userId, int newStatus) { try { var user = _repository.Get(userId); if (user == null) throw new UpdateException(string.Format("Failed to find user #{0} when trying to increase status to {1}", userId, newStatus)); user.Status = newStatus; _repository.Save(user); } catch (DataSourceException err) { var errMsg = string.Format("Failed to find modify user #{0} when trying to increase status to {1}", userId, newStatus); throw new UpdateException(errMsg, err); }
نحوهی طراحی استثناءها
برای ایجاد یک استثناء سفارشی میتوانید از کلاس Exception ارثبری کنید و چهار سازندهی آن را اضافه کنید:
public NewException() public NewException(string description ) public NewException(string description, Exception inner) protected or private NewException(SerializationInfo info, StreamingContext context)
سازندهی دوم برای تعیین description بوده و همانطور که عنوان شد ارائه دادن context information از اهمیت بالایی برخوردار است. به عنوان مثال فرض کنید استثناء KeyNotFoundException که توسط کلاس Dictionary صادر شده است را دریافت کردهاید. این استثناء زمانی صادر خواهد شد که بخواهید به عنصری که درون دیکشنری پیدا نشده است دسترسی داشته باشید. در این حالت پیام زیر را دریافت خواهید کرد:
“The given key was not present in the dictionary.”
“The key ‘abrakadabra’ was not present in the dictionary.”
سازندهی سوم شبیه به سازندهی قبلی عمل میکند با این تفاوت که توسط پارامتر دوم میتوانیم یک استثناء دیگر را catch کرده یک استثناء جدید صادر کنیم.
سازندهی سوم زمانی مورد استفاده قرار میگیرد که بخواهید از Serialization پشتیبانی کنید (به عنوان مثال ذخیرهی استثناءها درون فایل و...)
خوب، برای یک استثناء سفارشی حداقل باید کدهای زیر را داشته باشیم:
public class SampleException : Exception { public SampleException(string description) : base(description) { if (description == null) throw new ArgumentNullException("description"); } public SampleException(string description, Exception inner) : base(description, inner) { if (description == null) throw new ArgumentNullException("description"); if (inner == null) throw new ArgumentNullException("inner"); } public SampleException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
اجباری کردن ارائهی Context information:
برای اجباری کردن context information کافی است یک فیلد اجباری درون سازنده تعریف کنیم. برای مثال اگر بخواهیم کاربر HTTP status code را برای استثناء ارائه دهد باید سازندهها را اینگونه تعریف کنیم:
public class HttpException : Exception { System.Net.HttpStatusCode _statusCode; public HttpException(System.Net.HttpStatusCode statusCode, string description) : base(description) { if (description == null) throw new ArgumentNullException("description"); _statusCode = statusCode; } public HttpException(System.Net.HttpStatusCode statusCode, string description, Exception inner) : base(description, inner) { if (description == null) throw new ArgumentNullException("description"); if (inner == null) throw new ArgumentNullException("inner"); _statusCode = statusCode; } public HttpException(SerializationInfo info, StreamingContext context) : base(info, context) { } public System.Net.HttpStatusCode StatusCode { get; private set; } }
public override string Message { get { return base.Message + "\r\nStatus code: " + StatusCode; } }
public class HttpException : Exception { // [...] public HttpException(SerializationInfo info, StreamingContext context) : base(info, context) { // this is new StatusCode = (HttpStatusCode) info.GetInt32("HttpStatusCode"); } public HttpStatusCode StatusCode { get; private set; } public override string Message { get { return base.Message + "\r\nStatus code: " + StatusCode; } } // this is new public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("HttpStatusCode", (int) StatusCode); } }
در حین صدور استثناءها همیشه باید در نظر داشته باشیم که چه نوع context information را میتوان ارائه داد، این مورد در یافتن راهحل خیلی کمک خواهد کرد.
طراحی پیامهای مناسب
پیامهای exception مختص به توسعهدهندگان است نه کاربران نهایی.
نوشتن این نوع پیامها برای برنامهنویس کار خستهکنندهایی است. برای مثال دو مورد زیر را در نظر داشته باشید:
throw new Exception("Unknown FaileType"); throw new Exception("Unecpected workingDirectory");
توسعهدهندگانی که exception message را در اولویت قرار میدهند، معتقد هستند که از لحاظ تجربهی کاربری پیامها تا حد امکان باید فاقد اطلاعات فنی باشد. همچنین همانطور که پیشتر عنوان گردید این نوع پیامها همیشه باید در بالاترین سطح نمایش داده شوند نه در لایههای زیرین. همچنین پیامهایی مانند Unknown FaileType نه برای کاربر نهایی، بلکه برای برنامهنویس نیز ارزش چندانی ندارد زیرا فاقد اطلاعات کافی برای یافتن مشکل است.
در طراحی پیامها باید موارد زیر را در نظر داشته باشیم:
- امنیت:
یکی از مواردی که از اهمیت بالایی برخوردار است مسئله امنیت است از این جهت که پیامها باید فاقد مقادیر runtime باشند. زیرا ممکن است اطلاعاتی را در خصوص نحوهی عملکرد سیستم آشکار سازند.
- زبان:
همانطور که عنوان گردید پیامهای استثناء برای کاربران نهایی نیستند، زیرا کاربران نهایی ممکن است اشخاص فنی نباشند، یا ممکن است زبان آنها انگلیسی نباشد. اگر مخاطبین شما آلمانی باشند چطور؟ آیا تمامی پیامها را با زبان آلمانی خواهید نوشت؟ اگر هم اینکار را انجام دهید تکلیف استثناءهایی که توسط Base Class Library و دیگر کتابخانههای thirt-party صادر میشوند چیست؟ اینها انگلیسی هستند.
در تمامی حالتهایی که عنوان شد فرض بر این است که شما در حال نوشتن این نوع پیامها برای یک سیستم خاص هستید. اما اگر هدف نوشتن یک کتابخانه باشد چطور؟ در این حالت نمیدانید که کتابخانهی شما در کجا استفاده میشود.
اگر هدف نوشتن یک کتابخانه نباشد این نوع پیامهایی که برای کاربران نهایی باشند، وابستگیها را در سیستم افزایش خواهند داد، زیرا در این حالت پیامها به یک رابط کاربری خاص گره خواهند خورد.
خب اگر پیامها برای کاربران نهایی نیستند، پس برای کسانی مورد استفاده قرار خواهند گرفت؟ در واقع این نوع پیام میتواند به عنوان یک documentation برای سیستم شما باشند.
فرض کنید در حال استفاده از یک کتابخانه جدید هستید به نظر شما کدام یک از پیامهای زیر مناسب هستند:
"Unecpected workingDirectory"
"You tried to provide a working directory string that doesn't represent a working directory. It's not your fault, because it wasn't possible to design the FileStore class in such a way that this is a statically typed pre-condition, but please supply a valid path to an existing directory. "The invalid value was: "fllobdedy"."
همیشه برای نوشتن پیامهای مناسب سعی کنید از لحاظ نوشتاری متن شما مشکلی نداشته باشد، اطلاعات کافی را درون پیام اضافه کنید و تا حد امکان نحوهی رفع مشکل را توضیح دهید
مروری کوتاه بر کارکرد Ocelot
حذف موجودیتهای منفصل
فرض کنید موجودیتی را از یک سرویس WCF دریافت کرده اید و میخواهید آن را برای حذف علامت گذاری کنید. مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل ما صورت حسابها و پرداختهای متناظر را ارائه میکند. در اپلیکیشن جاری یک سرویس WCF پیاده سازی کرده ایم که عملیات دیتابیسی کلاینتها را مدیریت میکند. میخواهیم توسط این سرویس آبجکتی را (در اینجا یک موجودیت پرداخت) حذف کنیم. برای ساده نگاه داشتن مثال جاری، مدلها را در خود سرویس تعریف میکنیم. برای ایجاد سرویس مذکور مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع WCF Service Library بسازید و نام آن را به Recipe5 تغییر دهید.
- روی پروژه کلیک راست کنید و گزینه Add New Item را انتخاب کنید. سپس گزینههای Data -> ADO.NET Entity Data Model را برگزینید.
- از ویزارد ویژوال استودیو برای اضافه کردن یک مدل با جداول Invoice و Payment استفاده کنید. برای ساده نگه داشتن مثال جاری، فیلد پیمایشی Payments را از موجودیت Invoice حذف کرده ایم (برای این کار روی خاصیت پیمایشی Payments کلیک راست کنید و گزینه Delete From Model را انتخاب کنید.) روی خاصیت TimeStamp موجودیت Payment کلیک راست کنید و گزینه Properties را انتخاب کنید. سپس مقدار Concurrency Mode آن را به Fixed تغییر دهید. این کار باعث میشود که مقدار این فیلد برای کنترل همزمانی بررسی شود. بنابراین مقدار TimeStamp در عبارت WHERE تمام دستورات بروز رسانی و حذف درج خواهد شد.
- فایل IService1.cs را باز کنید و تعریف سرویس را مانند لیست زیر تغییر دهید.
[ServiceContract] public interface IService1 { [OperationContract] Payment InsertPayment(); [OperationContract] void DeletePayment(Payment payment); }
- فایل Service1.cs را باز کنید و پیاده سازی سرویس را مانند لیست زیر تغییر دهید.
public class Service1 : IService1 { public Payment InsertPayment() { using (var context = new EFRecipesEntities()) { // delete the previous test data context.Database.ExecuteSqlCommand("delete from [payments]"); context.Database.ExecuteSqlCommand("delete from [invoices]"); var payment = new Payment { Amount = 99.95M, Invoice = new Invoice { Description = "Auto Repair" } }; context.Payments.Add(payment); context.SaveChanges(); return payment; } } public void DeletePayment(Payment payment) { using (var context = new EFRecipesEntities()) { context.Entry(payment).State = EntityState.Deleted; context.SaveChanges(); } } }
- برای تست این سرویس به یک کلاینت نیاز داریم. یک پروژه جدید از نوع Console Application به راه حل جاری اضافه کنید و کد آن را مطابق لیست زیر تغییر دهید. فراموش نکنید که ارجاعی به سرویس هم اضافه کنید. روی پروژه کلاینت کلیک راست کرده و Add Service Reference را انتخاب نمایید. ممکن است پیش از آنکه بتوانید سرویس را ارجاع کنید، نیاز باشد پروژه سرویس را ابتدا اجرا کنید (کلیک راست روی پروژه سرویس و انتخاب گزینه Debug -> Start Instance).
class Program { static void Main() { var client = new Service1Client(); var payment = client.InsertPayment(); client.DeletePayment(payment); } }
شرح مثال جاری
در مثال جاری برای بروز رسانی و حذف موجودیتهای منفصل از الگویی رایج استفاده کرده ایم که در سرویسهای WCF و Web API استفاده میشود.
در کلاینت با فراخوانی متد InsertPayment یک پرداخت جدید در دیتابیس ذخیره میکنیم. این متد، موجودیت Payment ایجاد شده را باز میگرداند. موجودیتی که به کلاینت باز میگردد از DbContext منفصل (disconnected) است، در واقع در چنین وضعیتی آبجکت context ممکن است در فضای پروسس دیگری قرار داشته باشد، یا حتی روی کامپیوتر دیگری باشد.
برای حذف موجودیت Payment از متد DeletePayment استفاده میکنیم. این متد به نوبه خود با فراخوانی متد Entry روی آبجکت context و پاس دادن موجودیت پرداخت بعنوان آرگومان، موجودیت را پیدا میکند. سپس وضعیت موجودیت را به EntityState.Deleted تغییر میدهیم که این کار آبجکت را برای حذف علامت گذاری میکند. فراخوانیهای بعدی متد ()SaveChanges موجودیت را از دیتابیس حذف خواهد کرد.
آبجکت پرداختی که برای حذف به context الحاق کرده ایم تمام خاصیت هایش مقدار دهی شده اند، درست مانند هنگامی که این موجودیت به دیتابیس اضافه شده بود. اما از آنجا که از foreign key association استفاده میکنیم، تنها فیلدهای کلید موجودیت، خاصیت همزمانی (concurrency) و TimeStamp برای تولید عبارت where مناسب لازم هستند که نهایتا منجر به حذف موجودیت خواهد شد. تنها استثنا درباره این قاعده هنگامی است که موجودیت شما یک یا چند خاصیت از نوع پیچیده یا Complex Type داشته باشد. از آنجا که خاصیتهای پیچیده، اجزای ساختاری یک موجودیت محسوب میشوند نمیتوانند مقادیر null بپذیرند. یک راه حل ساده این است که هنگامی که EF مشغول ساختن عبارت SQL Delete لازم برای حذف موجودیت بر اساس کلید و خاصیت همزمانی آن است، وهله جدیدی از نوع داده پیچیده خود بسازید. اگر فیلدهای complex type را با مقادیر null رها کنید، فراخوانی متد ()SaveChanges با خطا مواجه خواهد شد.
اگر از یک independent association استفاده میکنید که در آن کثرت (multiplicity) موجودیت مربوطه یک، یا صفر به یک است، EF انتظار دارد که کلیدهای موجودیتها بدرستی مقدار دهی شوند تا بتواند عبارت where مناسب را برای دستورات بروز رسانی و حذف تولید کند. اگر در مثال جاری از یک رابطه independent association بین موجودیتهای Invoice و Payment استفاده میکردیم، لازم بود تا خاصیت پیمایشی Invoice را با وهله ای از صورت حساب مقدار دهی کنیم که خاصیت InvoiceId آن نیز بدرستی مقدار دهی شده باشد. در این صورت عبارت where نهایی شامل فیلدهای PaymentId, TimeStamp و InvoiceId خواهد بود.
نکته: هنگام پیاده سازی معماریهای n-Tier با Entity Framework، استفاده از رویکرد Foreign Key Association برای موجودیتهای مرتبط باید با ملاحظات جدی انجام شود. پیاده سازی رویکرد Independent Association مشکل است و میتواند کد شما را بسیار پیچیده کند. برای مطالعه بیشتر درباره این رویکردها و مزایا و معایب آنها به این لینک مراجعه کنید که توسط یکی از برنامه نویسان تیم EF نوشته شده است.
اگر موجودیت شما تعداد متعددی Independent Association دارد، مقدار دهی تمام آنها میتواند خسته کننده شود. رویکردی سادهتر این است که وهله مورد نظر را از دیتابیس دریافت کنید و آن را برای حذف علامت گذاری نمایید. این روش کد شما را سادهتر میکند، اما هنگامی که آبجکت را از دیتابیس دریافت میکنید EF کوئری جاری را بازنویسی میکند تا تمام روابط یک، یا صفر به یک بارگذاری شوند. مگر آنکه از گزینه NoTracking روی context خود استفاده کنید. اگر در مثال جاری رویکرد Independent Association را پیاده سازی کرده بودیم، هنگامی که موجودیت Payment را از دیتابیس دریافت میکنیم (قبل از علامت گذاری برای حذف) EF یک Object state entry برای موجودیت پرداخت و یک Relationship entry برای رابطه بین Payment و Invoice میساخت. سپس وقتی که موجودیت پرداخت را برای حذف علامت گذاری میکنیم، EF رابطه بین پرداخت و صورت حساب را هم برای حذف علامت گذاری میکند. در اینجا عبارت where تولید شده مانند قبل، شامل فیلدهای PaymentId, TimeStamp و InvoiceId خواهد بود.
یک گزینه دیگر برای حذف موجودیتها در Independent Associations این است که تمام موجودیتهای مرتبط را مشخصا بارگذاری کنیم (eager loading) و کل Object graph را برای حذف به سرویس WCF یا Web API بفرستیم. در مثال جاری میتوانستیم موجودیت صورتحساب مرتبط با موجودیت پرداخت را مشخصا بارگذاری کنیم. اگر میخواستیم موجودیت Payment را حذف کنیم، میتوانستیم کل گراف را که شامل هر دو موجودیت میشود به سرویس ارسال کنیم. اما هنگام استفاده از چنین روشی باید بسیار دقت کنید، چرا که این رویکرد پهنای باند بیشتری مصرف میکند و زمان پردازش بیشتری هم برای مرتب سازی (serialization) صرف میکند. بنابراین هزینه این رویکرد نسبت به سادگی کدی که بدست میآید به مراتب بیشتر است.
معرفی کتابخانه PdfReport
بررسی یک مثال:
ابتدا کلاسی به صورت زیر ایجاد نمایید. در این کلاس میتوانید کدهای لازم برای سفارشی کردن شی ServiceHost را قرار دهید:
public class CustomServiceHost : ServiceHost { public CustomServiceHost( Type t, params Uri baseAddresses ) : base( t, baseAddresses ) {} public override void OnOpening() { this.Description.Add( new MyServiceBehavior() ); } }
public class CustomServiceHostFactory : ServiceHostFactory { public override ServiceHost CreateServiceHost( Type t, Uri[] baseAddresses ) { return new CustomServiceHost( t, baseAddresses ) } }
<%@ ServiceHost Language="C#" Debug="true" Service="WcfService1.Service1" CodeBehind="Service1.svc.cs" %>
<%@ ServiceHost Language="C#" Debug="true" Factory="CustomServiceHostFactory" Service="WcfService1.Service1" CodeBehind="Service1.svc.cs" %>
از این به بعد عملیات وهله سازی از سرویس بر اساس تنظیمات پیش فرض صورت گرفته در این کلاسها انجام میگیرد.
در قسمت قبل « کار با اسکنر در برنامههای تحت وب (قسمت اول) » دیدی از کاری که قرار است انجام دهیم، رسیدیم. حالا سراغ یک پروژهی عملی و پیاده سازی مطالب مطرح شده میرویم.
ابتدا پروژهی WCF را شروع میکنیم. ویژوال استودیو را باز کرده و از قسمت New Project > Visual C# > WCF یک پروژهی WCF Service Application جدید را مثلا با نام "WcfServiceScanner" ایجاد نمایید. پس از ایجاد، دو فایل IService1.cs و Service1.scv موجود را به IScannerService و ScannerService تغییر نام دهید. سپس ابتدا محتویات کلاس اینترفیس IScannerService را به صورت زیر تعریف نمایید :
[ServiceContract] public interface IScannerService { [OperationContract] [WebInvoke(Method = "GET", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, UriTemplate = "GetScan")] string GetScan(); }
public class ScannerService : IScannerService { public string GetScan() { // TODO Add code here } }
بر روی پروژهی خود راست کلیک کرده و Add Reference را انتخاب نموده و سپس در قسمت COM، گزینهی Microsoft Windows Image Acquisition Library v2.0 را به پروژهی خود اضافه نمایید.
با اضافه شدن این ارجاع به پروژه، دسترسی به فضای نام WIA برای ما امکان پذیر میشود که ارجاعی از آن را در کلاس ScannerService قرار میدهیم.
using WIA;
public string GetScan() { var imgResult = String.Empty; var dialog = new CommonDialogClass(); try { // نمایش فرم پیشفرض اسکنر var image = dialog.ShowAcquireImage(WiaDeviceType.ScannerDeviceType); // ذخیره تصویر در یک فایل موقت var filename = Path.GetTempFileName(); image.SaveFile(filename); var img = Image.FromFile(filename); // img جهت ارسال سمت کاربر و نمایش در تگ Base64 تبدیل تصویر به imgResult = ImageHelper.ImageToBase64(img, ImageFormat.Jpeg); } catch { // از آنجاییه که امکان نمایش خطا وجود ندارد در صورت بروز خطا رشته خالی // بازگردانده میشود که به معنای نبود تصویر میباشد } return imgResult; }
CommonDialogClass کلاس اصلی در اینجا جهت نمایش فرم کار با اسکنر میباشد و متدهای مختلفی را جهت ارتباط با اسکنر در اختیار ما قرار میدهد که بسته به نیاز خود میتوانید از آنها استفاده کنید. برای نمونه در مثال ما نیز متد اصلی که مورد استفاده قرار گرفته، ShowAcquireImage میباشد که این متد، فرم پیش فرض دریافت اسکنر را به کاربر نمایش میدهد و کاربر از طریق آن میتواند قبل از شروع اسکن، یکسری تنظیمات را انجام دهد.
این متد ابتدا به صورت خودکار فرم تعیین دستگاه اسکنر ورودی را نمایش داده :
و سپس فرم پیش فرض اسکنرهای TWAIN را جهت تعیین تنظیمات اسکن نمایش میدهد که این امکان نیز در این فرم فراهم است تا دستگاههای Feeder یا Flated انتخاب گردند.
خروجی این متد همان عکس اسکن شده است که از نوع WIA.ImageFile میباشد و ما پس از دریافتش، ابتدا آن را در یک فایل موقت ذخیره نموده و سپس با استفاده از یک متد کمکی آن را به فرمت Base64 برای درخواست کننده اسکن ارسال مینماییم.
کدهای کلاس کمکی ImageHelper:
public static string ImageToBase64(Image image, System.Drawing.Imaging.ImageFormat format) { if (image != null) { using (MemoryStream ms = new MemoryStream()) { // Convert Image to byte[] image.Save(ms, format); byte[] imageBytes = ms.ToArray(); // Convert byte[] to Base64 String string base64String = Convert.ToBase64String(imageBytes); return base64String; } } return String.Empty; }
این مثال به سادهترین شکل نوشته شد. کلاس دیگری هم در اینجا وجود دارد و در صورتیکه از اسکنر نوع Feeder استفاده میکنید، میتوانید از کدهای آن استفاده کنید.
جهت رفع این خطا، در قسمت Referenceهای پروژه خود، WIA را انتخاب نموده و از Propertiesهای آن خصوصیت Embed Interop Types را به False تغییر دهید؛ مشکل حل میشود.
به سراغ پروژهی ویندوز فرم جهت هاست کردن این WCF سرویس میرویم. میتوانید این سرویس را بر روی یک Console App یا Windows Service هم هاست کنید که در اینجا برای سادگی مثال، از WinForm استفاده میکنیم.
یک پروژهی WinForm جدید را ایجاد کنید و سپس از قسمت Add Reference > Solution به مسیر پروژهی قبلی رفته و dllهای آن را به پروژه خود اضافه نمایید.
Form1.cs را باز کرده و ابتدا دو متغیر زیر را در آن به صورت عمومی تعریف نمایید:
private readonly Uri _baseAddress = new Uri("http://localhost:6019"); private ServiceHost _host;
حال در رویداد Form_Load برنامه، کدهای زیر را جهت هاست کردن سرویس اضافه مینماییم:
private void Form1_Load(object sender, EventArgs e) { _host = new ServiceHost(typeof(WcfServiceScanner.ScannerService), _baseAddress); _host.Open(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { _host.Close(); }
فایل App.Config پروژهی WinForm را باز کرده و کدهای آنرا مطابق زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="BehaviourMetaData"> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> <services> <service name="WcfServiceScanner.ScannerService" behaviorConfiguration="BehaviourMetaData"> <endpoint address="" binding="basicHttpBinding" contract="WcfServiceScanner.IScannerService" /> </service> </services> </system.serviceModel> </configuration>
اگر موفق به اجرا نشدید و احیانا با خطای زیر مواجه شدید، اطمینان حاصل کنید که ویژوال استودیو Run as Administrator باشد. مشکل حل خواهد شد.
به سراغ پروژهی بعدی، یعنی وب سایت خود میرویم. یک پروژهی MVC جدید ایجاد نمایید و در View مورد نظر خود، کدهای زیر را جهت صدا زدن متد GetScan اضافه میکنیم.
( از آنجا که کدها به صورت جاوا اسکریپت میباشد، پس مهم نیست که حتما پروژه MVC باشد؛ یک صفحهی HTML ساده هم کافی است).
<a href="#" id="get-scan">Get Scan</a> <img src="" id="img-scanned" /> <script> $("#get-scan").click(function () { var url = 'http://localhost:6019/'; $.get(url, function (data) { $("#img-scanned").attr("src","data:image/Jpeg;base64, "+ data.GetScanResult); }); }); </script>
راه حلهای زیادی برای این مشکل ارائه شده است، و متاسفانه بسیاری از آنها در شرایط پروژهی ما جوابگو نمیباشد (به دلیل هاست روی یک پروژه ویندوزی). تنها راه حل مطمئن (تست شده) استفاده از یک کلاس سفارشی در پروژهی WCF Service میباشد که مثال آن در اینجا آورده شده است.
برای رفع مشکل به پروژه WcfServiceScanner بازگشته و کلاس جدیدی را به نام CORSSupport ایجاد کرده و کدهای زیر را به آن اضافه کنید:
public class CORSSupport : IDispatchMessageInspector { Dictionary<string, string> requiredHeaders; public CORSSupport(Dictionary<string, string> headers) { requiredHeaders = headers ?? new Dictionary<string, string>(); } public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { var httpRequest = request.Properties["httpRequest"] as HttpRequestMessageProperty; if (httpRequest.Method.ToLower() == "options") instanceContext.Abort(); return httpRequest; } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { var httpResponse = reply.Properties["httpResponse"] as HttpResponseMessageProperty; var httpRequest = correlationState as HttpRequestMessageProperty; foreach (var item in requiredHeaders) { httpResponse.Headers.Add(item.Key, item.Value); } var origin = httpRequest.Headers["origin"]; if (origin != null) httpResponse.Headers.Add("Access-Control-Allow-Origin", origin); var method = httpRequest.Method; if (method.ToLower() == "options") httpResponse.StatusCode = System.Net.HttpStatusCode.NoContent; } } // Simply apply this attribute to a DataService-derived class to get // CORS support in that service [AttributeUsage(AttributeTargets.Class)] public class CORSSupportBehaviorAttribute : Attribute, IServiceBehavior { #region IServiceBehavior Members void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { var requiredHeaders = new Dictionary<string, string>(); //Chrome doesn't accept wildcards when authorization flag is true //requiredHeaders.Add("Access-Control-Allow-Origin", "*"); requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS"); requiredHeaders.Add("Access-Control-Allow-Headers", "Accept, Origin, Authorization, X-Requested-With,Content-Type"); requiredHeaders.Add("Access-Control-Allow-Credentials", "true"); foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers) { foreach (EndpointDispatcher ed in cd.Endpoints) { ed.DispatchRuntime.MessageInspectors.Add(new CORSSupport(requiredHeaders)); } } } void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } #endregion }
using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher;
[CORSSupportBehavior] public class ScannerService : IScannerService {
کار تمام است، یکبار دیگر ابتدا پروژهی WcfServiecScanner و سپس پروژه هاست را Build کرده و برنامهی هاست را اجرا کنید. اکنون مشاهده میکنید که با زدن دکمهی اسکن، اسکنر فرم تنظیمات اسکن را نمایش میدهد که پس از زدن دکمهی Scan، پروسه آغاز شده و پس از اتمام، تصویر اسکن شده در صفحهی وب سایت نمایش داده میشود.