Parameter Binding
در بخش قبل دیدیم اگر Route Parameter نتواند به Parameter Function مسئول پردازش Request Bind شود، Response برابر 400 Bad Request است. اکنون Fundamentals این فرایند را دقیقتر بررسی میکنیم.
Parameter Binding دقیقاً چیست؟
Parameter Binding یعنی ASP.NET Core مقدارهای موجود در HTTP Request را میگیرد و آنها را به Parameterهای Typed مورد نیاز Function پردازشکننده تبدیل میکند. علاوه بر Primitive Typeها، ASP.NET Core 8 میتواند موارد پیچیدهتری را نیز Bind کند:
- Collectionها مانند List، Dictionary و Array.
- تقریباً هر Complex Object، بهجز Objectهای Recursive در Minimal API؛ یعنی Objectای که Property از Type خودش دارد.
- Serviceهایی که از Dependency Injection قابل Inject هستند.
Listing 4-7 نمونهای از Address را نشان میدهد که به دلیل وجود Property از Type خودش، در Minimal API قابل Binding نیست.
Listing 4-7. کلاس Address دارای Recursion
public class Address
{
public int StreetNumber { get; set; }
public string streetName { get; set; }
public string StreetType { get; set; }
public string City { get; set; }
public string Country { get; set; }
public int PostalCode { get; set; }
public Address AlternateAddress { get; set; }
}
Parameter Binding با مثال
Minimal API در ASP.NET Core 8 میتواند Parameter را از Sourceهای زیر Bind کند:
- Route
- QueryString
- Body با Data از نوع JSON
- Body بهشکل Form Data با Key/Value
- Headerها
- Sourceهای دیگر، شامل Instanceهای Dependency Injection و Custom Binding
Binding میتواند Explicit باشد؛ یعنی Source هر Parameter با Attribute مشخص شود. در Function دارای چند Source، Explicit Binding خوانایی و ابهامزدایی را بهتر میکند.
جدول ۴-۴. Attributeهای Parameter Binding در Minimal API| Data Source | Binding Attribute |
|---|
| Route | FromRoute |
| QueryString | FromQuery |
| Header | FromHeader |
| Body | FromBody |
| Form | FromForm |
Listing 4-8 یک POST Request برای ساخت Address از JSON Body را نشان میدهد. نسخهٔ این مثال Property بازگشتی AlternateAddress را ندارد.
Listing 4-8. ساخت Address از JSON Body
app.MapPost("/Addresses", ([FromBody] Address address) => {
return Results.Created();
});
فعلاً از Results.Created() صرفنظر کنید؛ بعداً Class ایستا Results را برای بازگرداندن HTTP Status مناسب بررسی میکنیم. اگر Breakpoint قرار دهید، میبینید JSON Request به Object از نوع Address Bind شده است.
شکل ۴-۷. Binding شیء Address از JSON Bodyشکل ۴-۸. POST Request ساخت Address در Postman
اکنون Route Parameter و Form Data را با هم ترکیب میکنیم. Listing 4-9 یک PUT Request برای Update کردن Address نشان میدهد: addressId از Route و Data مربوط به Address از Form میآیند.
Listing 4-9. Update آدرس با Route و Form
app.MapPut("/Addresses/{addressId}", ([FromRoute] int
addressId, [FromForm] Address address) => {
return Results.NoContent();
}).DisableAntiforgery();
Sourceها Explicit و جدا هستند و Binding بهدرستی انجام میشود.
شکل ۴-۹. Bind شدن صحیح addressId بهعنوان Route Parameterشکل ۴-۱۰. Bind شدن Address بهعنوان Form Parameter
در مثال از DisableAntiforgery استفاده شده است. Requestهایی که از HTML Form و FromForm استفاده میکنند باید AntiForgery را مدیریت کنند. این Feature برای جلوگیری از Cross-Site Request Forgery (XSRF/CSRF) طراحی شده است. در ASP.NET Core 8 اگر AntiForgery را نه پیکربندی و نه غیرفعال کنید، Endpoint مبتنی بر Form Data در Development میتواند Error ایجاد کند و در Production Warning بدهد. راه اصولی دیگر تولید و اعتبارسنجی AntiForgery Token است. نویسنده برای ساده نگه داشتن مثال و جلوگیری از HTTP Call اضافی، در اینجا Feature را غیرفعال کرده است.
Minimal API نمیتواند Sourceهای مختلف را در Propertyهای یک Object بهطور همزمان Bind کند؛ اگر مثل Listing 4-10 Attributeها را Property-by-Property از Sourceهای گوناگون قرار دهید، Body/Form اولویت میگیرد و بعضی Parameterها Resolve نمیشوند.
Listing 4-10. Binding Propertyها از Sourceهای متفاوت
public class Address
{
[FromRoute]
public int AddressId { get; set; }
[FromForm]
public int StreetNumber { get; set; }
[FromForm]
public string StreetName { get; set; }
[FromForm]
public string StreetType { get; set; }
[FromForm]
public string City { get; set; }
[FromForm]
public string Country { get; set; }
[FromForm]
public int PostalCode { get; set; }
}
در این حالت AddressId Bind نمیشود و با مقدار پیشفرض صفر باقی میماند.
شکل ۴-۱۱. Bind نشدن AddressId هنگام ترکیب Sourceهای متفاوت در یک Objectدر Lambda میتوان Attribute Binding را روی Parameter اصلی قرار داد و لازم نیست Attributeها حتماً روی خود Propertyها باشند. نویسنده ترجیح میدهد تا حد امکان یک Attribute روی Object Parameter در Lambda داشته باشد.
شکل ۴-۱۲. PUT Request با Form Data در Postmanاکنون QueryString و Header را با GET ترکیب میکنیم. فرض کنید Endpointی فهرست Addressها را بر اساس Coordinate ارسالشده در Header و حداکثر تعداد نتیجهٔ اختیاری در Query String برمیگرداند.
Listing 4-11. GET با Header و QueryString
app.MapGet("/Addresses", ([FromHeader] string coordinates,
[FromQuery] int? limitCountSearch) => {
return Results.Ok();
});
Query Parameterها غالباً Optional هستند. اگر Type بهطور طبیعی Nullable نیست، بهتر است آن را Nullable تعریف کنید؛ در مثال، int? برای limitCountSearch استفاده شده است.
شکل ۴-۱۳. Bind شدن coordinates از Headerشکل ۴-۱۴. Bind شدن limitCountSearch از QueryStringListing 4-12 نحوهٔ Bind کردن Array از Query String و Header را نشان میدهد.
Listing 4-12. Array در Query و Header
// Represents ?id=1&id=2
app.MapGet("/Ids", ([FromQuery] int[] id) =>
{
return Results.Ok();
});
app.MapGet("/Languages", ([FromHeader(Name = "lng")]
string[] lng) =>
{
return Results.Ok();
});
برای IDها میتوان Query را به شکل ?id=1&id=2&id=3 ارسال کرد. در مثال Header، مقدار Name = "lng" به ASP.NET Core میگوید Array پارامتر lng از Header با همان نام خوانده شود. امکان Customize کردن نام روی دیگر Binding Attributeها نیز وجود دارد.
شکل ۴-۱۵. Bind شدن Array مربوط به id از Query Stringشکل ۴-۱۶. Bind شدن Array مربوط به lng از Header
شکل ۴-۱۷. ارسال Array شناسهها در Query String با Postmanشکل ۴-۱۸. ارسال Array رشتهها در Header با Postmanدر نمونهٔ شکل ۴-۱۸، یکی از Headerها عمداً با نام lang بهجای lng ارسال شده و Bind نمیشود؛ این موضوع نشان میدهد Custom Name دقیقاً چگونه عمل میکند.
مثالهای بالا رایجترین سناریوها هستند. در حالتهای خاص میتوان Object کامل را نیز از Header یا Query String ساخت، مثلاً برای جستوجوی چندمعیاره. Explicit Binding بهجز در بعضی موارد مانند FromForm و FromHeader همیشه اجباری نیست، اما نویسنده توصیه میکند برای Readability Source Parameterها را صریح بنویسید.
Validating Inputs (اعتبارسنجی ورودیها)
پس از Routing و Binding، مرحلهٔ حیاتی بعدی Input Validation است. هرگز نباید ورودی User را بدون بررسی قابل اعتماد فرض کرد. Validation دو هدف مهم دارد:
- اطمینان از رعایت Business Ruleها؛ برای مثال Email باید واقعاً قالب Email داشته باشد یا اگر فقط HTTPS مجاز است، URL مبتنی بر HTTP رد شود.
- محافظت در برابر Data مخرب؛ مثلاً Dataای که بعداً در Web Page نمایش داده میشود میتواند شامل HTML/JavaScript برای XSS باشد. همانطور که در فصل ۳ دیدیم، ورودی کنترلنشده همچنین زمینهٔ Injection را فراهم میکند.
DataAnnotations که در MVC، Razor Pages یا Web API استفاده میشوند، در Minimal API به همان صورت پشتیبانی نمیشوند. Listing 4-13 یک Country با Ruleهای Required و RegularExpression را نشان میدهد.
Listing 4-13. کلاس Country با DataAnnotation
using System.ComponentModel.DataAnnotations;
namespace AspNetCore8MinimalApis.Models;
public class Country
{
[Required]
[RegularExpression("^[a-zA-Z0-9]+$")]
public string Name { get; set; }
public string Description { get; set; }
[Required]
[RegularExpression("^(https:\\/\\/.)[-a-zA-Z0-9@:%._\\+~#=]
{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$")]
public string FlagUri { get; set; }
}
Name اجباری است و فقط Alphanumeric را میپذیرد؛ در نتیجه HTML Tag Match نمیشود. FlagUri نیز اجباری است و باید HTTPS URL باشد. چون DataAnnotation در Minimal API محدودیت دارد و برای برخی «عدم Match»ها مناسب نیست، نویسنده از Library با نام FluentValidation استفاده میکند.
FluentValidation امکان Ruleهای Built-in یا Custom و Error Message سفارشی میدهد. Package مربوط به Dependency Injection با فرمان زیر نصب میشود:
Install-Package FluentValidation.DependencyInjectionExtensions
شکل ۴-۱۹. باز کردن Package Manager Console در Visual Studio 2022
شکل ۴-۲۰. اجرای فرمان نصب NuGet Package مربوط به FluentValidation.DependencyInjectionپس از نصب، Validator از AbstractValidator<T> ارث میبرد. Methodهای مهم عبارتاند از:
RuleFor: Property هدف Rule را انتخاب میکند.NotEmpty: خالی نبودن مقدار.WithMessage: Error Message در زمان شکست Rule؛ میتواند از Placeholder برابر {PropertyName} استفاده کند.Custom: Rule سفارشی، مثلاً Regex و ایجاد Error با AddFailure.Matches: Match کردن مقدار با Regular Expression.
این Ruleها—بهجز AddFailure—بهصورت Extension Method قابل Chain هستند.
Listing 4-14. FluentValidation برای Country
using AspNetCore8MinimalApis.Models;
using FluentValidation;
using FluentValidation.Results;
using System.Text.RegularExpressions;
namespace AspNetCore8MinimalApis.Validators;
public class CountryValidator : AbstractValidator<Country>
{
public CountryValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("{PropertyName} is required")
.Custom((name, context) =>
{
Regex rg = new Regex("<.*?>"); // Matches HTML tags
if (rg.Matches(name).Count > 0)
{
// Raises an error
context.AddFailure(
new ValidationFailure(
"Name",
"The parameter has invalid content"
)
);
}});
RuleFor(x => x.FlagUri)
.NotEmpty()
.WithMessage("{PropertyName} is required")
.Matches("^(https:\\/\\/.)[-a-zA-Z0-9@:%._\\+~#=]
{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$")
.WithMessage("{PropertyName} must match an HTTPS URL");
}
}
برای استفاده از Validator در Dependency Injection، همهٔ Validatorهای Assembly با یک Scan ثبت میشوند.
Listing 4-15. ثبت Validatorهای FluentValidation در Assembly برنامه
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidatorsFromAssemblyContaining<P
rogram>();
var app = builder.Build();
استفاده از Program بهعنوان Generic Parameter باعث میشود Assembly مربوط به ASP.NET Core Application شناسایی و Validatorهای موجود در آن پیدا شوند.
Listing 4-16. POST /countries با Validation
app.MapPost("/countries", ([FromBody] Country country,
IValidator<Country> validator) => {
var validationResult = validator.Validate(country);
if (validationResult.IsValid)
{
//Do something
return Results.Created();
}
return Results.ValidationProblem(validationResult.
ToDictionary(), statusCode: (int)HttpStatusCode.
BadRequest);
});
Dependency Injection بهصورت خودکار Implementation مناسب IValidator<Country> یعنی CountryValidator را فراهم میکند. Validate روی Object اجرا میشود و IsValid نتیجه را مشخص میکند. در صورت شکست، Errorها به Dictionary تبدیل و با ValidationProblem به Client برگردانده میشوند.
در نمونهٔ کتاب، قرار دادن JavaScript Alert داخل Name و حذف HTTPS از FlagUri باعث Validation Error میشود.
شکل ۴-۲۱. ValidationProblem هنگام نامعتبر بودن Name و FlagUriErrorها با جزئیات مناسب برگردانده میشوند؛ در صورت موفقیت Validation، Endpoint Response موفق خواهد داشت.