[Yugong Series] September 2023.NET/C# Knowledge Points-EF Configuration Soft Deletion


Preface

Soft deletion refers to marking data in a database rather than actually deleting data records. Marking a data record is usually accomplished by adding an extra field (such as "deleted_at") to indicate that the data record has been marked for deletion. The role of soft delete is to preserve data integrity and history while avoiding permanent deletion of data records. This helps in recovering data records and analyzing historical data trends. Soft deletes can also reduce physical deletions in the database, improving data security and system performance.

1. EF configuration soft deletion

1. Introduction to Interceptor

EF's Interceptor is an interceptor that can intervene or modify database operations before and after they are executed to better control and improve database access behavior.

Using EF's Interceptor requires the following steps:

1. Define an interceptor class, inherited from DbCommandInterceptorand IDbCommandTreeInterceptorinterface, where the former is used to intercept SQL commands, and the latter is used to intercept the database operation tree.

2. In the interceptor class, implement the methods that need to be intercepted or modified, such as ScalarExecuting, ScalarExecuted, NonQueryExecuting, NonQueryExecutedand other methods. In these methods, you can modify SQL statements, add parameters, record logs and other operations.

3. Register the interceptor in the EF configuration file, for example:

DbInterception.Add(new MyInterceptor());

At this time, EF will automatically call the corresponding method in the interceptor to intervene and modify the database operation.

It should be noted that interceptors may have a certain impact on performance, so they need to be used with caution and their impact evaluated in actual tests. Additionally, operations within interceptors may have unintended side effects and require careful consideration and testing.

EF's Interceptor is a set of APIs that can be used to intercept commands and results when manipulating a database. Its main function is to monitor the communication between EF and the database, intercept the execution of SQL statements and the return of results, and modify or record them to achieve some special functions.

According to the scope and time of its action, Interceptors can be divided into the following categories:

  1. DbCommandInterceptor: This interceptor is designed to intercept and modify commands sent by EF to the database.

  2. DbContextInterceptor: This interceptor is designed to intercept before or after performing operations such as reading/writing to the database.

  3. DbContextTransactionInterceptor: This interceptor is designed to intercept database transactions between multiple operations.

  4. SaveChangesInterceptor: This interceptor is designed to intercept changes in the process before or after the save changes operation is performed. It can intercept add, modify and delete operations and modify or record them.

This article mainly introduces the SaveChangesInterceptor interceptor

2.Basic use of SaveChangesInterceptor interceptor

EF's SaveChangesInterceptor interceptor is a DbContextInterceptor, mainly used to intercept changes in the process before or after the SaveChanges operation is executed. It can intercept addition, modification and deletion operations, and modify or record them to achieve some special functions.

To use SaveChangesInterceptor, you need to implement the BeforeSaveChanges and AfterSaveChanges methods in the DbContextInterceptor class to intercept/modify before and after the SaveChanges operation is performed respectively.

The following is an example of SaveChangesInterceptor, which implements the function of automatically adding creation time and modification time to an entity when it is created:

public class TimestampInterceptor : SaveChangesInterceptor
{
    
    
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData, InterceptionResult<int> result)
    {
    
    
        foreach (var entry in eventData.Context.ChangeTracker.Entries())
        {
    
    
            if (entry.State == EntityState.Added ||
                entry.State == EntityState.Modified)
            {
    
    
                entry.Property("ModifiedTime").CurrentValue = DateTime.UtcNow;
                
                if (entry.State == EntityState.Added)
                {
    
    
                    entry.Property("CreatedTime").CurrentValue = DateTime.UtcNow;
                }
            }
        }

        return base.SavingChanges(eventData, result);
    }
}

In the above code, we first inherit SaveChangesInterceptor and override its SavingChanges method. In this method, we traverse all EntityEntry in ChangeTracker and detect whether its status is added or modified (the Deleted status does not require adding time information). If it is an add or modify operation, set the current time on the ModifiedTime attribute of the corresponding entity, and set the CreatedTime attribute at the same time when adding the operation. Finally, the result of the interception operation is returned.

In order to use the above Interceptor, you need to add the corresponding Interceptor to the Options of DbContext:

var optionsBuilder = new DbContextOptionsBuilder<BlogContext>()
    .UseSqlServer(connectionString)
    .AddInterceptors(new TimestampInterceptor());

In this way, when the SaveChanges operation is triggered, the SavingChanges method of TimestampInterceptor will be executed to realize the function of automatically adding time information.

It should be noted that the use of Interceptor requires careful design and testing based on actual conditions to avoid unexpected consequences.

3. Case implementation

3.1 Define soft deletion interface

public interface ISoftDeleteEntity
{
    
    
}

public interface ISoftDeleteEntityWithDeleted : ISoftDeleteEntity
{
    
    
 bool IsDeleted {
    
     get; set; }
}

3.2 Define soft delete interceptor

public sealed class SoftDeleteInterceptor : SaveChangesInterceptor
{
    
    
    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
    
    
        OnSavingChanges(eventData);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
        CancellationToken cancellationToken = new CancellationToken())
    {
    
    
        OnSavingChanges(eventData);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private static void OnSavingChanges(DbContextEventData eventData)
    {
    
    
        ArgumentNullException.ThrowIfNull(eventData.Context);
        eventData.Context.ChangeTracker.DetectChanges();
        foreach (var entityEntry in eventData.Context.ChangeTracker.Entries())
        {
    
    
            if (entityEntry is {
    
     State: EntityState.Deleted, Entity: ISoftDeleteEntityWithDeleted softDeleteEntity })
            {
    
    
                softDeleteEntity.IsDeleted = true;
                entityEntry.State = EntityState.Modified;
            }
        }
    }
}

3.3 DbContext global query filtering

public class SoftDeleteSampleContext : DbContext
{
    
    
    public SoftDeleteSampleContext(DbContextOptions<SoftDeleteSampleContext> options) : base(options)
    {
    
    
    }

    public virtual DbSet<SoftDeleteEntity> TestEntities {
    
     get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    
    
        //apply global query filter
        modelBuilder.Entity<SoftDeleteEntity>().HasQueryFilter(x => x.IsDeleted == false);
        base.OnModelCreating(modelBuilder);
    }
}

public class SoftDeleteEntity : ISoftDeleteEntityWithDeleted
{
    
    
    public int Id {
    
     get; set; }
    public string Name {
    
     get; set; } = "test";
    public bool IsDeleted {
    
     get; set; }
}

We can add a global query filter to the objects to be soft deleted, so that every time we query, we can automatically filter the objects that have been soft deleted.

3.4 Application interceptor

var services = new ServiceCollection();
services.AddLogging(loggingBuilder =>
{
    
    
    loggingBuilder.AddConsole();
});
services.AddDbContext<SoftDeleteSampleContext>(options =>
{
    
    
    options
        .UseSqlite("Data Source=SoftDeleteTest.db")
        .AddInterceptors(new SoftDeleteInterceptor());
});
using var serviceProvider = services.BuildServiceProvider();
var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<SoftDeleteSampleContext>();
// initialize
context.Database.EnsureCreated();
context.TestEntities.IgnoreQueryFilters().ExecuteDelete();
context.SaveChanges();

// add test data
context.TestEntities.Add(new SoftDeleteEntity()
{
    
    
    Id = 1,
    Name = "test" 
});
context.SaveChanges();

// remove test
var testEntity = context.TestEntities.Find(1);
ArgumentNullException.ThrowIfNull(testEntity);
context.TestEntities.Remove(testEntity);
context.SaveChanges();

// query
var entities = context.TestEntities.AsNoTracking().ToArray();
Console.WriteLine(entities.ToJson());

// query without query filter
entities = context.TestEntities.AsNoTracking().IgnoreQueryFilters().ToArray();
Console.WriteLine(entities.ToJson());

// cleanup test db
context.Database.EnsureDeleted();

Guess you like

Origin blog.csdn.net/aa2528877987/article/details/132871075