Mert Özen Her Satırda Daha İleri
Backend ASP.NET Core MVC ve Razor Pages — #13

.NET 8'den .NET 11'e MVC ve Razor Pages'te Gelenler

Mert Özen 8 Kas 2026 10 dk 2 görüntülenme
.NET 8'den .NET 11'e MVC ve Razor Pages'te Gelenler

.NET 8 ve 9'un desteği 10 Kasım 2026'da bittiği için MVC ve Razor Pages uygulamalarının hedefi .NET 10. Aynı projeyi net8.0 ve net10.0 ile çalıştırıp cookie auth'un API endpoint'lerinde 302 yerine 401 dönmesini, Minimal API form binding farkını, ASP0026 uyarısını ve RazorComponentResult'u gösteriyor, .NET 11'in MVC maddelerini ve bir yükseltme kontrol listesini veriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.402) ile derlenip denenmiştir; sürüm karşılaştırmaları aynı projenin net8.0 (runtime 8.0.25) ve net10.0 (runtime 10.0.13) hedefleriyle çalıştırılmasından geliyor. .NET 11 maddeleri bu yazıyı yazarken RC (Release Candidate) aşamasında olan sürümün dokümanından alındı ve derlenmedi.

Önce Tahmin Et

Bookstore'un MVC (Model-View-Controller) uygulamasında cookie authentication var ve view'ların yanında küçük bir JSON (JavaScript Object Notation) API'si (Application Programming Interface) de duruyor: [ApiController] ve [Authorize] işaretli bir OrdersApiController. Oturum açmamış bir kullanıcının JavaScript'i GET /api/orders çağırıyor. Uygulama .NET 8'deyken bu istek ne döner? Aynı kodu hiçbir satırını değiştirmeden net10.0'a taşıdığında cevap değişir mi? İkinci soru: class'ın üstünde [AllowAnonymous], action'ın üstünde [Authorize] olan bir rapor sayfasına oturumsuz istek gelirse ne olur?

Neden Şimdi?

Microsoft'un destek politikası sayfasına göre .NET 8 (LTS (Long-Term Support)) ve .NET 9 (STS (Standard-Term Support)) desteği aynı gün bitiyor: 10 Kasım 2026. STS sürümleri artık 24 ay destek alıyor, bu yüzden .NET 9 da .NET 8 ile aynı tarihe uzandı. .NET 10 bir LTS sürüm ve 14 Kasım 2028'e kadar destekleniyor. .NET 11 ise tek numaralı bir sürüm olarak STS; bu yazıyı yazarken RC aşamasında ve GA (General Availability) 10 Kasım 2026'da bekleniyor.

Yani .NET 8 ya da 9 üzerindeki her MVC ve Razor Pages uygulaması için hedef bu hafta itibarıyla .NET 10. Bu yazı, serinin konularını sürüm sürüm bir araya getiriyor ve yükseltmede gözden kaçan davranış değişikliklerini gerçek çıktılarla gösteriyor. Web API serisinin .NET 8, .NET 9 ve .NET 10 yenilik yazıları API tarafını anlatıyor; burada view, form ve Razor tarafındayız.

Karşılaştırma için tek bir proje yazdım ve iki framework'ü birden hedeflettim:

<TargetFrameworks>net8.0;net10.0</TargetFrameworks>

Aynı kod dotnet run -f net8.0 ile 17830 portunda, dotnet run -f net10.0 ile 17831 portunda çalışıyor.

.NET 8: Razor Component'ler MVC'nin İçinde

.NET 8'in büyük hikâyesi Blazor Web App'ti: Blazor component'leri artık interaktif olmadan, sunucuda statik olarak render edilebiliyor (static SSR (Server-Side Rendering)). Microsoft'un UI (User Interface) seçimi dokümanının yeni projeler için Blazor'ı önermesi bu sürümle başladı; serinin ilk yazısında bu kararı tartışmıştık. MVC ve Razor Pages tarafı için pratik sonuç şu: bir Razor component'i (.razor dosyası) artık bir MVC action'ından doğrudan dönebiliyorsun:

// MVC action'ından dönen bir Razor component'i
[HttpGet("/mvc/badge")]
public IResult Badge() =>
    new RazorComponentResult<BookBadge>(new { Title = "Clean Lines", Year = 2023 });
<span class="badge">@Title (@Year)</span>

@code {
    [Parameter] public string Title { get; set; } = "";
    [Parameter] public int Year { get; set; }
}

builder.Services.AddRazorComponents() kaydından sonra iki sürümde de aynı yanıt geliyor:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
<span class="badge">Clean Lines (2023)</span>

Bu, view'ların içindeki <component> tag helper'ının (serinin partial view ve view component yazısında görmüştük) bir adım ötesi: component'i bir view'a gömmek yerine htmx yazısındaki partial yanıtlar gibi tek başına döndürebiliyorsun. MVC uygulamasını yavaş yavaş Blazor component'lerine taşımak isteyen ekipler için bir köprü.

.NET 8'in MVC ve Razor Pages'e dokunan diğer maddeleri: MapIdentityApi ile SPA (Single-Page Application) ve mobil istemciler için Identity endpoint'leri (cookie tabanlı Identity UI'nin yerini almıyor), Minimal API'ler ve Blazor için UseAntiforgery middleware'i (MVC ve Razor Pages kendi filter'larıyla devam ediyor), routing'den hemen sonra yanıt dönen ShortCircuit() endpoint convention'ı ve Web API serisinde gördüğümüz keyed DI (Dependency Injection) ile IExceptionHandler.

.NET 9: MapStaticAssets ve Yeni Analyzer'lar

.NET 9'un MVC ve Razor Pages için en görünür değişikliği MapStaticAssets ve template'teki .WithStaticAssets() çağrıları. Static asset yazısında detaylı ölçtük: build sırasında gzip ve Brotli, fingerprint'li URL'ler (Uniform Resource Locator), immutable cache. Aynı yazıda, template'in MapControllerRoute(...).WithStaticAssets() satırının MVC view'larında hiçbir şey yapmadığını ve MapControllers().WithStaticAssets() ile düzeldiğini de gördük. O bug .NET 10.0.13 runtime'ında da duruyor; yükseltirken ilk bakılacak yerlerden biri.

Template'lerin getirdiği client library'ler de değişti. SDK 8.0.419 ile oluşturduğum bir dotnet new mvc projesiyle SDK 10.0.402'nin ürettiği proje arasında wwwroot/lib farkı:

Library.NET 8 template.NET 10 template
Bootstrap5.1.05.3.3
jQuery3.6.03.7.1
jQuery Validation1.19.51.21.0
jQuery Validation Unobtrusive4.0.04.0.0

Eski bir projeyi yalnızca TFM (Target Framework Moniker) değiştirerek yükseltirsen bu dosyalar kendiliğinden güncellenmiyor; wwwroot/lib altındaki kopya ne zaman oluşturulduysa öyle kalıyor. Static asset yazısında LibMan'i bu yüzden önermiştik.

.NET 9 ayrıca yanıltıcı yetki attribute'larını yakalayan bir analyzer getirdi. İkinci sorunun cevabı:

[AllowAnonymous]
public class ReportsController : Controller
{
    [Authorize]
    [HttpGet("/reports/sales")]
    public IActionResult Sales() => Content("sales report");
}

Action'a yakın duran [Authorize] "kazanır" diye düşünmek doğal. Gerçek yanıt, iki sürümde de:

HTTP/1.1 200 OK
sales report

Oturumsuz kullanıcı satış raporunu görüyor. Action'a daha yakın durması [Authorize]'ı kazandırmıyor: endpoint'in metadata'sında bir [AllowAnonymous] bulunduğu sürece authorization middleware'i yetki kontrolünü atlıyor. Davranış değişmedi, ama net10.0 build'i artık uyarıyor; net8.0 hedefi aynı kodu sessizce derliyor:

Controllers/BooksController.cs(36,6): warning ASP0026: This [Authorize] attribute is overridden by an [AllowAnonymous] attribute from farther away on 'ReportsController'. See https://aka.ms/aspnetcore-warnings/ASP0026 for more details. (https://aka.ms/aspnet/analyzers) [Bookstore.Web.csproj::TargetFramework=net10.0]

Yükseltme sonrası yeni gelen uyarıları "build kalabalığı" diye bastırma; bu uyarı açık bir güvenlik deliğini gösteriyor. Razor Pages'te aynı tuzağın klasör convention'larındaki karşılığını Razor Pages yazısında görmüştük.

.NET 10: Sessiz Davranış Değişiklikleri

Yükseltmenin en riskli kısmı yeni API'ler değil, aynı kodun farklı davranması. İlk sorunun cevabı:

##### .NET 8.0.25
HTTP/1.1 302 Found
Location: http://localhost:17830/login?ReturnUrl=%2Fapi%2Forders

##### .NET 10.0.13
HTTP/1.1 401 Unauthorized
Location: http://localhost:17831/login?ReturnUrl=%2Fapi%2Forders

.NET 10'da cookie authentication, bilinen API endpoint'lerine gelen oturumsuz isteklere login sayfasına redirect yerine 401 (yetkisizse 403) dönüyor. "API endpoint'i" tanımı IApiEndpointMetadata ile yapılıyor; [ApiController] işaretli controller'lar, JSON okuyan ya da yazan Minimal API endpoint'leri, TypedResults dönenler ve SignalR endpoint'leri bunu otomatik alıyor. Yanıtta hâlâ bir Location header'ı olması ilginç bir ayrıntı, ama status 401 olduğu için tarayıcı redirect'i takip etmiyor.

Bu değişiklik büyük ölçüde iyi: fetch ile API çağıran bir JavaScript, 200 ile gelen bir login HTML'ini (HyperText Markup Language) JSON diye parse etmeye çalışmaktan kurtuluyor. Ama eski kod "302 gelirse login'e git" diye yazıldıysa artık kırılıyor. Eski davranışa ihtiyacın varsa CookieAuthenticationEvents.OnRedirectToLogin ve OnRedirectToAccessDenied ile redirect'i kendin yapıyorsun; Microsoft'un dokümanı bunun kodunu veriyor. View dönen normal controller'lar ve Razor Pages bu değişiklikten etkilenmiyor; Razor Pages yazısındaki /Manage klasörü hâlâ 302 ile login'e gidiyor.

İkinci sessiz değişiklik form binding'de. .NET 10 notlarında "[FromForm] ile complex object'te boş string artık null'a çevriliyor" maddesi var; ama bu madde yalnız Minimal API'ler için. Aynı record'u iki tarafta da kullandım:

public record BookDraft(string Title, DateOnly? PublishedOn, decimal? Price);

Title=Clean Lines&PublishedOn=&Price= gövdesiyle gerçek sonuçlar:

##### .NET 8.0.25
-- minimal
HTTP/1.1 400 Bad Request
-- mvc
MVC: valid=True published=null price=null

##### .NET 10.0.13
-- minimal
HTTP/1.1 200 OK
Minimal: published=null price=null
-- mvc
MVC: valid=True published=null price=null

MVC model binding boş string'i nullable value type'a zaten her zaman null olarak bağlıyordu; değişen yalnız Minimal API tarafı. Serinin form yazısındaki MVC ve Razor Pages formları için yapman gereken bir şey yok. Ama aynı uygulamada Minimal API ile form işleyen bir endpoint varsa (ör. htmx'in çağırdığı küçük bir endpoint), .NET 8'de 400 dönen istekler artık başarılı oluyor ve "zorunlu" sandığın alan sessizce null geliyor.

.NET 10'un diğer maddeleri: [ProducesResponseType], [Produces] ve [ProducesDefaultResponseType] attribute'larına OpenAPI dokümanına giden bir Description parametresi eklendi. IExceptionHandler'ın TryHandleAsync'ten true döndüğü exception'lar artık exception handler middleware'i tarafından error seviyesinde loglanmıyor; global hata yönetimini Web API serisindeki gibi kurduysan ve alarmlarını bu log'a bağladıysan, yükseltmeden sonra alarmların susması bir başarı değil. ExceptionHandlerOptions.SuppressDiagnosticsCallback ile eski davranışı geri alabilirsin. Minimal API'ler için yerleşik validation (AddValidation) da bu sürümde geldi; MVC ve Razor Pages'in ModelState tabanlı validation'ı (serinin validation yazısı) aynen devam ediyor.

.NET 11: Neler Geliyor?

.NET 11 GA olmadığı ve elimde .NET 11 SDK'sı olmadığı için bu bölümde kod yok; maddeler Microsoft'un "What's new in ASP.NET Core in .NET 11" sayfasından. MVC ve Razor Pages'i ilgilendirenler:

  • [ShortCircuit] attribute'u: ShortCircuit() convention'ının attribute hali; MVC controller ve action'larına da uygulanabiliyor, [ShortCircuit(404)] ile status code verilebiliyor.
  • C# union type'ları JSON request ve response body'lerinde hem Minimal API'lerde hem MVC'de çalışıyor; route, query string, header ve form değerlerinde ise desteklenmiyor.
  • IAuthorizationRequirementData attribute'ları artık MVC controller ve action'larında da uygulanıyor; .NET 11 öncesinde yalnız Minimal API ve route edilen endpoint'lerde geçerliydi.
  • Yetki metadata'sı (IAuthorizeData, AuthorizationPolicy, IAuthorizationRequirementData) MVC filter'larında, SignalR'da ve Blazor'ın AuthorizeView'ında aynı şekilde birleştiriliyor; ortak kod yeni AuthorizationPolicy.CombineAsync overload'u. EnableEndpointRouting = false ile çalışan eski MVC yolu değişmiyor.
  • ApiExplorer, aynı status code için birden fazla [ProducesResponseType] tanımını artık tek bir tanıma indirmeden koruyor.
  • ASP.NET Core, HTTP (Hypertext Transfer Protocol) server activity'sine OpenTelemetry semantic convention attribute'larını kendisi ekliyor; ayrı instrumentation paketi gerekmiyor.
  • Response compression middleware'i, yanıt sıkıştırılmasa bile her yanıta Vary: Accept-Encoding ekliyor.

Microsoft.Extensions.Validation'ın localization desteği de .NET 11'de geliyor, ancak doküman bunun MVC ve Razor Pages'e uygulanmadığını açıkça yazıyor; localization yazısındaki AddDataAnnotationsLocalization yolu MVC için geçerli kalıyor. .NET 11 GA olduktan sonra bu maddeleri kararlı sürümle derlenip denenmiş örneklerle Web API serisinin .NET 11 yazısında ele alacağız.

Yükseltme Kontrol Listesi

Bir MVC ya da Razor Pages uygulamasını .NET 8 veya 9'dan .NET 10'a taşırken sırayla:

  • global.json ve TargetFramework'ü güncelle, Microsoft.* paketlerini 10.0.x'e çek; Entity Framework Core (EF Core) gibi bağımlılıkları aynı major sürüme hizala.
  • Build'i temiz yap ve yeni analyzer uyarılarını (özellikle ASP0026) tek tek oku; hiçbirini toplu bastırma.
  • Program.cs'i güncel template'le karşılaştır: MapStaticAssets, WithStaticAssets ve MVC için MapControllers().WithStaticAssets() düzeltmesi.
  • Publish edilmiş çıktıda bir MVC view'ının ve bir Razor Page'in <head>'ine bak: fingerprint'li adresler ve import map geliyor mu?
  • Cookie authentication ile korunan her [ApiController] endpoint'ini oturumsuz çağır; 302 bekleyen JavaScript var mı?
  • IExceptionHandler kullanıyorsan hata log'larının ve alarmların hâlâ geldiğini doğrula.
  • wwwroot/lib altındaki Bootstrap ve jQuery sürümlerini kontrol et; güncelleyeceksen LibMan ya da npm'e taşı.
  • Container image'ını .NET 10 runtime'ına güncelle; localization kullanıyorsan ICU'nun (International Components for Unicode) image'da olduğunu doğrula.

Üretimde Dikkat

Sürüm yükseltmesini davranış testleriyle koru. Bu yazıdaki karşılaştırmalar curl ile yapıldı, ama aynı kontrolleri integration test olarak yazmak (ilerideki Test ve Kalite serilerinde WebApplicationFactory ile) bir sonraki yükseltmede aynı soruları otomatik cevaplar: oturumsuz API isteği hangi status'u alıyor, boş form alanı nasıl bağlanıyor, <head>'de fingerprint var mı.

Çoklu hedefleme bir geçiş aracı olarak işe yarıyor. Bu yazıdaki gibi net8.0;net10.0 hedefleyen bir proje, aynı testleri iki runtime'da koşturup farkları görmeni sağlıyor. Üretime ise tek bir hedefle çık; iki runtime'ı birden desteklemek kalıcı bir maliyet.

STS sürümlerini takvime göre seç. .NET 11'e geçmek .NET 10'un destek süresini uzatmıyor, tam tersine 24 aylık bir STS takvimine giriyorsun. Yeni bir özelliğe (ör. MVC için [ShortCircuit] ya da union type'lar) gerçekten ihtiyacın yoksa LTS'te kalmak çoğu kurumsal uygulama için daha az yorucu.

Sık Düşülen Tuzaklar

Yükseltmeyi "derleniyorsa bitti" diye kapatmak. Bu yazıdaki iki önemli fark (401 ve Minimal API form binding) derleme hatası vermiyor, yalnızca davranışı değiştiriyor.

Analyzer uyarılarını NoWarn ile toplu kapatmak. ASP0026 bir güvenlik açığını işaret ediyor.

Template'in yazdığı WithStaticAssets() satırının MVC'de çalıştığını varsaymak. Production çıktısında gözünle doğrula.

Sürüm notlarındaki bir maddeyi kendi framework'üne uyarlamadan okumak. "FromForm'da boş string null olur" maddesi Minimal API içindi; MVC'de hiçbir şey değişmedi. Doğrulamanın en kısa yolu iki hedefle çalıştırmak.

.NET 9'da kalmanın .NET 8'den daha uzun destek verdiğini sanmak. İkisi de 10 Kasım 2026'da bitiyor.

Küçük Bir Deneme

★☆☆ Kendi projende <TargetFrameworks>net8.0;net10.0</TargetFrameworks> ile iki hedef aç ve dotnet build çıktısını karşılaştır. Yalnız net10.0 hedefinde çıkan uyarılar hangileri?

★★☆ Bookstore'a cookie authentication ile korunan bir [ApiController] ekle ve oturumsuz isteğe .NET 10'da 401 döndüğünü gör. Ardından OnRedirectToLogin event'iyle yalnız /api altındaki isteklerde 401, geri kalanlarda redirect yapan bir kural yaz ve iki yolu da curl ile doğrula.

★★★ Bu yazıdaki karşılaştırmaları bir integration test projesine çevir: aynı test class'ını net8.0 ve net10.0 hedefleriyle koştur ve sürüme göre beklenen sonucu (302'ye karşı 401, 400'e karşı 200) koşullu olarak doğrula.

Bu yazıyla ASP.NET Core MVC ve Razor Pages serisini kapatıyoruz. MVC pattern'inden başlayıp Razor syntax'ına, partial view ve view component'lere, tag helper'lara, formlara, validation'a, Razor Pages'e, static asset'lere, localization'a, htmx ile etkileşime, MVC 5'ten geçişe ve sürüm farklarına kadar server-side render edilen bir uygulamanın bütün katmanlarını Bookstore üzerinde kurduk. Buradan sonra yolun iki yöne ayrılıyor: aynı sunucu tarafı yaklaşımı component modeliyle sürdürmek istersen Frontend kategorisindeki Blazor serisi, bu seride bilerek yüzeyde bıraktığımız cookie, Identity ve policy konularını derinlemesine öğrenmek istersen ilerideki Authentication ve Authorization serisi doğal devam noktaları.