AuditLog.EntityFrameworkCore.SoftDelete
0.5.0
dotnet add package AuditLog.EntityFrameworkCore.SoftDelete --version 0.5.0
NuGet\Install-Package AuditLog.EntityFrameworkCore.SoftDelete -Version 0.5.0
<PackageReference Include="AuditLog.EntityFrameworkCore.SoftDelete" Version="0.5.0" />
<PackageVersion Include="AuditLog.EntityFrameworkCore.SoftDelete" Version="0.5.0" />
<PackageReference Include="AuditLog.EntityFrameworkCore.SoftDelete" />
paket add AuditLog.EntityFrameworkCore.SoftDelete --version 0.5.0
#r "nuget: AuditLog.EntityFrameworkCore.SoftDelete, 0.5.0"
#:package AuditLog.EntityFrameworkCore.SoftDelete@0.5.0
#addin nuget:?package=AuditLog.EntityFrameworkCore.SoftDelete&version=0.5.0
#tool nuget:?package=AuditLog.EntityFrameworkCore.SoftDelete&version=0.5.0
AuditLog
Biblioteca de auditoria automática para EF Core com Source Generators Roslyn.
Pacotes
| Pacote | Descrição |
|---|---|
AuditLog.Abstractions |
Contratos: AuditConfigurator<T>, IAuditDescriptor, builders |
AuditLog.EntityFrameworkCore |
Integração EF Core: AuditSaveInterceptor, extensions |
AuditLog.Generator |
Source generator — gera *AuditLog, maps, descriptors |
AuditLog.Historico |
Transforma as tabelas *AuditLog em histórico legível pro usuário final |
AuditLog.EntityFrameworkCore.SoftDelete |
Runtime: interfaces, interceptor, query filters para soft delete |
AuditLog.Generator.SoftDelete |
Source generator — gera handlers tipados de cascade/restrict/set-null |
AuditLog — Auditoria de Entidades
1. Defina um configurador
[GenerateAuditLog]
public sealed class PacienteAuditConfigurator : AuditConfigurator<Paciente>
{
public PacienteAuditConfigurator()
{
For(x => x.Id).Key();
For(x => x.Nome).HasMaxLength(200).IsRequired();
For(x => x.Cpf).Sensitive().HasMaxLength(11);
For(x => x.DataAtualizacao).Ignore();
}
}
2. Adicione o interceptor no DbContext
public class AppDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new AuditSaveInterceptor());
}
}
3. O generator produz automaticamente
PacienteAuditLog— tabela de auditoria com snapshot dos dadosPacienteAuditLogDescriptor— mapeiaPaciente → PacienteAuditLogPacienteAuditLogEntityMap— EF Core configuration (column types, max length)ServiceCollectionExtensions.AddGeneratedAuditLogs()— DI registration
AuditLog.Historico — Histórico Legível
A tabela *AuditLog gerada por [GenerateAuditLog] guarda um snapshot textual cru de cada revisão (CamposAlteradosJson + colunas) — ótimo pra auditoria/consulta técnica, mas não pra mostrar direto ao usuário final: FK aparece como Id, enum como Enum.ToString(), data sem formatação fixa, booleano como "True"/"False". O AuditLog.Historico lê esse snapshot e resolve os campos alterados pra texto legível — o material pronto pra uma tela de "ver histórico" de um registro.
Instalação
<ItemGroup>
<PackageReference Include="AuditLog.Historico" Version="1.0.0" />
</ItemGroup>
1. Configure os campos com AuditHistoricoConfigurator
var mapper = new AuditHistoricoMapper<ProdutoAuditLog>(cfg =>
{
cfg.For(x => x.Nome).HasLabel("Nome");
cfg.For(x => x.CategoriaId).HasLabel("Categoria").ResolveGuidLookup(categoriaPorId);
cfg.For(x => x.Status).HasLabel("Status").ResolveEnum<StatusProduto>();
cfg.For(x => x.Ativo).HasLabel("Ativo").ResolveBool();
cfg.For(x => x.AtualizadoEm).HasLabel("Atualizado em").ResolveData();
cfg.For(x => x.FotoId).HasLabel("Foto").IsArquivo();
});
For(x => x.Campo) é o ponto de entrada — mesma pegada do AuditConfigurator<T>.For do AuditLog.Abstractions, de propósito, pra ficar familiar pra quem já configura auditoria com esse pacote. Campo nunca configurado aqui ainda aparece no histórico, só que com o nome cru da propriedade como rótulo e sem nenhuma resolução no valor.
Métodos do AuditCampoBuilder<TLog> retornado por For:
| Método | Efeito |
|---|---|
HasLabel(string label) |
Rótulo exibido pro campo. Sem isso, usa o nome cru da propriedade. |
IsArquivo() |
Marca o campo como referência a um arquivo (ex: Id de upload), não texto pra exibir cru. |
ResolveWith(Func<string?, string?> resolver) |
Resolve o valor cru com uma função arbitrária. |
ResolveEnum<TEnum>() |
Resolve um valor gravado como Enum.ToString() (convenção do AuditLog.Generator pra campos de enum) pro [Display(Name=...)] do membro correspondente. Sem o atributo, ou se o valor não bater com nenhum membro, mantém o valor cru. |
ResolveLookup(IReadOnlyDictionary<string, string> mapa) |
Resolve por um dicionário simples (string → label). Chave ausente mantém o valor cru. |
ResolveGuidLookup(IReadOnlyDictionary<Guid, string> descricoesPorId) |
Resolve uma FK persistida como Guid (string) pra sua descrição, via dicionário Id → descrição (tipicamente carregado uma vez por chamada, só com os IDs vistos no lote de logs). Id sem correspondência (ex: registro referenciado foi excluído depois) mantém o valor cru. |
ResolveBool(string valorVerdadeiro = "Sim", string valorFalso = "Não") |
Resolve um valor gravado como Convert.ToString(bool) ("True"/"False") pro rótulo desejado. Qualquer outro valor (incluindo null) passa direto. |
ResolveData() |
Resolve data/hora gravada via Convert.ToString(...) pro formato fixo dd/MM/yyyy (ou dd/MM/yyyy HH:mm:ss quando o valor original carrega horário diferente de meia-noite). Valor que não faz parse mantém o valor cru. |
2. Resolvers prontos
ResolveData()(usaDateResolver.ToDisplayDateinternamente) — pra campos de data/hora gravados sem cultura/formato explícito peloAuditLog.Generator. O parse roda sob a mesmaCultureInfo.CurrentCulturedo processo vigente na leitura; como nem a gravação (AuditSaveInterceptor) nem este resolver fixam cultura explícita, as duas rodam sob a mesma cultura do processo, o que torna o round-trip seguro independente de qual cultura seja essa.ResolveEnum<TEnum>()(usaEnumDisplayResolver.ToDisplayNameinternamente) — resolve o nome do membro (Enum.ToString()) pro[Display(Name=...)]; sem esse atributo, tenta[Description]; sem nenhum dos dois, mantém o valor cru.
Campo sem nenhum resolver configurado aparece no histórico com o valor exatamente como foi persistido na tabela *AuditLog, sem nenhuma transformação — retornar o próprio valor de entrada é o fallback seguro nesse caso.
3. Monte o histórico com AuditHistoricoMapper<TLog>
IReadOnlyList<AuditHistoricoEntrada<ProdutoAuditLog>> entradas = mapper.Mapear(logs);
TLog precisa seguir a convenção fixa que o AuditLog.Generator sempre produz nos tipos *AuditLog: propriedades Id (long), Operacao (AuditOperation), OcorridoEm (DateTimeOffset), UsuarioId/CorrelationId/CamposAlteradosJson (string?) e AuditLogAnteriorId (long?). Se TLog não seguir essa convenção, o construtor do mapper falha cedo com uma mensagem indicando qual propriedade está faltando, em vez de deixar o mapeamento silenciosamente incompleto.
Mapear espera o lote completo de revisões relevantes: a revisão "anterior" de cada log é resolvida via AuditLogAnteriorId dentro do próprio lote passado, sem nenhuma consulta adicional. Só entradas com Operacao == AuditOperation.Modified têm CamposAlterados preenchido — Added/Deleted retornam lista vazia.
Resultado, um AuditHistoricoEntrada<TLog> por log:
| Propriedade | Descrição |
|---|---|
Id |
Id da linha na tabela *AuditLog. |
Operacao |
AuditOperation (Added/Modified/Deleted). |
OcorridoEm |
DateTimeOffset do evento. |
UsuarioId |
Autor da alteração, como persistido pelo AuditSaveInterceptor. |
CorrelationId |
Correlation id da requisição/job que gerou o log. |
CamposAlterados |
Lista de AuditHistoricoCampo — só preenchida quando Operacao == Modified. |
Log |
A linha *AuditLog original — nada no formato comum cobre concerns específicos da entidade (ex: qual participante foi alterado), então o chamador recorre a este valor pra montar a descrição/agrupamento que só ele conhece. |
E cada AuditHistoricoCampo:
| Propriedade | Descrição |
|---|---|
Campo |
Rótulo configurado via HasLabel, ou o nome cru da propriedade se não configurado. |
ValorAnterior |
Valor antes da alteração, já resolvido — null se não havia revisão anterior. |
ValorNovo |
Valor depois da alteração, já resolvido. |
EhArquivo |
true quando o campo foi marcado com IsArquivo() — sinaliza pro consumidor (ex: a UI) que o valor é um identificador de arquivo, não texto pra exibir cru. |
Exemplo completo
// ProdutoAuditLog é gerado por [GenerateAuditLog] a partir de um AuditConfigurator<Produto>.
var logs = await db.Set<ProdutoAuditLog>()
.AsNoTracking()
.Where(log => log.ProdutoId == produtoId)
.ToListAsync(ct);
// Lookup construído só com os IDs de categoria que aparecem no lote — nunca a tabela inteira.
var categoriaIds = logs
.SelectMany(l => new[] { l.CategoriaId })
.Where(v => Guid.TryParse(v, out _))
.Select(v => Guid.Parse(v!))
.Distinct()
.ToList();
var categoriaPorId = await db.Categorias
.AsNoTracking()
.Where(c => categoriaIds.Contains(c.Id))
.ToDictionaryAsync(c => c.Id, c => c.Nome, ct);
var mapper = new AuditHistoricoMapper<ProdutoAuditLog>(cfg =>
{
cfg.For(x => x.Nome).HasLabel("Nome");
cfg.For(x => x.CategoriaId).HasLabel("Categoria").ResolveGuidLookup(categoriaPorId);
cfg.For(x => x.Status).HasLabel("Status").ResolveEnum<StatusProduto>();
cfg.For(x => x.Ativo).HasLabel("Ativo").ResolveBool();
cfg.For(x => x.AtualizadoEm).HasLabel("Atualizado em").ResolveData();
});
var entradas = mapper.Mapear(logs);
Uma entrada resultante (simplificada):
new AuditHistoricoEntrada<ProdutoAuditLog>(
Id: 42,
Operacao: AuditOperation.Modified,
OcorridoEm: DateTimeOffset.Parse("2026-08-10T14:32:00-03:00"),
UsuarioId: "3f2a1c4e-7b21-4e9a-9c31-2a8f0b1d5e6a",
CorrelationId: "9b7e2f31-...",
CamposAlterados:
[
new AuditHistoricoCampo("Categoria", ValorAnterior: "Descartáveis", ValorNovo: "Insumos", EhArquivo: false),
new AuditHistoricoCampo("Ativo", ValorAnterior: "Sim", ValorNovo: "Não", EhArquivo: false),
new AuditHistoricoCampo("Atualizado em", ValorAnterior: "10/08/2026 11:00:00", ValorNovo: "10/08/2026 14:32:00", EhArquivo: false)
],
Log: /* a linha ProdutoAuditLog original */);
Cabe ao consumidor (controller, DTO de aplicação) projetar AuditHistoricoEntrada<TLog> pro formato exposto na API — o pacote não define esse contrato de saída de propósito, porque cada aplicação tem seus próprios campos de exibição (ex: nome do usuário resolvido a partir de UsuarioId, descrição textual da operação).
Relação com o resto da suíte
- Depende só de
AuditLog.Abstractions(usaAuditOperation) — sem dependência de EF Core nem de nenhum framework web. - Consome as tabelas
*AuditLogproduzidas por[GenerateAuditLog](AuditLog.Generator) — não gera nem grava nada, só lê e resolve o que já foi persistido. Mapearrecebe qualquerIEnumerable<TLog>já carregado — a origem das linhas (EF Core, outra fonte) é responsabilidade do chamador.
SoftDelete — Exclusão Lógica
Dois pacotes complementares:
| Package | Função |
|---|---|
AuditLog.EntityFrameworkCore.SoftDelete |
Runtime: interceptor, interfaces, query filters |
AuditLog.Generator.SoftDelete |
Source generator (opcional): gera handlers com tipagem forte |
Instalação
<ItemGroup>
<PackageReference Include="AuditLog.EntityFrameworkCore.SoftDelete" Version="1.0.0" />
<PackageReference Include="AuditLog.Generator.SoftDelete" Version="1.0.0" />
</ItemGroup>
1. Implemente ISoftDeleteEntity nas entidades
public class Paciente : ISoftDeleteEntity
{
public Guid Id { get; set; }
public string Nome { get; set; }
// Obrigatório para soft delete
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
// Relacionamentos (Fluent API configurada no DbContext)
public List<Notificacao> Notificacoes { get; set; } = [];
}
2. Marque o DbContext com [GenerateSoftDelete]
using AuditLog.EntityFrameworkCore.SoftDelete;
[GenerateSoftDelete]
public class AppDbContext : DbContext
{
public DbSet<Paciente> Pacientes { get; set; }
public DbSet<Notificacao> Notificacoes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Paciente>(e =>
{
e.HasMany(x => x.Notificacoes)
.WithOne(x => x.Paciente)
.HasForeignKey(x => x.PacienteId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.ApplySoftDeleteQueryFilter();
}
}
3. Registre na DI
// Com o source generator (handlers tipados)
var registry = new SoftDeleteHandlerRegistry();
registry.AddGeneratedSoftDeleteHandlers();
services.AddSoftDelete(registry);
// Ou sem o generator (fallback via reflection)
services.AddSoftDelete();
4. Use normalmente
db.Pacientes.Remove(paciente);
await db.SaveChangesAsync();
// → IsDeleted = true, DeletedAt = now
// → Cascade: Notificacoes também são marcadas como deletadas
// → Query filter global: db.Pacientes retorna apenas não-deletados
Comportamentos por FK
OnDelete() |
Efeito |
|---|---|
Cascade |
Dependentes são soft-deletados recursivamente |
Restrict |
Lança RestrictDeleteViolationException se houver dependentes |
SetNull |
FK dos dependentes é setada como null |
Convenções (quando OnDelete não é especificado)
| Navigation | Comportamento |
|---|---|
Collection (List<T>) |
Cascade |
Reference (T) |
Restrict |
FK nullable (Guid?) |
SetNull |
Consultas
// Query filter automático — só não-deletados
db.Pacientes.ToList();
// Incluir deletados
db.Pacientes.IgnoreQueryFilters().ToList();
Suporte a herança indireta de IEntityTypeConfiguration<T>
O gerador detecta entity maps que implementam IEntityTypeConfiguration<T> através de toda a cadeia de herança, incluindo casos como AuditEntityMap<T> → IContextEntityMap<T> → IEntityTypeConfiguration<T>. Isso funciona tanto com ApplyConfiguration(new ConcreteEntityMap()) quanto com ApplyConfigurationsFromAssembly().
Descoberta de entidades sem DbSet<T>
O gerador descobre entidades mesmo quando o DbContext não possui propriedades DbSet<T>, desde que as entidades sejam registradas via modelBuilder.Entity<T>() no OnModelCreating ou via ApplyConfiguration/ApplyConfigurationsFromAssembly com entity maps que implementam IEntityTypeConfiguration<T>.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.5.0 | 65 | 8/11/2026 |
| 0.4.12 | 78 | 8/10/2026 |
| 0.4.11 | 463 | 7/28/2026 |
| 0.4.10 | 96 | 7/28/2026 |
| 0.4.9 | 112 | 7/28/2026 |
| 0.4.8 | 93 | 7/28/2026 |
| 0.4.7 | 299 | 7/23/2026 |
| 0.4.6 | 105 | 7/23/2026 |
| 0.4.5 | 119 | 7/23/2026 |
| 0.4.4 | 112 | 7/22/2026 |
| 0.4.3 | 104 | 7/22/2026 |
| 0.4.2 | 82 | 7/22/2026 |
| 0.4.1 | 92 | 7/22/2026 |
| 0.4.0 | 102 | 7/22/2026 |
| 0.3.3 | 122 | 7/20/2026 |
| 0.3.1 | 86 | 7/20/2026 |
| 0.3.0 | 96 | 7/20/2026 |
| 0.2.0 | 121 | 6/30/2026 |