ASP.NET Core 6/Model Binding
Daha önceki dersimizde FormController sınıfının SubmitForm() action metodu şöyleydi:
[HttpPost]
public IActionResult SubmitForm() {
foreach (string key in Request.Form.Keys.Where(k => !k.StartsWith("_"))) {
TempData[key] = string.Join(", ", Request.Form[key]);
}
return RedirectToAction(nameof(Results));
}
Koddan da göreceğiniz üzere SubmitForm action'ı form verilerine ulaşmak için ana sınıftan devralınan Request özelliği üzerinden Form özelliğini çağırıyor. Elbetteki bu şekilde direkt form verilerini işleyebilirsiniz. Benzer şekilde rota değişkenlerine ve query string değerlerine de daha önce gördüğümüz şekilde direkt erişebiliriz. Ancak bu yol uğraştırıcı, hataya açık, bakımı zor, okunabilirliği düşük kod üretmemize neden olacaktır. Daha şık bir yol model binding'i kullanmaktır. Model binding, gelen request'i inceler, request'teki bilgilerden C# değişkenini oluşturur ve action metotta veya Razor sayfası handler'ında söz konusu değişkene direkt erişmemize imkan sağlar. Söz konusu request verisine ulaşmak için action'da ve Razor sayfası handler'ında aynı isimli bir parametre tanımlarız.
Model binding, action ve Razor sayfası handler'ında belirli bir isimli bir parametre gördüğü zaman aynı isimli parametreyi sırasıyla request'in şu kısımlarında arar:
- Form verisi
- Request'in gövdesi (sadece ApiController attribute'u ile işaretlenmiş controller'larda)
- Rota değişkenleri
- Query string
Basit veri tiplerini bağlama
[değiştir]Model binding sürecinde basit tipler ve karmaşık tipler bağlanabilir. Bu bölümde basit tiplerin nasıl bağlanacağı anlatılacaktır. Model binding sürecinde parse edilebilecek basit tipler nümerik tipler (int, float, vb.), bool, tarihler ve string olabilir. Basit veri tiplerinin bağlanması sürecinde request'ten tek bir değer alınır, bu değer C#'taki ilgili temel tipe dönüştürülür. Şimdi yukarıdaki SubmitForm() metodunu şöyle değiştirelim:
[HttpPost]
public IActionResult SubmitForm(string name, decimal price) {
TempData["name param"] = name;
TempData["price param"] = price.ToString();
return RedirectToAction(nameof(Results));
}
Artık foreach döngüsüyle form kontrollerinin üzerinde dönmeye gerek kalmadı. İstediğimiz belirli bir isimli form kontrolünün değerine direkt erişebiliyoruz. Bu form kontrollerinin değerleri metodumuza direkt name ve price isimli değişkenler olarak geliyor, tür dönüşümü model binding süreci tarafından otomatik yapılıyor.
Razor sayfalarında basit veri tiplerini bağlama
[değiştir]Razor sayfalarında da basit veri tiplerinin bağlanması benzer şekilde yapılır. Ancak dikkat etmemiz gerekir ki Razor sayfalarının görünüm kısımları model olarak PageModel nesnesi alır. Razor sayfasının görünüm kısmındaki form kontrolleri asıl modele aşağıdaki şekilde bağlanır:
<input asp-for="Product.Name" /><br />
<input asp-for="Product.Price" />
Kodu bu şekilde bıraktığımızda form kontrollerinin isimleri "Product.Name" ve "Product.Price" olacaktır ve mevcut haliyle bu form kontrollerinin değerlerine Razor sayfasının handler metodundan erişemezsiniz. Çünkü C#'ta nokta işaretinin özel anlamı vardır. Bu sorundan kurtulmak için Razor sayfasının görünüm kısmındaki form kontrollerini şöyle değiştirmeliyiz:
<input asp-for="Product.Name" name="name" /><br />
<input asp-for="Product.Price" name="price" />
Artık form kontrollerinin değerlerine name ve price değişkenleri üzerinden Razor sayfasının handler metodunda erişebileceğiz. Şimdi aynı Razor sayfasının kod kısmındaki form verilerini işleyecek handler metodu şöyle değiştirelim.
public IActionResult OnPost(string name, decimal price) {
TempData["name param"] = name;
TempData["price param"] = price.ToString();
return RedirectToPage("FormResults");
}
Varsayılan bağlama değerlerinin kullanımı
[değiştir]Model binding, bir action veya handler metodun istediği isimde bir değeri request'te bulamazsa hata döndürmez, action veya handler'ı ilgili parametre tipinin varsayılan değeriyle çalıştırır. Varsayılan değerler sayısal tipler için 0, bool için false, referans tipleri için null'dur. Çoğunlukla model binding'in bu varsayılan davranışı istediğimiz bir şey değildir ve istenmeyen sonuçlara yol açar. Bu sorunun üç şekilde üstesinden gelebiliriz.
Action veya handler'ın opsiyonel parametre alması
[değiştir]Örnek:
public async Task<IActionResult> Index(long id = 1) { //...
Bu action model binding mekanizmasından id isimli bir değer alamadığında id parametresi için 1 değerini kullanmaktadır.
Rota belirtiminde varsayılan değerli segment kullanılması
[değiştir]Eğer model binding ilgili değeri bir rota değişkeninden alacaksa rota belirtiminde varsayılan değerli segment kullanılması mantıklı olabilir. Bu sayede aynı rota segmentine birden fazla action veya handler erişecekse her action veya handler'da aynı varsayılan değeri belirtme zahmetinden kurtuluruz. Örnek (Program.cs dosyası):
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id=1}");
veya bir Razor sayfasının başında
@page "{id=1}"
Metodun gövdesinde varsayılan değerle uğraşma
[değiştir]Üçüncü bir yol olarak metot gövdesinde, varsayılan değerli bir parametre aldığında yapacağı şeyi kendisi belirtebilir. Örnek:
public async Task<IActionResult> Index(long id) {
return View("Form", await context.Products.FirstOrDefaultAsync(p => p.ProductId == id));
}
Bu action id için 0 değerini aldığı durumla kendisi baş etmektedir. LINQ'teki FirstOrDefaultAsync() metodu eğer istenilen öge veritabanında bulunamadıysa geriye Product tipinin varsayılan değeri olan null'u döndürür, dolayısıyla view'a null değer gider. Form tag helper'ları modelin değerinin null olması durumunda form kontrollerine bir şey yazmaz.
Bazen gerçekten model binding mekanizması request'ten 0 değerini alabilir. Örneğin kullanıcı bir rota değişkenine 0 yazabilir veya form kontrolüne 0 değerini girebilir. Böyle bir durumda kullanıcının 0 verdiği durumla değer vermediği durumu ayırt etmek için nullable tipler kullanılabilir. Örnek:
public async Task<IActionResult> Index(long? id) {
return View("Form", await context.Products.FirstOrDefaultAsync(p => id == null || p.ProductId == id));
}
Bu örneğimizde model binding mekanizması id için bir değer bulamazsa null değerini id değişkenine yazacaktır. Bu sayede kullanıcının id için bilinçli bir şekilde 0 verdiği durumla değer vermediği durum ayırt edilebilecektir. LINQ sorgusu da güncellenmiştir. LINQ sorgusuna göre id değeri verilmemişse veritabanındaki ilk kayıt, verilmişse belirtilen id'li kayıt veritabanından çekilecektir. Eğer belirtilen id'li kayıt veritabanında yoksa null değer view'a gönderilecektir.
Karmaşık tipleri bağlama
[değiştir]Model binding basit tipler için kullanılabilir. Ancak model binding'in asıl faydası karmaşık tiplerin bağlanmasında ortaya çıkmaktadır. Karmaşık tipin bağlanması sürecinde request'teki birden fazla veriden tek bir karmaşık tip nesnesi üretilir. Request'teki her bir veri karmaşık tipin aynı isimli public bir özelliğine eşlenir. Örnek:
[HttpPost]
public IActionResult SubmitForm(Product product) {
TempData["product"] = System.Text.Json.JsonSerializer.Serialize(product);
return RedirectToAction(nameof(Results));
}
Örneğimizde SubmitForm action'ına verilen Product nesnesi model binding süreci tarafından üretilmektedir. Gelen request'teki Name ve Price isimli form verileri Product nesnesinin Name ve Price özelliklerinin değerleri olmaktadır. Model binding sürecinin çalışması için request'in ilgili tipin her bir public özelliğine veri sağlamasına gerek yoktur. Request'te verisi olmayan özellikler varsayılan değerine atanır.
Özelliğe bağlama
[değiştir]Kuşkusuz Razor sayfalarındaki handler metotlar da parametre yoluyla karmaşık tip nesnesi alabilir. Örnek:
public IActionResult OnPost(Product product) {
TempData["product"] = System.Text.Json.JsonSerializer.Serialize(product);
return RedirectToPage("FormResults");
}
Kuşkusuz bu kod çalışacaktır. Ancak Razor sayfalarında çoğunlukla asıl modeli temsil eden ve PageModel sınıfında bir özellik olarak tanımlanan modeli kullanırız. Yukarıdaki örneğimizde PageModel sınıfında zaten üzerinde çalıştığımız Product tipinde bir özellik vardır. Bu kodu şöyle değiştirsek daha güzel olacaktır (projenin Pages klasöründeki FormHandler.cshtml dosyası):
@page "/pages/form/{id:long?}"
@model FormHandlerModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@using Microsoft.EntityFrameworkCore
<div>
<h5>HTML Form</h5>
<form asp-page="FormHandler" method="post">
<label>Name</label>
<input asp-for="Product.Name" /><br />
<label>Price</label>
<input asp-for="Product.Price" /><br />
<button type="submit">Submit</button>
</form>
</div>
@functions {
public class FormHandlerModel : PageModel
{
private DataContext context;
public FormHandlerModel(DataContext dbContext)
{
context = dbContext;
}
[BindProperty]
public Product Product { get; set; } = new();
public async Task OnGetAsync(long id = 1)
{
Product = await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstAsync(p => p.ProductId == id);
}
public IActionResult OnPost()
{
TempData["product"] = System.Text.Json.JsonSerializer.Serialize(Product);
return RedirectToPage("FormResults");
}
}
}
Bir özelliğin BindProperty attribute'u ile işaretlenmesi o özelliğin değerinin request'ten model binding süreciyle geleceğini belirtir. Ayrıca dikkat ettiyseniz form kontrollerine eklenen name seçenekleri kaldırılmıştır. Çünkü artık form kontrollerinin değerleri parametre ile alınmamaktadır. Razor sayfasındaki Product özelliğine değerinin doğru bir şekilde atanabilmesi için form kontrollerinin isimlerinin tam olarak Product.Name ve Product.Price olması gerekmektedir. Varsayılan durumda GET request'leri için özelliğe bağlama çalışmamaktadır. Ancak bu varsayılan durumun önüne BindProperty attribute'unun SupportsGet argümanını true yaparak geçilebilmektedir.
BindProperty attribute'unun PageModel sınıfının bir özelliğine uygulanmasının bir alternatifi BindProperties attribute'unun direkt Product sınıfına uygulanmasıdır. Bu sayede bir Product özelliği hangi PageModel sınıfında kullanılırsa kullanılsın, otomatik olarak model binding mekanizması o özelliğe eşleme yapacak şekilde aktif hale getirilecektir. Product sınıfında model binding süreci tarafından eşleme yapılmasını istemediğimiz özellikleri BindNever attribute'u ile işaretleriz.