VodiSoft
Bu yazıda C# dünyasına gelen Union Types yapısını en ince ayrıntısına kadar inceliyoruz. Union Type nedir, hangi problemleri çözer, class ve record yapılarından farkı nedir, API response modellerinde nasıl kullanılır, Result Pattern ile ilişkisi nedir, ne zaman kullanılmalı ve hangi durumlarda tercih edilmemelidir gibi konuları gerçek proje örnekleriyle detaylı şekilde ele alıyoruz. Ayrıca payment, login, validation, CQRS, domain-driven design ve state management gibi modern .NET senaryolarında Union Types kullanımını profesyonel mimari bakış açısıyla açıklıyoruz.

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ü:

  • object kullanı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.

İlgili Yazılar

C# Class vs Record: Farkları, Kullanım Senaryoları ve Gerçek Proje Örnekleri

C# dünyasında class ve record, farklı amaçlar için kullanılan iki önemli veri modelleme yaklaşımıdır. Bu yazıda class ve record yapılarının çalışma mantığını, referans ve değer bazlı eşitlik farklarını, immutable yapı kullanımını, DTO ve Entity senaryolarını, gerçek proje örneklerini ve profesyonel kullanım stratejilerini detaylı şekilde inceleyeceğiz. Özellikle modern .NET projelerinde hangi durumda class, hangi durumda record tercih edilmesi gerektiğini pratik örneklerle açıklayacağız.

Devamını Oku

VodiSoft

Yazılım projenizi gelir üreten bir ürüne dönüştürelim

Web, mobil, .NET, SaaS ve entegrasyon projeleri için teknik keşif yapalım; riskleri, takvimi ve en hızlı ticari kazanımı netleştirelim.

Teklif Al Toplantı