.NET 11 Yenilikleri: ASP.NET Core'da Async Validation, OpenAPI 3.2 ve C# 15 Union Types
.NET 11 STS ile minimal API'de async validation ve Türkçe validation mesajları, [ShortCircuit], C# 15 union types, OpenAPI 3.2 ile QUERY ve itemSchema, kaldırılan API'ler ve 2028-11-14'e kadar destek.
Sürüm notu: Bu yazı .NET 11 ile gelen yenilikleri anlatır; kodlar .NET 11 içindir.
Önce Tahmin Et
Ürün eklerken SKU'nun (Stock Keeping Unit, stok kodu) benzersiz olmasını istiyoruz. Kuralı bir attribute'a koyduk: [UniqueSku] veritabanına async bir sorgu atıyor. Aynı request DTO'su (Data Transfer Object) hem bir minimal API endpoint'inde hem de bir [ApiController] controller'ında kullanılıyor:
builder.Services.AddValidation();
builder.Services.AddProblemDetails();
// ...
app.MapPost("/products", (CreateProductRequest request) =>
TypedResults.Created($"/products/42", new ProductDto(42, request.Name, request.Sku, request.Price)));
// Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<ProductDto> Create(CreateProductRequest request) =>
CreatedAtAction(nameof(Create), new ProductDto(42, request.Name, request.Sku, request.Price));
}
public class CreateProductRequest
{
[Required, StringLength(100)]
public string Name { get; set; } = "";
[Required, UniqueSku]
public string Sku { get; set; } = "";
[Range(0.01, 100000)]
public decimal Price { get; set; }
}
Veritabanında MS-002 SKU'lu bir ürün zaten var. {"name":"Wireless Mouse","sku":"MS-002","price":29.90} body'sini iki endpoint'e de POST edersen hangi status code'ları alırsın? İkisi de 400 mü?
10 Kasım 2026'da çıkan .NET 11 bir STS (Standard-Term Support, standart süreli destek) sürümü. Bu yazıda yalnızca Web API geliştiricisini ilgilendiren kısmına bakacağız: async validation ve Türkçe validation mesajları, binding hatasını gören endpoint filter'lar, [ShortCircuit] attribute'u, C# 15 union type'ları, OpenAPI 3.2 ve geçişte seni bekleyen breaking change'ler. Blazor, SignalR ve MAUI tarafı bu yazının konusu değil.
Async Validation: Veritabanına Soran Kural
Model validation yazısında kendi attribute'umuzu ValidationAttribute'tan türetmiştik. Oradaki IsValid senkrondu; veritabanına gitmek isteseydin ya thread'i bloklayacak ya da kuralı service katmanına taşıyacaktın. .NET 11'de Data Annotations'a AsyncValidationAttribute geldi:
public sealed class UniqueSkuAttribute : AsyncValidationAttribute
{
// Kural yalnızca async çalışır; senkron yol bilerek hata verir
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) =>
throw new InvalidOperationException("UniqueSku only validates asynchronously.");
protected override async Task<ValidationResult?> IsValidAsync(object? value, ValidationContext validationContext, CancellationToken cancellationToken)
{
IProductRepository products = validationContext.GetRequiredService<IProductRepository>();
if (value is string sku && await products.SkuExistsAsync(sku, cancellationToken))
{
return new ValidationResult($"SKU '{sku}' is already in use.", [validationContext.MemberName!]);
}
return ValidationResult.Success;
}
}
public interface IProductRepository
{
Task<bool> SkuExistsAsync(string sku, CancellationToken cancellationToken);
}
public class InMemoryProductRepository : IProductRepository
{
private readonly HashSet<string> _skus = ["KB-001", "MS-002"];
public async Task<bool> SkuExistsAsync(string sku, CancellationToken cancellationToken)
{
// Gerçek bir veritabanı sorgusunu taklit ediyoruz
await Task.Delay(50, cancellationToken);
return _skus.Contains(sku);
}
}
Bu attribute'u net10.0 hedefli bir projede derlemeye çalıştığımda CS0234 aldım; sınıf .NET 10'da yok. Senkron IsValid de abstract, yani override etmek zorundasın. Kural yalnızca async çalışacaksa Learn oradan exception fırlatmanı öneriyor; böylece senkron bir validator kuralı sessizce atlayamıyor. Minimal API'de AddValidation() async yolu kendisi çağırıyor:
$ curl -i -X POST localhost:5111/products -H "Content-Type: application/json" -d '{"name":"Wireless Mouse","sku":"MS-002","price":29.90}'
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Sku":["SKU 'MS-002' is already in use."]},"traceId":"00-ae3645e3bc2a62c62115473e9dfa81b4-8ba4b6aba01fca4d-02"}
Tahminin ilk yarısı tuttu. Controller ise şunu döndü (çıktıyı kısalttım):
$ curl -i -X POST localhost:5111/api/products -H "Content-Type: application/json" -d '{"name":"Wireless Mouse","sku":"MS-002","price":29.90}'
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.6.1","title":"System.InvalidOperationException","status":500,"detail":"UniqueSku only validates asynchronously.", ...}
MVC'nin model validation'ı .NET 11'de de senkron ve attribute'un senkron IsValid'ini çağırıyor; bizim exception'ımız 500'e dönüştü. Async validation şimdilik Microsoft.Extensions.Validation kullanan tarafta, yani minimal API'de geçerli. Birden fazla property'ye bakan kurallar için de IAsyncValidatableObject var; ValidateAsync sonuçları IAsyncEnumerable<ValidationResult> olarak üretiyor. Framework mümkün olduğunda validator'ları paralel çalıştırıyor: aynı member'daki async attribute'lar birlikte başlıyor, collection elemanları paralel doğrulanıyor.
Validation Mesajları Türkçe Olabiliyor
.NET 10'da AddValidation()'ın mesajları sabitti. .NET 11'de AddLocalization()'ı da çağırırsan mesajlar ve display name'ler IStringLocalizer üzerinden çözülüyor; ayrı bir paket gerekmiyor. Mesajlar resx dosyasından gelebilir, ama kendi IStringLocalizerFactory'ni register edersen veritabanından ya da JSON'dan da okuyabilirsin. Denemek için bir dictionary yetti:
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Localization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IStringLocalizerFactory, DictionaryStringLocalizerFactory>();
builder.Services.AddLocalization();
builder.Services.AddValidation();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseRequestLocalization("en-US", "tr-TR");
app.MapPost("/products", (CreateProductRequest request) => TypedResults.Ok(request));
app.Run();
public class CreateProductRequest
{
[Required]
public string Name { get; set; } = "";
[Range(0.01, 100000)]
public decimal Price { get; set; }
}
public class DictionaryStringLocalizerFactory : IStringLocalizerFactory
{
public IStringLocalizer Create(Type resourceSource) => new DictionaryStringLocalizer();
public IStringLocalizer Create(string baseName, string location) => new DictionaryStringLocalizer();
}
public class DictionaryStringLocalizer : IStringLocalizer
{
// Türkçe mesajlar; {0} alan adı, {1} ve {2} Range sınırları
private static readonly Dictionary<string, string> Turkish = new()
{
["RequiredAttribute_Error"] = "{0} alanı zorunludur.",
["RangeAttribute_Error"] = "{0} alanı {1} ile {2} arasında olmalıdır.",
};
public LocalizedString this[string name]
{
get
{
bool found = Thread.CurrentThread.CurrentUICulture.Name == "tr-TR" && Turkish.ContainsKey(name);
return new LocalizedString(name, found ? Turkish[name] : name, resourceNotFound: !found);
}
}
public LocalizedString this[string name, params object[] arguments] =>
new(name, string.Format(this[name].Value, arguments), this[name].ResourceNotFound);
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) =>
Turkish.Select(pair => new LocalizedString(pair.Key, pair.Value));
}
Attribute'ta ErrorMessage vermediğimiz için localization en özelden en genele {DeclaringType}_{MemberName}_{AttributeType}_Error, {DeclaringType}_{AttributeType}_Error ve {AttributeType}_Error key'lerini deniyor. Biz en genelini doldurduk; uygulamadaki bütün [Required]'lar tek satırla Türkçe oldu:
$ curl -X POST localhost:5130/products -H "Accept-Language: en-US" -H "Content-Type: application/json" -d '{"name":"","price":0}'
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Name":["The Name field is required."],"Price":["The field Price must be between 0.01 and 100000."]},"traceId":"00-73b5c57553c5392018c3d25b25c78a38-fd1e478d32dcf931-02"}
$ curl -X POST localhost:5130/products -H "Accept-Language: tr-TR" -H "Content-Type: application/json" -d '{"name":"","price":0}'
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Name":["Name alanı zorunludur."],"Price":["Price alanı 0,01 ile 100000 arasında olmalıdır."]},"traceId":"00-5efeb75403f01868d6b9b35df319ff18-4c9e4532975af61d-02"}
Key bulunamayınca validation attribute'un kendi mesajına düşüyor; İngilizce istek bu yüzden eskisi gibi geldi. Dikkat: 0,01 da kültüre göre biçimlendi.
Endpoint Filter'lar ve [ShortCircuit]
.NET 7 yazısında endpoint filter'ları görmüştük. Learn'ün .NET 11 listesinde şu madde var: endpoint'te filter varsa parametre binding'i başarısız olsa da filter pipeline çalışıyor; filter 400'ü görüp kendi body'sini yazabiliyor. Pagination yazısındaki sayfa parametrelerini düşün:
builder.Services.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = false);
// ...
app.MapGet("/products", (int page, int pageSize) =>
TypedResults.Ok(new { page, pageSize }))
.AddEndpointFilter(async (context, next) =>
{
// Binding başarısızsa status code zaten 400
if (context.HttpContext.Response.StatusCode == StatusCodes.Status400BadRequest)
{
app.Logger.LogWarning("Binding failed for {Path}{Query}", context.HttpContext.Request.Path, context.HttpContext.Request.QueryString);
return TypedResults.Problem(
title: "Invalid paging parameters.",
detail: "page and pageSize must be integers.",
statusCode: StatusCodes.Status400BadRequest);
}
return await next(context);
});
$ curl -i "localhost:5111/products?page=abc&pageSize=20"
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Invalid paging parameters.","status":400,"detail":"page and pageSize must be integers.","traceId":"00-9308a63ef575c391d4a62b9ad7e51a90-ab025b5d9ebc6507-02"}
Burada dürüst bir not: aynı kodu .NET 10.0.13 runtime'ında çalıştırdığımda da filter 400'ü gördü. Farkı 10.0.3 runtime'ında yakaladım: AddValidation() açıkken bozuk bir Guid ile gelen istekte handler yine de çalışıyor ve 400'ün body'sine kendi sonucunu yazıyordu. .NET 11 handler'ın binding hatasından sonra hiç çalışmamasını garanti ediyor; .NET 10'da kalıyorsan runtime yamalarını güncel tut.
.NET 8 yazısındaki ShortCircuit() ve MapShortCircuit minimal API'ye özgüydü. .NET 11'de attribute hali geldi ve controller'larda da çalışıyor:
[ApiController]
[Route("robots.txt")]
[ShortCircuit]
public class RobotsController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Content("User-agent: *\nDisallow:", "text/plain");
}
// Program.cs
app.MapGet("/health", [ShortCircuit] () => "Healthy");
Pipeline'a her response'a X-Pipeline header'ı ekleyen bir middleware koydum. /products yanıtında header vardı; /robots.txt ve /health yanıtlarında yoktu. Endpoint routing'den hemen sonra çalıştı, geri kalan middleware'ler atlandı. Health check yazısındaki liveness endpoint'i için iyi bir aday.
C# 15 Union Types: Bir Field, İki Şekil
Ürün aramada istemci fiyat filtresini ya tek bir üst sınır (50) ya da bir aralık ({"min":50,"max":400}) olarak gönderebilsin. .NET 10'da ya iki nullable property açıyor ya da custom JsonConverter yazıyordun. C# 15'in union type'ları System.Text.Json'ın kullanıldığı her yerde, yani minimal API ve MVC body'lerinde de çalışıyor:
app.MapPost("/products/search", (ProductSearch search) =>
TypedResults.Ok(catalog.Where(p => search.Price switch
{
decimal max => p.Price <= max,
PriceRange range => p.Price >= range.Min && p.Price <= range.Max,
})));
public record PriceRange(decimal Min, decimal Max);
public union PriceFilter(decimal, PriceRange);
public record ProductSearch(PriceFilter Price);
$ curl -X POST localhost:5111/products/search -H "Content-Type: application/json" -d '{"price":50}'
[{"id":2,"name":"Wireless Mouse","sku":"MS-002","price":29.90}]
$ curl -X POST localhost:5111/products/search -H "Content-Type: application/json" -d '{"price":{"min":50,"max":400}}'
[{"id":1,"name":"Mechanical Keyboard","sku":"KB-001","price":89.90},{"id":3,"name":"4K Monitor","sku":"MN-003","price":349.00}]
Switch'te default kolu yok ve derleyici uyarı vermedi; union'ın case'lerini biliyor. Polymorphism'den farklı olarak JSON'da $type gibi bir discriminator yok, değerin şekli case'i belirliyor. OpenAPI belgesinde field anyOf olarak çıktı:
"PriceFilter": {
"anyOf": [
{
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
"type": [
"number",
"string"
],
"format": "double"
},
{
"$ref": "#/components/schemas/PriceRange"
}
]
}
Union'lar yalnızca body'de çalışıyor; route, query string, header ve form binding desteklenmiyor. İki case aynı JSON şekline serialize oluyorsa Learn [JsonUnion] ile ayırt etmeni söylüyor.
OpenAPI 3.2 Varsayılan Oldu
.NET 10 yazısında AddOpenApi()'nin 3.1.1 ürettiğini görmüştük. .NET 11'de aynı çağrı:
$ curl localhost:5111/openapi/v1.json
{
"openapi": "3.2.0",
Altta Microsoft.OpenApi 3.x'e geçildi; benim projeme 3.11.0 geldi. 3.2 ile gelen üç şey API tarafında hemen işe yarıyor. Birincisi HTTP QUERY metodu: GET gibi güvenli ve idempotent ama body taşıyabiliyor. Arama kriterleri URL'ye sığmayacak kadar büyükse POST'a kaçmak yerine bunu kullanabilirsin. Routing her verb'ü zaten kabul ediyordu; yeni olan belgenin onu tanıması:
app.MapMethods("/products/search", ["QUERY"], (ProductSearch search) =>
TypedResults.Ok(catalog.Where(p => search.Price switch
{
decimal max => p.Price <= max,
PriceRange range => p.Price >= range.Min && p.Price <= range.Max,
})));
$ curl -X QUERY localhost:5111/products/search -H "Content-Type: application/json" -d '{"price":100}'
[{"id":1,"name":"Mechanical Keyboard","sku":"KB-001","price":89.90},{"id":2,"name":"Wireless Mouse","sku":"MS-002","price":29.90}]
$ curl localhost:5111/openapi/v1.json
"/products/search": {
"post": { ... },
"query": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductSearch"
}
}
},
"required": true
},
...
İkincisi [Obsolete]. Versioning yazısında eski endpoint'leri deprecate etmeyi konuşmuştuk; artık C# tarafındaki işaret belgeye kendiliğinden geçiyor:
#pragma warning disable CS0618
app.MapGet("/v1/products/{id:int}", GetLegacyProduct);
#pragma warning restore CS0618
// ...
#pragma warning disable CS0618
[Obsolete("Use /products/{id}.")]
static ProductDto GetLegacyProduct(int id) => new(id, "Mechanical Keyboard", "KB-001", 89.90m);
#pragma warning restore CS0618
"/v1/products/{id}": {
"get": {
...
"deprecated": true
}
}
Üçüncüsü Server-Sent Events. .NET 10 yazısındaki sipariş durumu akışını SseItem<T> döndürecek şekilde yazınca belge, her event'in şeklini 3.2'nin itemSchema alanıyla tarif etti:
app.MapGet("/orders/{id:int}/status-stream", (int id, CancellationToken cancellationToken) =>
TypedResults.ServerSentEvents(GetStatusesAsync(id, cancellationToken)));
static async IAsyncEnumerable<SseItem<OrderStatus>> GetStatusesAsync(int orderId, [EnumeratorCancellation] CancellationToken cancellationToken)
{
string[] statuses = ["Received", "Packed", "Shipped"];
foreach (string status in statuses)
{
await Task.Delay(500, cancellationToken);
yield return new SseItem<OrderStatus>(new OrderStatus(orderId, status), "order-status");
}
}
"text/event-stream": {
"itemSchema": {
"required": [
"data"
],
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/OrderStatus"
},
"event": {
"type": "string"
},
"id": {
"type": "string"
}
}
}
}
Kullandığın araçlar 3.2'yi henüz okuyamıyorsa sürümü elle düşür:
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});
Bu ayarla belge 3.1.2 oldu; QUERY operation'ı x-oai-additionalOperations, SSE şeması da x-oai-itemSchema extension'ı altına taşındı.
Kısa Kısa: Gözlemlenebilirlik, Rate Limiting ve Diğerleri
ASP.NET Core artık HTTP server activity'sine OpenTelemetry semantic convention tag'lerini kendisi yazıyor. Hiç paket kurmadan, Microsoft.AspNetCore activity source'unu dinleyen bir ActivityListener ile QUERY isteğinde http.request.method=QUERY, http.route=/products/search ve http.response.status_code=200 tag'lerini gördüm. OpenTelemetry kullanıyorsan AddSource("Microsoft.AspNetCore") yeterli; OpenTelemetry.Instrumentation.AspNetCore paketine gerek kalmıyor.
.NET 7 yazısında rate limiter'ın OnRejected callback'inde Retry-After yazmayı önermiştik. Fixed window limiter'ın verdiği RetryAfter metadata'sı artık bir sonraki pencere sınırını doğru gösteriyor, yani aynı kod doğru süreyi yazıyor. Output cache'te policy'yi çalışma anında, örneğin tenant'a göre seçmek için IOutputCachePolicyProvider geldi.
Response compression ve request decompression middleware'lerine Zstandard eklendi ve varsayılan olarak açık. Compression açıkken her response'a Vary: Accept-Encoding ekleniyor; CDN arkasındaysan iyi haber. SDK'da dotnet new mcpserver template'i hazır geliyor, dotnet user-jwts create --file app.cs da tek dosyalık uygulamalarla çalışıyor. ASP.NET Core'un shared framework kütüphaneleri de runtime-async ile derlendi: async state machine'i derleyici yerine runtime üretiyor. Microsoft daha az allocation ve daha okunur stack trace vaat ediyor; senin kodunda değişiklik gerekmiyor.
Sık Düşülen Tuzaklar
Birincisi, async attribute'u controller'da kullanmak. Önce Tahmin Et'te gördük: aynı DTO minimal API'de 400, controller'da 500 döndü. DTO'yu iki tarafta paylaşıyorsan async kuralı controller tarafında service katmanında kontrol et.
İkincisi, Development ortamında ThrowOnBadRequest. Bu ayarı kapatmadan denediğimde filter hiç çalışmadı; yanıt, başlığı Microsoft.AspNetCore.Http.BadHttpRequestException olan ve exception detaylarını taşıyan bir 400 oldu. Production'da bu ayar zaten false; "lokalde çalışmıyor, prod'da çalışıyor" şaşkınlığına düşme.
Üçüncüsü, union'ı query string'de kullanmak. PriceFilter'ı bir GET endpoint'inin parametresi yaptığımda ilk istekte Body was inferred but the method does not allow inferred body parameters mesajlı bir InvalidOperationException geldi; framework union'ı body sanıyor.
Dördüncüsü, union'da sayıların string kabulü. {"price":"50"} da 200 döndü ve decimal case'ine düştü; web varsayılanları sayıların string olarak okunmasına izin veriyor. Belgedeki ["number", "string"] tipi de bundan. ConfigureHttpJsonOptions ile NumberHandling'i Strict yaptığımda aynı istek 400 döndü.
.NET 11'e Geçiş
<TargetFramework>net11.0</TargetFramework>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="11.0.0" />
.NET 11 SDK gerekiyor; örnekleri 11.0.100 SDK ile derledim. Microsoft.* paketlerini 11.0.x'e çek. C# 15 net11.0 hedefleyen projelerin varsayılan dili; union type'lar için ek ayar gerekmiyor.
Derlemeyi kıran iki kaldırma var. Eski projeleri okuma yazısında incelediğimiz Startup.cs döneminin projelerinde hâlâ rastlanan SetCompatibilityVersion, ASP.NET Core 3.0'dan beri bir şey yapmıyordu ve artık yok. .NET 8'den beri obsolete olan ConcurrencyLimiter middleware'i de kaldırıldı; yerine rate limiter'ın concurrency limiter'ını kullan. İkisini birden derlediğimde:
error CS1061: 'IMvcBuilder' does not contain a definition for 'SetCompatibilityVersion' ...
error CS0103: The name 'CompatibilityVersion' does not exist in the current context
error CS1061: 'IServiceCollection' does not contain a definition for 'AddQueuePolicy' ...
error CS1061: 'WebApplication' does not contain a definition for 'UseConcurrencyLimiter' ...
Davranış değişikliklerinden API'leri etkileyenler: Kestrel HTTP/1.1'de hem Content-Length hem Transfer-Encoding taşıyan bir request'ten sonra bağlantıyı kapatıyor (request smuggling'e karşı), HTTP/2 ve HTTP/3'te connection-specific header'ları reddediyor; eski bir proxy'nin arkasındaysan test et. Aynı status code için birden fazla [ProducesResponseType] yazdıysan belgede artık hepsi görünüyor. Transformer yazdıysan Microsoft.OpenApi 3.x'teki tip değişikliklerine bak. OpenTelemetry tag'leri artık varsayılan olduğundan instrumentation paketini güncelle, yoksa tag'ler iki kez yazılabilir.
.NET 11 STS olarak iki yıl, 2028-11-14'e kadar destekleniyor. Bu tarih .NET 10 LTS'in destek bitişiyle aynı gün. .NET 8 ve .NET 9 ise 2026-11-10'da destek dışı kaldı. Yani .NET 11'e geçmek sana fazladan destek süresi kazandırmıyor; karar yeni özelliklere ihtiyacın olup olmadığına bağlı. Production'da LTS'te kalmak isteyen ekip .NET 10'da kalıp bir sonraki LTS'i bekleyebilir.
Küçük Bir Deneme
★☆☆ CreateProductRequest'i controller'dan ayır: controller'a async attribute'u olmayan ayrı bir DTO ver ve SKU kontrolünü bir service'e taşı. Aynı isteğin iki tarafta da 400 döndüğünü gör.
★★☆ IAsyncValidatableObject ile "aynı kategoride aynı isimde ürün olamaz" kuralını yaz. Validate'ten exception fırlat, ValidateAsync'te repository'ye sor ve hatanın errors içinde hangi key'e düştüğüne bak.
★★★ PriceFilter'a üçüncü bir case ekle: {"currency":"EUR","max":50} gibi para birimli bir üst sınır. Bu case'in PriceRange ile karışıp karışmadığını curl ile gör; karışıyorsa [JsonUnion]'a bak ve OpenAPI belgesindeki anyOf'un nasıl değiştiğini incele.
.NET 6'da kurduğumuz kemik yapı sürüm yazılarıyla bugünün .NET 11'ine ulaştı. Sıradaki durak, Kasım 2027'de gelmesi beklenen ve politikaya göre LTS olacak .NET 12; çıktığında neyin kalıcı olduğuna bakıp seriye yine buradan devam edeceğiz.