Entity delete interceptor with EF Core SaveChanges interceptors

Emre Teoman
Borda Technology
4 min readMay 6, 2021

--

Clean architecture is an architectural style that allows business applications to be tested and developed separately from the rest of the infrastructure. Therefore, in clean architecture, there is a certain layer for business logic, often called domain or core.

Besides core business operations, some business rules are also checked within this layer. Business logic exceptions may be thrown in case of violation of any of these rules. These controls can be done while creating or updating an entity instance. For example, checking the name field uniqueness during creating a category entity, or checking the subcategory limit when adding a subcategory to an existing category. What if we want to check some business rules while deleting an existing entity? In this article, a solution is proposed by using the SaveChanges interceptors that come with EF Core 5.0.

Why we need an entity? What do we do with an entity? Often we need to store entities in persistent storage by performing some fundamental operations called CRUD (create-read-update-delete). Although, doing business operations and saving an entity are different scenarios, create, update and delete operations may require some business rule validations. In the case of creating or updating an entity, business logic can be handled and rules can be checked on an entity instance. If all rule conditions are satisfied, save operation will proceed. If there is any violation, an exception can be thrown before saving entity. However, deleting an entity is different scenario than creating or updating an entity because deleting is about persistence, not the instance of the entity. What if we had business rules that we needed to check during an entity deletion? This article proposes an approach that allows an existing entity to be intercepted just before deleting using the EF Core Interceptors.

SaveChanges Interception

SaveChanges interception feature was announced with EF Core 5.0 version. Before moving forward, let’s remember how Microsoft explains interceptors:

Entity Framework Core (EF Core) interceptors enable interception, modification, and/or suppression of EF Core operations. This includes low-level database operations such as executing a command, as well as higher-level operations, such as calls to SaveChanges.

They are added with AddInterceptors method and registered per DbContext instance while the context is configuring. Multiple interceptors can be chained and they have executed the order provided.

public class ApplicationContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new AuditInterceptor());
}

The focus of the article is SaveChanges interception. Basically, the purpose of SaveChanges interceptors is modification or suppression of the SaveChanges operation which is intercepted.

Two methods can be intercepted. These are SaveChanges and its async version SaveChangesAsync. They are defined by the ISaveChangesInterceptor interface. There are six methods in this interface.

  • SavingChanges and SavingChangesAsync called at the start of DbContext.SaveChanges and DbContext.SaveChangesAsync.
  • SavedChanges and SavedChangesAsync called at the end of DbContext.SaveChanges and DbContext.SaveChangesAsync.
  • SaveChangesFailed and SaveChangedFailedAsync called when an exception has been thrown in DbContext.SaveChanges and DbContext.SaveChangesAsync.

If you do not need to implement all of these methods, SaveChangesInterceptor abstract class can be considered instead of ISaveChangesInterceptor interface.

To get more detailed information about interceptors:

Entity Delete Interceptor Library

In our projects, we have business rules to prevent an existing entity from being deleted. For example, to delete a category, it should not belong to any other category. In addition, for situations where we use event sourcing, it may be necessary to publish the information that an entity has been deleted. We can also create the relevant event in the interceptor.

We developed a library called EntityDeleteInterceptor using save changes interceptors. In our library, there is an interface that needs to be implemented for each entity to be intercepted. IEntityDeleteInterceptor generic interface takes the type of entity as a type parameter. It provides an interceptor method that will be called in SavingChanges and SavingChangesAsync methods in the interceptor.

public interface IEntityDeleteInterceptor<in TEntity>                           {                                          
void DeletingEntity(TEntity entity);
}

Let’s look at the example for Person entity:

public class PersonDeleteInterceptor : IEntityDeleteInterceptor<Person>
{
public void DeletingEntity(Person person)
{
if (person.FirstName == "Emre")
{
throw new BusinessException("Cannot delete person Emre");
}
}
}

All entity delete interceptors which implement IEntityDeleteInterceptor interface are scanned according to given assemblies. Then, they have registered automatically for dependency injection by calling AddEntityDeleteInterceptor method in ConfigureServices in Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
services.AddEntityDeleteInterceptor(typeof(Startup).Assembly);
}

The final part is adding entity delete interceptor to the DbContext. To add entity delete interceptor, AddEntityDeleteInterceptors extension method must be called in the OnConfiguring method in our DbContext class. This method requires a service provider instance to access registered interceptors in ConfigureServices.

public class ApplicationContext : DbContext
{
private readonly IServiceProvider _serviceProvider;

public ApplicationContext(DbContextOptions options, IServiceProvider serviceProvider)
: base(options)
{
_serviceProvider = serviceProvider;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddEntityDeleteInterceptors(_serviceProvider);
}
}

How the interceptor works?

In the library, there is an internal EntityDeleteInterceptor class that implements SaveChangesInterceptor and overrides SavingChanges and SavingChangesAsync methods. In these methods, deleted entities are obtained from change tracker. For each of these deleted entities, any implementation of the IEntityDeleteInterceptor generic interface is trying to get from the service provider. If there is an implementation, DeletingEntity method is invoked.

Project repository and a sample project that implements the library are available at:

https://github.com/bordatech/entity-delete-interceptor

Thanks for reading.

--

--