Implementando soft delete em Entity Framework com Query Filters
Em muitos sistemas atuais, a exclusão definitiva de registros do banco de dados não é a abordagem mais adequada. O Soft Delete (exclusão lógica) tornou-se uma técnica essencial para manter a integridade histórica dos dados enquanto “oculta” registros que não devem mais aparecer nas consultas regulares do sistema. Neste artigo, exploraremos como implementar Soft Delete no Entity Framework Core usando Query Filters, com um exemplo prático de cadastro de produtos.
O que é Soft Delete?
Soft Delete é um padrão onde, em vez de remover fisicamente um registro do banco de dados, marcamos esse registro como excluído através de um campo (como IsDeleted ou DeletedAt). Isso permite:
- Manter o histórico completo de dados
- Possibilitar a recuperação de registros “excluídos”
- Evitar problemas de integridade referencial
- Cumprir requisitos de compliance e auditoria
Existe algo para ajudar a implementar?
Sim, o Entity Framework possui a opção de utilizarmos Query Filters, que irão atuar nas queries executadas com um filtro “invisível” para o usuário, mas aplicado diretamente sobre o contexto.
Exemplo
Vamos criar um cadastro bem simples para um cadastro de produtos, ele irá possuir 2 campos com a finalidade de indicar que um item encontra-se deletado e quando isso foi feito.
Arquivo produtos.cs
public class Product : BaseEntity
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public abstract class BaseEntity
{
public bool IsDeleted { get; set; } = false;
public DateTime? DeletedAt { get; set; }
public string TenantId { get; set; } = string.Empty;
}Você pode ver que temos 2 classes, a classe Product possui herança da classe BaseEntity.
Na classe BaseEntity temos os campos “IsDeleted” e “DeletedAt”, onde o papel do primeiro é assinalar que um item foi deletado, e o outro é armazenar a data da deleção lógica.
Mas como fazer isso funcionar? O segredo de tudo está na classe “AppDbContext” que define o nosso contexto do banco de dados.
public class AppDbContext : DbContext
{
private readonly string _currentTenantId;
public AppDbContext(DbContextOptions<AppDbContext> options, ITenantProvider tenantProvider)
: base(options)
{
_currentTenantId = tenantProvider.GetCurrentTenantId();
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// soft delete filter
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.IsDeleted);
base.OnModelCreating(modelBuilder);
}
public override int SaveChanges()
{
ProcessSoftDelete();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
ProcessSoftDelete();
return base.SaveChangesAsync(ct);
}
private void ProcessSoftDelete()
{
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAt = DateTime.UtcNow;
}
}
}
}Vejam no código acima dois detalhes:
- No método OnModelCreating é implementado o seguinte trecho
// soft delete filter
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.IsDeleted);Veja que criamos um filtro especificando que devemos visualizar apenas quando “não” é verdadeiro o campo “IsDeleted”.
- o segundo detalhe fica por conta do outro trecho onde definimos o método “ProcessSoftDelete” dentro de “SaveChangesAsync” fazendo com que toda vez que salvarmos uma entidade e ela mudar o seu estado para “deleted” ela irá inserir valores nos campos a fim de marcá-los como deletado, conforme se vê abaixo
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAt = DateTime.UtcNow;
}Portanto, isto faz com que o sistema faça de forma automática a marcação, incluindo a propriedade como “true” e a outra com a data da deleção (podemos incluir usuário também se necessário).
Mas e como consultar isso?
Existem meios de aplicar ou não o filtro, portanto nos nossos endpoints veremos como isso foi implementado:
Nossa aplicação utiliza Minimal Api para criar endpoints, então temos 3 endpoints:
1 — Lista todos os itens, deixando o filtro como padrão
2- Lista os itens ignorando o filtro, permitindo mostrar tudo que está na base.
3- Lista apenas os itens deletados, para isto, ignora-se o query filter e depois aplica o filtro “Where” para mostrar apenas os itens deletados.
Desta forma podemos manipular a forma como o Entity Framework manipula nossos dados, desabilitando os filtros através do comando “IgnoreQueryFilters”.
Melhorias do .NET 10 !
Agora com as melhorias que virão do .NET 10, teremos novas funcionalidades para Query Filters:
- incluir múltiplos filtros na mesma entidade
- permitir dar nomes para eles e desativar apenas os filtros que queremos.
Quais vantagens deste uso? para isso vou incluir no mesmo exemplo anterior 2 filtros, um filtro para delete e outro para filtrar um “Tenant” onde meus dados seriam filtrados para apenas locatários específicos (recurso muito usado em softwares vendidos na modalidade SAAS).
Vamos ao código, modificamos o contexto para estabelecer que temos 2 query filters
Mas o que é o “EntityFilterNames”? nada mais do que uma classe com valores estáticos que representam o nome dos filtros.
Além disso, voltamos ao nosso program.cs, onde agora os filtros que iremos ignorar serão “nominais”, pois iremos citar o nome do filtro que queremos desligar.
E podemos criar para a parte administrativa, endpoints que removem somente o Tenant, ou os dois.
Pronto, está aí o exemplo que o .NET 10 nos trará !
Gostou do artigo? clique no ícone👏e me siga para ver as próximas publicações !! Quer ver mais conteúdos, acesse minhas redes através do Linktree: https://linktree.com/nizzola
Referência
Se quiser saber mais, veja o manual completo no site: Filtros de Consulta Global — EF Core | Microsoft Learn
