ساخت یک پروژه MVC
بسم الله الرحمن الرحیم
این دوره آموزشی دارای پیش نیاز می باشد، برای مطالعه پیش نیاز این دوره لطفا مقاله "آموزش WebApi" مطالعه کنید.
مشاهده تمامی آموزش های دوره آموزش Web API
آموزش Web API– سطح پیشرفته
بخش اول– آموزش Web API و ASP.NET MVC و RESTfull
آموزش Web API
برای دوستان عزیز که در تیم های توسعه وب MVC فعالیت می کنند مطلبی را آماده کردم که انشالله مفید واقع شود البته این مطلب برای دوستانی مفید خواهد بود که تازه به جمع شرکت اضافه شده اند و هنوز به MVC مسلط نیستند.
البته دوستان لطفا قبل از شروع به خواندن این مطلب، مطلبی را که در همین بخش با عنوان معرفی Web Api قرار داده ام را مطالعه کنید.
خوب از آنجا که همه دوستان عزیز تیم های توسعه وب Net. با VS 2012 کار می کنند این مثال را با VS 2012 انجام می دهیم برای شروع یک پروژه جدید به صورت Internet Application ایجاد می کنیم.
Entity Framework
ما از EF به صورت Code First برای Data Model استفاده خواهیم کرد. EF Code First اجازه می دهد تا جداول پایگاه داده را تولید کنیم با چیزی بیشتر از چند (Plain Old CLR Objects (POCO. به علاوه EF به ما اجازه می دهد تا از LINQ to Entities و Lambda expressions استفاده کنیم که باعث می شود صدور فرامین و کوئری ها آسان تر گردد.
سایت ما برای بررسی است. کار را با کلاسی که با نام Review است شروع می کنیم. کلاس زیر را در فایل مربوطه در قسمت Model می نویسیم.
public class Review
{
public int Id { get; set; }
[Required]
public string Content { get; set; }
[Required]
[StringLength(128)]
public string Topic { get; set; }
[Required]
public string Email { get; set; }
[Required]
public bool IsAnonymous { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public virtual IEnumerable<Comment> Comments { get; set; }
}
پراپرتی Id کلید اصلی است.
پراپرتی Content برای ذخیره نتیجه بررسی است.
پراپرتی Topic برای تعیین موضوع است.
پراپرتی Email همانطور که از نام آن مشخص است برای ذخیره ایمیل است.
پراپرتی IsAnonymous برای اینکه تعیین کنیم منتقد فردناشناس است یا خیر.
پراپرتی CategoryId و Category برای ایجاد رابطه و کلید خارجی با کلاس Category است.
پراپرتی Comments برای توضیحات است.
حال کلاس Comment را ایجاد می کنیم.
public class Comment
{
public int Id { get; set; }
public string Content { get; set; }
public string Email { get; set; }
public bool IsAnonymous { get; set; }
public int ReviewId { get; set; }
public Review Review { get; set; }
}
کلاس Comment یک Id دارد که همان کلید اصلی است و Content برای ذخیره توضیحات است و Email برای نگهداری ایمیل و IsAnonymous برای تعیین ناشناس بودن یا کاربر سیستم بودن فرد و ReviewId - Review برای ایجاد رابطه با کلاس Review است.
در آخر کلاس Category را ایجاد می کنیم.
public class Category
{
public int Id { get; set; }
[Required]
[StringLength(32)]
public string Name { get; set; }
}
همانطور که مشخص است ما از Required Data annotation در کلاس ها استفاده کرده ایم که باعث می شود این فیلد در پایگاه داده non-nullable باشد و کاربر در فرم حتما آن را تکمیل کند یا به اصطلاح اجباری باشد و همچنین می توانید از پراپرتی های این تگ (Required) برای ساختن یک Validator سفارشی جهت کار خود استفاده کنید.
در بعضی پراپرتی ها ما از تگ virtual استفاده کرده ایم این تگ برای ایجاد کلید خارجی یا رابطه با کلاس دیگر استفاده می شود(البته در این جا). ما باید کلاس DbContext را برای ایجاد جداول مربوط به این کلاس ها در پایگاه داده ایجاد کنیم که نام این کلاس را ReviewedContext می گذاریم.
public class ReviewedContext : DbContext
{
public DbSet<Review> Reviews { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Comment> Comments { get; set; }
public ReviewedContext()
{
Configuration.ProxyCreationEnabled = false;
}
}
هر تیبل معادل با کلاس های ما در پایگاه داده ایجاد می گردد و کد ProxyCreationEnabled = false باعث می شود از بازیابی تمام موجودیت های شی های کلاس مربوطه اطمینان حاصل کنیم، خطایابی از proxies–making آسانتر گردد.
بعد از راه انداری یک بارگذار پایگاه داده یا همان database initializer، یک initializer تضمین می کند که پایگاه داده هنگامی که data model ما دچار هر گونه تغییری گردد به درستی ایجاد گردد.
بدون استفاده از یک initializer شما می بایست به ازای هر تغییر پایگاه داده خود را دستی حذف تغییرات نیاز را بر روی تمامی
POCOs اعمال نمایید.
Plain Old CLR Objects یا همان POCO در بخش جداگانه به صورت کامل توضیح داده ام.
چند نمونه از این initializer موجود است مانند DropCreateDatabaseAlway اما ما از دستور DropCreateDatabaseIfModelChanges برای کار خود استفاده می کنیم.
هر دو دستور DropCreateDatabaseAlways و DropCreateDatabaseIfModelChanges دارای یک تاثیر هستند: هنگامی که ساختار شما تغییر کند تیبل ها حذف می گردند که طبیعتا داده های آنها نیز حذف خواهند شد اما EF راه سومی را برای تولید پایگاه داده دارد
Migrations که در فارسی به معنای مهاجرت است.
این ویژگی تغییرات جدید را بر روی پایگاه داده را ردیابی می کند و داده ها را حذف نمی کند مانند تغییرات POCO classes
public class ReviewedContextInitializer : DropCreateDatabaseIfModelChanges<ReviewedContext>
{
protected override void Seed(ReviewedContext context)
{
// Use the context to seed the db.
}
}
کلاس ReviewedContextInitializer متد Seed را بازنویسی یا همان overrides کرده است. این باعث می شود ما بتوانیم پایگاه داده خود را با یک سری داده تست بارگذاری کنیم حال ما باید در فایل Global.asax خط زیر را به متد Application_Start اضافه کنیم:
Database.SetInitializer(new ReviewedContextInitializer());
اجازه دهید یک Repositories برای دریافت دادها از پایگاه داده ایجاد کنیم. همچنین باید تزیق وابستگی یا همان (dependency injection (DI را در آینده به وسیله Ninject انجام دهیم.
مباحث زیر را در بخشی جداگانه به صورت کامل توضیح داده ام:
-
Repositories
-
(dependency injection (DI
-
Ninject
اگر شمابحث های ذکر شده یا (Inversion of Control (IoC را نمی دانید بهتر است برای آنها وقتی در نظر بگیرید.
در واقع ایده dependency injection جهت تزریق وابستگی محکم یا به اصطلاح concrete به یک کلاس است. به عنوان مخالف کد نویسی پیچیده و ایجاد یک وابستگی محکم به کلاس، به عبارت دیگر این کلاس باید از کلاس دیگر جدا باشد و رابطه ای صحیح بین دو کلاس ایجاد شود اگر هنوز متوجه نشده اید به این مثال توجه کنید:
public class Foo
{
private Bar _bar;
public Foo()
{
_bar = new Bar();
}
این کد یک کلاس با نام Foo ایجاد می کند و این کلاس وابستگی ای دارد به یک شی از نوع Bar و شی Bar در کلاس Foo ساخته شده است این کد خیلی کثیف است و نگهداری و تست آن بسیار مشکل است.Foo و Bar محکم با هم چفت شده اند یا در اصطلاح tightly coupled شده اند. اما نتیجه چیست؟ نگداری آن اصلا ایده آل نیست و همچنین Foo با پیاده سازی Bar وابستگی دارد یعنی تا وقتی Bar ساخته نشود خبری از Foo نیست (چقدر کثیف!) و باعث می شود تست این کلاس بسیار مشکل شود.
این کد می تواند کمی بهتر شود با اضافه کردن چند تغییر ساده در کلاس Foo مجددا تجدید نظر کردیم، آن را ببینید:
public class Foo
{
private IBar _bar;
public Foo(IBar bar)
{
_bar = bar;
}
}
حالا دیگر کلاس Foo وابسته به پیاده سازی نوع Bar ندارد! چون در این کد ما Bar را از Interface ایجاد کردیم پس بخش بزرگی از مشکل حل شده است بجای روش قبل یک شی از کلاس پیاده سازی شده از روی Interface Bar در سازنده ایجاد می گردد.
این روش تا حد زیادی نگهداری را بهبود میبخشد و همچنین اجازه می دهد که تزریق هر شی IBar را راحت تر تست نماییم. با همین توضیح مختصر اجازه دهید برویم سراغ کار با Ninject، برای شروع کار نیاز است آن را نصب نماییم که برای نصب از قسمتPackage Manager Console دستور Install-Package Ninject.MVC3 اجرا می کنیم. این دستور Ninject را به پروژه ما اضافه می کند.
برای اولین repository ما کلاس ReviewsRepository را می سازیم این کلاس پیاده سازی شده از روی Interface ایی با نام IReviewRepository است.
کد IReviewRepository:
public interface IReviewRepository
{
Review Get(int id);
IQueryable<Review> GetAll();
Review Add(Review review);
Review Update(Review review);
void Delete(int reviewId);
IEnumerable<Review> GetByCategory(Category category);
IEnumerable<Comment> GetReviewComments(int id);
}
این اینترفیس اطمینان می دهد که reviewRepositories ساخته شده از روی این اینترفیس حتما شامل عملگرهای CRUD باشد(همان چیزی که در همه پروژه ها وجود دارد).
سایر متدهای این ایترفیس: ما همچنین یک سری reviews به وسیله category مشخص دریافت می کنیم و نظراتی در برای یک reviews مشخص بازیابی می کنیم.
حال به سراغ پیاده سازی این اینترفیس می رویم:
public class ReviewRepository : IReviewRepository
{
private ReviewedContext _db { get; set; }
public ReviewRepository()
:this (new ReviewedContext())
{
}
public ReviewRepository(ReviewedContext db)
{
_db = db;
}
public Review Get(int id)
{
return _db.Reviews.SingleOrDefault(r => r.Id == id);
}
public IQueryable<Review> GetAll()
{
return _db.Reviews;
}
public Review Add(Review review)
{
_db.Reviews.Add(review);
_db.SaveChanges();
return review;
}
public Review Update(Review review)
{
_db.Entry(review).State = EntityState.Modified;
_db.SaveChanges();
return review;
}
public void Delete(int reviewId)
{
var review = Get(reviewId);
_db.Reviews.Remove(review);
}
public IEnumerable<Review> GetByCategory(Category category)
{
return _db.Reviews.Where(r => r.CategoryId == category.Id);
}
public IEnumerable<Comment> GetReviewComments(int id)
{
return _db.Comments.Where(c => c.ReviewId == id);
}
}
این repository متکی به شی ReviewedContext است و متغییر کلاس را ذخیره می کند.
این ما را قادر می سازد که بتوانیم در تمامی متدهای repository از LINQ استفاده نماییم و تعامل با پایگاه داده آسان شود.
Web Api یک ویژگی خوب دارد که به ما اجازه می دهد تا DI framework خود را انطور که دوست داریم اضافه کنیم.این ویژگی فراتر از حدود این آموزش است و در آموزش دیگر به آن خواهم پرداخت.
یکی از مهمترین مکان ها برای کد ما پوشه App_Start است،که شامل یک فایل با نام NinjectCommonWeb.cs است (هنگامی که آن را نصب می کردیم به صورت خودکار در این پوشه اضافه می شود) این فایل شامل یک کلاس استاتیک با نام NinjectWebCommon است و زیباتر از این کلاس متد RegisterServices است
کد زیر را در این متد اضافه کنید:
kernel.Bind<IReviewRepository>().To<ReviewRepository>();
kernel.Bind<ICategoriesRepository>().To<CategoriesRepository>();
kernel.Bind<ICommentsRepository>().To<CommentsRepository>();
GlobalConfiguration.Configuration.DependencyResolver = new NinjectResolver(kernel);
سه خط ابتدایی این کد یک اینترفیس را به یک پیاده سازی محکم از اینترفس بایند می کند و خط چهارم DI را برای WebAPI تنظیم می کند.
WebAPI
حال وقت ساخت یک کنترلر برای API است. WebApi یک Framework شبیه MVC است که ما به آسانی می توانیم یک RESTful service بسازیم و می تواند درون خود یک MVC4 application را اجرا کند و حتی می تواند خودش هاست شود درون سک IIS. اما این همه چیز نیست خیلی ویژگی های زیادی هنوز باقی است مانند content negotiation یا به اصطلاح مذاکر محتوا که به این معنی می باشد که داده را به فرمت درخواست شده به صورت خودکار serialize می شود،model binding, validation و امکانات دیگر.
ما ابتدا نیاز داریم یک endpoint را با WebApi بسازیم و ما این کار را با ساختن یک کلاس که از ApiController ارث می برد انجام می دهیم. شروع کردن با Visual Studio 2012 آسانتر است زیرا ویژگی جدیدی دارد که partially scaffolded controller می سازد.
این یک کنترلر که تعدادی متد ازقبل تعریف شده را دارا است می سازد. در اینجا یک مثال قرارداده شده است:
// GET api/default1
public IEnumerable Get()
{
return new string[] { "value1", "value2" };
}
// GET api/default1/5
public string Get(int id)
{
return "value";
}
// POST api/default1
public void Post(string value)
{
}
// PUT api/default1/5
public void Put(int id, string value)
{
}
// DELETE api/default1/5
public void Delete(int id)
{
}
نام متدها با افعال HTTP نماینده آنها مطابقت دارد. اکنون کلاس ReviewsController را می سازیم، کمی کد طولانی است اما بسیار ساده است.
public class ReviewsController : ApiController
{
private ICategoriesRepository _categoriesRepository { get; set; }
private IReviewRepository _reviewRepository { get; set; }
public ReviewsController(IReviewRepository reviewRepository, ICategoriesRepository categoriesRepository)
{
_reviewRepository = reviewRepository;
_categoriesRepository = categoriesRepository;
}
// GET api/review
public IEnumerable Get()
{
var reviews = _reviewRepository.GetAll();
return reviews;
}
// GET api/review/5
public HttpResponseMessage Get(int id)
{
var category = _reviewRepository.Get(id);
if (category == null)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
return Request.CreateResponse(HttpStatusCode.OK, category);
}
// POST api/review
public HttpResponseMessage Post(Review review)
{
var response = Request.CreateResponse(HttpStatusCode.Created, review);
// Get the url to retrieve the newly created review.
response.Headers.Location = new Uri(Request.RequestUri, string.Format("reviews/{0}", review.Id));
_reviewRepository.Add(review);
return response;
}
// PUT api/review/5
public void Put(Review review)
{
_reviewRepository.Update(review);
}
// DELETE api/review/5
public HttpResponseMessage Delete(int id)
{
_reviewRepository.Delete(id);
return Request.CreateResponse(HttpStatusCode.NoContent);
}
// GET api/reviews/categories/{category}
public HttpResponseMessage GetByCategory(string category)
{
var findCategory = _categoriesRepository.GetByName(category);
if (findCategory == null)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
return Request.CreateResponse(HttpStatusCode.OK,_reviewRepository.GetByCategory(findCategory));
}
// GET api/reviews/comments/{id}
public HttpResponseMessage GetReviewComments(int id)
{
var reviewComments = _reviewRepository.GetReviewComments(id);
if (reviewComments == null)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
return Request.CreateResponse(HttpStatusCode.OK, reviewComments);
}
}