Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Çözüm Desenleri — #05

Enum mı Lookup Tablosu mu? Reference Data'yı Kodda ve Veritabanında Tutmak

Mert Özen 7 Kas 2026 13 dk 6 görüntülenme
Enum mı Lookup Tablosu mu? Reference Data'yı Kodda ve Veritabanında Tutmak

Kodun davranışını belirleyen kapalı listeler (sipariş durumu) enum olarak kalmalı ve veritabanında HasData ile seed'lenen, FK ile bağlı bir aynası olmalı; iş biriminin yönettiği açık listeler (masraf tipi) ise IsActive'li bir lookup tablosunda deploy'suz değişmeli. Yazı yeniden numaralandırma ve string enum rename tuzaklarını gerçek çıktılarla gösteriyor.

Sürüm notu: Bu yazıdaki C# kodları .NET 10 ve Entity Framework Core (EF Core) 10 ile, SQL (Structured Query Language) örnekleri SQL Server 2025 ile denenmiştir.

Önce Tahmin Et: 3 Ne Demek?

Raporlama ekibi İK (insan kaynakları) Portalı'nın veritabanına salt okunur erişim aldı ve ilk iş satın alma siparişlerini duruma göre gruplayan bir rapor yazdı:

SELECT StatusId, COUNT(*) AS Orders, SUM(TotalAmount) AS Total
FROM dbo.PurchaseOrders
GROUP BY StatusId;
StatusId Orders Total
-------- ------ -----
1 1 310.0000
2 1 4200.0000
3 2 11050.5000

Uygulamanın kodunda PurchaseOrderStatus diye bir enum var. Raporlama ekibinde ise C# bilen kimse yok. 3 "onaylandı" mı, "reddedildi" mi? Aynı hafta finans birimi de bir istek gönderiyor: satın alma formundaki "masraf tipi" listesine "Seyahat" eklensin, ama bir sonraki deploy'u beklemeden, bugün. Bu iki istek aynı tasarım kararının iki yüzü. Bu yazının sonunda biri enum, diğeri tablo olacak; hangisinin hangisi olacağını tahmin et.

Bağlam ve Problem: Reference Data'nın İki Doğası

Reference data, uygulamanın diğer kayıtlarını sınıflandırmak için kullanılan küçük, nadiren değişen listeler: sipariş durumu, izin tipi, masraf tipi, para birimi, ülke. Hepsi aynı görünür ama iki farklı doğaları var.

Birincisi kapalı listeler. PurchaseOrderStatus değerleri kodun davranışını belirler: Approved olan sipariş tedarikçiye gönderilir, Draft olan düzenlenebilir. Yeni bir durum eklemek kod değişikliği demek; zaten deploy olmadan anlamı yok. Bu listelerin doğal yeri kod, yani bir enum.

İkincisi açık listeler. Masraf tipleri kodun davranışını değiştirmez; raporlamada ve bütçede gruplama için vardır. Finans birimi bu listeyi kendisi yönetmek ister: ekler, adını değiştirir, kullanımdan kaldırır. Bu listelerin doğal yeri veritabanı, yani bir lookup tablosu.

Sorun, iki doğanın karıştırılmasından çıkıyor. Kapalı bir liste yalnız enum olarak tutulunca veritabanı onu bilmiyor: raporlama ekibi 3'ü çözemiyor, foreign key yok, çok dilli etiket yok. Açık bir liste enum olarak tutulunca her yeni masraf tipi bir deploy bekliyor. Ve en kötüsü, ikisi arasındaki bağ belgelenmediği için enum ile tablo zamanla farklı şeyler söylemeye başlıyor.

Belirtiler

Raporlarda ve view'larda CASE Status WHEN 1 THEN 'Draft' WHEN 2 THEN ... blokları var ve her raporda biraz farklı.

İş birimi basit bir liste değişikliği için sprint planlamasına bilet açıyor.

Bir enum'a yeni değer eklendikten sonra eski kayıtlar ekranda yanlış durumla görünüyor.

Angular tarafında aynı listenin bir kopyası const STATUSES = [...] olarak duruyor ve backend'deki enum'la senkron değil.

Lookup tablosundan "artık kullanılmayan" bir satır silinmek isteniyor ve ya foreign key hatası alınıyor ya da, daha kötüsü, cascade ile siparişler de siliniyor.

Naif Çözüm ve Neden Kırılır

Naif çözüm enum'ı yazıp değerlerini açıkça vermemek ve veritabanına int olarak bırakmak. Enum'ın ilk sürümü ve aylar sonra birinin "mantıklı yerine" bir değer eklediği ikinci sürümü:

// Sürüm 1: değerler açıkça yazılmamış
public enum StatusV1 { Draft, PendingApproval, Approved, Rejected }

// Sürüm 2: biri "Submitted"ı mantıklı göründüğü yere ekledi
public enum StatusV2 { Draft, Submitted, PendingApproval, Approved, Rejected }

Veritabanındaki kayıtlar değişmedi; yalnız kod değişti:

int[] stored = [(int)StatusV1.Approved, (int)StatusV1.Rejected];
foreach (int value in stored)
    Console.WriteLine($"stored {value}: v1 reads {(StatusV1)value}, v2 reads {(StatusV2)value}");
stored 2: v1 reads Approved, v2 reads PendingApproval
stored 3: v1 reads Rejected, v2 reads Approved

Onaylanmış sipariş artık "onay bekliyor", reddedilmiş sipariş "onaylandı" görünüyor. Derleyici bir şey söylemedi, test'ler büyük ihtimalle geçti, çünkü test verisi her seferinde yeni enum'la oluşturuluyor. Kırılan şey veritabanındaki eski satırlar.

Yaygın ikinci refleks, enum'ı HasConversion<string>() ile string olarak saklamak. Raporlama ekibi artık Approved okuyabiliyor ve sıra değişikliği bir şey bozmuyor. Ama bu kez isim kırılgan hâle geliyor. Biri Cancelled'ı Amerikan yazımıyla Canceled yaparsa:

public enum RenamedStatus { Draft, Approved, Canceled }

b.Entity<LegacyOrder>().Property(o => o.Status).HasConversion<string>().HasMaxLength(30);
InvalidOperationException: Cannot convert string value 'Cancelled' from the database to any value in the mapped 'RenamedStatus' enum.

Veritabanında 40.000 siparişin bir kısmında Cancelled yazıyor; uygulama o satırları okuyan her sorguda exception fırlatıyor. String saklamak sorunu yok etmedi, sayıdan isme taşıdı. Rename refactoring'i bir IDE (Integrated Development Environment) kısayolu, veri migration'ı ise değil.

Seçenekler

A. Yalnız enum (int ya da string olarak saklama)

En az parça. Değerler açıkça yazılırsa (Draft = 1) yeniden numaralandırma riski kalkar; string saklama raporları okunabilir yapar. Ama veritabanı listeyi bilmez: foreign key yoktur, StatusId = 9 gibi geçersiz bir değer sessizce yazılabilir, etiketler ve çeviriler kodda kalır ve raporlama ekibi kodu okumak zorundadır.

B. Yalnız lookup tablosu

Liste tamamen veride: Id, Code, çok dilli adlar, sıralama ve IsActive. Foreign key geçersiz değeri engeller, raporlar join ile okunabilir olur ve iş birimi listeyi bir admin ekranından yönetir. Bedel, kodun bu değerlere göre davranamaması. if (order.ExpenseTypeId == 3) yazmaya başladığın an, 3 yeniden bir sihirli sayı olur ve üçüncü yazıdaki sentinel'lere geri dönersin.

C. İkisi birden: enum, seed'li tablo ve FK

Kapalı listeler için: kod enum'la çalışır, veritabanında enum'ın aynası olan bir tablo vardır ve sipariş tablosu ona foreign key ile bağlanır. Tablonun primary key'i doğrudan enum tipinde:

public enum PurchaseOrderStatus
{
    Draft = 1,
    PendingApproval = 2,
    Approved = 3,
    Rejected = 4,
    Cancelled = 5
}

// Kod tarafındaki enum'ın veritabanındaki aynası
public class PurchaseOrderStatusRow
{
    public PurchaseOrderStatus Id { get; set; }
    public string Code { get; set; } = "";
    public string NameTr { get; set; } = "";
    public string NameEn { get; set; } = "";
}

Kod order.Status == PurchaseOrderStatus.Approved yazmaya devam eder; raporlama ekibi join ile Code ve NameTr görür; FK (foreign key) geçersiz değeri reddeder. Bedel, iki kaynağın senkron tutulması. Bu yazının Uygulama bölümü tam olarak bu senkronu otomatikleştiriyor.

D. Smart enum class

Değerler davranış taşıyorsa enum'ın yerine kapalı bir class listesi. Microsoft'un .NET mikroservis rehberindeki "Use enumeration classes instead of enum types" bölümü bu yaklaşımı anlatıyor; Ardalis.SmartEnum gibi hazır paketler de var. Bağımlılık eklemeden basit bir hâli:

// Davranış taşıyan, kapalı bir değer listesi
public sealed record LeaveType(int Id, string Code, bool RequiresDocument, bool IsPaid)
{
    public static readonly LeaveType Annual = new(1, "annual", RequiresDocument: false, IsPaid: true);
    public static readonly LeaveType Sick = new(2, "sick", RequiresDocument: true, IsPaid: true);
    public static readonly LeaveType Unpaid = new(3, "unpaid", RequiresDocument: false, IsPaid: false);

    public static IReadOnlyList<LeaveType> All { get; } = [Annual, Sick, Unpaid];

    public static LeaveType FromId(int id) => All.SingleOrDefault(t => t.Id == id)
        ?? throw new ArgumentOutOfRangeException(nameof(id), $"Unknown leave type {id}.");
}

if (type == LeaveType.Sick && !hasDocument) yerine if (type.RequiresDocument && !hasDocument) yazılır; yeni bir izin tipi eklemek tek bir satır. Bedeli, EF Core'da bir value converter (Id ↔ LeaveType) ve switch ifadelerinde enum'ın sağladığı derleyici kontrolünün kaybı. Davranış yoksa bu kadar yapıya gerek yok.

Uygulama: Durum Kodda, Masraf Tipi Veride

İK Portalı'nda iki listeyi doğalarına göre ayırıyoruz. PurchaseOrderStatus kapalı bir liste: Seçenek C. Masraf tipi açık bir liste: Seçenek B.

// Deploy'suz büyüyen liste: kodda enum'ı yok
public class ExpenseType
{
    public int Id { get; set; }
    public string Code { get; set; } = "";
    public string NameTr { get; set; } = "";
    public string NameEn { get; set; } = "";
    public int SortOrder { get; set; }
    public bool IsActive { get; set; } = true;
}

public class PurchaseOrder
{
    public int Id { get; set; }
    public PurchaseOrderStatus Status { get; set; }
    public int ExpenseTypeId { get; set; }
    public decimal TotalAmount { get; set; }
}

Etiketler kodun içinde tek bir yerde duruyor:

static readonly Dictionary<PurchaseOrderStatus, (string Tr, string En)> StatusLabels = new()
{
    [PurchaseOrderStatus.Draft] = ("Taslak", "Draft"),
    [PurchaseOrderStatus.PendingApproval] = ("Onay bekliyor", "Pending approval"),
    [PurchaseOrderStatus.Approved] = ("Onaylandı", "Approved"),
    [PurchaseOrderStatus.Rejected] = ("Reddedildi", "Rejected"),
    [PurchaseOrderStatus.Cancelled] = ("İptal edildi", "Cancelled"),
};

Konfigürasyonun kalbi HasData. Lookup tablosunun satırları enum'dan üretiliyor; enum'a bir değer eklendiğinde bir sonraki migration bu satırı ekleyen bir InsertData içerir. Böylece enum ve tablo aynı commit'te değişir.

b.Entity<PurchaseOrderStatusRow>(s =>
{
    s.ToTable("PurchaseOrderStatuses");
    s.Property(x => x.Id).ValueGeneratedNever();
    s.HasIndex(x => x.Code).IsUnique();
    // Enum'daki her değer için bir satır: enum değişirse migration da değişir
    s.HasData(Enum.GetValues<PurchaseOrderStatus>().Select(v => new PurchaseOrderStatusRow
    {
        Id = v,
        Code = v.ToString(),
        NameTr = StatusLabels[v].Tr,
        NameEn = StatusLabels[v].En
    }));
});

b.Entity<PurchaseOrder>(p =>
{
    p.Property(x => x.TotalAmount).HasPrecision(19, 4);
    p.Property(x => x.Status).HasColumnName("StatusId");
    p.HasOne<PurchaseOrderStatusRow>().WithMany().HasForeignKey(x => x.Status)
        .OnDelete(DeleteBehavior.Restrict);
    p.HasOne<ExpenseType>().WithMany().HasForeignKey(x => x.ExpenseTypeId)
        .OnDelete(DeleteBehavior.Restrict);
});

İki OnDelete(DeleteBehavior.Restrict) satırı süs değil. EF Core zorunlu ilişkilerde varsayılan olarak cascade delete kurar; bu satırlar olmadan ilk denemede üretilen script'te şunu gördük:

CONSTRAINT [FK_PurchaseOrders_PurchaseOrderStatuses_StatusId] FOREIGN KEY ([StatusId]) REFERENCES [PurchaseOrderStatuses] ([Id]) ON DELETE CASCADE

Lookup tablosundan bir durumu silmek, o durumdaki bütün siparişleri de silerdi. Reference data'ya bağlanan foreign key'ler neredeyse her zaman NO ACTION olmalı. Restrict ile üretilen şema ve seed satırları:

CREATE TABLE [PurchaseOrderStatuses] (
    [Id] int NOT NULL,
    [Code] nvarchar(100) NOT NULL,
    [NameTr] nvarchar(100) NOT NULL,
    [NameEn] nvarchar(100) NOT NULL,
    CONSTRAINT [PK_PurchaseOrderStatuses] PRIMARY KEY ([Id])
);
...
INSERT INTO [PurchaseOrderStatuses] ([Id], [Code], [NameEn], [NameTr])
VALUES (1, N'Draft', N'Draft', N'Taslak'),
(2, N'PendingApproval', N'Pending approval', N'Onay bekliyor'),
(3, N'Approved', N'Approved', N'Onaylandı'),
(4, N'Rejected', N'Rejected', N'Reddedildi'),
(5, N'Cancelled', N'Cancelled', N'İptal edildi');
...
    CONSTRAINT [FK_PurchaseOrders_PurchaseOrderStatuses_StatusId] FOREIGN KEY ([StatusId]) REFERENCES [PurchaseOrderStatuses] ([Id]) ON DELETE NO ACTION

Raporlama ekibinin sorgusu artık okunabilir:

SELECT s.Code, s.NameTr, COUNT(*) AS Orders, SUM(p.TotalAmount) AS Total
FROM dbo.PurchaseOrders p
JOIN dbo.PurchaseOrderStatuses s ON s.Id = p.StatusId
GROUP BY s.Code, s.NameTr;
Code NameTr Orders Total
---- ------ ------ -----
Draft Taslak 1 310.0000
PendingApproval Onay bekliyor 1 4200.0000
Approved Onaylandı 2 11050.5000

Senkron tek yönlü değil. Biri veritabanına elle bir durum ekleyebilir ya da bir migration atlanmış olabilir. Uygulama açılırken enum ile tabloyu karşılaştıran küçük bir kontrol bunu ilk dakikada yakalar:

// Uygulama açılırken: enum ile tablo aynı şeyi mi söylüyor?
public static async Task<bool> VerifyAsync(HrPortalContext db, TextWriter log)
{
    var rows = await db.PurchaseOrderStatuses.AsNoTracking().ToDictionaryAsync(r => (int)r.Id, r => r.Code);
    var code = Enum.GetValues<PurchaseOrderStatus>().ToDictionary(v => (int)v, v => v.ToString());

    var problems = code.Where(c => !rows.TryGetValue(c.Key, out var dbCode) || dbCode != c.Value)
        .Select(c => $"enum {c.Key}={c.Value} has no matching row")
        .Concat(rows.Where(r => !code.ContainsKey(r.Key)).Select(r => $"row {r.Key}={r.Value} is unknown to the code"))
        .ToList();

    foreach (string p in problems)
        log.WriteLine($"PurchaseOrderStatus mismatch: {p}");
    log.WriteLine(problems.Count == 0 ? "PurchaseOrderStatus lookup is in sync." : $"{problems.Count} mismatch(es).");
    return problems.Count == 0;
}

Veritabanında birinin elle OnHold eklediğini ve Cancelled'ı sildiğini düşünelim:

INSERT dbo.PurchaseOrderStatuses (Id, Code, NameTr, NameEn) VALUES (6, N'OnHold', N'Beklemede', N'On hold');
DELETE FROM dbo.PurchaseOrderStatuses WHERE Id = 5;
PurchaseOrderStatus mismatch: enum 5=Cancelled has no matching row
PurchaseOrderStatus mismatch: row 6=OnHold is unknown to the code
2 mismatch(es).

Bu kontrolü health check'e ya da başlangıçta fail-fast bir adıma bağlamak ekibin kararı; önemli olan farkın sessiz kalmaması.

Masraf tipleri için ise kodda hiçbir şey yok; liste API (Application Programming Interface) üzerinden yönetiliyor. Angular'daki dropdown verisini /api/lookups/expense-types'tan alıyor, admin ekranı da ekleme ve kullanımdan kaldırma endpoint'lerini çağırıyor:

app.MapGet("/api/lookups/expense-types", async (string? lang, HrPortalContext db) =>
    await db.ExpenseTypes
        .Where(t => t.IsActive)
        .OrderBy(t => t.SortOrder)
        .Select(t => new { t.Id, t.Code, Name = lang == "tr" ? t.NameTr : t.NameEn })
        .ToListAsync());

app.MapPost("/api/admin/expense-types", async (ExpenseType body, HrPortalContext db) =>
{
    db.ExpenseTypes.Add(body);
    await db.SaveChangesAsync();
    return Results.Created($"/api/admin/expense-types/{body.Id}", new { body.Id, body.Code });
});

// Silme yok: kullanımdan kaldır, eski siparişler bozulmasın
app.MapPost("/api/admin/expense-types/{id:int}/deactivate", async (int id, HrPortalContext db) =>
    await db.ExpenseTypes.Where(t => t.Id == id)
        .ExecuteUpdateAsync(s => s.SetProperty(t => t.IsActive, false)) == 1
        ? Results.NoContent() : Results.NotFound());

Finans biriminin bugünkü isteği ve ardından bir masraf tipinin kullanımdan kaldırılması:

GET  /api/lookups/expense-types?lang=tr
[{"id":1,"code":"office-supplies","name":"Ofis malzemesi"},{"id":2,"code":"software","name":"Yazılım lisansı"},{"id":3,"code":"training","name":"Eğitim"}]

POST /api/admin/expense-types  {"code":"travel","nameTr":"Seyahat","nameEn":"Travel","sortOrder":40}
{"id":4,"code":"travel"} [201]

POST /api/admin/expense-types/2/deactivate
[204]

GET  /api/lookups/expense-types?lang=en
[{"id":1,"code":"office-supplies","name":"Office supplies"},{"id":3,"code":"training","name":"Training"},{"id":4,"code":"travel","name":"Travel"}]

GET  /api/purchase-orders/2?lang=tr
{"id":2,"status":"Approved","statusName":"Onaylandı","expenseType":"Yazılım lisansı","expenseTypeActive":false}

"Seyahat" deploy olmadan listeye girdi. "Yazılım lisansı" artık yeni siparişlerin dropdown'ında yok, ama onu kullanan 2 numaralı sipariş adını göstermeye devam ediyor. Kullanımdan kaldırmak yerine silmeye çalışsaydık, Restrict sayesinde veritabanı izin vermezdi:

Msg 547, Level 16, State 1, Server b8be943222fa, Line 1
The DELETE statement conflicted with the REFERENCE constraint "FK_PurchaseOrders_ExpenseTypes_ExpenseTypeId". The conflict occurred in database "HrPortal", table "dbo.PurchaseOrders", column 'ExpenseTypeId'.

Lookup verisi nadiren değişir ve her ekranda istenir. Her istekte veritabanına gitmek yerine IMemoryCache ya da .NET 9 ile gelen HybridCache ile cache'lemek ve admin endpoint'lerinde cache'i temizlemek doğal bir sonraki adım; bu mekanizma ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Caching: IMemoryCache, Redis ve Response Caching" yazısında anlatıldı.

Trade-off Tablosu

KriterA. Yalnız enumB. Yalnız lookup tablosuC. Enum + seed'li tablo + FKD. Smart enum class
Veri bütünlüğüZayıf; FK yok, geçersiz değer yazılabilirGüçlü; FKGüçlü; FK ve kodda tipKodda güçlü; FK için C gibi tablo gerekir
Sorgu karmaşıklığıRaporlarda CASE bloklarıJoin ile okunabilirJoin ile okunabilirConverter; raporlar için tablo gerekir
PerformansEn iyi; join yokJoin veya cacheJoin veya cacheBellekte, sorguya etkisi yok
Geri dönülebilirlikZor; saklanan sayılar ve isimler değişmez kalmalıKolay; veri değişikliğiOrta; enum ve migration birlikteOrta; converter ve kullanım yerleri
Ekip bilişsel yüküDüşük, ama raporlama ekibi için yüksekOrta; koda sihirli sayı sızma riskiOrta; senkron kontrolü bilinmeliOrta; class'ın davranışı öğrenilmeli
Deploy'suz değişiklikHayırEvetHayır (bilinçli olarak)Hayır

Ne Zaman Kullanma

Kod bir değere göre dallanıyorsa o listeyi yalnız lookup tablosuna koyma. ExpenseTypeId == 3 gibi bir koşul gördüğün an liste kapalı bir listeye dönüşmüş demektir; ya enum'la aynalanmalı (C) ya da davranış tabloya bir kolon olarak taşınmalı (RequiresReceipt gibi).

Teknik ve dışarıya hiç görünmeyen enum'lar için lookup tablosu kurma. Bir background job'ın iç state'i ya da bir [Flags] izin maskesi için tablo, senkron kontrolü ve migration yalnız maliyet. [Flags] enum'larının bitleri zaten tek bir tablo satırına karşılık gelmez.

Davranışı olmayan bir liste için smart enum yazma. Üç property'si ve sıfır method'u olan bir smart enum, bir enum artı bir sözlükten farksızdır ve EF Core'da ek bir converter getirir.

Çok dilli etiket ihtiyacı ikiden fazla dile çıkacaksa NameTr/NameEn kolonlarını çoğaltma; ayrı bir translation tablosu on ikinci yazının konusu.

Kontrol Listesi

  • Her listenin kapalı mı (kod davranışı) açık mı (iş birimi yönetir) olduğu belirlendi mi?
  • Enum değerleri açıkça yazılmış mı (Draft = 1) ve araya değer ekleme yasak mı?
  • Enum string olarak saklanıyorsa üye adlarının yeniden adlandırılmasını engelleyen bir kural ya da test var mı?
  • Lookup tablosuna bağlanan foreign key'ler NO ACTION (Restrict) mı, cascade değil mi?
  • Enum ile aynası olan tablo HasData ile aynı kaynaktan mı üretiliyor ve uygulama açılırken karşılaştırılıyor mu?
  • Kullanımdan kalkan değerler silinmek yerine IsActive = false ile mi işaretleniyor ve eski kayıtlar adlarını göstermeye devam ediyor mu?
  • Angular'daki listeler API'den mi besleniyor, yoksa kodda bir kopyası mı var?
  • Lookup verisi cache'leniyor ve admin değişikliğinde cache temizleniyor mu?

İlgili Pattern'ler ve Kaynaklar

Birincil kaynaklar: Microsoft Learn'de EF Core Value Conversions sayfası (HasConversion<string>() ve yerleşik EnumToStringConverter), EF Core Data Seeding sayfası (HasData ile model seeding), .NET Microservices: Architecture for Containerized .NET Applications rehberindeki "Use enumeration classes instead of enum types" bölümü ve Ardalis.SmartEnum paketinin dokümantasyonu.

Yayınlı serilerden C# serisinin "C# Enum: Underlying Type, [Flags], Parse ve switch" yazısı enum'ın dil tarafını, EF Core serisinin "EF Core'da Seeding: HasData ile Model Seeding ve Uygulama Tarafında Seed" ve "EF Core'da Owned Type ve Value Converter: Adres, Enum ve Strongly Typed Id" yazıları mekaniği anlatıyor. Cache tarafı için "ASP.NET Core Web API'de Caching: IMemoryCache, Redis ve Response Caching" ve ".NET 9 Yenilikleri: ASP.NET Core'da Yerleşik OpenAPI, Scalar ve HybridCache" yazıları. Serinin içinde durumların hangi sırayla değişebileceği (transition table ve state machine) dokuzuncu, çok dilli veri on ikinci, tenant'a göre değişen ayarlar on dördüncü yazıda. Durumun kendisi davranış taşımaya başladığında ise planlanan Design Patterns serisinin "State Pattern: Sipariş Durumlarını switch Yerine State Object'leriyle Yönetmek" yazısı devreye giriyor.

Küçük Bir Deneme

★☆☆ PurchaseOrderStatus'a OnHold = 6 ekle ve uygulamayı yeniden çalıştır. LookupCheck ne diyor? Bir migration üretip InsertData satırını bul.

★★☆ Enum üye adlarının değişmesini yakalayan bir test yaz: veritabanındaki PurchaseOrderStatuses.Code değerleri ile Enum.GetNames<PurchaseOrderStatus>() birebir aynı olmalı. Bu test hangi refactoring'i engeller, hangisine izin verir?

★★★ Masraf tiplerine RequiresReceipt kolonu ekle ve satın alma siparişi oluşturma endpoint'inde bu kurala göre doğrulama yap. Ardından lookup endpoint'ini HybridCache ile cache'le ve admin endpoint'lerinde cache'i temizle; kullanımdan kaldırılan bir tipin en geç ne kadar süre dropdown'da görünebileceğini hesapla.

Bu yazıyla serinin ilk bloğu kapanıyor: kimlik, sanal kayıt, sentinel ve referans değerleri. Sıradaki blok kaydın yaşam döngüsüne bakıyor ve birinci yazıda küçük bir Pattern Template ile başladığımız soruyu bu kez bütünüyle ele alıyor: soft delete'i doğru yapmak.