ASP.NET Core 9 MVC — Bölüm 2: Controller'lar, View'lar, Model Binding ve Tag Helpers
Controller action return type'ları, binding kaynakları (FromQuery, FromBody, FromServices vs.), ViewModel pattern, DataAnnotations ve FluentValidation ile doğrulama, Razor syntax, Layout + Partial + View Component, built-in ve custom Tag Helper'lar ve anti-forgery — MVC'nin frontend tarafının komple anatomisi.
Merhaba arkadaşlar, bu makalemizde ASP.NET Core 9 MVC serisinin ikinci bölümünde Controller'ların ve View'ların detaylarına giriyoruz. Bir önceki bölümde projeyi kurduk ve mimariyi konuştuk. Şimdi sıra gerçek işe: HTTP isteğinin Controller'a nasıl ulaştığı, action'ların neler döndürebildiği, Razor view'ların nasıl render edildiği, model binding'in büyüsü ve Tag Helper'ların hayatımızı ne kadar kolaylaştırdığı. Çok örnek olacak, kemerinizi bağlayın.
Controller Nedir, Ne Değildir?
MVC pattern'inde Controller, gelen HTTP isteğini alıp, gerekli iş kurallarını servise delege eden ve sonuç olarak bir View veya başka bir response türü döndüren sınıftır. Kural olarak "Controller" ekiyle biter ve Controller base class'ından türer:
using Microsoft.AspNetCore.Mvc;
public class UrunlerController : Controller
{
public IActionResult Index()
{
return View();
}
}
ControllerBase ile Controller arasındaki farkı bilin — API projelerinde ControllerBase yeterli (View döndürmez), MVC'de Controller lazım (View metodunu içerir).
Action Return Type'ları
Bir action metodunun dönüş tipi pek çok şey olabilir. En yaygınları:
public class DemoController : Controller
{
// 1. View dondurur
public IActionResult Sayfa() => View();
// 2. JSON dondurur
public IActionResult Json() => Json(new { mesaj = "merhaba" });
// 3. Dosya indirir
public IActionResult Dosya()
=> File("~/files/rapor.pdf", "application/pdf", "rapor.pdf");
// 4. Yonlendirir
public IActionResult Yonlendir() => RedirectToAction("Index", "Home");
// 5. Plain text
public IActionResult Metin() => Content("Duz metin", "text/plain");
// 6. HTTP status kod
public IActionResult YokBu() => NotFound();
public IActionResult Yasak() => Forbid();
// 7. Asenkron
public async Task<IActionResult> Async()
{
await Task.Delay(100);
return View();
}
}
Visual Studio'dan: Visual Studio tarafinda .cshtml dosyasi acildiginda asp-* yaz-oku hemen aciliyor, Ctrl+Boslik ile onerileri gorebilirsiniz.
Best practice: IActionResult veya Task<IActionResult> kullanın. Generic ActionResult<T> özellikle API'lerde type-safe response için güzel.
Model Binding — Sihir Ama Kontrol Edin
ASP.NET Core, gelen HTTP isteğindeki verileri action parametrelerine otomatik dönüştürür. Buna model binding denir. Veri kaynakları (binding sources):
public IActionResult Ornek(
[FromRoute] int id, // URL route parametresi: /urunler/5
[FromQuery] string arama, // Query string: ?arama=foo
[FromForm] string ad, // Form POST verisi
[FromBody] UrunDto urun, // Request body (JSON)
[FromHeader(Name="X-Api-Key")] string apiKey,
[FromServices] IUrunServisi servis) // DI'dan otomatik gelir
{
// ...
return Ok();
}
Attribute yazmadığınızda framework kendi mantığı ile seçim yapar:
- Primitive tipler (int, string, vb.) → Route > Query > Form sırasıyla.
- Kompleks tipler (class, record) → Form > Route > Query (API'de ayarlanmamışsa Body).
- [ApiController] işaretli controller'larda kompleks tipler otomatik [FromBody].
Benim tavsiyem: açıkça belirtin. Niyetiniz net olsun, çünkü implicit'lik bug kaynağıdır.
ViewModel — Controller ve View Arasında Sözleşme
Entity'leri direkt view'a taşımak yaygın bir hata. Bunun yerine ViewModel kullanın:
// Core/Entities/Urun.cs (DB entity)
public class Urun
{
public int Id { get; set; }
public required string Ad { get; set; }
public decimal Fiyat { get; set; }
public decimal MaliyetFiyat { get; set; } // dis dunyaya gitmesin!
public DateTime OlusturmaTarihi { get; set; }
}
// Web/ViewModels/UrunViewModel.cs
public record UrunViewModel(
int Id,
string Ad,
decimal Fiyat,
string FiyatFormatli);
// Controller
public async Task<IActionResult> Liste()
{
var urunler = await _servis.TumunuGetirAsync();
var vm = urunler.Select(u => new UrunViewModel(
u.Id, u.Ad, u.Fiyat, $"{u.Fiyat:C2}"));
return View(vm);
}
ViewModel'in faydaları: güvenlik (hassas alan sızmaz), performans (sadece ihtiyaç olan alanlar gelir), bakım kolaylığı (entity değişse view etkilenmez).
Validation — DataAnnotations ve FluentValidation
Model validation için iki temel yol var. Önce built-in DataAnnotations:
public class UrunEkleViewModel
{
[Required(ErrorMessage = "Urun adi gerekli")]
[StringLength(200, MinimumLength = 3, ErrorMessage = "3-200 karakter arasi olmali")]
public required string Ad { get; set; }
[Range(0.01, 1_000_000, ErrorMessage = "Fiyat 0-1M arasi olmali")]
public decimal Fiyat { get; set; }
[EmailAddress]
public string? IletisimEmail { get; set; }
[RegularExpression(@"^\d{4}-\d{2}$", ErrorMessage = "2024-01 formatinda olmali")]
public string? DonemKodu { get; set; }
}
Controller'da doğrulama:
[HttpPost]
public async Task<IActionResult> Ekle(UrunEkleViewModel vm)
{
if (!ModelState.IsValid)
return View(vm);
await _servis.EkleAsync(vm);
return RedirectToAction(nameof(Liste));
}
Daha kompleks kurallar için FluentValidation çok güçlü:
dotnet add package FluentValidation.AspNetCore
public class UrunEkleValidator : AbstractValidator<UrunEkleViewModel>
{
public UrunEkleValidator()
{
RuleFor(x => x.Ad)
.NotEmpty().WithMessage("Urun adi gerekli")
.Length(3, 200);
RuleFor(x => x.Fiyat)
.GreaterThan(0)
.LessThanOrEqualTo(1_000_000);
RuleFor(x => x.DonemKodu)
.Matches(@"^\d{4}-\d{2}$")
.When(x => !string.IsNullOrEmpty(x.DonemKodu));
}
}
// Program.cs
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<UrunEkleValidator>();
FluentValidation async kurallar, conditional validation, cross-field validation gibi kompleks senaryolarda çok rahat.
Razor Syntax — HTML İçinde C#
Razor view'lar .cshtml uzantılı dosyalar. HTML içinde @ ile C# yazıyoruz:
@model IEnumerable<UrunViewModel>
@{
ViewData["Title"] = "Urunler";
var bugun = DateTime.Now.ToString("dd.MM.yyyy");
}
<h1>@ViewData["Title"]</h1>
<p>Bugun: @bugun</p>
@if (!Model.Any())
{
<div class="alert alert-info">Henuz urun yok</div>
}
else
{
<table class="table">
<thead>
<tr><th>Ad</th><th>Fiyat</th></tr>
</thead>
<tbody>
@foreach (var u in Model)
{
<tr>
<td>@u.Ad</td>
<td>@u.FiyatFormatli</td>
</tr>
}
</tbody>
</table>
}
Bilmeniz gereken nüanslar:
- @model: View'ın alacağı model tipini belirler, IntelliSense bundan doğar.
- @{ ... }: Çoklu satır C# bloğu.
- @(expression): Parantez içindeki ifadeyi render eder.
- @Html.Raw(html): XSS uyarısı! Sadece güvendiğiniz içeriği raw basın.
- @await Component.InvokeAsync("ComponentAdi"): View Component çağırma.
Layout ve _ViewStart / _ViewImports
Ortak layout için Views/Shared/_Layout.cshtml:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="utf-8" />
<title>@ViewData["Title"] - SemGoksuMvc</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
</head>
<body>
<nav> <!-- Menu --> </nav>
<main role="main" class="container">
@RenderBody()
</main>
<footer> <!-- footer --> </footer>
@RenderSection("Scripts", required: false)
</body>
</html>
_ViewStart.cshtml tüm view'lar için default layout'u set eder:
@{
Layout = "_Layout";
}
_ViewImports.cshtml ortak using, tag helper kayıtlarını barındırır:
@using SemGoksuMvc.Web
@using SemGoksuMvc.Web.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Partial View — Kod Tekrarını Öldürmek
Tekrar eden markup parçaları için partial view:
// _UrunKart.cshtml (Views/Shared/)
@model UrunViewModel
<div class="card">
<h3>@Model.Ad</h3>
<p>@Model.FiyatFormatli</p>
</div>
// Kullanici view'da
@foreach (var urun in Model)
{
<partial name="_UrunKart" model="urun" />
}
View Component — Partial'in Güçlü Kardeşi
Partial view sadece HTML parçası, View Component ise kendi iş mantığı olan bağımsız bir bileşen — controller-action-view üçlüsüne benzer. Örneğin sepet özeti:
// ViewComponents/SepetOzetViewComponent.cs
public class SepetOzetViewComponent(ISepetServisi servis) : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
var sepet = await servis.AktifSepetiGetirAsync();
return View(sepet); // Views/Shared/Components/SepetOzet/Default.cshtml
}
}
// Layout'ta cagrilir
@await Component.InvokeAsync("SepetOzet")
// Veya tag helper ile
<vc:sepet-ozet />
Async veri çekebilen, DI kullanan bağımsız bileşenler için ideal.
Tag Helpers — HTML Dostu Server-Side Helper'lar
HTML Helper'ların modern yerine geçen Tag Helper'lar, HTML'i bozmadan server-side işlevsellik ekler. En sık kullanılanlar:
@model UrunEkleViewModel
<form asp-action="Ekle" asp-controller="Urunler" method="post">
<div class="mb-3">
<label asp-for="Ad" class="form-label"></label>
<input asp-for="Ad" class="form-control" />
<span asp-validation-for="Ad" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Fiyat" class="form-label"></label>
<input asp-for="Fiyat" class="form-control" />
<span asp-validation-for="Fiyat" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Kaydet</button>
</form>
<a asp-controller="Urunler" asp-action="Liste">Geri</a>
<img src="~/images/logo.png" asp-append-version="true" />
Dikkat edilecek sihirler:
- asp-for: Model property'sine bağlar — id, name, value, validation state hepsi otomatik.
- asp-validation-for: O alan için validation mesajını gösterir.
- asp-action, asp-controller, asp-route-*: URL oluşturma.
- asp-append-version: Static asset'lere cache-busting query string ekler.
- asp-antiforgery: Form'da otomatik anti-forgery token eklenir (default true).
Custom Tag Helper Yazma
Kendi tag helper'ımızı yazabiliriz. Örneğin ikonlu buton:
[HtmlTargetElement("ikon-buton")]
public class IkonButonTagHelper : TagHelper
{
public string Icon { get; set; } = "";
public string Text { get; set; } = "";
public string Href { get; set; } = "#";
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", Href);
output.Attributes.SetAttribute("class", "btn btn-secondary");
output.Content.SetHtmlContent(
$"<i class=\"bi bi-{Icon}\"></i> {Text}");
}
}
// _ViewImports.cshtml
@addTagHelper *, SemGoksuMvc.Web
// Kullanim
<ikon-buton icon="save" text="Kaydet" href="@Url.Action("Ekle")" />
Çıktı: <a href="..." class="btn btn-secondary"><i class="bi bi-save"></i> Kaydet</a>.
Anti-Forgery Token — CSRF'e Karşı Kalkan
<form method="post"> ile form gönderirken cross-site request forgery (CSRF) saldırılarına karşı anti-forgery token kullanıyoruz. ASP.NET Core bunu büyük ölçüde otomatik yapar:
- Form tag helper otomatik __RequestVerificationToken hidden field'ı ekler.
- Controller'da [AutoValidateAntiforgeryToken] veya global filter ile tüm POST'lar doğrulanır:
// Program.cs
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add<AutoValidateAntiforgeryTokenAttribute>();
});
AJAX isteklerinde token'ı header'a eklemeniz lazım:
// _Layout.cshtml
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
<script>
window.antiforgeryToken = '@Antiforgery.GetAndStoreTokens(Context).RequestToken';
</script>
// JS
fetch('/urunler/sil/5', {
method: 'POST',
headers: { 'RequestVerificationToken': window.antiforgeryToken }
});
Bu Bölümde Ne Yaptık?
Controller action return type'larını, model binding'in tüm kaynaklarını, ViewModel pattern'ini, DataAnnotations + FluentValidation ile validation'ı, Razor syntax'ın nüanslarını, Layout + Partial + View Component yaklaşımlarını, Tag Helper'ların built-in ve custom kullanımlarını ve anti-forgery korumasını gördük.
Bir sonraki bölümde EF Core 9 ile veritabanı tarafına geçiyoruz: DbContext setup, migration workflow, Repository ve Service pattern, LINQ best practices ve .NET 9 EF yeniliklerini uygulayacağız. Bol kolay gelsin, bir sonraki bölümde görüşürüz!