ASP.NET Core’da Union ve Closed Hiyerarşi Kullanımı
Bir API sözleşmesinde değer birden fazla JSON türünden biri olabiliyorsa C# union türleri bu alternatifi doğrudan metot imzasına taşıyabilir. C# 15 ve.NET 11 ile gelen union bildirimi, izin verilen türleri sabit bir küme olarak tanımlar. ASP.NET Core bu türleri System.Text.Json üzerinden JSON gövdelerinde, SignalR’da, Blazor JavaScript interop akışında ve bileşen durumunda kullanabilir.
Union türleri API sözleşmesini nasıl tanımlar?
Kubernetes’teki maxUnavailable alanı buna pratik bir örnektir. Değer mutlak sayı, örneğin 2, veya yüzde, örneğin "25%", olabilir. ASP.NET Core endpoint’i bu sözleşmeyi şöyle ifade edebilir:
public union IntOrString(int, string);
app.MapGet("/deployments/{name}/max-unavailable", IntOrString (string name) => Deployments.GetMaxUnavailable(name));
Bu union yalnızca int veya string kabul eder. bool, DateTime ve başka türler bu kümenin parçası değildir. Union case’lerinin aynı sınıf hiyerarşisindeki sınıflardan oluşması da gerekmez; ilkel türler, sınıflar, arayüzler ve nullable türler birlikte kullanılabilir.
Case değerleri union’a doğrudan dönüştürülebilir:
IntOrString absolute = 2;
IntOrString percentage = "25%";
Aktif case, standart C# pattern matching ile ele alınabilir:
static string Describe(IntOrString value) => value switch
{
int count => $"{count} pods",
string percentage => percentage,
};
Burada varsayılan bir _ => kolu yoktur. Derleyici union’ın izin verdiği tüm case’leri bildiği için switch ifadesinin eksiksiz olup olmadığını denetler. Union’a yeni bir case eklendiğinde, bu case’i ele almayan mevcut switch ifadeleri uyarı üretir.
Örneğin aşağıdaki tanıma sonradan string case’i eklenirse:
public union SettingValue(bool, decimal, string);
static string Describe(SettingValue value) => value switch
{
bool enabled => enabled ? "enabled" : "disabled",
decimal number => number.ToString(),
// missing string case
};
Derleyici şu uyarıyı verir:
warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'string' is not covered.
Bu kontrol union’ın kullanıldığı her noktada yapılır. Böylece yeni bir case mevcut kodda sessizce gözden kaçmaz.
Union’dan önce yaygın seçenekler object, ortak bir temel tür veya özel wrapper türleriydi. object gereğinden fazla tür kabul eder. Ortak bir temel tür ise int ve string gibi birbiriyle ilgisiz mevcut türleri aynı sözleşmede birleştiremez. Özel wrapper bu sınırı uygulayabilir; ancak bunun için kendi oluşturma ve eşleştirme API’leri gerekir. Union tür sözleşmesini metot imzasında tutar, normal C# pattern matching ile çalışır ve System.Text.Json desteği sunar.
Union, polimorfizm ve closed hiyerarşi farkı
Polimorfizm, bir temel sınıftan türeyen ilişkili türleri modellemek için kullanılır. Örneğin Circle ve Square, ortak bir Shape temel sınıfından türeyebilir. Bu türler ortak üyeleri ve davranışları paylaşır.
System.Text.Json bu hiyerarşileri discriminator kullanarak serileştirebilir. Temel tür [JsonPolymorphic] ile işaretlenir, desteklenen türetilmiş türler de [JsonDerivedType] ile kaydedilir. JSON içinde aktif türü belirten bir $type alanı yer alır:
{"$type":"circle","radius":5}
System.Text.Json yalnızca açıkça kaydedilmiş türetilmiş türleri desteklenen küme içinde sayar. Temel C# sınıfı açık kaldığında, başka türlerin gelecekte türemesini dil düzeyinde engellemez. Bu nedenle böyle bir hiyerarşi üzerinde yapılan switch ifadesi genellikle varsayılan bir kol gerektirir.
C# 15’in closed class hiyerarşileri ise temel sınıfa başka assembly’lerden doğrudan türemeyi engeller. Derleyici bilinen türetilmiş türleri tamamlanmış bir küme olarak değerlendirebilir:
public closed record class PaymentEvent(string PaymentId);
public sealed record class PaymentInitiated(string PaymentId) : PaymentEvent(PaymentId);
public sealed record class PaymentAuthorized(string PaymentId, decimal Amount) : PaymentEvent(PaymentId);
public sealed record class PaymentFailed(string PaymentId, string Reason) : PaymentEvent(PaymentId);
Closed hiyerarşi union’a benzer biçimde sabit alternatifler sunar; ancak sonuçta bir kalıtım hiyerarşisidir. Case’ler ortak bir temel sınıftan türeyebilir ve ortak üyeleri paylaşabilir. closed anahtar sözcüğü C# tür ilişkisini sınırlar, JSON biçimini tek başına değiştirmez.
Yeni bir API tasarlıyor ve tüm alternatif sınıfları kontrol ediyorsanız closed hiyerarşi ile discriminator kullanımı tercih edilebilir. PaymentInitiated, PaymentAuthorized ve PaymentFailed gibi olaylar ortak bir PaymentEvent temel sınıfından türeyebilir. Discriminator JSON’u açıklayıcı hâle getirir; closed ise derleyicinin bilinen tüm olayların ele alınıp alınmadığını kontrol etmesini sağlar.
Mevcut hiyerarşinin başka assembly’ler tarafından genişletilmesi gerekiyorsa temel sınıf açık bırakılabilir. Bu durumda desteklenen türetilmiş türlerin System.Text.Json’a açıkça kaydedilmesi gerekir. Derleyici açık hiyerarşiyi eksiksiz kabul edemeyeceğinden fallback kolu da kullanılmalıdır.
Union, discriminator içermeyen mevcut JSON sözleşmesini korumak veya case’leri tek bir temel sınıftan türetmenin mümkün olmadığı durumlarda daha uygundur. Bu yaklaşım ilkel türler ve kontrol etmediğiniz mevcut türler için de kullanılabilir. Ancak discriminator içermeyen JSON’da iki case aynı yapıya sahip olabilir; okuma sırasında hangi case’in seçileceği için ek sınıflandırma gerekebilir.
System.Text.Json ile serileştirme
Union değer serileştirilirken wrapper kaldırılır ve yalnızca aktif case’in kendi JSON karşılığı yazılır. Union için ek bir zarf nesnesi, $type alanı veya discriminator eklenmez:
JsonSerializer.Serialize(new UnionIntString(42)); // 42
JsonSerializer.Serialize(new UnionIntString("hello")); // "hello"
JsonSerializer.Serialize(new UnionPet(new Cat("Whiskers", "Tabby"))); // { "name": "Whiskers", "coat": "Tabby" }
Aşağıdaki tanımlar, farklı JSON türleri ve nesne türleri içeren union’ları gösterir:
public union UnionIntString(int, string);
public union UnionBoolString(bool, string);
public union UnionNullableIntString(int?, string);
public record Cat(string Name, string Coat);
public record Dog(string Name, string Breed);
public union UnionPet(Cat, Dog);
Case’ler farklı JSON token türlerine karşılık geliyorsa System.Text.Json otomatik seçim yapabilir. UnionBoolString(bool, string) bu açıdan belirsiz değildir: JSON boolean değeri bool ile, JSON string değeri string ile eşleşir.
Her iki case de JSON nesnesi olduğunda otomatik seçim mümkün olmayabilir. UnionPet(Cat, Dog) için yapısal sınıflandırıcı kullanılabilir:
[JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))]
public union UnionPet(Cat, Dog);
Yapısal sınıflandırıcı, System.Text.Json serileştirme işleminden önce JSON nesnesini tarar. Bu işlem yükün boyutuyla orantılı ek çalışma getirir. Karar case’lerdeki özellik adlarına dayandığı için JSON yapılarındaki değişiklikler mevcut değerlerin sınıflandırılmasını etkileyebilir veya belirsizlik yaratabilir. Yeni ve kontrol edilen sözleşmelerde discriminator kullanan closed hiyerarşi bu nedenle daha uygun olabilir. Yapıyla ayırt edilemeyen ileri senaryolarda özel bir JsonTypeClassifier sağlanabilir.
HTTP JSON bağlamında başka bir belirsizlik de ortaya çıkabilir. Web JSON ayarları sayıların string olarak okunmasına izin verdiğinden, IntOrString bir istek gövdesi olarak kullanıldığında JSON string’i hem sayı hem de string case’iyle eşleşebilir. Böyle bir sözleşmeyi deserialize etmek için açık bir özel sınıflandırma gerekir.
Closed hiyerarşilerde closed kullanmak tek başına polimorfik JSON üretmez. Somut türetilmiş tür kullanılırsa normal serileştirme yapılır:
var json = JsonSerializer.Serialize(new PaymentAuthorized("p-123", 42.5m), JsonSerializerOptions.Web);
var payment = JsonSerializer.Deserialize<PaymentAuthorized>(json, JsonSerializerOptions.Web);
{"paymentId":"p-123","amount":42.5}
API temel tür olan PaymentEvent üzerinden çalışıyorsa JSON’un hangi türe dönüştürüleceğini belirtmek gerekir. Closed hiyerarşinin türetilmiş türlerini otomatik keşfetmek için polimorfizm etkinleştirilebilir:
[JsonPolymorphic(InferClosedTypePolymorphism = true)]
public closed record class PaymentEvent(string PaymentId);
Bu durumda System.Text.Json türetilmiş türleri closed hiyerarşiden çıkarır ve tür adlarını discriminator olarak kullanır:
app.MapPost("/payment-event", (PaymentEvent paymentEvent) => paymentEvent);
{"$type":"PaymentAuthorized","paymentId":"p-123","amount":42.5}
Aynı opt-in davranışı JSON pipeline’ında JsonSerializerOptions.InferClosedTypePolymorphism ile de yapılandırılabilir.
ASP.NET Core, SignalR ve Blazor desteği
ASP.NET Core’daki union desteği System.Text.Json üzerinden sağlandığı için JSON kullanan farklı akışlarda da kullanılabilir. Buna Minimal API istek ve yanıt gövdeleri, MVC input-output formatter’ları, SignalR’ın JsonHubProtocol protokolü ve Blazor’ın System.Text.Json kullanan sınırları dahildir.
Minimal API’lerde union hem istek gövdesi parametresi hem de dönüş türü olabilir. Runtime yolu olan RequestDelegateFactory ile kaynak üreten Request Delegate Generator (RDG) arasında davranış aynıdır:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Request body: UnionBoolString is unambiguous (bool vs string), so it binds without a classifier.
app.MapPost("/flag", (UnionBoolString flag) => flag);
// Request body: UnionPet's cases are both objects, so it uses the built-in classifier shown earlier.
app.MapPost("/pet", ([FromBody] UnionPet pet) => TypedResults.Ok(pet));
// Return types: only the active case is serialized, and no classifier is needed to write.
app.MapGet("/value", () => new UnionIntString(42));
app.MapGet("/pet", () => new UnionPet(new Cat("Whiskers", "Tabby")));
app.Run();
Union’lar asenkron dönüşlerde, nullable wrapper içinde ve TypedResults ile birlikte kullanılabilir:
app.MapGet("/maybe", () => new UnionNullableIntString((int?)null));
app.MapGet("/typed", () => TypedResults.Ok(new UnionPet(new Cat("Whiskers", "Tabby"))));
Başka bir modelin özelliği, IAsyncEnumerable<T> üzerinden aktarılan öğe veya [AsParameters] kapsayıcısının gövde alanı da union olabilir. Union serileştirmesi ConfigureHttpJsonOptions ile yapılandırılan seçeneklere uyar.
MVC controller’larında union’lar action parametreleri ve dönüş türleri olarak kullanılabilir. Task<TUnion> ve ValueTask<TUnion> sonuçları da desteklenir. Controller’lar Minimal API’ler gibi JsonSerializerDefaults.Web kullandığından, HTTP isteklerinde sayı ve string ayrımıyla ilgili belirsizlik burada da geçerlidir.
SignalR’da union desteği JsonHubProtocol üzerinden çalışır. Union’lar hub metotlarının parametreleri, dönüş değerleri ve stream öğeleri olabilir. Okuma sırasında çağrı bağlayıcısından çözümlenen tür, union converter’ını ve varsa [JsonUnion] sınıflandırıcısını yönlendirir.
HTTP JSON bağlamından farklı olarak JsonHubProtocol, JSON string token’ını sayısal case’ler için belirsiz kabul etmez. Bu nedenle UnionIntString(int, string) sınıflandırıcı olmadan iki case’i de karşılıklı olarak taşıyabilir. Ancak UnionPet(Cat, Dog) gibi her iki case’i de JSON nesnesi olan union’lar okuma sırasında hâlâ belirsizdir ve sınıflandırıcı gerektirir. Union’lar yalnızca JsonHubProtocol ile desteklenir; MessagePack ve Newtonsoft.Json hub protokollerinde bu destek yoktur.
Blazor’da davranış, değerin serileştirme sınırını geçip geçmediğine bağlıdır. Razor işaretlemesiyle veya RenderTreeBuilder.AddComponentParameter ile yapılan işlem içi bileşen parametreleri doğrudan atanır ve serileştirme gerektirmez. JavaScript interop, kalıcı bileşen durumu ve prerender edilmiş parametreler ise System.Text.Json kullanır.
JavaScript’in Element.scrollIntoView API’si boolean kısaltma veya seçenek nesnesi kabul eder. Bu sözleşme union ile modellenebilir:
public sealed record ScrollIntoViewOptions(string Behavior, string Block, string Inline);
public union ScrollIntoViewArgument(bool, ScrollIntoViewOptions);
private ValueTask ScrollAsync(
ElementReference element,
ScrollIntoViewArgument argument) =>
JS.InvokeVoidAsync("scrollElementIntoView", element, argument);
await ScrollAsync(target, false);
await ScrollAsync(target, new ScrollIntoViewOptions("instant", "start", "nearest"));
window.scrollElementIntoView = (element, argument) =>
element.scrollIntoView(argument);
Bu kullanımda union zarfı veya discriminator eklenmez; aktif case, JavaScript’in beklediği JSON boolean ya da seçenek nesnesi olarak yazılır. PersistentComponentState, PersistAsJson ve TryTakeFromJson üzerinden union değerlerini saklayıp geri yükleyebilir. Aktif case’i JSON null olan nullable union’lar da bu akışa dahildir.
Blazor Server veya Blazor WebAssembly prerender işleminde bileşen parametreleri marker içine System.Text.Json ile yazılır ve istemci başlatılırken deserialize edilir. Union’lar bu sınırda da desteklenir; JSON’da null olarak temsil edilen aktif case’ler de buna dahildir.
Union’lar query string, route value, header veya form alanlarında desteklenmez. Destek, System.Text.Json kullanan veri akışlarıyla sınırlıdır.
OpenAPI şemasında union gösterimi
OpenAPI belgesinde union, her case türü için bir giriş içeren anyOf şeması olarak temsil edilir. UnionIntString için şema integer ve string alternatiflerini içerir:
"UnionIntString": {
"anyOf": [
{ "type": "integer", "format": "int32" },
{ "type": "string" }
]
}
Nesne case’lerinden oluşan UnionPet ise ilgili modelleri referanslayan bir anyOf yapısı kullanır:
"UnionPet": {
"type": "object",
"anyOf": [
{ "$ref": "#/components/schemas/Cat" },
{ "$ref": "#/components/schemas/Dog" }
]
}
Model seçimi API sözleşmesinin yapısına göre yapılır. İlişkili ve kontrol edilen sınıflar discriminator içeren closed hiyerarşiyle modellenebilir. Mevcut discriminator’siz biçimin korunması veya ilgisiz türlerin aynı sözleşmede birleştirilmesi gerekiyorsa union tercih edilir. Her iki yaklaşımda da alternatifler genişletildiğinde exhaustive switch kontrolleri devreye girer ve mevcut çağıranların ele alması gereken yeni durumları gösterir.
Kaynaklar ve İleri Okuma
- Microsoft kaynak yazısı: ASP.NET Core’da union ve closed hiyerarşiler
- System.Text.Json genel bakışı
- JsonPolymorphicAttribute API başvurusu
- .NET 11 kütüphanelerindeki yenilikler
- Request Delegate Generator (RDG)
- ASP.NET Core TypedResults API’si
- Blazor IJSRuntime API’si
- OpenAPI Specification
- C# 15’te Union Types hakkında önceki yazı







Yorum gönder