İçeriğe atla

ASP.NET Core 6/MVC

Vikikitap, özgür kütüphane

MVC, ASP.NET Core tarafından desteklenen bir tasarım desenidir. Eski öneminden azalma olsa da günümüzde ASP.NET Core'da web geliştirme için kullanılan en popüler framework'tur. Aslında MVC'nin bazı bileşenlerini daha önce görmüştük. İlk bileşeni olan modeli Entity Framework Core kullanımı konusunda görmüştük. Web servisleri konusunda da controller bileşenini gördük. Ancak web servislerdeki controller'lar JSON döndürüyordu. MVC'nin ortasındaki "V" view anlamına gelmektedir ve controller'ların JSON tipinde veri yerine bir view döndürmesini sağlar. Web servis yaklaşımında uygulama sadece veriyi oluşturmakla sorumlu iken, verinin güzel bir formatta gösterilmesinden servis tüketicisi sorumluyken MVC yaklaşımında veritabanından sunuma kadar tam bir uçtan uca geliştirme söz konusudr. Model veritabanındaki nesneleri, bu nesneler üzerinde yapılabilecek işlemleri, yeni sağlanacak nesnelere uygulanacak kısıtları, vs. sağlar. Geliştiriciye veritabanına direkt SQL sorgusu göndermektense daha uygulamanın iş mantığına uygun bir geliştirme ortamı sağlar. Controller istemci tarafından gönderilen istekleri karşılar, mantıksal işlemler yapar, modelden veri alır, aldığı verileri render'lanmak üzere bir view'a gönderir. View'ın görevi ise controller'dan aldığı veriyi uygun bir şekilde kullanıcıya göstermek ve formlar aracılığıyla kullanıcıdan aldığı verileri tekrar controller'a iletmektir.

MVC'yi en güzel bir restoran analojisi açıklar. Restoranın sahibi olan patron kasada bekler, müşterileri karşılar, müşteriden siparişleri alır ve siparişleri mutfakta yemekleri hazırlayacak olan aşçıya iletir. Aşçı yemeği yapar ve geri patrona verir. Bu aşamada yemek sunuma pek uygun değildir. Tabak, çanak, çatal, tepsi, ekmek, sos, vs. yoktur. Patron yemeği bir haliyle garsona verir. Garson yemeği süsler. Tuzu azsa tuz atar, sos sıkar, yemeği güzel bir tabağa koyar, yanına nizami olarak çatal, bıçak, kaşık koyar, yanına birkaç dilim ekmek koyar ve müşteriye servis eder. Daha sonrasında müşteriden gelen her türlü şikayeti, isteği ve geri bildirimi patrona iletir. Patron, garsondan gelen bilgilere göre yeni kararlar alır. Bu analojide patron controller, aşçı model, view ise garsondur. Aslında bu modelde olmayan bir katman da veritabanıdır. Veritabanı aşçının malzeme temin ettiği tedarikçidir.

Bu bölümde geçen bölümden kalan örneği kullanacağız. Öncelikle Program.cs dosyamızı şöyle sadeleştirelim:

using Microsoft.EntityFrameworkCore;
using WebApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:ProductConnection"]);
    opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseStaticFiles();
app.MapControllers();
var context = app.Services.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
SeedData.SeedDatabase(context);
app.Run();

Bu Program.cs dosyamızda veritabanıyla çalışmamızı sağlayan kodlar, statik içeriğe erişime izin veren kodlar ve controller şeklinde olan web servis sınıflarını kullanabilmemizi ve bu sınıflara ve içindeki action'lara rotalama yapabilmemizi sağlayan kodlar bulunmaktadır. Uygulamamızı yavaş yavaş web servisten MVC'ye çevireceğiz.

Uygulamamızı MVC'ye çevirme

[değiştir]

Web servis uygulamamızı MVC'ye çevirmek çok kolaydır. Şimdi Program.cs dosyamızı şöyle değiştirelim:

using Microsoft.EntityFrameworkCore;
using WebApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:ProductConnection"]);
    opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseStaticFiles();
app.MapControllerRoute("Default", "{controller=Home}/{action=Index}/{id?}");
var context = app.Services.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
SeedData.SeedDatabase(context);
app.Run();

Bu kod önceki versiyona yalnızca iki satır kod eklemektedir. Öncelikle AddControllers() servis çağrısı yerine AddControllersWithViews() servis çağrısı kullanılmıştır. Bunun anlamı controller ve view'ların beraber kullanılacağıdır, ki bu da MVC'ye tekabül etmektedir. İkinci değişiklik ise request pipeline'a MapControllers() yerine MapControllerRoute() middleware'ini ekler. Bu değişiklik rotalamayı MVC'ye göre ayarlar. Bu rotalama mekanizmasına convention routing denir ve rotalama için controller ve action'ların atrribute'ları yerine isimleri kullanılır. MVC'deki rotalama mekanizması birazdan detaylı olarak ele alınacaktır.

MVC controller'ı oluşturma

[değiştir]

MVC controller'ları web servis controller'ına büyük ölçüde benzer, ancak arada bazı farklar da vardır. Şimdi projemizdeki Controllers klasörüne HomeController isimli bir controller ekleyelim ve içeriği şöyle olsun:

using Microsoft.AspNetCore.Mvc;
using WebApp.Models;
namespace WebApp.Controllers
{
    public class HomeController : Controller
    {
        private DataContext context;
        public HomeController(DataContext ctx)
        {
            context = ctx;
        }
        public async Task<IActionResult> Index(long id = 1)
        {
            return View(await context.Products.FindAsync(id));
        }
    }
}

MVC için olan controller sınıfları ControllerBase yerine Controller sınıfından türemektedir. Controller sınıfı da ControllerBase sınıfından türemiştir ve view'larla çalışmamıza izin verecek ekstra üyeler içerir. Ayrıca MVC controller'larındaki action'lar kafalarına göre nesne döndüremezler, asenkron çalışıyorlarsa Task<IActionResult> tipinde nesne döndürebilirler, yani tip parametresi ya IActionResult'tır ya da IActinResult arayüzünü uygulamış bir sınıftır. Eğer action senkron çalışıyorsa IActionResult veya IActionResult'ı uygulamış herhangi bir sınıfla geri dönmelidir.

Örneğimizde View() metodu çağrılmakta ve metoda parametre olarak veritabanından çekilen bir nesne verilmektedir. Bu metot çağrısının anlamı action için varsayılan view'ın render'lanması ve view'a model olarak ilgili Product nesnesinin verilmesidir. Buradaki View() metodu Controller sınıfından devralınan bir metottur. View() metodu IActionResult arayüzünü uygulamış olan ViewResult tipinden bir nesne oluşturur, bu nesne bir view'ı ve view'ın model nesnesini kapsüller ve bir action'ın geri dönüş nesnesi olarak kullanıldığında "bu view'ı bu model nesnesiyle beraber render'la" anlamını taşır. Örneğimizde şimdilik herhangi bir view yoktur ve programı bu haliyle çalıştırırsanız bir çalışma zamanı hatası oluştuğunu görürsünüz.

Bu örneğimizde controller sınıfı için ApiController attribute'u, action'lar için Route ve HTTP metot attribute'ları kullanılmamıştır. Bu attribute'lar gelen isteğin path'ına göre hangi controller ve action'ın seçileceğini belirtmek için kullanılmaktadır. MVC'de bu süreç farklıdır.

Convention routing

[değiştir]

MVC'deki rotalama mekanizması web servisten farklıdır. Web servis attribute'lar üzerine kurulu bir rotalama mekanizması kullanılır. MVC ise controller ve action isimlerine dayalı bir rotalama mekanizması kullanılır. Az önce Program.cs dosyasına eklediğimiz

app.MapControllerRoute("Default", "{controller=Home}/{action=Index}/{id?}");

ile Default isimli yeni bir rota oluşturmaktayız. Bu rotaya göre gelen request'in path'ındaki ilk segment controller değişkenine atanır. Controller MVC'de segment değişkenleri için özel bir isimdir ve ilgili isimli controller'ı sonunda "Controller" takısı olmadan seçmeye yarar. İkinci segment değişkeninin ismi action'dır ve ilgili controller'daki bir action'ı seçmeye yarar. Üçüncü segment ilgili action'a verilecek id isimli parametredir. Controller kullanılmadığı zaman HomeController, action kullanılmadığı zaman Index seçilir, id parametresi opsiyoneldir. Bu yüzden sunucumuza hiç path kısmı olmadan bir talep gönderirsek HomeController controller'ının Index action'ına talep gider. Bu tür bir rotalama mekanizmasına convention routing denir. Yukarıda belirttiğimiz rota MVC'de sık kullanıldığından MapControllerRoute() metot çağrısı yerine ilgili yere

app.MapDefaultControllerRoute();

Bu kod ASP.NET Core'a "MVC için varsayılan rotayı kullan" demektedir ve yukarıda elle oluşturduğumuz rotaya denktir. Bir MVC uygulaması istediği kadar rota tanımlayabilir.

Bir MVC uygulaması bir controller sınıfının içindeki bütün peblic metotları action varsayar ve bu action'lar bütün HTTP taleplerine metodu fark etmeksizin cevap verir. Bir controller'daki bir metodun action olmadığını belirtmek için ya private yaparız veya NonAction attribute'u ile işaretleriz. Ayrıca bir action'ın yalnızca belirli bir tipteki HTTP isteklerine cevap vrebilmesini sağlamak için ilgili action'ı HttpGet, HttpPost, vb. attribute'lar ile işaretleriz.

Action'ların view arama süreci

[değiştir]

Örneğimizde Home controller'ındaki Index action'ı return View(await context.Products.FindAsync(id)); satırıyla varsayılan view'ı aramakta, ama bulamamaktadır. İlk aradığı yer Views/<controller> klasörüdür. Örneğin Home controller'ının içindeki Index action'ı varsayılan view olarak Views/Home klasöründe Index.cshtml isimli bir dosyayı arar. Eğer bu konumda ilgili dosya bulunamazsa bu sefer Views/Shared klasöründeki Index.cshtml isimli dosyayı arar. Örneğimizde aranan dosya iki konumda da bulunamadığı için çalışma zamanı hatası oluşmuştur.

View'ın oluşturulması

[değiştir]

Uygulamamızın istediği view'ı oluşturmak için öncelikle Views isimli bir klasör, bu klasörün içinde Home isimli bir alt klasör oluşturalım. Daha sonra Home klasörünün içinde Index.cshtml isimli bir view oluşturalım. Bu dosyanın içeriği şöyle olsun:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
	<h6>Product Table</h6>
	<table>
		<tr><th>Name</th><td>@Model.Name</td></tr>
		<tr><th>Price</th><td>@Model.Price.ToString("c")</td></tr>
	</table>
</body>
</html>

İlk bakışta standart bir HTML dosyası gibi gözüküyor. Ancak @ ile başlayan direktifler controller'dan view'a gönderilen veriye ulaşmayı sağlıyor. Örneğimizde Index action'ı return View(await context.Products.FindAsync(id)); satırıyla view'a bir Product nesnesi göndermiştir. View bu Product nesnesinin adını ve para birimi formatlamasında fiyatını HTML kodundaki ilgili yere basmaktadır.

Bir view'ın ismiyle çağrılması

[değiştir]

Bazen bir action'da varsayılan view'ı render'lamak yerine belirli bir view'ı render'lamak isteyebiliriz. Bu durumda view'ı açık bir şekilde belirtmek gerekir. Örneğin Home controller'ındaki Index action'ını şöyle değiştirin:

public async Task<IActionResult> Index(long id = 1)
{
    Product prod = await context.Products.FindAsync(id);
    if (prod.CategoryId == 1)
    {
        return View("Watersports", prod);
    }
    else
    {
        return View(prod);
    }
}

Bu action eğer gelen ürünün CategoryId'si 1 ise Watersports isimli view'ı, değilse varsayılan view'ı render'lamaktadır. Her iki view'a da model verisi olarak ilgili Product nesnesi verilmektedir. Watersports view'ı da yine öncelikle Views/Home klasöründe, sonrasında Views/Shared klasöründe aranacaktır.

View'lar için mutlak yolu gösterme

[değiştir]

Yukarıdaki yöntemde belirli bir view'ın aranmasını işlemini ASP.NET Core'a delege ederiz. ASP.NET Core daha önce de belirttiğimiz şekilde önce ilgili Views klasörünün ilgili controller klasöründe arama yapar, burada alamazsa Views/Shared klasöründe arama yapar. Eğer ASP.NET Core'un bu arama sürecini devralmasını istemiyorsak, view için kendimiz kesin bir yol belirtip ilgili view'ı adeta elimizle koymuş gibi bulabiliriz. Örnek:

public IActionResult Index()
{
    return View("/Views/Shared/Common.cshtml");
}

Bu tür bir belirtimde view'ın uzantısının belirtilmesi zorunludur. Ayrıca gördüğünüz üzere view'lara model verisi gönderilmek zorunda değildir.

View'ların arkaplanı

[değiştir]

View'lar hem HTML hem de C# kodu içerebilir. View'daki C# kodu içeren ifadeler sunucu tarafında çalıştırılarak sonuçları HTML koda gömülür. İstemciye salt HTML gönderilir. Esasında view'lar da RazorPage sınıfından türeyen birer sınıftır. View sınıfının yaptığı iş aslında kendinde bulunan HTML kodlarını response'a yazmaktır. Bu response'a yazma işlemi yapılırken HTML kodları direkt response'a yazılırken C# kodları ise çalıştırıldıktan sonra sonuçları response'a yazılır.

Varsayılan durumda view sınıfları direkt DLL'e derlenir, ara bir C# kodu oluşturulmaz. Oluşturulan ara C$ kodunu görmek istiyorsanız projenizin proje dosyasının iöeriğine aşağıdaki girdiyi ekleyin:

<PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Projeyi, proje dosyasına yukarıdaki kod eklenmiş şekilde çalıştırırsanız oluşturulan C# dosyası obj/Debug/net6.0/generated klasörüne konulur.

Bir view ASP.NET Core tarafından esasında arkaplanda şuna benzer bir C# sınıfına dönüştürülür:

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCoreGeneratedDocument
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Xml.Linq;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Razor;
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using Microsoft.AspNetCore.Razor.Runtime.TagHelpers;

    using Microsoft.AspNetCore.Razor.TagHelpers;

    internal sealed class Views_Home_Watersports : RazorPage<dynamic>
    {
        public async override Task ExecuteAsync()
        {
            WriteLiteral("<!DOCTYPE html>\r\n<html>\r\n");
            HeadTagHelper = CreateTagHelper<TagHelpers.HeadTagHelper>();
            __tagHelperExecutionContext.Add(HeadTagHelper);
            Write(__tagHelperExecutionContext.Output);
            WriteLiteral("\r\n");
            __tagHelperExecutionContext = __tagHelperScopeManager.Begin("body", TagMode.StartTagAndEndTag, "76ad69...", async () => {
                WriteLiteral("<h6>Watersports</h6>\r\n<div><table>\r\n<tr>");
                WriteLiteral("<th>Name</th><td>");
                Write(Model.Name);
                WriteLiteral("</td></tr>\r\n<tr><th>Price</th><td>");
                Write(Model.Price.ToString("c"));
                WriteLiteral("</td></tr>\r\n<tr><th>Category ID</th><td>");
                Write(Model.CategoryId);
                WriteLiteral("</td></tr>\r\n</table>\r\n</div>\r\n");
            });
            BodyTagHelper = CreateTagHelper<TagHelpers.BodyTagHelper>();
            __tagHelperExecutionContext.Add(BodyTagHelper);
            Write(__tagHelperExecutionContext.Output);
            WriteLiteral("\r\n</html>\r\n");
        }
        public IModelExpressionProvider ModelExpressionProvider { get; private set; }
        public IUrlHelper Url { get; private set; }
        public IViewComponentHelper Component { get; private set; }
        public IJsonHelper Json { get; private set; }
        public IHtmlHelper<dynamic> Html { get; private set; }
    }
}

Evet, sınıf oldukça karmaşık gözüküyor. Üstelik önemli olan kısımlara odaklanabilmemiz için bu basitleştirilmiş hali. Gerçek sınıf bu kadar basit değil. Bu sınıfta öne çıkan bazı şeyler var. Sınıf RazorPage<dynamic> sınıfından türüyor. RazorPage<T> sınıfının önemli üyeleri şunlardır:

Context: Geçerli request'in HttpContext nesnesi.
ExecuteAsync(): View'ın istemciye gönderilen HTML kodunu üreten metot.
Layout: View'ın layout'unu belirtir.
Model View'a action tarafından gönderilen model nesnesini döndürür.
RenderBody(): Layout view'ları tarafından kullanılır. İlgili yere layout'u kullanan view'ın içeriği yerleştirilir.
RenderSection(): Layout view'ları tarafından kullanılır. Bir view'daki bir section'ın ilgili yere yerleştirilmesini sağlar.
TempData: TempData verisine ulaşmayı sağlar.
ViewBag: ViewBag verisine ulaşmayı sağlar.
ViewContext: İlgili view'ın bağlam verisini döndürür.
ViewData: ViewData verisine ulaşmayı sağlar.
Write(str): Response'a yazma yapar. Yazma yapılırken olası HTML kodları sayfada olduğu gibi gözükecek şekilde encode edilir. Örneğin Write() metoduna parametre olarak <html> verilirse bu kod &lt;html&gt; koduna dönüştürülür.
WriteLiteral(str): Response'a olduğu gibi yazma yapar. Herhangi bir encode işlemi yapılmaz. Response'a HTML kodu yazmak için kullanılabilir.

Ana sınıftan devralınan bu üyeler dışında sınıfın kendisinin tanımladığı üyeler şunlardır:

Component: View component'larla çalışma için bir yardımcı döndürür.
Html: Html yardımcısını döndürür. HTML encoding'in nasıl yapılacağı bu özellik üzerinden ayarlanabilir.
Json: Json yardımcısını döndürür. Bu yardımcı JSON'a encoding yapmak için kullanılabilir.
ModelExpressionProvider: Modelden özellikleri seçen ifadelere erişim sağlar. Tag hepler'lar vasıtasıyla kullanılabilir.
Url: URL'lerle çalışmak için bir yardımcı döndürür.

View'da model tipinin belirtilmesi

[değiştir]

Örneğimizde view'a action'ın gönderdiği model nesnesinin tipi view'da belirtilmemiştir. Bunu üretilen view sınıfının türediği RazorPage sınıfının tip parametresinin dynamic olmasından anlayabiliriz. Bunun sonucu view'a gönderilen model nesnesinden her türlü üye çağrımını yapabilmemizdir. Bu tercih edilebilecek bir durum değildir ve derleme zamanında tespit edilebilecek hataları çalışma zamanına iteler. Bu sorunun üstesinden gelmek için view'ın başında aşağıdaki gibi view'ın model olarak ne tip nesne aldığını belirtebiliriz:

@model WebApp.Models.Product
<!DOCTYPE html>
<html>
<head>
</head>
<body>
	<h6>Product Table</h6>
	<table>
		<tr><th>Name</th><td>@Model.Name</td></tr>
		<tr><th>Price</th><td>@Model.Price.ToString("c")</td></tr>
	</table>
</body>
</html>

View'ın başına eklenen bu direktif sayesinde artık Visual Studio ilgili model sınıfında olmayan bir üyeye erişmeye çalıştığımızda ilgili yeri kırmızı ile çizecek ve programı çalıştırmaya izin vermeyecektir. Ayrıca Intellisense bir @Model ibaresinden bir üyeye ulaşmaya çalıştığımızda olası seçenekleri göstererek bize yardımcı olacaktır. Eğer model sınıfımız üretilen view sınıfının bağlı olduğu AspNetCoreGeneratedDocument isim alanında veya üretilen sınıfın using ile kullandığı isim alanlarından birinde değilse (ki çoğunlukla değildir) model sınıfını isim alanıyla beraber tam olarak yazmak zorundayız (şimdilik).

_ViewImports.cshtml dosyasının kullanımı

[değiştir]

Az önce view'ın kabul ettiği model nesnesinin tipini isim alanıyla beraber tam olarak belirtmek zorunda kalmıştık. Bu tür durumlardaki kod tekrarından kurtulmak için _ViewImports.cshtml dosyası kullanılabilir. Bu dosya projemizde Views klasörüne konulmalıdır ve örneğimiz için içeriği aşağıdaki gibi olmalıdır:

@using WebApp.Models

Bu şekilde istediğimiz kadar isim alanı ekleyebiliriz. Bu isim alanlarındaki sınıflar view'larda direkt kullanılabilecektir. Eğer yalnızca bir view'da isim alanı belirtmeden sınıfa erişim hakkı istiyorsak bu ifadeyi sadece ilgili view'ın başında da kullanabiliriz.