Merhaba,
Veritabanı işlemlerinde sık karşılaşılan ihtiyaçlardan biri, ekleme, güncelleme ve silme işlemlerinde otomatik tarih güncelleme ve soft delete uygulamaktır. Bu yazıda, EF Core interceptor kullanarak bu işlemleri nasıl otomatikleştirebileceğinizi göstereceğim.
Tüm entitylerimizin aşağıdaki ortak özelliklere sahip olmasını istiyoruz
-
CreatedAt → Oluşturulma zamanı
-
UpdatedAt → Güncellenme zamanı
-
DeletedAt → Soft delete zamanı
-
IsDeleted → Silinmiş mi kontrolü
Entityler
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? DeletedAt { get; set; }
public bool IsDeleted { get; set; }
}
public class Product : BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
}
DbContext ve Interceptor
EF Core 9 ile SaveChangesInterceptor veya SaveChangesAsyncInterceptor kullanarak değişiklikleri yakalayabiliriz.
Context Sınıfı
public AppDbContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(DeleteFilter(entityType.ClrType));
}
}
}
private static LambdaExpression DeleteFilter(Type type)
{
var parameter = Expression.Parameter(type, "e");
var prop = Expression.Property(parameter, nameof(BaseEntity.IsDeleted));
var condition = Expression.Equal(prop, Expression.Constant(false));
return Expression.Lambda(condition, parameter);
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
DbContextOptions, EF Core’a context’in nasıl davranacağını (hangi veritabanının kullanılacağı, connection string gibi ayarlar) bildirir ve : base(options) ifadesi bu parametrelerin DbContext temel sınıfına iletilmesini sağlar. Böylece EF Core’a “bu ayarlarla çalış” denmiş olur ve constructor içinde başka bir işlem yapılmaz. OnModelCreating metodu, EF Core tarafından model oluşturulurken çağrılır ve tablolar ile ilişkilerin yapılandırıldığı yerdir.
modelBuilder.Model.GetEntityTypes() ile DbSet’lerde tanımlı tüm entity tipleri alınır ve typeof(BaseEntity).IsAssignableFrom(entityType.ClrType) kontrolü ile yalnızca BaseEntity sınıfından türetilen entity’ler işleme dahil edilir; dolayısıyla Product ve Category gibi sınıfların BaseEntity’den türemesi gerekir.
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(...) ifadesi ile global query filter tanımlanır ve bu filtre her sorguya otomatik olarak eklenir. Buradaki amaç, yalnızca IsDeleted == false olan kayıtların sorgu sonucuna dahil edilmesidir; böylece soft delete edilmiş kayıtlar otomatik olarak filtrelenmiş olur. Bu sırada Expression.Parameter(type, "e") lambda parametresini (örneğin e => ...) tanımlar, Expression.Property(parameter, nameof(BaseEntity.IsDeleted)) ile e.IsDeleted ifadesi oluşturulur,
Expression.Equal(prop, Expression.Constant(false)) ile e.IsDeleted == false koşulu tanımlanır ve son olarak Expression.Lambda(condition, parameter) ile e => e.IsDeleted == false şeklindeki lambda ifadesi tamamlanarak global query filter’da kullanılır.
DbContextInterceptor Sınıfı
Interceptor, entitylerin durumunu kontrol eder ve tarihleri otomatik olarak set eder:
public class DbContextInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null)
return new ValueTask<InterceptionResult<int>>(result);
foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.Now;
break;
case EntityState.Modified:
if (!entry.Entity.IsDeleted)
{
entry.Entity.UpdatedAt = DateTime.Now;
entry.Property(t=>t.CreatedAt).IsModified = false;
}
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAt = DateTime.Now;
entry.Property(t => t.CreatedAt).IsModified = false;
entry.Property(t => t.UpdatedAt).IsModified = false;
break;
}
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
EF Core’da context.ChangeTracker, hafızadaki değişiklikleri takip eden mekanizmadır. Entries<BaseEntity>() kullanıldığında yalnızca BaseEntity sınıfından türeyen entity’ler alınır ve böylece eklenen, güncellenen veya silinen tüm entity’ler üzerinde döngü kurulabilir. Eğer bir entity Added durumundaysa, yani yeni eklenmişse, CreatedAt alanı otomatik olarak mevcut zaman ile set edilir. Entity Modified durumundaysa ve silinmiş değilse (IsDeleted = false), UpdatedAt alanı güncellenir; böylece soft delete uygulanmış kayıtlar güncellenmez.
Deleted durumundaki entity ise normalde fiziksel olarak veritabanından silinecek iken, burada soft delete uygulanır: gerçek silme yerine entity güncellenir (entry.State = EntityState.Modified), IsDeleted = true ile işaretlenir ve DeletedAt alanına silinme zamanı yazılır. Bu mekanizma sayesinde eklenen kayıtlara CreatedAt, güncellenen kayıtlara UpdatedAt, silinen kayıtlara ise IsDeleted = true ve DeletedAt eklenerek soft delete işlemi uygulanmış olur. Tüm bu süreç SavingChangesAsync override’ı içerisinde otomatik olarak çalıştığı için, kullanıcı veya servis herhangi bir ek işlem yapmadan tarih alanları ve soft delete yönetimi sistem tarafından otomatik şekilde gerçekleştirilir.
Kullanım Örneği
var product = new Product { Name = "Laptop", Price = 12000 };
dbContext.Products.Add(product);
await dbContext.SaveChangesAsync(); // CreatedAt otomatik set edilir
product.Price = 12500;
await dbContext.SaveChangesAsync(); // UpdatedAt otomatik set edilir
dbContext.Products.Remove(product);
await dbContext.SaveChangesAsync(); // Soft delete uygulanır, IsDeleted = true
Avantajları
-
Tekrarlayan koddan kurtulma: Her entity için tarihleri manuel set etmeye gerek yoktur.
-
Soft delete: Silinen kayıtlar veritabanında kalır, gerektiğinde geri alınabilir.
-
Global filter:IsDeleted ile sorgularda filtre otomatik uygulanır.
-
Temiz ve sürdürülebilir kod: Tüm entityler aynı interceptor tarafından yönetilir.
İlişkili Tablolar ve Soft Delete Dikkat Edilmesi Gerekenler
Soft delete, veriyi silmeden “işaretleme” yöntemiyle yönetir. İlişkili tablolar söz konusu olduğunda dikkat edilmesi gerekenler:
-
Bağlı kayıtlar: Bir kategori silindiğinde, ona bağlı ürünleri de soft delete ile işaretlemek genellikle mantıklıdır. Aksi hâlde veritabanında tutarsızlık olabilir.
-
Yeni kayıt ekleme: Silinmiş bir kategori hâlâ veritabanında durduğu için, yeni ürün eklerken teknik olarak bu kategoriye bağlanabilirsiniz. Ama çoğu zaman uygulama mantığı açısından istenmez.
-
Kontrol: Gerektiğinde soft deleted kategorileri kontrol edip, istenirse yeniden aktif yapabilirsiniz.
Özet: Soft delete ilişkili tablolarla birlikte kullanılırken, veri bütünlüğü ve uygulama mantığını göz önünde bulundurmak çok önemlidir. Yeni veri ekleme veya güncelleme yapmadan önce, mutlaka ilişkili tablodaki kaydın IsDeleted = false olduğundan emin olmak gerekir.
Diğer Bloglarımda Görüşmek Üzere 👋