İçeriğe atla

ASP.NET Core 6/Tag Helper'lar/Form Tag Helper'ları

Vikikitap, özgür kütüphane

Form tag helper'ları view'larda HTML formlarını render'lamak için kullanılan bir tag helper kümesidir. İlgili form kontrollerine girilen verilerin hangi action'a veya Razor sayfası handler'ına gönderileceğinin belirtimi ve model özellikleri ile form kontrolleri arasındaki eşleme form tag helper'ları ile yapılabilir. Form kontrollerine uygulanan tag helper'lar sayesinde ilgili <input> etiketinin type, value ve name seçeneklerinin değerleri otomatik olarak belirlenir.

Get/Post/Redirect/Get deseni

[düzenle]

Form tabanlı sayfaların işlevini yerine getirmesi için çoğunlukla get-post-redirect-get deseni kullanılır. Bütün bu aşamalar aşağıdaki listede verilmiştir:

  1. GET: Kullanıcı, verileri girebilmesi için öncelikle GET request'ini kullanarak ile form sayfasını getirir.
  2. POST: Formdaki veriler bir POST request'i aracılığıyla bir action veya sayfa handler'ına gönderilir.
  3. REDIRECT: Action veya handler metot verileri veritabanına yazdıktan sonra ilgili verilerin veritabanına yazıldığını gösterecek bir web sayfasına yönlendirme içeren bir cevapla geri döner.
  4. GET: İstemci son olarak verilerin veritabanına yazıldığını gösteren sayfaya talepte bulunur.

3. aşamada sunucunun direkt verilerin veritabanına yazıldığını gösteren bir içerikle geri dönmemesinin sebebi böyle yapılması durumunda istemci sayfayı yenilediğinde aynı form verilerinin tekrar sunucuya gönderilecek olmasıdır.

Form verilerini işlemesi için controller oluşturulması

[düzenle]

Bu bölümde controller kullanarak form verilerini işlemeyi göreceğiz. Şimdi projenizin Controllers klasöründe içeriği aşağıdaki gibi olan ve ismi FormController.cs olan dosyayı oluşturun:

using Microsoft.AspNetCore.Mvc;
using WebApp.Models;
namespace WebApp.Controllers {
    public class FormController : Controller {
        private DataContext context;
        public FormController(DataContext dbContext) {
            context = dbContext;
        }
        public async Task<IActionResult> Index(long id = 1) {
            return View("Form", await context.Products.FindAsync(id));
        }
        [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));
        }
        public IActionResult Results() {
            return View();
        }
    }
}

Bu controller sınıfındaki Index action'ı formu belirtilen id'li kaydın bilgileriyle doldurarak render'layacak olan action'dır. Kullanıcı formdaki bilgileri güncelleyip submit butonuna bastığında SubmitForm() metodu tetiklenir. Bu action HttpPost attribute'u ile işaretlenmiştir, dolayısıyla sadece POST request'lerine karşılık verecektir. Controller sınıfından devralınan Request özelliği HttpRequest tipindedir ve geçerli request'i tutmaktadır. HttpRequest sınıfının Form özelliği gelen request'in body'sindeki form kontrollerine erişim sağlar. Bu özellik bir IFormCollection nesnesi döndürür. IFormCollection tipinde tanımlı Keys özelliği bir string kolleksiyonu döndürür ve formdaki kontrollerin isimlerini içerir. İsmi alt tire (_) ile başlayan kontroller ASP.NET Core tarafından oluşturulmuş arkaplan kontrolleridir ve bizim ilgimizi çekmemektedir. Belirli bir form kontrolünün değerine IFormCollection tipinde tanımlı indeksleyici ile erişilmektedir. Bu indeksleyici geriye bir StringValues nesnesi döndürür. StringValues tipini bir string dizisi gibi düşünebiliriz, birden fazla string içerebilir. Örneğimizde form kontrolünün değerinin birden fazla string içermesi durumunda string'ler virgülle ayrılarak tek bir string'e çevrilmektedir. Kuşkusuz textbox gibi HTML kontrollerinin çoğu tek bir string değer içermektedir. Ancak ileride göreceğimiz bazı kontroller birden fazla değer içerebilmektedir. İşte bu kontrollerin de ihtiyaçlarının karşılanması için kontrol değerlerini tutan tip StringValues seçilmiştir. Ancak değeri gönderecek kontrolün tek bir değer içereceğinden eminseniz rahatlıkla Request.Form[key][0] şeklinde ilgili form kontrolünün değerlerinin ilkine erişebilirsiniz. Örneğimizde ayrıca ilgili form kontrolünün değerleri form kontrolüyle aynı isimli bir TempData değişkenine atanmaktadır. Bu sayede form değişkenlerinin değerlerine Results action'ının render'ladığı view'da erişebileceğiz. Sonuç olarak form verilerini işleyen SubmitForm() action'ı form verileriyle yapacağı işlemleri bitirdiğinde aynı controller'daki Results() action'ına yönlendirme cevabıyla geri dönmektedir. Results action'ı ise kendi view'ını render'lamaktadır. Şimdi Index action'ının render'layacağı Form action'ı oluşturalım (Views/Form klasöründeki Form.cshtml dosyası):

@model Product
<h5>HTML Form</h5>
<form action="/form/submitform" method="post">
	<div>
		<label>Name</label>
		<input name="Name" value="@Model.Name" />
	</div>
	<button type="submit">Submit</button>
</form>

Burası view'ın bir kısmıdır. Gerisi layout'tan gelecektir. View, aldığı model nesnesine ait Name özelliğinin değerini bir textbox'a yazmakta, kullanıcının yaptığı güncellemeyi /form/submitform path'ına göndermeketdir. Şimdi form verileri işlendikten sonra Results action'ı tarafından render'lanacak olan view'ı şöyle oluşturalım (Views/Form klasöründeki Results.cshtml dosyası):

<table>
	<thead>
		<tr>
			<th colspan="2">Form Data</th>
		</tr>
	</thead>
	<tbody>
		@foreach (string key in TempData.Keys)
		{
			<tr>
				<th>@key</th>
				<td>@TempData[key]</td>
			</tr>
		}
	</tbody>
</table>
<a asp-action="Index">Return</a>

Bu view, TempData'daki verileri bir tablo halinde göstermektedir.

Form verilerini işlemesi için Razor sayfası oluşturma

[düzenle]

Form verileri controller'lardaki aynı mantıkla Razor sayfaları tarafından da işlenebilir. Razor sayfaları yaklaşımında bir form sayfasını gösterecek ve form verilerini işleyecek bir Razor sayfası, bir de form işleme sonucunu gösterecek ayrı bir Razor sayfası oluşturacağız. Öncelikle projenizin Pages klasöründe FormHandler.cshtml isminde bir Razor sayfası oluşturun, içeriği şöyle olsun:

@page "/pages/form/{id:long?}"
@model FormHandlerModel
@using Microsoft.AspNetCore.Mvc.RazorPages
<div>
	<h5>HTML Form</h5>
	<form action="/pages/form" method="post">
		<div>
			<label>Name</label>
			<input name="Name" value="@Model.Product.Name" />
		</div>
		<button type="submit">Submit</button>
	</form>
</div>
@functions {
	[IgnoreAntiforgeryToken]
	public class FormHandlerModel : PageModel
	{
		private DataContext context;
		public FormHandlerModel(DataContext dbContext)
		{
			context = dbContext;
		}
		public Product Product { get; set; }
		public async Task OnGetAsync(long id = 1)
		{
			Product = await context.Products.FindAsync(id);
		}
		public IActionResult OnPost()
		{
			foreach (string key in Request.Form.Keys.Where(k => !k.StartsWith("_")))
			{
				TempData[key] = string.Join(", ", Request.Form[key]);
			}
			return RedirectToPage("FormResults");
		}
	}
}

Bu Razor sayfası hem form sayfasını görüntülemek hem de form verilerini işlemekle görevlidir. Şimdi form işleme sonucunu gösterecek olan Razor sayfasını ekleyelim (Pages klasöründeki FormResults.cshtml Razor sayfası):

@page "/pages/results"
<div>
	<table>
		<thead>
			<tr>
				<th colspan="2">Form Data</th>
			</tr>
		</thead>
		<tbody>
			@foreach (string key in TempData.Keys)
			{
				<tr>
					<th>@key</th>
					<td>@TempData[key]</td>
				</tr>
			}
		</tbody>
	</table>
	<a asp-page="FormHandler">Return</a>
</div>

HTML formlarını geliştirmek için tag helper'ların kullanılması

[düzenle]

Şimdiye kadar öğrendiklerimizi tazelemek için bir controller ve Razor sayfasında nasıl form verilerini işleyebileceğimizi gördük, Ancak formları oluştururken giç tag helper kullanmadık. Şimdi asıl konumuz olan form tag helper'larına sıra geldi.

<form> etiketine uygulanabilecek tag helper'lar

[düzenle]

<form> etiketine uygulanabilecek tag helper sınıfı FormTagHelper'dır. Bu tag helper aşağıdaki seçenekleri desteklemektedir. Bu seçeneklerin tamamı HTML <form> etiketindeki action seçeneğinin değerinin belirlenmesinde kullanılmaktadır.

asp-controller: Form verileri hangi controller'a gidecek? Eğer bu seçenek atlanırsa veriler view'ı render'layan controller'a gider.
asp-action: Form verileri hangi action'a gidecek? Eğer bu seçenek atlanırsa veriler view'ı render'layan action'a gider.
asp-page: Form verileri hangi Razor sayfasına gidecek?
asp-page-handler: Form verileri Razor sayfasının hangi handler metoduna gidecek?
asp-route-*: <form> etiketinin action seçeneğindeki URL ayarlanırken kullanılacak rota değişkenlerini belirtmek için kullanılır. View'dan contoller'a (veya Razor sayfasına) taşınan form verilerine ilave veri taşımak için kullanılabilir. Örneğin asp-route-id seçeneği id rota değişkeni için değer belirtir.
asp-route: URL oluşturulurken hangi rotanın kullanılacağını belirtmeye yarar.
asp-antiforgery: Kullanıldığı view'a anti-forgery bilgisini eklenip eklenmeyeceğini kontrol eder. Anti-forgery konusu ileride işlenecektir.
asp-fragment: HTML'de linkin sonuna eklenen ve # ile başlayan kısmı belirtmek için kullanılır.

Örnek kullanım:

<form asp-action="submitform" method="post"> ... </form>

Bu örneğimizde formdaki veriler formu'u render'layan controller'ın submitform action'ına gidecektir. Bir <form> etiketi için method seçeneği belirtilmezse varsayılan olarak post değeri kullanılacaktır. Başka bir örnek:

<form asp-page="FormHandler" method="post"> ... </form>

Bu örneğimizde ise form verileri FormHandler Razor sayfasına gidecektir.

<button> etiketine uygulanabilecek tag helper'lar

[düzenle]

Form verilerini sunucuya gönderecek buton formun dışında tanımlanabilir. Bu durumda <button> etiketinin form seçeneği butonun ilişkili olduğu formu; asp-controller, asp-action, asp-page, vb. seçenekler ise form verilerinin gönderileceği action veya Razor sayfasını belirtmeye yarar. Örnek:

<form asp-action="submitform" method="post" id="htmlform"> ... </form>
<button form="htmlform" asp-action="submitform">Buton Metni</button>

<button> etiketine biraz önce gördüğümüz <form> etiketinin action HTML seçeneğinin değerini oluşturmak için kullandığımız bütün tag helper seçenekleri eklenebilir. Formun dışında oluşturulan buton, çoğunlukla form verilerini varsayılan action veya Razor sayfası dışında bir yere göndermek istediğimiz zaman kullanılır.

<input> etiketine uygulanabilecek tag helper'lar

[düzenle]

HTML'deki <input> etiketi kullanıcıdan genel anlamda veri almak için kullanılır ve HTML'deki en yaygın form kontrolüdür. type seçeneğinde belirtilen değerlerle form kontrolünün tipi belirtilir. <input> etiketine etki eden tag helper sınıfı InputTagHelper'dır. Aşağıdaki iki seçenek <input> etiketlerine eklenebilir:

asp-for: Bu <input> etiketine hangi model özelliğine bağlanacak?
asp-format: <input> etiketinin içinde gösterilecek model özelliğinin değerinin hangi formatta gösterileceğini belirtmeye yarar.

asp-for seçeneğinde eklenen model özelliğinin ismi, tipi ve değerine bakılarak üretilecek HTML kodunun name, id, type ve value özelliklerinin değerleri belirlenir. Örnek:

@model Product
<form asp-action="submitform" method="post">
	<input asp-for="Name" />
	<button type="submit">Submit</button>
</form>

Bu örneğimizde <input> etiketi için üretilen HTML kodu şöyle bir şey olacaktır:

<input type="text" data-val="true" data-val-required="The Name field is required." id="Name" name="Name" value="Kayak">

Buradaki data-val ve data-val-required seçenekleri bu aşamada bizi ilgilendirmiyor. Burada dikkat etmemiz gereken nokta model özelliğinin ismiyle id ve name seçeneklerinin değerlerinin, model özelliğinin değeriyle value seçeneğinin değerinin belirlenmesi. Ayrıca model özelliğimiz bir string olduğu için <input> etiketinin type seçeneğinin değeri text olarak ayarlandı. Aşağıdaki tablo hangi C# tiplerinin hangi tip <input> etiketi oluşturacağını belirtmektedir:

C# Tipi <input> etiketinin type seçeneğinin değeri
byte, sbyte, int, uint, short, ushort, long, ulong number
float, double, decimal text (validasyon için ekstra seçenekler eklenir, bu seçenekler ileriki derslerde gösterilecektir.)
bool checkbox
string text
DateTime datetime

Bu tablodaki değerler sadece varsayılan değerlerdir ve istenirse <input> etiketi için bir type seçeneği belirtilerek bu varsayılan değerlerin geçersiz kılınması sağlanabilir. Ancak böyle bir senaryoda farklı view'larda aynı model özelliği için oluşturulan bütün form kontrollerinde aynı type seçeneğinin belirtilmesi gerekmektedir. Daha şık bir çözüm model sınıfında ilgili özellik için hangi form kontrolünün oluşturulacağının şöyle belirtilmesidir:

C# Attribute'u <input> etiketinin type seçeneğinin değeri
HiddenInput hidden
Text text
Phone tel
Url url
EmailAddress email
DataType(DataType.Password) password
DataType(DataType.Time) time
DataType(DataType.Date) date

Soldaki attribute'lar ilgili C# model sınıfındaki özelliğe attribute olarak eklenir. Sağdakiler ise tag helper tarafından ilgili özellik için üretilecek <input> etiketinin type seçeneğinin değeridir.

NOT: Eğer model özelliğinin tipi yukarıdaki tabloda listelenen C# tiplerinden birisi değilse ve tip belirtici herhangi bir attribute ile işaretlenmediyse <input> etiketinin type seçeneğinin değeri text olacaktır.

Model özelliğinin formatını belirtme

[düzenle]

Bir <input> etiketi için format seçeneğiyle belirli bir format belirtilmezse ilgili kontrolün içinde gösterilecek model özelliğinin değeri için varsayılan formatlama yapılır. Örnek:

<input asp-for="Price" />

kodu sonucunda üretilen HTML şöyle bir şey olacaktır:

<input type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="79500.00">

Burada konumuz için önemli olan value seçeneğinin değeridir. Değerde noktadan sonra iki basamak gözükmektedir. Bu iki basamağın sebebi model sınıfında ilgili özelliğe eklenen şu attribute'tur:

[Column(TypeName = "decimal(8, 2)")]
public decimal Price { get; set; }

Bu attribute veritabanı tarafından ilgili özelliğin ne şekilde depolanacağını belirtmektedir. Attribute sayesinde veritabanındaki Price değerlerinin ondalık kısım dahil toplam 8 basamağı olacaktır, virgülden sonra ise 2 basamak olacaktır. Veritabanında veriler bu şekilde depolandığı için herhangi bir değişime maruz kalmadan textbox'ta da (eğer formatlanma uygulanmazsa) bu şekilde gözükecektir. Bu varsayılan formatlamayı şöyle değiştirebiliriz:

<input asp-for="Price" asp-format="{0:#,###.00}" />

Bu örneğimizde ilgili Price özelliği textbox'ta gösterilirken virgül ile üçlü gruplara ayrılacak, ondalık ayırıcısı olarak da nokta kullanılacaktır.

Formatlamanın model sınıfında belirtilmesi

[düzenle]

Formatlamanın view'da belirtilmesi birden fazla view'da aynı model özelliğinin değerinin form kontrolünde gösterilmek istendiği durumda dezavantaj oluşturur. Böyle bir durumda aynı formatlamanın tekrar ve tekrar belirtilmesi gerekmektedir. Daha şık bir çözüm formatlamanın model sınıfında belirtilmesidir. Örnek:

// Bu attribute'un çalışması için using System.ComponentModel.DataAnnotations; satırını model sınıfının başına ekleyin.
[DisplayFormat(DataFormatString = "{0:c2}", ApplyFormatInEditMode = true)]
public decimal Price { get; set; }

Bu örneğimizde formatlama için ondalık kısma 2 basamak ayrılan para birimi formatı kullanılmıştır. Ayrıca formatlama, özelliğin değerinin <input> gibi düzenlenebilir bir form kontrolünün içinde gösterildiği durumda da uygulanacaktır.

Eğer hem model sınıfına eklenen attribute, hem de kontrol etiketine eklenen seçenek vasıtasıyla formatlama belirtilmişse etikete uygulanan formatlama öncelik kazanır.

UYARI: Form kontrollerinde gösterilen değerlerin formatlanması kullanıcı açısından güzeldir. Örneğin kullanıcı textbox'ın içinde 1250000 yerine ₺1.250.000,00 gibi bir değer görür. Ancak bunun anlamı kullanıcı sayfayı submit ettiğinde sunucuya ₺1.250.000,00 gibi bir değerin gideceğidir. Sunucu tarafında bu verinin parse edilmesi geliştiricinin sorumluluğundadır. Elbette ki istemci tarafında JavaScript kullanılarak kullanıcı submit butonuna bastığında veriler gönderilmeden önce verilerin parse edilerek gönderilmesi, bu sayede sunucu tarafındaki geliştiriciye iş bırakılmaması sağlanabilir.

NOT: Model sınıfında belirtilen formatlamalar yalnızca ilgili model özelliği <input asp-for="Ozellik" /> şeklinde bir form kontrolünü oluşturma amacıyla kullanıldığında uygulanır. Örneğin ilgili özellik çağrısı <p>@Model.Ozellik</p> şeklinde normal kullanıldığında formatlama uygulanmaz.

<input> etiketlerinde ilişkili verilerin gösterilmesi

[düzenle]

Entity Framework Core kullanarak veritabanından çektiğimiz veriler ilişkili veri içerebilir. Bu ilişkili verilerin input kontrollerinde gösterilmesi çok kolaydır. Öncelikle controller sınıfındaki Index action metodunu şöyle değiştirelim:

// sayfanın başına using Microsoft.EntityFrameworkCore; direktifini ekleyin.
public async Task<IActionResult> Index(long id = 1) {
    return View("Form", await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstAsync(p => p.ProductId == id));
}

Şimdi Form view'ını şöyle değiştirelim:

@model Product
<input asp-for="Category.Name" />

Bu örneğimizde input form kontrolüne direkt model nesnesinin bir özelliğini bağlamak yerine model nesnesinin bir özelliğinin bir özelliğini bağladık. Aynı yöntem Razor sayfalarında da kullanılabilir. Tek fark referans zincirini bir seviye artırmamızdır. Örnek:

@page "/pages/form/{id:long?}"
@model FormHandlerModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@using Microsoft.EntityFrameworkCore
<form asp-page="FormHandler" method="post">
	<input asp-for="Product.Category.Name" />
	<button type="submit">Submit</button>
</form>
@functions {
	[IgnoreAntiforgeryToken]
	public class FormHandlerModel : PageModel
	{
		// ...
		public async Task OnGetAsync(long id = 1)
		{
			Product = await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstAsync(p => p.ProductId == id);
		}
		//...
	}
}

<label> etiketine uygulanabilecek tag helper'lar

[düzenle]

LabelTagHelper sınıfı <label> etiketlerini dönüştürmek için kullanılan tag helper sınıfıdır. LabelTagHelper sınıfının tanımladığı tek seçenek asp-for'dur ve ilgili label'ın bağlı olduğu model özelliğini belirtir. asp-for seçeneği için sağlanan değer hem <label> etiketinin içeriğini oluşturur, hem de aynı model özelliğine bağlanan <input> etiketiyle ilişkili olmasını sağlar. Bu ilişki sayesinde kullanıcı label metnine tıkladığında otomatik olarak ilgili <input> kontrolüne odaklanılır. Örnek:

<label asp-for="ProductId"></label>
<input asp-for="ProductId" />
<br />
<label asp-for="Name"></label>
<input asp-for="Name" />
<br />
<label asp-for="Price"></label>
<input asp-for="Price" />
<br />
<label asp-for="Category.Name">Category</label>
<input asp-for="Category.Name" />
<label asp-for="Supplier.Name">Supplier</label>
<input asp-for="Supplier.Name" />

<input> etiketlerinde ilişkili verilerin gösterilmesi durumunda <label> etiketinin içeriğini elle belirtmek faydalı olacaktır. Çünkü <label> etiketi model özelliğinin özelliğine bağlı olduğu zaman sadece son özellik ismini içeriğine yazacaktır. Bu durumda eğer biz <label> etiketinin içeriğini sağlamasaydık üçüncü ve dördüncü <label etiketlerinin içeriği sadece Name olacaktı. Bu durumu istemediğimiz için <label> etiketlerinin metnini elle yazdık.

<select> etiketine uygulanabilecek tag helper'lar

[düzenle]

<select> etiketi HTML'de birden fazla seçenekten birini seçebileceğimiz bir form kontrolü oluşturur. SelectTagHelper sınıfı <select> etiketlerini dönüştürmekten sorumludur. Bu sınıf aşağıdaki iki seçeneği tanımlar:

asp-for: <select> etiketinin bağlı olduğu model özelliğini belirtir.
asp-items: İlgili <select> etiketinde hangi seçeneklerin olacağını belirtmeye yarar.

Örnek kullanım:

<select asp-for="CategoryId">
    <option value="1">Watersports</option>
    <option value="2">Soccer</option>
    <option value="3">Chess</option>
</select>

<select> etiketinin asp-for seçeneği için belirtilen değer üretilen HTML'de aynı <select> etiketinin id ve name seçeneklerinin değeri olacaktır. Ayrıca model özelliğinin değerine göre seçilen <option> etiketi değişecektir. Örneğin view'ın model nesnesinin CategoryId'si 2 ise üretilen HTML şöyle olacaktır:

<select id="CategoryId" name="CategoryId">
    <option value="1">Watersports</option>
    <option value="2" selected="selected">Soccer</option>
    <option value="3">Chess</option>
</select>

<select> etiketinin seçeneklerini oluşturma

[düzenle]

Yukarıda <select> etiketinin seçenekleri elle oluşturulmuştur. <select> etiketinin seçeneklerini oluşturmak için asp-items seçeneği kullanılabilir. Şimdi Controllers kalsöründeki FormController.cs dosyasındaki Index action'ını şöyle değiştirelim:

// Dosyanın başına using Microsoft.AspNetCore.Mvc.Rendering; satırını ekleyin.
public async Task<IActionResult> Index(long id = 1) {
    ViewBag.Categories = new SelectList(context.Categories, "CategoryId", "Name");
    return View("Form", await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstAsync(p => p.ProductId == id));
}

Örneğimizde ViewBag yoluyla veritabanındaki kategoriler tag helper'ın kullanılacağı view'a gönderilmektedir. "CategoryId" gönderilen Category nesnelerinin bir özelliğidir ve oluşturulacak <option> etiketlerinin value değerlerini oluşturacaktır. Gönderilen üçüncü parametre olan "Name" ise gönderilen Category nesnelerinin bir özelliğidir ve oluşturulacak <option> etiketlerinin içeriğini oluşturacaktır. Gönderilen bu seçenekler view tarafında şöyle kullanılabilir:

<select asp-for="CategoryId" asp-items="@ViewBag.Categories"></select>

<textarea> etiketiyle kullanılabilecek tag helper'lar

[düzenle]

HTML'de <textarea> etiketi çok miktarda metin cinsinden verinin kulllanıcı tarafından girilmesine imkan verir. type seçeneği text olan <input> etiketinin çok satırlı versiyonu gibi düşünebiliriz. TextAreaTagHelper sınıfı <textarea> etiketlerini dönüştürmekten sorumludur. Desteklediği tek seçenek asp-for'dur ve kontrolü bir model özelliğine bağlamaya yarar. asp-for seçeneği için sağlanan değer <textarea> etiketinin name ve id seçeneğini ayarlamaya yarar. İlgili özelliğin değeri <textarea> etiketinin içeriğini teşkil eder. Örnek kullanım:

<textarea asp-for="Supplier.Name"></textarea>

Anti-forgery özelliğinin kullanımı

[düzenle]

Anti-forgery özelliği Cross Site Request Forgery saldırılarına önlem amaçlı ASP.NET Core tarafından sağlanan bir güvenlik özelliğidir. Siz formu ilk kez talep ettiğinizde forma gizli bir kontrol ekler. Bu kontrolün ismi _RequestVerificationToken'dir ve değeri sunucu tarafından gönderilen oldukça uzun bir string'tir. Sunucu gönderdiği bu uzun string'in kaydını tutar ve form verilerinin tekrar sunucuya gönderildiği durumda _RequestVerificationToken form kontrolü için aynı değerin sağlanmasını ister.

Cross Site Request Forgery saldırısı şu mantıkla çalışmaktadır. Örneğin A sizin oturum açtığınız ve kullandığınız güvenli site olsun. B ise saldırganın web sitesi olsun. A sitesinde oturum açıp, oturumu kapatmadan B sitesini açtığınızda B sitesi gönderdiği HTML'in içine A sitesine bir POST request'i gönderen bir javaScript kodu gömer. Bu kod çalışabilir. Çünkü istek sonuçta sizin tarayıcınızdan yapılmaktadır, siz de A sitesiyle olan oturumunuzu kapatmadığınız için sizin tarayıcınızdan A sitesine yapılan her türlü talep yetkili olduğunuzu gösteren bir cookie gönderecektir.

Bu aşamada A sitesine yapılan POST request'i çok çeşitli şeyleri yapabilir. A sitesinde sizinle ilgili tutulmuş bir kaynağı silebilir, daha da kötüsü bir GET request'i ile A sitesinden size ait bir kaynağı çekip bir POST request'iyle bu kaynağı kendi sunucusuna gönderebilir.

Eğer bir <form> etiketinin açık bir action seçeneği yoksa, diğer bir deyişle asp-controller, asp-action, asp-page gibi seçeneklerle action seçeneğinin değeri tag helper tarafından otomatik üretilmişse FormTagHelper sınıfı forma _RequestVerificationToken isimli gizli bir kontrol ekler.

Controller'larda anti-forgery özelliğinin etkinleştirilmesi

[düzenle]

Varsayılan durumda controller'lar _RequestVerificationToken kontrolünün değerini içersin, içermesin bütün POST request'lerine kapıyı açar. _RequestVerificationToken kontrolünün değerini içermeyen taleplerin bir controller'a POST request'i yapamamasını sağlamak için ilgili controller AutoValidateAntiForgeryToken attribute'u ile işaretlenir. AutoValidateAntiForgeryToken attribute'u sayesinde GET, HEAD, OPTIONS ve TRACE dışında kalan bütün talep tiplerinde anti-forgery token'i istenir.

IgnoreValidationToken attribute'u bir action veya controller için kullanılabilir ve ilgili controller veya action için anti-forgery token belirtilmesine gerek olmadığını belirtir. ValidateAntiForgeryToken attribute'u bir action veya controller için kullanılabilir ve ilgili controller veya action için anti-forgery token belirtilmesi gerektiğini belirtir. ValidateAntiForgeryToken attribute'u bir controller için kullanıldığı zaman o controller'daki bütün action'lar için anti-forgery token validasyonunu zorunlu kılar.

Razor sayfalarında anti-forgery özelliğinin etkinleştirilmesi

[düzenle]

Razor sayfalarında varsayılan durumda anti-forgery özelliği etkindir. Bir Razor sayfasından anti-forgery özelliğini kaldırmak için PageModel sınıfı IgnoreAntiForgeryToken attribute'u ile işaretlenebilir.

JavaScript istemcilerinde anti-forgery özelliğinin etkinleştirilmesi

[düzenle]

MVC ve Razor Pages'ta anti-forgery özelliğini etkinleştirmek kolaydır. Sadece ilgili controller, action veya PageModel sınıfının ilgili attribute ile işaretlenmesi yeterlidir. MVC ve Razor Pages'ta anti-forgery özelliği gönderilen form kontrollerinin arasına gizli bir form kontrolü eklenmesi esasına dayanır. Ancak uygulamamızı web servis mantığında JavaScript istemcileri kullanıyorsa gönderilecek form kontrolleri olmadığı için anti-forgery özelliğini etkinleştirmek için farklı bir yol bulmamız lazım.

Uygulamamızı web servis mantığında JavaScript istemcileri kullanıyorsa anti-forgery özelliğini etkinleştirmek için öncelikle sunucu cookie şeklinde istemciye anti-forgery token'ini göndermeli, daha sonraki POST isteğinde istemci aynı token'i bir header'da sunucuya geri göndermelidir. Şimdi projenizin Program.cs dosyasını şöyle değiştirin:

// ...
using Microsoft.AspNetCore.Antiforgery;
// ...
// Burası Program.cs dosyasındaki servis aboneliklerinin yapıldığı yer
builder.Services.Configure<AntiforgeryOptions>(opts => { opts.HeaderName = "X-XSRF-TOKEN"; });
// ...
// Burası Program.cs dosyasındaki middleware eklemelerinin yapıldığı yer
IAntiforgery antiforgery = app.Services.GetRequiredService<IAntiforgery>();
app.Use(async (context, next) => {
    if (!context.Request.Path.StartsWithSegments("/api")) {
        string token = antiforgery.GetAndStoreTokens(context).RequestToken;
        if (token != null) {
            context.Response.Cookies.Append("XSRF-TOKEN", token, new CookieOptions { HttpOnly = false });
        }
    }
    await next();
});
// ...

Burası Program.cs dosyasının bir kısmıdır ve sadece eklemeleri göstermektedir. Örneğimizde 5. satırda istemcinin anti-forgery token'i hangi header'da göndereceği belirtilmektedir, daha doğrusu sunucu tokeni bu header'da arayacaktır. 9. ve 17. satırlar arasındaki kodlarda bir custom middleware oluşturulmaktadır. Bu middleware "XSRF-TOKEN" isimli cookie'nin değerini üretilen token yapmaktadır. Gönderilen cookie'nin HttpOnly özelliğinin değeri false'tur. Bu, gönderilen cookie değişkenine JavaScript istemcilerinin de erişebileceği anlamına gelir. Anti-forgery özelliğinin kullanımını göstermek için JavaScript kodu yazmak zorundayız. Bunu yapmak için projenizin Pages klasörüne JavaScriptForm.cshtml isimli bir Razor sayfası ekleyin, içeriği şöyle olsun:

@page "/pages/jsform"
<script type="text/javascript">
	async function sendRequest() {
		const token = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, "$1");
		let form = new FormData();
		form.append("name", "Paddle");
		form.append("price", 100);
		form.append("categoryId", 1);
		form.append("supplierId", 1);
		let response = await fetch("@Url.Page("FormHandler")", {
			method: "POST",
			headers: { "X-XSRF-TOKEN": token },
			body: form
		});
		document.getElementById("content").innerHTML = await response.text();
	}
	document.addEventListener("DOMContentLoaded", () => document.getElementById("submit").onclick = sendRequest);
</script>
<button id="submit">Submit JavaScript Form</button>
<div id="content"></div>

Bu JavaScript kodu cookie şeklinde gönderilen tokeni almakta, form verilerini oluşturmakta, gönderilen isteğin "X-XSRF-TOKEN" header'ına aldığı tokeni yazmakta, gövdesine form verilerini yazmakta ve sunucuya göndermekte, form verilerinin gönderilmesi sonucu alınan cevabı ise "content" id'li div'in içine yazmaktadır. Bütün bunlar "submit" id'li butona tıklandığında olmaktadır.