Routing با ASP.NET Core 8
در فصل ۱ با اصول HTTP و REST آشنا شدیم. اکنون URLهایی را که بر اساس آن اصول طراحی میشوند با Routing در ASP.NET Core 8 پیادهسازی میکنیم. دو روش را میبینیم: Route مستقل برای یک Endpoint و RouteGroups برای بهاشتراکگذاری قسمتهای قابل استفادهٔ مجدد Route میان چند Endpoint.
ASP.NET Core Routing
Routing توانایی پاسخدادن به HTTP Request کلاینت است. ASP.NET Core با Pattern Matching درخواست را تحلیل میکند تا Endpoint متناظر با URL را پیدا کند. اگر Endpoint پیدا نشود، Response برابر 404 Not Found برگردانده میشود.
شکل ۴-۱. Routing در ASP.NET Coreلازم نیست وارد جزئیات Pattern Matching شویم. تمرکز ما روی Mapping Verbهای HTTP به Route و اعمال Constraint روی Route Parameterها است.
تنظیم HTTP Verb درست
ASP.NET Core 8 برای هر Verb اصلی Method اختصاصی Mapping دارد. هر Method معمولاً Route Pattern و Delegate مربوط به Handler را میگیرد.
جدول ۴-۱. Verbهای HTTP و Methodهای مرتبط| HTTP Verb | Method |
|---|
| GET | MapGet |
| POST | MapPost |
| PATCH | MapPatch |
| PUT | MapPut |
| DELETE | MapDelete |
| Verbهای دیگر | Method اختصاصی ندارند |
نمونهٔ Signature برای POST:
app.MapPost("/yourRouteName", () => /* Do action */);
Verbهایی مانند OPTIONS، TRACE و HEAD Method اختصاصی ندارند، اما میتوان با MapMethods چند Verb را روی یک Route تعریف کرد:
app.MapMethods("/routeName", new List<string> { "OPTIONS",
"HEAD", "TRACE" }, () => { /* Do action */});
Delegate میتواند Parameter دریافت کند. در ادامه و در فصل بعد، بهخصوص هنگام Custom Parameter Binding، نمونههای بیشتری خواهیم دید.
نوشتن Routeها
نوشتن Route ساده است؛ نوشتن Route خوب و سازگار با REST نیازمند دقت در Readability است. Best Practice این است که Routeها معنیدار باشند. این موضوع برای REST API تمیز بسیار مهم است. Route Parameterها بهصورت خودکار به Parameterهای Lambda Function مربوط به Endpoint Bind میشوند. Route بدون Parameter نیز یک Route ثابت است.
شکل ۴-۲. Routing پایهبرای مثال، /countries/{id} یک Country را از میان مجموعهٔ Countries بر اساس ID مشخص میکند و /countries کل فهرست Countries را نشان میدهد.
ASP.NET Core 8 اجازه میدهد Primitive Typeهای متعددی بهعنوان Route Parameter استفاده شوند، از جمله bool، byte، sbyte، short، ushort، int، uint، long، ulong، char، double، decimal و float.
Typeهای پیچیدهتری که به String قابل Parse شدن هستند، مانند DateTime و Guid، نیز قابل استفادهاند.
Listing 4-1. Routeهایی با DateTime و Guid
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/date/{date}", (DateTime date) => date.ToString());
app.MapGet("/uniqueidentifier/{id}", (Guid id) =>
id.ToString());
app.Run();
شکل ۴-۳. تلاش برای Bind کردن String بهجای Integer و دریافت 400 Bad Requestاگر Route وجود داشته باشد ولی HTTP Verb با آن Match نشود، 405 Method Not Allowed برگردانده میشود. Listing 4-2 یک Route مشترک برای PUT و PATCH را نشان میدهد.
Listing 4-2. Endpoint با PUT و PATCH روی یک Route
app.MapMethods("/users/{userId}", new List<string> { "PUT",
"PATCH" }, (int userId, HttpRequest request) =>
{
var id = request.RouteValues["id"];
var lastActivityDate = request.Form["lastactivitydate"];
/* code to update user */
});
فراخوانی همین Route با POST به 405 Method Not Allowed منجر میشود.
شکل ۴-۴. Verb اشتباه و Response برابر 405 Method Not Allowedاگر اصلاً Endpoint متناظر با Route پیدا نشود، بدون توجه به Verb، پاسخ 404 Not Found داده میشود.
شکل ۴-۵. Route ناموجود و Response برابر 404 Not Found
جدول ۴-۲. رفتار Routing و Responseهای ممکن| رفتار | Response |
|---|
| Route Match است و Verb درست است، اما Parameter Binding شکست میخورد. | 400 Bad Request |
| Route Match است اما Verb نادرست است. | 405 Method Not Allowed |
| Route وجود ندارد. | 404 Not Found |
| Route و Verb درستاند و Binding موفق است. | 2XX Success |
ASP.NET Core 8 با این رفتارها به شما کمک میکند Routeهای REST مناسب بنویسید و نوع خطا را تشخیص دهید.
Route Constraintها
میتوان روی Route Parameterها Constraint تعریف کرد تا تنها مقادیر مطابق Pattern مشخص Route را Match کنند. Constraint با Validation ورودی یکسان نیست؛ Validation را جداگانه بررسی خواهیم کرد.
Listing 4-3 Constraintای روی provinceId تعریف میکند که باید Integer باشد.
Listing 4-3. Constraint از نوع int برای provinceId
app.MapGet("/provinces/{provinceId:int}", (int provinceId) =>
$"ProvinceId {provinceId}");
Syntax عمومی به این شکل است:
اگر Constraint رعایت نشود، ASP.NET Core 8 پاسخ 404 Not Found میدهد، نه 400.
شکل ۴-۶. ارسال String برای Route دارای Constraint عددی و دریافت 404
علت منطقی است: Constraint بخشی از هویت Route محسوب میشود؛ اگر آن شرط برقرار نباشد، Route اساساً Match نشده است. Constraint پیش از Parameter Binding بررسی میشود، بنابراین خطا 400 نیست.
به همین دلیل توصیه نمیکنم Route Constraint را جایگزین Parameter Validation کنید، چون در آن صورت تشخیص اینکه 404 ناشی از Route اشتباه است یا Constraint نامعتبر دشوار میشود. Constraint نوعی مانند Integer یا DateTime میتواند منطقی باشد، زیرا Type بخشی از Contract Route است؛ اما Constraintهای Validationمحور باید با احتیاط استفاده شوند.
جدول ۴-۳. Route Constraintهای موجود در ASP.NET Core 8| Constraint | Pattern | توضیح |
|---|
| int | {p:int} | Integer |
| bool | {p:bool} | Boolean |
| datetime | {p:datetime} | DateTime |
| decimal | {p:decimal} | Decimal |
| double | {p:double} | Double |
| float | {p:float} | Float |
| guid | {p:guid} | Guid |
| long | {p:long} | Long |
| minlength | {p:minlength(n)} | حداقل Length |
| maxlength | {p:maxlength(n)} | حداکثر Length |
| length | {p:length(n)} | Length دقیق |
| length(min,max) | {p:length(n1, n2)} | بازهٔ Length |
| min | {p:min(n)} | حداقل مقدار Integer |
| max | {p:max(n)} | حداکثر مقدار Integer |
| range | {p:range(n1, n2)} | بازهٔ Integer |
| alpha | {p:alpha} | حروف Alphabetic بدون حساسیت به Case |
| regex | {p:regex(\...)} | Regular Expression |
| required | {p:required} | Parameter غیر Null |
از minlength به بعد، بسیاری از Constraintها عملاً شبیه Validation عمل میکنند و نویسنده استفاده از آنها را برای Validation توصیه نمیکند. ترجیح او محدود کردن Constraint به Type مورد انتظار است.
Constraintها را میتوان Chain کرد:
app.MapGet("/provinces/{provinceId:int:max(12)}", (int
provinceId) => $"ProvinceId {provinceId}");
در این مثال نخست Integer بودن و سپس حداکثر مقدار ۱۲ بررسی میشود؛ اما بخش دوم Parameter Validation است و بهتر است مسئولیت Routing نباشد. ASP.NET Core امکان Custom Route Constraint را نیز دارد، ولی نویسنده آن را Practice خوبی نمیداند. اصل مهم این است که Parameter Validation طبق RFCها باید به 400 Bad Request منجر شود.
RouteGroups
وقتی Endpointهای زیادی دارید که به Functionality یکسان تعلق دارند—مثلاً مجموعه Endpointهای مدیریت Countries—Route Grouping در ASP.NET Core مفید است. این قابلیت اجازه میدهد Routeها و حتی پیادهسازی آنها را در Function جدا Isolate کنید و Ruleهای مشترک، مانند URL Trunk یا بعداً Authorization مشترک، را روی Group اعمال کنید.
سه Endpoint زیر را در نظر بگیرید که همگی Trunk برابر /countries دارند: فهرست Countries، دریافت Country با ID، و دریافت زبانهای Country. Listing 4-4 آنها را در Extension Methodای به نام GroupCountries روی RouteGroupBuilder گروهبندی میکند.
Listing 4-4. سه Endpoint مربوط به Countries
namespace AspNetCore8MinimalApis.RouteGroups;
public static class MyGroups
{
public static RouteGroupBuilder GroupCountries(this
RouteGroupBuilder group)
{
var countries = new string[]
{
"France",
"Canada",
"USA"
};
var languages = new Dictionary<string, List<string>>()
{
{ "France", new List<string> { "french" } },
{ "Canada", new List<string> { "french",
"english" } },
{ "USA", new List<string> { "english",
"spanish" } }
};
group.MapGet("/", () => countries);
group.MapGet("/{id}", (int id) => countries[id]);
group.MapGet("/{id}/languages", (int id) =>
{
var country = countries[id];
return languages[country];
});
return group;
}
}
سپس این Group در ASP.NET Core Pipeline و در Program.cs ثبت میشود.
Listing 4-5. ثبت Route Group مربوط به Countries
using AspNetCore8MinimalApis.RouteGroups;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGroup("/countries").GroupCountries();
app.Run();
پیش از ثبت Group، Trunk مشترک با MapGroup تعریف میشود. همهٔ Routeها آن را به ارث میبرند و URLهای نهایی عبارتاند از:
/countries/countries/{id}/countries/{id}/languages
در Endpoint اول Slash تنها را میتوان نگه داشت یا حذف کرد؛ نویسنده بهعنوان Convention آن را نگه میدارد.
Route Grouping فراتر از Trunk مشترک است و میتواند Constraint مشترک را نیز بین Endpointها Reuse کند. دو Endpoint آخر هر دو {id} دارند؛ در Listing بعدی این قسمت با یک Group داخلی مشترک میشود.