Yazılar · 06.09.26 · 11 dk

.NET 10: özellikler, performans ve tek dosyalık bir demo

.NET 10 LTS çıkalı on ay oldu. C# 14'ün field ve extension block'larından, ASP.NET Core'un zaman aşımı ve OpenAPI 3.1 desteğine; performans notlarından tek bir .cs dosyasıyla ayağa kalkan bir demo'ya kadar bu yazıda işime yarayanları topladım.

.NET 10: özellikler, performans ve tek dosyalık bir demo

Bir yıla yaklaşan bir süredir hem bu sitenin API'sini hem de günlük işlerimi .NET 10 ile yürütüyorum. Kasım 2025'te LTS olarak yayınlanan sürüm, bugün itibarıyla 10.0.11 kararlı sürümünde; .NET 11'in preview'ları da bir yandan dönüyor. Bu yazıda .NET 10'da gerçekten işime yarayanları, performans konusunda dürüst notlarımı ve tek bir .cs dosyasından ayağa kalkan küçük bir demo'yu bulacaksın.

C# 14: en çok sevdiğim ikisi

C# 14'ün duyurulan özellik listesi uzun ama benim günlük kodumda karşılığını gördüğüm iki tanesi var: field anahtar sözcüğü ve extension block'lar.

field sayesinde artık şöyle bir şey için ayrıca backing field bildirmek gerekmiyor:

public class Kupon
{
    public int KalanKullanim { get; set => field = Math.Max(0, value); }
}

Setter'da doğrulama yapmak istediğimde her seferinde _kalanKullanim diye bir alan açıp property'yi ona bağlamak zorundaydım. Şimdi field o işi görüyor; daha az satır, daha az hata ihtimali.

Extension block'lar ise "sadece metot eklenebilir" sınırını kaldırıyor. Bir tür için blok açıp içine özellik bile koyabiliyorsun:

public static class MetinUzantilari
{
    extension (string metin)
    {
        public int SozcukSayisi =>
            metin.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

Demo'da bu ikisini de kullanacağım; ikisi de derlenip çalışırken göreceksin. Listede nameof(parameter), params collection'lar ve standart collection expression'lar da vardı; onları şimdilik geçiyorum, bu yazı zaten uzun olacak.

ASP.NET Core 10: iki özellik öne çıktı

İlk olarak OpenAPI. AddOpenApi() çağrısı artık OpenAPI 3.1 dokümanı üretiyor ve uygulamaya eklemek tek satır. Demo'da göreceğin gibi /openapi/v1.json adresinde 3.1.1 sürümlü doküman hazır şekilde geliyor. Paket ismi Microsoft.AspNetCore.OpenApi; ben yazıyı hazırlarken güncel kararlı sürümü 10.0.11.

İkincisi request timeouts, yani istek zaman aşımı. Bu özellik .NET 9'da preview olarak gelmişti, .NET 10'da kararlı hale geldi. Farklı uç noktalara farklı politikalar tanımlayabiliyorsun:

builder.Services.AddRequestTimeouts(options =>
{
    options.AddPolicy("yavas", TimeSpan.FromSeconds(2));
});

app.MapGet("/yavas-iste", async (HttpContext ctx) =>
{
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(8), ctx.RequestAborted);
        return Results.Text("8 saniye sonra döndüm");
    }
    catch (OperationCanceledException)
    {
        return Results.StatusCode(StatusCodes.Status504GatewayTimeout);
    }
}).WithRequestTimeout("yavas");

Burada incelikli bir nokta var: zaman aşımı kooperatif çalışıyor. Framework isteği iptal ediyor ama iptali ancak RequestAborted token'ını dinlersen görüyorsun. Task.Delay'e token'ı geçtim, iptal olunca OperationCanceledException fırladı ve 504 döndürdüm. Token'ı hiç dinlemeyen, senkron takılan bir uç noktada zaman aşımı da işe yaramaz; bunu aklında tut.

Performans ne durumda?

.NET 10'un performans hikâyesi "tek bir büyük sıçrama" değil; JIT, GC ve ThreadPool tarafında istikrarlı iyileştirmelerin toplamı. Benim takip ettiğim başlıklar şunlar:

  • Server GC'de DATAS (GC regions) artık varsayılan. Heap büyüyünce yaşanan uzun duraklamalar yerine bölgesel, daha kısa duraklamalar hedefleniyor; bellek baskısı altındaki servislerde gözle görülür fark yaratıyor.
  • ThreadPool tarafında kuyruk gecikmesini azaltan değişiklikler var; yoğun kısa istek trafiğinde p95 değerlerine olumlu yansıyor.
  • NativeAOT olgunlaşmaya devam ediyor. Soğuk başlangıcın kritik olduğu yerlerde (lambda, CLI araçları) gerçek bir seçenek.
  • ASP.NET Core ve JSON serileştirme her sürümde olduğu gibi mikro iyileştirmeler aldı.

Resmî blogda sürüm sürüm ölçümler yayınlanıyor; linkleri yazının sonuna bırakıyorum. Ama sana tavsiyem şu: hangi framework'ü kullanırsan kullan, "şu kadar hızlı" iddialarına değil kendi iş yükündeki ölçümüne güven. BenchmarkDotnet ile iki satırda kendi mikro kıyasını alabilirsin; prod verisi de zaten sana gerçeği söyler.

Bu arada işin "haber" tarafı: .NET 10 ile birlikte Microsoft.Extensions.AI kararlı hale geldi ve OpenAI'nin resmî .NET istemcisi bu ekosistemin parçası oldu. Yani LLM çağrılarını soyutlayan ChatClient gibi arayüzler artık bir preview değil, desteklenen bir katman. Ayrıca 10.0.x servis sürümleri aylık yayınlanmaya devam ediyor; güvenlik düzeltmeleri için güncel kalmakta fayda var.

Demo: tek dosyadan ayakta kalan bir API

Gelelim asıl meseleye. .NET 10'un gözümde en büyük olaylarından biri dosya tabanlı uygulamalar: proje dosyası olmadan, doğrudan tek bir .cs dosyasını dotnet run ile çalıştırabiliyorsun. Dosyanın başına #:sdk ve #:package direktiflerini koyuyorsun, gerisini SDK hallediyor. Şu dosya tam olarak benim demo'm:

#:sdk Microsoft.NET.Sdk.Web
#:package Microsoft.AspNetCore.OpenApi@10.0.11

using Microsoft.AspNetCore.Http.Timeouts;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();
builder.Services.AddRequestTimeouts(options =>
{
    options.AddPolicy("yavas", TimeSpan.FromSeconds(2));
});

var app = builder.Build();
app.UseRouting();
app.UseRequestTimeouts();
app.MapOpenApi();

var kuponlar = new List<Kupon>
{
    new("KUA10", 10, 5),
    new("HUK20", 20, 2),
};

app.MapGet("/", () =>
{
    const string mesaj = "Hoş geldin! Bu API tek bir .cs dosyasından ayağa kalktı.";
    return Results.Ok(new AnaSayfaDto(mesaj, mesaj.SozcukSayisi, kuponlar.Count));
});

app.MapGet("/kuponlar/{kod}", (string kod) =>
{
    var kupon = kuponlar.FirstOrDefault(k => k.Kod == kod.ToUpperInvariant());
    return kupon is null ? Results.NotFound() : Results.Ok(kupon);
});

app.MapPost("/kuponlar/{kod}/kullan", (string kod) =>
{
    var kupon = kuponlar.FirstOrDefault(k => k.Kod == kod.ToUpperInvariant());
    if (kupon is null)
        return Results.NotFound();
    if (!kupon.Kullan())
        return Results.Conflict(new HataDto("Kuponun kullanım hakkı kalmadı."));
    return Results.Ok(new KullanimDto(kupon.Kod, kupon.KalanKullanim));
});

app.MapGet("/yavas-iste", async (HttpContext ctx) =>
{
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(8), ctx.RequestAborted);
        return Results.Text("8 saniye sonra döndüm");
    }
    catch (OperationCanceledException)
    {
        return Results.StatusCode(StatusCodes.Status504GatewayTimeout);
    }
}).WithRequestTimeout("yavas");

app.Run();

public record AnaSayfaDto(string Mesaj, int SozcukSayisi, int KuponSayisi);
public record KullanimDto(string Kod, int KalanKullanim);
public record HataDto(string Hata);

public static class MetinUzantilari
{
    extension (string metin)
    {
        public int SozcukSayisi =>
            metin.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

public class Kupon
{
    public string Kod { get; }
    public int IndirimYuzde { get; }

    public int KalanKullanim { get; set => field = Math.Max(0, value); }

    public Kupon(string kod, int indirimYuzde, int kalanKullanim)
    {
        Kod = kod;
        IndirimYuzde = indirimYuzde;
        KalanKullanim = kalanKullanim;
    }

    public bool Kullan()
    {
        if (KalanKullanim <= 0)
            return false;
        KalanKullanim--;
        return true;
    }
}

Çalıştırması tek komut: dotnet run demo.cs. Başka bir şey yok; NuGet paketi de dosya içindeki direktiften çözülüyor. Terminalde gerçek çıktısı şöyle:

.NET 10 demo terminal çıktısı

Ekrandaki her satırı ben çalıştırdım, uydurma yok. Kısa bir tur:

  • Kök uç nokta, C# 14 extension block ile tanımlanan SozcukSayisi özelliğini kullanıyor; JSON'da sozcukSayisi: 10 olarak görünüyor. Türkçe cümledeki kelime sayısını doğru hesapladığına dikkat et.
  • Kuponu 5 kez kullandım; KalanKullanim 4'ten 0'a indi, altıncı istek field keyword'ü sayesinde 0'ın altına inemedi ve 409 döndü. Negatife düşmeyi engelleyen şey property setter'ındaki Math.Max(0, value) satırı.
  • /yavas-iste isteğini 8 saniyelik bir iş gibi düşün; 2 saniyelik politika devreye girdi ve istek 504 ile kesildi. Süreyi de görüyorsun: 2.00 saniye, yani zaman aşımı gerçekten işliyor.

Kapanış

.NET 10 bana göre "her şey yerli yerinde" sürümü: C# tarafında günlük hayatı kolaylaştıran dil özellikleri, framework tarafında uzun süredir beklenen zaman aşımı ve OpenAPI desteği, altta da istikrarlı bir performans çizgisi. LTS olduğu için Kasım 2028'e kadar desteklenecek; bu da onu yeni projeler için kolay bir tercih yapıyor.

Kendi iş yükünle denemek istersen yukarıdaki dosyayı kopyala, dotnet run demo.cs de. Bu kadar.

Faydalı linkler:

.NET 10: özellikler, performans ve tek dosyalık bir demo — Aziz Osmanoğlu