1. پشتیبانی داخلی از Dependency injection در Asp.net core
با dependency injection به کد با قابلیت تست بالاتر و loosely coupled دسترسی پیدا میکنیم. این قابلیت از اهمیت
بالایی برخوردار است چراکه در نوشتن Unit testing به ما کمک میکند. در asp.net mvc نسخه 5، 4 یا حتی اپلیکیشنهای
کلاسیک بر پایه ASPX باید از نگهدارنده DI جداگانه مثل
Unity، AutoFac، StructureMap
و غیره استفاده میکردیم.
حالا در asp net core بهصورت داخلی از این قابلیت پشتیبانی میشود و خبری از پیشنیازهای سنگین نیست. تنها کافی است
که سرویسهای موردنیاز خود را در فایل startup.cs به IserviceCollection اضافه کنیم.
هدف اصلی این فایل و کلاس، اضافه کردن سرویسهایی نظیر EF، authentication، IEmailSender و
ISmsSender است.
2. User secrets در aps net core
گاهی اوقات در زمان توسعه پروژه ممکن است که اطلاعات مهم و حساس پروژه را که نباید در معرض دید عموم باشند مثل
اطلاعات ارتباط با پایگاه داده یا توکنهای شناسایی در زمان استفاده از API های خارجی، سهواً به public repository
ها کامیت کنیم و باید از چنین کاری اجتناب کرد.
در asp net mvc core ابزاری به نام user secrets وجود دارد که در آن میتوانیم اطلاعات اینچنین را در مکانی خارج از
پروژه نگهداری کنیم تا این اطلاعات به TFS یا سورس کنترل آپلود نشوند. تفاوتهای بسیاری بین این ابزار با نسخههای
قبلی و شیوههای جلوگیری از آپلود این اطلاعات که در گذشته استفاده میشده وجود دارد و به همین دلیل وارد جزئیات
نمیشویم، جهت مطالعه در این زمینه به مقاله The Secrets of ASP.NET Core User Secrets در منابع مراجعه فرمایید.
3. مقایسه razor pages با mvc
بهعنوان بخشی از
.NET Core 2.0
آپدیتهایی هم برای asp.net عرضه شد. یکی از این آپدیتها فریمورک جدیدی تحت عنوان
Razor pages بود که نسخهای سبکتر از mvc framework است و در مواقعی شبیه به نسخهای بهبودیافته از فرمهای
.aspx
در WebForms به نظر میرسد.
Asp.net razor pages چیست
یک Razor page بسیار شبیه به view component در asp.net mvc است. نحوه نوشتن و قابلیتهای مشابه دارد.
تفاوت اساسی در اینجاست که مدل، ویو (View) و کنترلر همگی در خود razor page قرار دارند. ساختار این فریمورک بیشتر به
MVVM (Model-view-viewmodel)
شباهت دارد. امکان two-way binding را فراهم میکند و در seperation of concerns یا soc یا
به فارسی: تفکیک دغدغهها، تجربه سادهتری را به همراه دارد.
در پایین میتوانیم یک مثال ساده از razor pages را ببینیم که کد بهصورت inline داخل بلوک @functions قرار دارد.
بد نیست بدانید که نوشتن کد در صفحه cshtml اصلاً توصیه نمیشود و بهتر است که تمام کدهای
C#
را به PageModel در
فایل جداگانه انتقال دهیم. این کار شبیه به ساختار کد و صفحات در web form است.
@page
@model IndexModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@functions {
public class IndexModel : PageModel
{
public string Message { get; private set; } = "In page model: ";
public void OnGet()
{
Message += $" Server seconds { DateTime.Now.Second.ToString() }";
}
}
}
<h2>In page sample</h2>
<p>
@Model.Message
</p>
حالا دو انتخاب پیش رو داریم: MVC یا MVVM
میتوان اینطور فکر کرد که حالا دو انتخاب برای فریمورک MVC یا MVVM پیش رو داریم.. قرار نیست وارد جزئیات این دو
ساختار بشویم اما تفاوتهای این دو را میتوانید در مقاله Understanding MVC, MVP and MVVM Design Patterns در بخش منابع
بخوانید. تنها به خاطر داشته باشید که فریمورکهای MVVM عموماً به خاطر ارتباط two-way binding خود با data model
موردتوجه هستند.
MVC برای پروژههای زیر مناسب است:
- اپلیکیشنهایی که تعداد زیادی ویوی دینامیک سمت سرور دارند.
- اپلیکیشنهای SPA یا Single page.
- REST API
- ajax calls
Razor pages برای صفحاتی که read-only هستند یا اطلاعات ابتدایی از کاربر میگیرند مناسب است.
خوبیها و بدیهای razor pages
توجه کنید کسی که این لیست را نوشته است 15 سال تجربه کار با فریمورکهای asp net را دارد:
1. نظم بیشتر و جادوی کمتر
کسی که برای اولین بار با asp net mvc آشنا میشود، درک شیوه مسیردهی URL و کنترلر برایش خیلی عجیب و غیرقابل درک
است و طول میکشد تا به آن عادت کند. بهعنوانمثال درک اینکه صفحه /home/ به اکشن index از HomeController متصل
میشود دشوار است.
درک Razor page با ساختار شبیه به webForm آن سادهتر است، چراکه تمام کدهای هر صفحه، پشت همان صفحه قرار میگیرد.
2. Single responsibility از SOLID
اگر قبلاً از فریمورک MVC استفاده کرده باشید کلاسهای عظیم کنترلر را دیدهاید که با تعداد زیادی اکشن پر میشوند.
مثل یک ویروس میمانند که بهمرورزمان رشد میکند.
با razor page، هر پیج کدهای خود را در فایل cs پشت ویو و محتوای ویو را در cshtml نگه میدارد. این رویکرد
تضمینکننده single responsibility نیز است.
3. استفاده از چندین اکشن POST یا GET به کمک handler ها
هر razor page بهصورت پیشفرض دو متد OnGetAsync و OnPostAsync دارد. اگر میخواهید که درون فایل cs پیج خود
اکشنهای مختلفی داشته باشید، باید از چیزی به نام handler استفاده کنید. اگر پیج شما AJAX callback، چند نقطه برای
submit فرم یا دیگر سناریوها داشته باشد، به این قابلیت نیاز پیدا میکنید.
پس بهعنوانمثال اگر از یک
Kendo
grid
استفاده کرده و میخواهید که برای آن از ajax call استفاده کنید، باید از یک handler برای پاسخ دادن به ajax call
استفاده کنید. هر نوعی از SPA از تعداد زیادی handler استفاده میکند و نهایتاً مجبور میشویم که تمام این AJAX call
ها را به سمت یک کنترلر MVC هدایت کنیم.
تصور کنید در فایل cs ویو، متدی اضافی به نام
OnGetHelloWorldAsync()
ایجاد کردیم. چطور میتوانیم این متد را
فراخوانی کنیم؟
3 راه متفاوت برای استفاده از handler ها وجود دارد:
در مقاله Introduction to Razor Pages in ASP.NET Core در بخش منابع
میتوانید مثالهای بیشتری ببینید.
4. یک سناریوی خوب استفاده از Razor pages
نویسنده در این قسمت پیشنهاد داده که تمام ویوها را بهصورت razor page ایجاد کنیم و موارد زیر را به کنترلر MVC
انتقال دهیم:
- تمام متدهایی که قرار است به ajax calls پاسخ دهند
- صرفاً یک عملیات را انجام میدهند و Status code بازگردانی میکنند.
- یک کالکشن یا شیء (Object) در قالب JSON بازگردانی میکنند.
این رویکرد مشکل بههمریختگی و سنگین بودن کنترلرها را برطرف میکند.
توجه کنید که از PageModel هم میتوان به کمک
JsonResult()
مقدار JSON بازگردانی کرد و محدودیت فنی نداریم. این
رویکرد صرفاً یک قرارداد خواهد بود.
5. مقایسه کد بین razor page و MVC
حالا میخواهیم که یک فرم ساده را در هردو حالت MVC و razor page مقایسه کنیم.
ویوی MVC:
@model RazorPageTest.Models.PageClass
<form asp-action="ManagePage">
<p class="form-horizontal">
<h4>Client</h4>
<hr />
<p asp-validation-summary="ModelOnly" class="text-danger"></p>
<input type="hidden" asp-for="PageDataID" />
<p class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<p class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</p>
</p>
<p class="form-group">
<p class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</p>
</p>
</p>
</form>
کنترلر MVC: (مدل این ویو PageCLass با تنها دو property است)
public class HomeController : Controller
{
public IConfiguration Configuration;
public HomeController(IConfiguration config)
{
Configuration = config;
}
public async Task<IActionResult> ManagePage(int id)
{
PageClass page;
using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
{
await conn.OpenAsync();
var pages = await conn.QueryAsync<PageClass>("select * FROM PageData Where PageDataID = @p1", new { p1 = id });
page = pages.FirstOrDefault();
}
return View(page);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ManagePage(int id, PageClass page)
{
if (ModelState.IsValid)
{
try
{
//Save to the database
using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
{
await conn.OpenAsync();
await conn.ExecuteAsync("UPDATE PageData SET Title = @Title WHERE PageDataID = @PageDataID", new { page.PageDataID, page.Title});
}
}
catch (Exception)
{
//log it
}
return RedirectToAction("Index", "Home");
}
return View(page);
}
}
ویوی Razor page:
@page "{id:int}"
@model RazorPageTest2.Pages.ManagePageModel
<form asp-action="ManagePage">
<p class="form-horizontal">
<h4>Manage Page</h4>
<hr />
<p asp-validation-summary="ModelOnly" class="text-danger"></p>
<input type="hidden" asp-for="PageDataID" />
<p class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<p class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</p>
</p>
<p class="form-group">
<p class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</p>
</p>
</p>
</form>
Razor PageModel (یا کد cs پشت ویو):
public class ManagePageModel : PageModel
{
public IConfiguration Configuration;
public ManagePageModel(IConfiguration config)
{
Configuration = config;
}
[BindProperty]
public int PageDataID { get; set; }
[BindProperty]
public string Title { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
{
await conn.OpenAsync();
var pages = await conn.QueryAsync("select * FROM PageData Where PageDataID = @p1", new { p1 = id });
var page = pages.FirstOrDefault();
this.Title = page.Title;
this.PageDataID = page.PageDataID;
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (ModelState.IsValid)
{
try
{
//Save to the database
using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
{
await conn.OpenAsync();
await conn.ExecuteAsync("UPDATE PageData SET Title = @Title WHERE PageDataID = @PageDataID", new { PageDataID, Title });
}
}
catch (Exception)
{
//log it
}
return RedirectToPage("/");
}
return Page();
}
}
مقایسه Razor Page با MVC
کد بین این دو تقریباً یکسان است. در پایین میتوانید تفاوتهای این دو را بخوانید:
- ویوی هردو کاملاً یکسان است. با این تفاوت که Razor page یک مقدار
@page اضافهتر دارد.
- ManagePageModel دو متد OnGetAsync و OnPostAsync داشت که جایگزین دو اکشن از کنترلر ManagePage شدند.
- ManagePageModel دارای دو پراپرتی است که قبلاً در کلاس جدای PageClass بودند.
در mvc برای یک درخواست HTTP POST شیء را به MVC Action ارسال میکردیم (مثل
ManagePage(int id, PageClass page))؛
اما با Razor page به جای این کار از two-way databinding استفاده میکنیم. برای اینکه databinding کار کند، باید
برای پراپرتیهایی که میخواهیم دیتا انتقال دهند، از صفت [BindProperty] استفاده کنیم. اگر بخواهیم هم میتوانیم
بالای کلاس موردنظر، صفت [BindProperties] قرار دهیم.
منابع