1. Union Type Nedir?
C# tarafında “Union Types” şu an yeni/preview aşamasındaki önemli özelliklerden biri.
Kısaca:
Bir değişkenin belirli ve kapalı bir tip listesinden yalnızca birini taşıyabilmesini sağlayan type-safe yapıdır.
Örneğin:
public union Pet(Dog, Cat, Bird);
public record Dog(string Name);
public record Cat(string Name);
public record Bird(string Species);
Burada Pet şu anlama gelir:
Pet ya Dog olabilir,
ya Cat olabilir,
ya Bird olabilir.
Başka bir şey olamaz.
Kullanımı:
Pet pet = new Dog("Karabaş");
Burada pet değişkeninin tipi Pet, ama içinde taşıdığı gerçek değer Dog.
2. Neden Union Type Kullanıyoruz?
Çünkü bazı durumlarda bir sonucun, cevabın veya modelin tek bir tip değil, sınırlı sayıda farklı tipten biri olabileceğini göstermek isteriz.
Bugün C#’ta genellikle şunları yapıyoruz:
object result = GetResult();
veya:
IResult result = GetResult();
Ama bunlar zayıf çözümler olabilir.
Çünkü:
objectkullanınca type safety kaybolur- Interface kullanınca hangi concrete tiplerin dönebileceği net ifade edilmez
Union Type ise şunu sağlar:
public union PaymentResult(
PaymentSuccess,
PaymentFailed,
PaymentPending
);
Artık bu method yalnızca bu üç sonuçtan birini dönebilir.
3. Pattern Matching ile Kullanımı
Union Type’ın en güçlü tarafı pattern matching ile birlikte kullanılmasıdır.
PaymentResult result = paymentService.Pay();
var message = result switch
{
PaymentSuccess success => $"Ödeme başarılı. İşlem No: {success.TransactionId}",
PaymentFailed failed => $"Ödeme başarısız. Hata: {failed.ErrorMessage}",
PaymentPending pending => $"Ödeme beklemede. Tahmini süre: {pending.EstimatedCompletionTime}",
};
Bu yaklaşım:
- Nullable property karmaşasını azaltır
- Boolean flag kullanımını azaltır
- Type-safe akış sağlar
- Kod okunabilirliğini artırır
4. Union Type Hangi Problemi Çözer?
En klasik problem şudur:
public class PaymentResponse
{
public bool IsSuccess { get; set; }
public string? TransactionId { get; set; }
public string? ErrorMessage { get; set; }
public bool IsPending { get; set; }
}
Bu model tehlikelidir.
Çünkü aynı anda geçersiz state’ler oluşabilir:
var response = new PaymentResponse
{
IsSuccess = true,
ErrorMessage = "Kart limiti yetersiz",
IsPending = true
};
Bu nesne mantıksal olarak bozuk bir state üretir.
Union Type bunu engeller.
public union PaymentResult(
PaymentSuccess,
PaymentFailed,
PaymentPending
);
Artık bir sonuç aynı anda hem başarılı hem başarısız olamaz.
5. API Response Senaryosu
public union GetUserResult(
UserFound,
UserNotFound,
UnauthorizedAccess
);
public record UserFound(int Id, string Name, string Email);
public record UserNotFound(int UserId);
public record UnauthorizedAccess(string Reason);
Service:
public GetUserResult GetUser(int id)
{
if (!currentUser.HasPermission)
return new UnauthorizedAccess("Bu kullanıcıyı görüntüleme yetkiniz yok.");
var user = repository.GetById(id);
if (user is null)
return new UserNotFound(id);
return new UserFound(user.Id, user.Name, user.Email);
}
Controller:
var result = userService.GetUser(id);
return result switch
{
UserFound user => Ok(user),
UserNotFound notFound => NotFound(),
UnauthorizedAccess unauthorized => Forbid()
};
Bu yaklaşım method’un hangi sonuçları dönebileceğini çok net şekilde ifade eder.
6. Exception Yerine Kullanımı
Exception her zaman doğru çözüm değildir.
Kötü yaklaşım:
throw new Exception("User not found");
Daha doğru yaklaşım:
public union GetUserResult(
UserFound,
UserNotFound
);
Exception:
- Beklenmeyen sistem hataları için
Union Type:
- Beklenen business sonuçları için
kullanılmalıdır.
7. Result Pattern ile İlişkisi
Birçok projede şu yapı kullanılır:
public class Result<T>
{
public bool IsSuccess { get; set; }
public T? Data { get; set; }
public string? Error { get; set; }
}
Union Type yaklaşımı ise daha güçlüdür:
public union GetUserResult(
UserFound,
UserNotFound,
ValidationFailed
);
Bu yapı:
- Daha okunabilir
- Daha güvenli
- Daha type-safe
- Daha maintainable
bir çözüm sunar.
8. Enum ile Farkı
Enum yalnızca durum adı taşır.
public enum PaymentStatus
{
Success,
Failed,
Pending
}
Union Type ise her duruma özel veri taşıyabilir.
public record PaymentSuccess(string TransactionId);
public record PaymentFailed(string ErrorMessage);
public record PaymentPending(DateTime EstimatedCompletionTime);
Yani:
Enum: Durum adı taşır.
Union: Durum + o duruma ait özel veri taşır.
9. Interface ile Farkı
Interface açık uçludur.
public interface IPaymentResult { }
Başka herhangi bir tip bu interface’i implement edebilir.
Union Type ise kapalı bir tip seti oluşturur.
public union PaymentResult(
PaymentSuccess,
PaymentFailed,
PaymentPending
);
Compiler bilir ki:
PaymentResult yalnızca bu üç tipten biri olabilir.
10. Ne Zaman Kullanılmalı?
Union Type şu durumlarda çok uygundur:
- Bir işlem birden fazla farklı sonuç dönebiliyorsa
- Boolean flag karmaşası oluşuyorsa
- Nullable property fazlaysa
- API response state yönetimi varsa
- Business rule sonuçları modelleniyorsa
- Pattern matching kullanılacaksa
11. Ne Zaman Kullanılmamalı?
Şu durumlarda gereksiz olabilir:
- Basit entity modellerinde
- Tek tip dönen methodlarda
- Basit DTO yapılarında
- Service/repository sınıflarında
12. CQRS ve MediatR Kullanımı
Union Types özellikle CQRS tarafında çok güçlüdür.
public union CreateProductResult(
ProductCreated,
ProductValidationFailed,
ProductAlreadyExists
);
Bu yapı command handler sonuçlarını çok daha temiz hale getirir.
13. Validation Senaryosu
public union CreateProductResult(
ProductCreated,
ProductValidationFailed,
ProductAlreadyExists
);
public record ProductValidationFailed(
IReadOnlyList<string> Errors
);
Validation state’lerini çok daha temiz modellemeyi sağlar.
14. Domain-Driven Design Kullanımı
DDD tarafında state modelleme için oldukça güçlüdür.
public union CancelOrderResult(
OrderCancelled,
OrderAlreadyShipped,
OrderNotFound
);
Bu yaklaşım business kurallarını type-safe şekilde ifade eder.
15. Nullable Kullanımını Azaltır
Önceden:
public UserDto? GetUser(int id)
{
}
Burada null ne anlama geliyor?
- Kullanıcı yok mu?
- Yetki mi yok?
- Validasyon mu hatalı?
Belirsizdir.
Union Type bunu netleştirir.
16. Boolean Flag Kullanımını Azaltır
Kötü yaklaşım:
public class LoginResponse
{
public bool IsSuccess { get; set; }
public bool RequiresTwoFactor { get; set; }
public bool IsLocked { get; set; }
}
Daha doğru yaklaşım:
public union LoginResult(
LoginSuccess,
TwoFactorRequired,
AccountLocked,
InvalidCredentials
);
17. OneOf Kütüphanesi ile İlişkisi
C#’ta native union olmadığı dönemlerde geliştiriciler OneOf gibi kütüphaneler kullanıyordu.
public OneOf<UserDto, NotFound, ValidationError> GetUser(int id)
{
}
Union Type bu yaklaşımı dil seviyesine taşır.
18. Class mı, Record mı, Union mı?
Basit karar mantığı:
Davranış ve kimlik taşıyan ana nesne -> class
Sadece veri taşıyan model -> record
Bir sonuç birden fazla farklı tipten biri olabilir -> union
19. Çok Önemli Profesyonel Kural
Union Type özellikle state modelleme için çok güçlüdür.
Yanlış state üretilebilen class’ları union’a çevirmek çoğu zaman kod kalitesini ciddi artırır.
20. Sonuç
Union Type şu soruya cevap verir:
Bu değer hangi tiplerden biri olabilir?
Class şu soruya cevap verir:
Bu nesne kimdir ve ne davranış gösterir?
Record şu soruya cevap verir:
Bu veri hangi değerlerden oluşur?
En net kullanım yaklaşımı:
Entity, service, repository -> class
DTO, request, response -> record
Alternatif sonuç state’leri -> union
Modern .NET projelerinde doğru yerde kullanıldığında Union Types kod kalitesini, okunabilirliği ve type safety seviyesini ciddi anlamda artırır.