Writing · 06.09.26 · 11 min

.NET 10: features, performance, and a single-file demo

.NET 10 LTS is ten months old. From C# 14's field keyword and extension blocks to request timeouts and OpenAPI 3.1 in ASP.NET Core — plus performance notes and a demo API that runs from a single .cs file.

.NET 10: features, performance, and a single-file demo

It has been nearly a year since I started running both this site's API and most of my daily work on .NET 10. The release went out as an LTS in November 2025, and as of today the latest stable servicing release is 10.0.11 — with .NET 11 previews already circulating. In this post I want to cover the parts of .NET 10 that actually matter to me: C# 14, request timeouts and OpenAPI in ASP.NET Core, honest notes about performance, and a small demo API that boots from a single .cs file.

C# 14: the two features I use the most

C# 14 shipped with a long list of features, but two of them show up in my everyday code: the field keyword and extension blocks.

With field, I no longer need a separate backing field just to validate a value in a setter:

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

Before, every property that needed setter validation forced me to declare a private field and wire the property to it. field removes that ceremony — fewer lines, fewer places for bugs.

Extension blocks go further than extension methods: you can open a block for a type and add properties, not just methods:

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

Both of these appear in the demo below, and you will see them compile and run. The rest of the C# 14 list — nameof(parameter), params collections, standard collection expressions — is nice, but this post is long enough already.

ASP.NET Core 10: two things stand out

First, OpenAPI. Calling AddOpenApi() now produces an OpenAPI 3.1 document and wiring it into the app is a single line. As you will see in the demo, /openapi/v1.json serves a 3.1.1 document out of the box. The package is Microsoft.AspNetCore.OpenApi, at 10.0.11 as I write this.

Second, request timeouts — preview in .NET 9, stable in .NET 10. You can define different policies per endpoint:

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");

There is a subtlety worth knowing: the timeout is cooperative. The framework cancels the request, but you only notice it if you listen to RequestAborted. I passed the token to Task.Delay, so cancellation threw OperationCanceledException and I returned 504. An endpoint that never checks the token — say, one blocked on synchronous code — will not be rescued by a timeout. Keep that in mind.

What about performance?

The .NET 10 performance story is not one giant leap; it is a stack of steady improvements across the JIT, the GC and the ThreadPool. The headlines I follow:

  • DATAS (GC regions) is now the default for server GC. Instead of long pauses when the heap grows, the collector targets shorter, regional pauses — a visible difference under memory pressure.
  • ThreadPool changes reduce queue latency, which shows up in p95 numbers under bursts of short requests.
  • NativeAOT keeps maturing and is a real option where cold start matters (lambdas, CLI tools).
  • ASP.NET Core and JSON serialization received their usual micro-improvements.

Microsoft publishes measurements per release on the official blog — links at the end. My advice though: trust your own workload over "X% faster" claims. BenchmarkDotnet gives you a micro-benchmark in a couple of lines, and production data tells you the truth anyway.

On the news side: Microsoft.Extensions.AI went stable with .NET 10, and OpenAI's official .NET client is part of that ecosystem. Abstractions like ChatClient are no longer a preview layer — they are supported. And the 10.0.x servicing train keeps shipping monthly updates, so staying current is easy and worth it for security fixes.

Demo: an API that runs from one file

File-based apps are, to me, one of the best things in .NET 10: no project file, just a .cs file you run with dotnet run. Directives at the top of the file — #:sdk and #:package — tell the SDK what it needs. Here is the whole demo:

#: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;
    }
}

Running it is one command: dotnet run demo.cs. Nothing else — the NuGet package is resolved from the in-file directive. The terminal below shows the actual output:

.NET 10 demo terminal output

Every line on that screen came from a real run; nothing was staged. A quick tour:

  • The root endpoint uses the SozcukSayisi property defined via a C# 14 extension block; the JSON shows sozcukSayisi: 10. Note it counted the words of the Turkish message correctly.
  • I redeemed the coupon five times; KalanKullanim went from 4 to 0, and the sixth request could not go below zero — thanks to the field keyword and the Math.Max(0, value) guard in the setter — so it returned 409.
  • /yavas-iste simulates an 8-second job. The 2-second policy kicked in and the request was cut with 504; the timing on screen — 2.00s — shows the timeout actually firing.

Wrapping up

.NET 10 feels like the "everything in its place" release: language features that make everyday code simpler, framework features that were overdue (request timeouts, first-class OpenAPI), and a steady performance line underneath. It is supported until November 2028 as an LTS, which makes it an easy choice for new projects.

If you want to try it on your own workload: copy the file above, run dotnet run demo.cs. That's it.

Useful links:

.NET 10: features, performance, and a single-file demo — Aziz Osmanoğlu