یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
app.UseMvcWithDefaultRoute();
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException("app"); return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"))); }
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
[Route("[controller]/[action]")]
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public ActionResult Hello() { return Content("Hello from DNT!"); } public ActionResult SiteName() { return Content("DNT"); } } }
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("About")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("SiteName")] public ActionResult SiteName() { return Content("DNT"); } } }
AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied: Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest) Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("[controller]")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("[action]")] public ActionResult SiteName() { return Content("DNT"); } } }
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
[Route("api/[controller]")]
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
//[Route("/Users/{userid}")] [Route("Users/{userid}")] public IActionResult GetUsers(int userId) { return Json(new { userId = userId }); }
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
[Route("/Users/{userid:int?}")]
[Route("Users/{userid:int}")]
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
[Route("/Users/{userid:int:max(1000):min(10)}")]
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
[Route("/Users/{userid:int}", Name="GetUserById")]
string uri = Url.Link("GetUserById", new { userid = 1 });
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
[Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
using System; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Core1RtmEmptyTest { public class CustomRouteConstraint : IRouteConstraint { public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (!values.TryGetValue(routeKey, out value) || value == null) { return false; } long longValue; if (value is long) { longValue = (long)value; return longValue != 10; } var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 10; } return false; } } }
public class CustomRouteConstraint : IRouteConstraint
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services) { services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
[Route("/Users/{userid:int:custom}")]
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "spa-fallback", template: "{*url}", defaults: new { controller = "Home", action = "Index" }); });
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007" },
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); });
// Serve wwwroot as root app.UseFileServer(); // Serve /node_modules as a separate root (for packages that use other npm modules client side) app.UseFileServer(new FileServerOptions { // Set root of file server FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")), // Only react to requests that match this path RequestPath = "/node_modules", // Don't expose file system EnableDirectoryBrowsing = false });