- چرا مسیریابی نشانه ای؟
- فعال سازی مسیریابی نشانه ای
- پارامترهای اختیاری URI و مقادیر پیش فرض
- پیشوند مسیر ها
- مسیر پیش فرض
- محدودیتهای مسیر ها
- محدودیتهای سفارشی
- نام مسیر ها
- ناحیهها (Areas)
چرا مسیریابی نشانه ای
- {productId:int}/{productTitle}
- {username}
- {username}/catalogs/{catalogId:int}/{catalogTitle}
routes.MapRoute( name: "ProductPage", url: "{productId}/{productTitle}", defaults: new { controller = "Products", action = "Show" }, constraints: new { productId = "\\d+" } );
[Route("{productId:int}/{productTitle}")] public ActionResult Show(int productId) { ... }
فعال سازی Attribute Routing
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapMvcAttributeRoutes(); } }
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapMvcAttributeRoutes(); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
پارامترهای اختیاری URI و مقادیر پیش فرض
public class BooksController : Controller { // eg: /books // eg: /books/1430210079 [Route("books/{isbn?}")] public ActionResult View(string isbn) { if (!String.IsNullOrEmpty(isbn)) { return View("OneBook", GetBook(isbn)); } return View("AllBooks", GetBooks()); } // eg: /books/lang // eg: /books/lang/en // eg: /books/lang/he [Route("books/lang/{lang=en}")] public ActionResult ViewByLanguage(string lang) { return View("OneBook", GetBooksByLanguage(lang)); } }
پیشوند مسیرها (Route Prefixes)
public class ReviewsController : Controller { // eg: /reviews [Route("reviews")] public ActionResult Index() { ... } // eg: /reviews/5 [Route("reviews/{reviewId}")] public ActionResult Show(int reviewId) { ... } // eg: /reviews/5/edit [Route("reviews/{reviewId}/edit")] public ActionResult Edit(int reviewId) { ... } }
[RoutePrefix("reviews")] public class ReviewsController : Controller { // eg.: /reviews [Route] public ActionResult Index() { ... } // eg.: /reviews/5 [Route("{reviewId}")] public ActionResult Show(int reviewId) { ... } // eg.: /reviews/5/edit [Route("{reviewId}/edit")] public ActionResult Edit(int reviewId) { ... } }
[RoutePrefix("reviews")] public class ReviewsController : Controller { // eg.: /spotlight-review [Route("~/spotlight-review")] public ActionResult ShowSpotlight() { ... } ... }
مسیر پیش فرض
[RoutePrefix("promotions")] [Route("{action=index}")] public class ReviewsController : Controller { // eg.: /promotions public ActionResult Index() { ... } // eg.: /promotions/archive public ActionResult Archive() { ... } // eg.: /promotions/new public ActionResult New() { ... } // eg.: /promotions/edit/5 [Route("edit/{promoId:int}")] public ActionResult Edit(int promoId) { ... } }
محدودیتهای مسیر ها
// eg: /users/5 [Route("users/{id:int}"] public ActionResult GetUserById(int id) { ... } // eg: users/ken [Route("users/{name}"] public ActionResult GetUserByName(string name) { ... }
مثال | توضیحات | محدودیت |
{x:alpha} | کاراکترهای الفبای لاتین را تطبیق (match) میدهد (a-z, A-Z). | alpha |
{x:bool} | یک مقدار منطقی را تطبیق میدهد. | bool |
{x:datetime} | یک مقدار DateTime را تطبیق میدهد. | datetime |
{x:decimal} | یک مقدار پولی را تطبیق میدهد. | decimal |
{x:double} | یک مقدار اعشاری 64 بیتی را تطبیق میدهد. | double |
{x:float} | یک مقدار اعشاری 32 بیتی را تطبیق میدهد. | float |
{x:guid} | یک مقدار GUID را تطبیق میدهد. | guid |
{x:int} | یک مقدار 32 بیتی integer را تطبیق میدهد. | int |
{(x:length(6} {(x:length(1,20} | رشته ای با طول تعیین شده را تطبیق میدهد. | length |
{x:long} | یک مقدار 64 بیتی integer را تطبیق میدهد. | long |
{(x:max(10} | یک مقدار integer با حداکثر مجاز را تطبیق میدهد. | max |
{(x:maxlength(10} | رشته ای با حداکثر طول تعیین شده را تطبیق میدهد. | maxlength |
{(x:min(10} | مقداری integer با حداقل مقدار تعیین شده را تطبیق میدهد. | min |
{(x:minlength(10} | رشته ای با حداقل طول تعیین شده را تطبیق میدهد. | minlength |
{(x:range(10,50} | مقداری integer در بازه تعریف شده را تطبیق میدهد. | range |
{(${x:regex(^\d{3}-\d{3}-\d{4} | یک عبارت با قاعده را تطبیق میدهد. | regex |
// eg: /users/5 // but not /users/10000000000 because it is larger than int.MaxValue, // and not /users/0 because of the min(1) constraint. [Route("users/{id:int:min(1)}")] public ActionResult GetUserById(int id) { ... }
// eg: /greetings/bye // and /greetings because of the Optional modifier, // but not /greetings/see-you-tomorrow because of the maxlength(3) constraint. [Route("greetings/{message:maxlength(3)?}")] public ActionResult Greet(string message) { ... }
محدودیتهای سفارشی
public class ValuesConstraint : IRouteConstraint { private readonly string[] validOptions; public ValuesConstraint(string options) { validOptions = options.Split('|'); } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase); } return false; } }
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint)); routes.MapMvcAttributeRoutes(constraintsResolver); } }
public class TemperatureController : Controller { // eg: temp/celsius and /temp/fahrenheit but not /temp/kelvin [Route("temp/{scale:values(celsius|fahrenheit)}")] public ActionResult Show(string scale) { return Content("scale is " + scale); } }
نام مسیر ها
[Route("menu", Name = "mainmenu")] public ActionResult MainMenu() { ... }
<a href="@Url.RouteUrl("mainmenu")">Main menu</a>
ناحیهها (Areas)
[RouteArea("Admin")] [RoutePrefix("menu")] [Route("{action}")] public class MenuController : Controller { // eg: /admin/menu/login public ActionResult Login() { ... } // eg: /admin/menu/show-options [Route("show-options")] public ActionResult Options() { ... } // eg: /stats [Route("~/stats")] public ActionResult Stats() { ... } }
Url.Action("Options", "Menu", new { Area = "Admin" })
[RouteArea("BackOffice", AreaPrefix = "back-office")]
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapMvcAttributeRoutes(); AreaRegistration.RegisterAllAreas(); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
پیاده سازی DbContext مورد نیاز
برای ساخت DbContext میتوان به صورت زیر عمل نمود:
namespace FsWeb.Repositories open System.Data.Entity open FsWeb.Models type FsMvcAppEntities() = inherit DbContext("FsMvcAppExample") do Database.SetInitializer(new CreateDatabaseIfNotExists<FsMvcAppEntities>()) [<DefaultValue()>] val mutable books: IDbSet<Guitar> member x.Books with get() = x.books and set v = x.books <- v
اگر syntax زبان #F برایتان نامفهوم است میتوانید از این دوره کمک بگیرید.
پیاده سازی کلاس BookRepository
ابتدا به کدهای زیر دقت کنید:
namespace FsWeb.Repositories type BooksRepository() = member x.GetAll () = use context = new FsMvcAppEntities() query { for g in context.Books do select g } |> Seq.toList
در بخش بعدی بک کوئری از DbSet مورد نظر گرفته میشود. این روش Query گرفتن در F# 3.0 مطرح شده است. در نتیجه در نسخههای قبلی آن (F# 2.0) اجرای این کوئری باعث خطا میشود. اگر قصد دارید با استفاده از F# 2.0 کوئریهای خود را ایجاد نماید باید به طریق زیر عمل نمایید:
ابتدا از طریق nuget اقدام به نصب package ذیل نمایید:
FSPowerPack.Linq.Community
query <@ seq { for g in context.Books -> g } @> |> Seq.toList
[<HandleError>] type BooksController(repository : BooksRepository) = inherit Controller() new() = new BooksController(BooksRepository()) member this.Index () = repository.GetAll() |> this.View
نکته : تنظیمات مروط به ConnectionString را فراموش نکنید:
<add name="FsMvcAppExample" connectionString="YOUR CONNECTION STRING" providerName="System.Data.SqlClient" />
معرفی کتابخانه PdfReport
در ساخت یک preferencesActivity اگر شما از یک ListPreference استفاده کنید که مقادیر پاس داده شده به آن از نوع integer باشند میبینید که در ذخیره و بازیابی دچار مشکل میشود و به طور پیش فرض از مقادیر string استفاده میکند مگر اینکه در این حین یا از تبدیل استفاده کنید یا اینکه این سورس را به پروژه خود اضافه کنید. با استفاده از این سورس به راحتی از مقادیر عددی را به entryValue پاس دهید. به غیر از این کلاس، دیگر کلاسهای این پروژه را هم نگاهی بیندازید. موارد خوبی یافت میشود.
یکپارچه کردن ELMAH با WCF RIA Services
ب - بهترین مرجع برای ELMAH ، فایل web.config پوشه samples آن است. در این مورد تنظیمات لازم وجود دارد (در مورد بستن دسترسی ریموت یا استفاده از authorization).
خلاصه این دو مورد رو یکی از اعضای اصلی تیم ASP.NET در عمل نشان داده:
http://haacked.com/archive/2007/07/24/securely-implement-elmah-for-plug-and-play-error-logging.aspx
+
http://code.google.com/p/elmah/wiki/SecuringErrorLogPages
TwitterBootstrapMVC
<add namespace="TwitterBootstrapMVC" /> <add namespace="TwitterBootstrap3" />
این افزونه جزو موفقترین کتابخانههای دات نتی مایکروسافت در سالهای اخیر به شما میرود؛ تا حدی که معادلهای بسیاری از آن برای زبانهای دیگر مانند Java، JavaScript، Python، CPP و غیره نیز تهیه شدهاند.
استفاده از Rx به همراه یک کوئری LINQ
یک برنامهی کنسول جدید را ایجاد کنید. سپس برای نصب کتابخانهی Rx، دستور ذیل را در کنسول پاورشل نیوگت اجرا نمائید:
PM> Install-Package Rx-Main
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Rx-Core" version="2.2.4" targetFramework="net45" /> <package id="Rx-Interfaces" version="2.2.4" targetFramework="net45" /> <package id="Rx-Linq" version="2.2.4" targetFramework="net45" /> <package id="Rx-Main" version="2.2.4" targetFramework="net45" /> <package id="Rx-PlatformServices" version="2.2.4" targetFramework="net45" /> </packages>
using System; using System.Linq; namespace Rx01 { class Program { static void Main(string[] args) { var query = Enumerable.Range(1, 5).Select(number => number); foreach (var number in query) { Console.WriteLine(number); } finished(); } private static void finished() { Console.WriteLine("Done!"); } } }
اکنون اگر بخواهیم همین عملیات را توسط Rx انجام دهیم، به شکل زیر خواهد بود:
using System; using System.Linq; using System.Reactive.Linq; namespace Rx01 { class Program { static void Main(string[] args) { var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(); observableQuery.Subscribe(onNext: number => Console.WriteLine(number), onCompleted: () => finished()); } private static void finished() { Console.WriteLine("Done!"); } } }
1 2 3 4 5 Done!
observableQuery.Subscribe(Console.WriteLine, finished);
در این مثال ساده صرفا یک Syntax دیگر را نسبت به حلقهی foreach متداول مشاهده کردیم که اندکی فشردهتر است. در هر دو حالت نیز عملیات انجام شده در تردجاری صورت گرفتهاند. اما قابلیتها و ارزشهای واقعی Rx زمانی آشکار خواهند شد که پردازش موازی و پردازش در تردهای دیگر را در آن فعال کنیم.
الگوی Observer
Rx پیاده سازی کنندهی الگوی طراحی شیءگرایی به نام Observer است. برای توضیح آن یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده میکند سوئیچ برق در حالت روشن قرار گرفتهاست، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته میشود. هر زمان که حالت سوئیچ تغییر میکند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راهحلهای عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف میشود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.
در Rx دو اینترفیس معادل observer و subject تعریف شدهاند. در اینجا اینترفیس IObserver معادل observer است و اینترفیس IObservable معادل subject میباشد:
class Subject : IObservable<int> { public IDisposable Subscribe(IObserver<int> observer) { } }
class Observer : IObserver<int> { public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(int value) { } }
مجموعههای Observable کلید کار با Rx هستند. در مثال قبل ملاحظه کردیم که با استفاده از متد الحاقی ToObservable بر روی یک کوئری LINQ و یا هر نوع IEnumerable ایی، میتوان یک مجموعهی Observable را ایجاد کرد. خروجی کوئری حاصل از آن به صورت خودکار اینترفیس IObservable را پیاده سازی میکند که دارای یک متد به نام Subscribe است.
در متد Subscribe کاری که به صورت خودکار صورت خواهد گرفت، ایجاد یک حلقهی foreach بر روی مجموعهی مورد آنالیز و سپس فراخوانی متد OnNext کلاس پیاده سازی کنندهی IObserver به ازای هر آیتم موجود در مجموعه است (فراخوانی observer.OnNext). در پایان کار هم فقط return this در اینجا صورت خواهد گرفت. در حین پردازش حلقه، اگر خطایی رخ دهد، متد observer.OnError انجام میشود.
در مثال قبل،کوئری LINQ نوشته شده، خروجی از نوع IObservable ندارد. به کمک متد الحاقی ToObservable:
public static System.IObservable<TSource> ToObservable<TSource>( this System.Collections.Generic.IEnumerable<TSource> source, System.Reactive.Concurrency.IScheduler scheduler)
البته استفاده از متد Subscribe به نحوی که در مثال قبل ذکر شد، خلاصه شدهی الگوی Observer است. اگر بخواهیم دقیقا مانند الگو عمل کنیم، چنین شکلی را خواهد داشت:
var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(); var observer = Observer.Create<int>(onNext: number => Console.WriteLine(number)); observableQuery.Subscribe(observer);
پردازش نتایج یک کوئری LINQ در تردی دیگر توسط Rx
برای اجرای نتایج متد Subscribe در یک ترد جدید، میتوان پارامتر scheduler متد ToObservable را مقدار دهی کرد:
using System; using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; namespace Rx01 { class Program { static void Main(string[] args) { Console.WriteLine("Thread-Id: {0}", Thread.CurrentThread.ManagedThreadId); var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(scheduler: NewThreadScheduler.Default); observableQuery.Subscribe(onNext: number => { Console.WriteLine("number: {0}, on Thread-id: {1}", number, Thread.CurrentThread.ManagedThreadId); }, onCompleted: () => finished()); } private static void finished() { Console.WriteLine("Done!"); } } }
Thread-Id: 1 number: 1, on Thread-id: 3 number: 2, on Thread-id: 3 number: 3, on Thread-id: 3 number: 4, on Thread-id: 3 number: 5, on Thread-id: 3 Done!
NewThreadScheduler.Default در فضای نام System.Reactive.Concurrency واقع شدهاست.
یک نکته
در نگارشهای آغازین Rx، مقدار scheduler را میشد معادل Scheduler.NewThread نیز قرار داد که در نگارشهای جدید منسوخ شده درنظر گرفته شده و به زودی حذف خواهد شد. معادلهای جدید آن اکنون NewThreadScheduler.Default، ThreadPoolScheduler.Default و امثال آن هستند.
مدیریت خاتمهی اعمال انجام شدهی در تردهای دیگر توسط Rx
یکی از مواردی که حین اجرای نتیجهی callbackهای پردازش شدهی در تردهای دیگر نیاز است بدانیم، زمان خاتمهی کار آنها است. برای نمونه در مثال قبل، نمایش Done پس از پایان تمام callbacks انجام شدهاست. فرض کنید، callback پایان عملیات را حذف کرده و متد finished را پس از فراخوانی متد observableQuery.Subscribe قرار دهیم:
observableQuery.Subscribe(onNext: number => { Console.WriteLine("number: {0}, on Thread-id: {1}", number, Thread.CurrentThread.ManagedThreadId); }/*, onCompleted: () => finished()*/); finished();
Thread-Id: 1 number: 1, on Thread-id: 3 Done! number: 2, on Thread-id: 3 number: 3, on Thread-id: 3 number: 4, on Thread-id: 3 number: 5, on Thread-id: 3
مدیریت استثناهای رخ داده در حین پردازش مجموعههای واکنشگرا
متد Subscribe دارای چندین overload است. تا اینجا نمونهای که دارای پارامترهای onNext و onCompleted بودند را بررسی کردیم. اگر بخواهیم مدیریت استثناءها را نیز در اینجا اضافه کنیم، فقط کافی است از overload دیگر آن که دارای پارامتر onError است، استفاده نمائیم:
observableQuery.Subscribe( onNext: number => Console.WriteLine(number), onError: exception => Console.WriteLine(exception.Message), onCompleted: () => finished());
مدیریت ترد اجرای نتایج حاصل از Rx در یک برنامهی دسکتاپ WPF یا WinForms
تا اینجا مشاهده کردیم که اجرای callbackهای observer در یک ترد دیگر، به سادگی تنظیم پارامتر scheduler متد ToObservable است. اما در برنامههای دسکتاپ برای به روز رسانی عناصر رابط کاربری، حتما باید در تردی قرار داشته باشیم که آن رابط کاربری در آن ایجاد شدهاست یا به عبارتی در ترد اصلی برنامه؛ در غیر اینصورت برنامه کرش خواهد کرد. مدیریت این مساله نیز در Rx بسیار سادهاست. ابتدا نیاز است بستهی Rx-WPF را نصب کرد:
PM> Install-Package Rx-WPF
observableQuery.ObserveOn(DispatcherScheduler.Current).Subscribe(...)
observableQuery.ObserveOnDispatcher().Subscribe(...)
و یا اگر از WinForms استفاده میکنید، ابتدا بستهی Rx خاص آنرا نصب کنید:
PM> Install-Package Rx-WinForms
observableQuery.ObserveOn(SynchronizationContext.Current).Subscribe(...)
یک نکته
در Rx فرض میشود که کوئری شما زمانبر است و callbackهای مشاهدهگر سریع عمل میکنند. بنابراین هدف از callbackهای آن، پردازشهای سنگین نیست. جهت آزمایش این مساله، اینبار query ابتدایی برنامه را به شکل ذیل تغییر دهید که در آن بازگشت زمانبر یک سری داده شبیه سازی شدهاند.
var query = Enumerable.Range(1, 5).Select(number => { Thread.Sleep(250); return number; });
نسخه جدید برنامه Eazfuscator به همراه دو قابلیت جالب یکی کردن و همچنین مدفون نمودن اسمبلیها ارائه شده است:
یکی کردن چند اسمبلی با هم
Eazfuscator برای یکی کردن اسمبلیها از برنامه معروف ILmerge استفاده میکند با این تفاوت که دیگر نیازی نیست تا پارامترهای آنرا تنظیم کرد و بسیاری از مسایل را به صورت خودکار مدیریت میکند.
جهت فعال کردن این قابلیت، یکی از روشهای کار به صورت زیر است:
فایلی به نام ObfuscationSettings.cs را به پروژه خود اضافه کرده، سپس محتویات آنرا حذف نموده و با چند سطر زیر جایگزین و کامپایل کنید:
using System;
using System.Reflection;
[assembly: Obfuscation(Feature = "merge with file1.dll", Exclude = false)]
[assembly: Obfuscation(Feature = "merge with file2.dll", Exclude = false)]
[assembly: Obfuscation(Feature = "merge with file3.dll", Exclude = false)]
همانطور که ملاحظه میکنید این چند سطر حاوی نام اسمبلیهایی میباشند که قرار است با اسمبلی جاری یکی شوند.
سپس اسمبلی جاری را (میخواهد فایل exe باشد یا یک dll ، فرقی نمیکند) بر روی Eazfuscator کشیده و رها کنید. پس از چند لحظه اسمبلی نهایی تولید شده شامل تمام کلاسها و منابع اسمبلیهایی خواهد بود که در فایل ObfuscationSettings.cs ذکر شدهاند؛ به همراه Obfuscation خودکار آنها.
مدفون کردن اسمبلیها در یک اسمبلی
قابلیت دیگر این برنامه دفن (embedding) چند اسمبلی در اسمبلی نهایی است. برای فعال سازی آن روش کار همانند قبل است با این تفاوت که بجای merge with باید نوشت embed . برای مثال:
[assembly: Obfuscation(Feature = "embed Common.dll", Exclude = false)]
به این ترتیب اسمبلیهای ذکر شده پس از رمزنگاری و فشرده شدن به صورت منابع اسمبلی جاری ذخیره خواهند شد. مدیریت استفاده از آنها هم خودکار است و نیازی نیست تا کاری در این مورد صورت گیرد.
برای نمونه برنامه معروف LINQPad از همین روش استفاده میکند و لازم به ذکر است که ... هنوز که هنوز است هیچ ک.ر.ک. کارسازی برای فعال سازی قسمت intellisense آن که رایگان نیست ارائه نشده و تمام وصلههای جدید ارائه شده کار نمیکنند ...
تفاوت مدفون کردن با یکی کردن چیست؟
در حالت یکی کردن اسمبلیها، سربار اولیه بارگذاری برنامه همانند روش مدفون سازی وجود ندارد. اما این سربار آنقدر ناچیز است که کسی آنرا احساس نخواهد کرد. مورد دیگر، عدم پشتیبانی از روش مدفون سازی در سایر سکوهای کاری مانند ویندوز فون، Compact Framework و غیره است. اما باید درنظر داشت که برای مثال ILMerge روی اسمبلیهای دارای XAML کار نمیکند (مطابق مستندات رسمی آن). بنابراین همیشه نمیتوان از روش یکی سازی استفاده کرد و محدودیتهای خاص خودش را دارد.
در کل روش مدفون سازی به دلیل Obfuscation ، فشرده سازی و رمزنگاری همزمان، امنیت بیشتری را نسبت به حالت Obfuscation تنها ارائه میدهد (حداقل شخص "علاقمند" به مطالعه این نوع اسمبلیها باید از چند لایه رد شود و تجربه برنامه LINQPad ثابت کرده که این روش در مقیاس کلان (در انظار عمومی هزاران علاقمند) بسیار موفق بوده است).
بروز رسانی موجودیتهای منفصل توسط WCF
سناریویی را در نظر بگیرید که در آن عملیات CRUD توسط WCF پیاده سازی شده اند و دسترسی دادهها با مدل Code-First انجام میشود. فرض کنید مدل اپلیکیشن مانند تصویر زیر است.
همانطور که میبینید مدل ما متشکل از پستها و نظرات کاربران است. برای ساده نگاه داشتن مثال جاری، اکثر فیلدها حذف شده اند. مثلا متن پست ها، نویسنده، تاریخ و زمان انتشار و غیره. میخواهیم تمام کد دسترسی دادهها را در یک سرویس WCF پیاده سازی کنیم تا کلاینتها بتوانند عملیات CRUD را توسط آن انجام دهند. برای ساختن این سرویس مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع Class Library بسازید و نام آن را به Recipe2 تغییر دهید.
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- سه کلاس با نامهای Post, Comment و Recipe2Context به پروژه اضافه کنید. کلاسهای Post و Comment موجودیتهای مدل ما هستند که به جداول متناظرشان نگاشت میشوند. کلاس Recipe2Context آبجکت DbContext ما خواهد بود که بعنوان درگاه عملیاتی EF عمل میکند. دقت کنید که خاصیتهای لازم WCF یعنی DataContract و DataMember در کلاسهای موجودیتها بدرستی استفاده میشوند. لیست زیر کد این کلاسها را نشان میدهد.
[DataContract(IsReference = true)] public class Post { public Post() { comments = new HashSet<Comments>(); } [DataMember] public int PostId { get; set; } [DataMember] public string Title { get; set; } [DataMember] public virtual ICollection<Comment> Comments { get; set; } } [DataContract(IsReference=true)] public class Comment { [DataMember] public int CommentId { get; set; } [DataMember] public int PostId { get; set; } [DataMember] public string CommentText { get; set; } [DataMember] public virtual Post Post { get; set; } } public class EFRecipesEntities : DbContext { public EFRecipesEntities() : base("name=EFRecipesEntities") {} public DbSet<Post> posts; public DbSet<Comment> comments; }
- یک فایل App.config به پروژه اضافه کنید و رشته اتصال زیر را به آن اضافه نمایید.
<connectionStrings> <add name="Recipe2ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- حال یک پروژه WCF به Solution جاری اضافه کنید. برای ساده نگاه داشتن مثال جاری، نام پیش فرض Service1 را بپذیرید. فایل IService1.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید.
[ServiceContract] public interface IService1 { [OperationContract] void Cleanup(); [OperationContract] Post GetPostByTitle(string title); [OperationContract] Post SubmitPost(Post post); [OperationContract] Comment SubmitComment(Comment comment); [OperationContract] void DeleteComment(Comment comment); }
- فایل Service1.svc.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. بیاد داشته باشید که پروژه Recipe2 را ارجاع کنید و فضای نام آن را وارد نمایید. همچنین کتابخانه EF 6 را باید به پروژه اضافه کنید.
public class Service1 : IService { public void Cleanup() { using (var context = new EFRecipesEntities()) { context.Database.ExecuteSqlCommand("delete from [comments]"); context. Database.ExecuteSqlCommand ("delete from [posts]"); } } public Post GetPostByTitle(string title) { using (var context = new EFRecipesEntities()) { context.Configuration.ProxyCreationEnabled = false; var post = context.Posts.Include(p => p.Comments).Single(p => p.Title == title); return post; } } public Post SubmitPost(Post post) { context.Entry(post).State = // if Id equal to 0, must be insert; otherwise, it's an update post.PostId == 0 ? EntityState.Added : EntityState.Modified; context.SaveChanges(); return post; } public Comment SubmitComment(Comment comment) { using (var context = new EFRecipesEntities()) { context.Comments.Attach(comment); if (comment.CommentId == 0) { // this is an insert context.Entry(comment).State = EntityState.Added); } else { // set single property to modified, which sets state of entity to modified, but // only updates the single property – not the entire entity context.entry(comment).Property(x => x.CommentText).IsModified = true; } context.SaveChanges(); return comment; } } public void DeleteComment(Comment comment) { using (var context = new EFRecipesEntities()) { context.Entry(comment).State = EntityState.Deleted; context.SaveChanges(); } } }
- در آخر پروژه جدیدی از نوع Windows Console Application به Solution جاری اضافه کنید. از این اپلیکیشن بعنوان کلاینتی برای تست سرویس WCF استفاده خواهیم کرد. فایل program.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. روی نام پروژه کلیک راست کرده و گزینه Add Service Reference را انتخاب کنید، سپس ارجاعی به سرویس Service1 اضافه کنید. رفرنسی هم به کتابخانه کلاسها که در ابتدای مراحل ساختید باید اضافه کنید.
class Program { static void Main(string[] args) { using (var client = new ServiceReference2.Service1Client()) { // cleanup previous data client.Cleanup(); // insert a post var post = new Post { Title = "POCO Proxies" }; post = client.SubmitPost(post); // update the post post.Title = "Change Tracking Proxies"; client.SubmitPost(post); // add a comment var comment1 = new Comment { CommentText = "Virtual Properties are cool!", PostId = post.PostId }; var comment2 = new Comment { CommentText = "I use ICollection<T> all the time", PostId = post.PostId }; comment1 = client.SubmitComment(comment1); comment2 = client.SubmitComment(comment2); // update a comment comment1.CommentText = "How do I use ICollection<T>?"; client.SubmitComment(comment1); // delete comment 1 client.DeleteComment(comment1); // get posts with comments var p = client.GetPostByTitle("Change Tracking Proxies"); Console.WriteLine("Comments for post: {0}", p.Title); foreach (var comment in p.Comments) { Console.WriteLine("\tComment: {0}", comment.CommentText); } } } }
Comment: I use ICollection<T> all the time
شرح مثال جاری
ابتدا با اپلیکیشن کنسول شروع میکنیم، که کلاینت سرویس ما است. نخست در یک بلاک {} using وهله ای از کلاینت سرویس مان ایجاد میکنیم. درست همانطور که وهله ای از یک EF Context میسازیم. استفاده از بلوکهای using توصیه میشود چرا که متد Dispose بصورت خودکار فراخوانی خواهد شد، چه بصورت عادی چه هنگام بروز خطا. پس از آنکه وهله ای از کلاینت سرویس را در اختیار داشتیم، متد Cleanup را صدا میزنیم. با فراخوانی این متد تمام دادههای تست پیشین را حذف میکنیم. در چند خط بعدی، متد SubmitPost را روی سرویس فراخوانی میکنیم. در پیاده سازی فعلی شناسه پست را بررسی میکنیم. اگر مقدار شناسه صفر باشد، خاصیت State موجودیت را به Added تغییر میدهید تا رکورد جدیدی ثبت کنیم. در غیر اینصورت فرض بر این است که چنین موجودیتی وجود دارد و قصد ویرایش آن را داریم، بنابراین خاصیت State را به Modified تغییر میدهیم. از آنجا که مقدار متغیرهای int بصورت پیش فرض صفر است، با این روش میتوانیم وضعیت پستها را مشخص کنیم. یعنی تعیین کنیم رکورد جدیدی باید ثبت شود یا رکوردی موجود بروز رسانی گردد. رویکردی بهتر آن است که پارامتری اضافی به متد پاس دهیم، یا متدی مجزا برای ثبت رکوردهای جدید تعریف کنیم. مثلا رکوردی با نام InsertPost. در هر حال، بهترین روش بستگی به ساختار اپلیکیشن شما دارد.
اگر پست جدیدی ثبت شود، خاصیت PostId با مقدار مناسب جدید بروز رسانی میشود و وهله پست را باز میگردانیم. ایجاد و بروز رسانی نظرات کاربران مشابه ایجاد و بروز رسانی پستها است، اما با یک تفاوت اساسی: بعنوان یک قانون، هنگام بروز رسانی نظرات کاربران تنها فیلد متن نظر باید بروز رسانی شود. بنابراین با فیلدهای دیگری مانند تاریخ انتشار و غیره اصلا کاری نخواهیم داشت. بدین منظور تنها خاصیت CommentText را بعنوان Modified علامت گذاری میکنیم. این امر منجر میشود که Entity Framework عبارتی برای بروز رسانی تولید کند که تنها این فیلد را در بر میگیرد. توجه داشته باشید که این روش تنها در صورتی کار میکند که بخواهید یک فیلد واحد را بروز رسانی کنید. اگر میخواستیم فیلدهای بیشتری را در موجودیت Comment بروز رسانی کنیم، باید مکانیزمی برای ردیابی تغییرات در سمت کلاینت در نظر میگرفتیم. در مواقعی که خاصیتهای متعددی میتوانند تغییر کنند، معمولا بهتر است کل موجودیت بروز رسانی شود تا اینکه مکانیزمی پیچیده برای ردیابی تغییرات در سمت کلاینت پیاده گردد. بروز رسانی کل موجودیت بهینهتر خواهد بود.
برای حذف یک دیدگاه، متد Entry را روی آبجکت DbContext فراخوانی میکنیم و موجودیت مورد نظر را بعنوان آرگومان پاس میدهیم. این امر سبب میشود که موجودیت مورد نظر بعنوان Deleted علامت گذاری شود، که هنگام فراخوانی متد SaveChanges اسکریپت لازم برای حذف رکورد را تولید خواهد کرد.
در آخر متد GetPostByTitle یک پست را بر اساس عنوان پیدا کرده و تمام نظرات کاربران مربوط به آن را هم بارگذاری میکند. از آنجا که ما کلاسهای POCO را پیاده سازی کرده ایم، Entity Framework آبجکتی را بر میگرداند که Dynamic Proxy نامیده میشود. این آبجکت پست و نظرات مربوط به آن را در بر خواهد گرفت. متاسفانه WCF نمیتواند آبجکتهای پروکسی را مرتب سازی (serialize) کند. اما با غیرفعال کردن قابلیت ایجاد پروکسیها (ProxyCreationEnabled=false) ما به Entity Framework میگوییم که خود آبجکتهای اصلی را بازگرداند. اگر سعی کنید آبجکت پروکسی را سریال کنید با پیغام خطای زیر مواجه خواهید شد:
The underlying connection was closed: The connection was closed unexpectedly
می توانیم غیرفعال کردن تولید پروکسی را به متد سازنده کلاس سرویس منتقل کنیم تا روی تمام متدهای سرویس اعمال شود.
در این قسمت دیدیم چگونه میتوانیم از آبجکتهای POCO برای مدیریت عملیات CRUD توسط WCF استفاده کنیم. از آنجا که هیچ اطلاعاتی درباره وضعیت موجودیتها روی کلاینت ذخیره نمیشود، متدهایی مجزا برای عملیات CRUD ساختیم. در قسمتهای بعدی خواهیم دید چگونه میتوان تعداد متدهایی که سرویس مان باید پیاده سازی کند را کاهش داد و چگونه ارتباطات بین کلاینت و سرور را سادهتر کنیم.