AuditLog.Abstractions 0.5.0

dotnet add package AuditLog.Abstractions --version 0.5.0
                    
NuGet\Install-Package AuditLog.Abstractions -Version 0.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AuditLog.Abstractions" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AuditLog.Abstractions" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="AuditLog.Abstractions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AuditLog.Abstractions --version 0.5.0
                    
#r "nuget: AuditLog.Abstractions, 0.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AuditLog.Abstractions@0.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AuditLog.Abstractions&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=AuditLog.Abstractions&version=0.5.0
                    
Install as a Cake Tool

AuditLog

Publish to NuGet

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 dados
  • PacienteAuditLogDescriptor — mapeia Paciente → PacienteAuditLog
  • PacienteAuditLogEntityMap — 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() (usa DateResolver.ToDisplayDate internamente) — pra campos de data/hora gravados sem cultura/formato explícito pelo AuditLog.Generator. O parse roda sob a mesma CultureInfo.CurrentCulture do 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>() (usa EnumDisplayResolver.ToDisplayName internamente) — 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 (usa AuditOperation) — sem dependência de EF Core nem de nenhum framework web.
  • Consome as tabelas *AuditLog produzidas por [GenerateAuditLog] (AuditLog.Generator) — não gera nem grava nada, só lê e resolve o que já foi persistido.
  • Mapear recebe qualquer IEnumerable<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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on AuditLog.Abstractions:

Package Downloads
AuditLog.EntityFrameworkCore

EF Core integration for AuditLog — AuditSaveInterceptor, service collection extensions, and model builder hooks.

AuditLog.Historico

Companion package for AuditLog.Abstractions — builds a display-friendly, field-level change history (AuditHistoricoEntrada) from [GenerateAuditLog] entities, with enum and date resolvers for readable values.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 275 8/11/2026
0.4.12 112 8/10/2026
0.4.11 555 7/28/2026
0.4.10 118 7/28/2026
0.4.9 126 7/28/2026
0.4.8 119 7/28/2026
0.4.7 325 7/23/2026
0.4.6 130 7/23/2026
0.4.5 144 7/23/2026
0.4.4 133 7/22/2026
0.4.3 132 7/22/2026
0.4.2 112 7/22/2026
0.4.1 116 7/22/2026
0.4.0 122 7/22/2026
0.3.3 152 7/20/2026
0.3.1 108 7/20/2026
0.3.0 123 7/20/2026
0.2.0 134 6/30/2026
0.1.0 126 6/30/2026