Cara Membuat Soft Delete di Entity Framework Core dengan Global Query Filter
Tags: csharp, dotnet, tutorial, indonesia
Published: 2022-12-08 06:58:18

## Permasalahan dan Business Requirement
Proses pembuatan sistem wajib hukumnya mengikuti business requirement yang telah ditentukan. Terkadang, requirement tersebut mewajibkan untuk tidak menghapus data secara permanen di database. Berbagai alasan menjadi pertimbangan, seperti
1. Tracing riwayat 
2. Backup data
3. Lebih mudah debugging code (percaya ga percaya 🤣).

## Pendekatan Pemecahan Permasalahan
Pendekatan yang dapat dilakukan adalah menggunakan metode soft delete. Metode ini sebenarnya hanya menyembunyikan data pada saat sistem berjalan dengan menambahkan flag true-false pada kolom IsDeleted di setiap table. 

## Metode Soft Delete dengan Global Query Filter
### Step 1: buat base interface dan builder nya
Hal yang pertama dilakukan adalah membuat interface dan builder sebagai basis untuk model yang akan menerapkan fitur soft delete.
Buat file `IEntityBase.cs` dan `IEntityBaseBuilder` pada folder `Models\EntityBases`.

**IEntityBase.cs**
```c#
namespace SoftDeleteTutorial.Models.EntityBases;

public interface IEntityBase
{
    Guid Id { get; set; } //ditaruh di interface karena setiap model butuh Id
    bool IsDeleted { get; set; } //flagging utk soft delete
}
```

**IEntityBaseBuilder.cs**
```c#
namespace SoftDeleteTutorial.Models.EntityBases;
public interface IEntityBaseBuilder<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class
{
}
```

### Step 2: buat abstract model dan builder yang mengimplement interface 
Lankah selanjutnya adalah membuat abstract basis model yang mengimplement interface pada **step 1**. Model yang akan dibuat adalah `EntityBase.cs` dan builder modelnya `EntityBaseBuilder.cs`.

**EntityBase**
```c#
namespace SoftDeleteTutorial.Models.EntityBases;

public abstract class EntityBase : IEntityBase //abstract class mengimplementasikan interface base
{
    public Guid Id { get; set; }
    public bool IsDeleted { get; set; }
}
```

**EntityBaseBuilder**
```c#
namespace SoftDeleteTutorial.Models.EntityBases;

public abstract class EntityBaseBuilder<TEntity> : IEntityBaseBuilder<TEntity> where TEntity : class, IEntityBase //implementasi base interface
{
    //konfigurasi global query filter
    public virtual void Configure(EntityTypeBuilder<TEntity> builder)
    {
        builder.HasQueryFilter(t => !t.IsDeleted); //filter data yang IsDeleted = 0 (false)
    }
}
```
### step 3: cara menggunakan EntityBase pada Model
Cara penggunaannya cukup simple yaitu dengan mengimplementasi interface EnitityBase dan EntityBaseBuilder pada model yang ingin menggunakan fitur soft delete. Contoh, kita membuat model `Student.cs` dan `StudentBuilder.cs`. Simpan file tersebut pada folder `Models\Entities`.

**Student.cs**
```c#
namespace SoftDeleteTutorial.Models.Entities;

public class Student : EntityBase //implement dari abstract class
{
    //tidak perlu proprty Id karena sudah ada di EntityBase
    public string Name { get; set; } = string.Empty;
    public string Nim { get; set; } = string.Empty;
}
```

**StudentBuilder.cs**
```c#
namespace SoftDeleteTutorial.Models.Entities;

public class StudentBuilder : EntityBaseBuilder<Student> //implement EntityBaseBuilder utk class model yang diinginkan
{
    // override konfigurasi yang sebelumnya telah ada query filternya
    public override void Configure(EntityTypeBuilder<Student> builder)
    {
        base.Configure(builder);
        builder.Property(s => s.Name).HasMaxLength(100);
        builder.Property(s => s.Nim).HasMaxLength(10);
        builder.HasIndex(s => s.Nim).IsUnique();
    }
}
```
### step 4: buat Ekstensi untuk tracking hapus data
Hal yang diharapkan saat soft delete adalah otomatis set IsDelete = true ketika ada penghapusan data. Kita tidak ingin setiap ada penghapusan data harus set IsDeleted=true. Oleh karena itu, kita perlu membuat ekstensi yang akan mentracking setiap penghapusan data. Kita beri nama `ChangeTrackerExtensions.cs` yang ditaruh pada folder `Extentions`.

**ChangeTrackerExtensions.cs**
```c#
using SoftDeleteTutorial.Models.EntityBases;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace SoftDeleteTutorial.Extensions;

public static class ChangeTrackerExtensions
{
    public static void SetAuditProperties(this ChangeTracker changeTracker)
    {
        changeTracker.DetectChanges();
        IEnumerable<EntityEntry> entities = changeTracker.Entries().Where
            (
                t =>
                t.Entity is IEntityBase
                && (t.State == EntityState.Deleted)
            );

        foreach (EntityEntry entry in entities)
        {
            IEntityBase entity = (IEntityBase)entry.Entity;

            if (entry.State == EntityState.Deleted)
            {
                entity.IsDeleted = true; //jika ada penghapusan data maka IsDeleted=true
                entry.State = EntityState.Modified;
            }
        }

    }
}
```
### step 5: override method savechange di ApplicationDbContext
Beberapa method yang harus dioverride ada 4 yaitu 
1. `SaveChangesAsync(CancellationToken cancellationToken = default)`
2. `SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)`
3. `SaveChanges()`
4. `SaveChanges(bool acceptAllChangesOnSuccess)`

Method tersebut akan ditambah ekstensi ChangeTracker yang sudah dibuat pada *step 4*.

**AppDbContext.cs**
```c#
namespace SoftDeleteTutorial.Models;
public class AppDbContext 
{
    public AppDbContext(DbContextOptions options) : base(options)
    {

    }
    public DbSet<Student> Students { get; set; } = default!;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        new StudentBuilder().Configure(modelBuilder.Entity<Student>());
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        ChangeTracker.SetAuditProperties();
        return await base.SaveChangesAsync(cancellationToken);
    }

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {
        ChangeTracker.SetAuditProperties();
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

    public override int SaveChanges()
    {
        ChangeTracker.SetAuditProperties();
        return base.SaveChanges();
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        ChangeTracker.SetAuditProperties();
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }
}
```

### step 6: sudah selesai ❤️


## Unique dan Index Coloumn
Jika menggunakan soft delete ada beberapa hal yang harus disesuaikan terutama untuk kolom yang unique atau berstatus index. Soft delete yang tidak menghapus secara fisik, artinya jika ada data baru yang ditambahkan dengan data yang sama bisa jadi kena error dulicate key. Contohnya, property Nim pada model Student adalah unique dengan data `new Student {Name="Rahmat", Nim="123"}`. Lalu dihapus tuh student atas nama *Rahmat*. Nah, jika ada data dengan Nim="123" maka akan error karena dianggap duplicate key. Oleh karena itu kita perlu mengecek ketika insert data Student dengan `IgnoreQueryFilter()`. Contoh kodingan seperti berikut.

```c#
private async Task<Student> InsertStudent(StudentInput input, CancellationToken cancellationToken)
    {
        //IgnoreQueryFilter() untuk mengambil semua data tanpa melihat flag IsDeleted.
        var student = await _dbContext.Students.IgnoreQueryFilters().SingleOrDefaultAsync(s => s.Nim == input.Nim, cancellationToken); 
        if (student != null && !student.IsDeleted)
            throw new BusinessLogicException($"NIM {input.Nim} sudah terdaftar. Silahkan daftar mahasiswa lainnya!");
        else if (student != null && student.IsDeleted)
        {
            student.IsDeleted = false; //*membangkitkan* yang sudah terhapus
            await _dbContext.SaveChangesAsync(cancellationToken);
            return student;
        }
        var newStudent = new Student { Name = input.Name, Nim = input.Nim };
        await _dbContext.Students.AddAsync(newStudent, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);
        return newStudent;
    }
```

### Saran dan Masukan
Jika terdapat pertanyaan pada artikel ini bisa bertanya di kolom komentar. Saya juga sangat menerima jika ada masukan dan kritik, siapa tahu anda bisa kasih saran yang lebih baik atau mendapati kesalahan code. Terimakasih semuanya 😊👍