Skip to content
master
Switch branches/tags
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 

NuGet version (EF.Auditor) Build Status

EF.Auditor is a simple and lightweight library that piggybacks on top of Entity Framework Core's internal ChangeTracker to give you auditing information about added, deleted and modified entities.

Installing via NuGet

Install-Package EF.Auditor

Getting Started

The main type you want is Audit. In your unit of work, when you're done with making changes in your DbContext (i.e. adding/deleting/modifying entities or their collections), this is how you get the auditing information:

// get one audit log per changed entity
var auditLogs = Audit.GetLogs(dbContext);

// or

// get one audit log per DDD aggregate boundary
var auditLogs = Audit.GetLogs<AggregateRootBase>(dbContext);

Note that this should be done before SaveChanges or anything else that resets EF's change tracker.

With the non-generic overload, you simply get one audit log per changed entity.

In the generic overload, AggregateRootBase is the top-level entry point for gathering audit logs in a DDD sense, so you get all deep changes within a DDD aggregate boundary that happened in your unit of work in one log.

AuditLog looks like this:

// The entity this audit log belongs to.
public object Entity { get; private set; }
// The type of change this audit log holds, e.g. Added, Deleted or Modified
public AuditLogChangeType ChangeType { get; private set; }
// Audit log information in JSON format
public string ChangeSnapshot { get; private set; }

You can then use these audit logs how you want, such as persisting them to the storage of your choice.

Wait, a static class/method? what about dependency injection? How do I mock it?!

Chillax. There's an Auditor class that implements the IAuditor interface:

public interface IAuditor
{
    IReadOnlyList<AuditLog> GetLogs<TAggregateRoot>(ChangeSnapshotType changeSnapshotType = ChangeSnapshotType.Inline, Formatting changeSnapshotJsonFormatting = Formatting.None) where TAggregateRoot : class;

    IReadOnlyList<AuditLog> GetLogs(ChangeSnapshotType changeSnapshotType = ChangeSnapshotType.Inline, Formatting changeSnapshotJsonFormatting = Formatting.None);
}

And you can instantiate it with a DbContext as a dependency:

var auditor = new Auditor(dbContext);

If you prefer to use it like that, it's best to set this up with your favourite IoC container and configure it to have the same lifetime as your DbContext.

ChangeSnapshot Type

There are two formats in which the ChangeSnapshot can be generated: Inline (default) and Bifurcate which can be specified as an argument when calling GetLogs:

var auditLogs = Audit.GetLogs<AggregateRootBase>(dbContext, ChangeSnapshotType.Inline)

// or

var auditLogs = Audit.GetLogs<AggregateRootBase>(dbContext, ChangeSnapshotType.Bifurcate)

The difference is that with Bifurcate, before and after entity property values are in separate top-level "Before" and "After" trees which follow the same schema as the entity, whereas with Inline, each entity property will have inline "Before" and "After" keys which hold respective values for the property.

To get a better idea of how these look like, refer to the below samples.

ChangeSnapshot samples

Considering these sample types:

public class AggregateRootBase
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; private set; }
}

public class Person : AggregateRootBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<Thought> Thoughts { get; set; } = new List<Thought>();

    public Person()
    { }
}

public class Thought
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; private set; }
    public string Description { get; set; }

    public Thought()
    { }
}

Here are a few samples of how ChangeSnapshot can look like:

var author = new Person() { FirstName = "Saeb", LastName = "Amini" };
dbContext.Add(author);
dbContext.SaveChanges();
author.FirstName = "Kaiser";

var auditLogsBifurcate = Audit.GetLogs<AggregateRootBase>(dbContext, ChangeSnapshotType.Bifurcate);

Console.WriteLine(auditLogsBifurcate.Single().ChangeSnapshot);

/*
{
  "Before": {
    "FirstName": "Saeb"
  },
  "After": {
    "FirstName": "Kaiser"
  }
}
*/

var auditLogsInline = Audit.GetLogs<AggregateRootBase>(dbContext, ChangeSnapshotType.Inline);

Console.WriteLine(auditLogsInline.Single().ChangeSnapshot);

/*
{
  "FirstName": {
    "Before": "Saeb",
    "After": "Kaiser"
  }
}
*/
var author = new Person() { FirstName = "Saeb", LastName = "Amini" };
author.Thoughts.Add(new Thought() { Description = "Peaceful" });
dbContext.Add(author);
dbContext.SaveChanges();
author.Thoughts.Single().Description = "Ommmmmmmmmm";

var auditLogsBifurcate = Audit.GetLogs<AggregateRootBase>(dbContext, ChangeSnapshotType.Bifurcate);

Console.WriteLine(auditLogsBifurcate.Single().ChangeSnapshot);

/*
{
  "Before": {
    "Thoughts": [
      {
        "Id": 1,
        "Description": "Peaceful"
      }
    ]
  },
  "After": {
    "Thoughts": [
      {
        "Id": 1,
        "Description": "Ommmmmmmmmm"
      }
    ]
  }
}
*/

var auditLogsInline = Audit.GetLogs<AggregateRootBase>(dbContext, ChangeSnapshotType.Inline);

Console.WriteLine(auditLogsInline.Single().ChangeSnapshot);

/*
{
  "Thoughts": [
    {
      "Id": {
        "Before": 1,
        "After": 1
      },
      "Description": {
        "Before": "Peaceful",
        "After": "Ommmmmmmmmm"
      }
    }
  ]
}
*/

Note that primary keys for non-top-level entities are always included so when multiple nested children are modified, the exact entity that each bit of the JSON is referring to can be determined.

About

A simple and lightweight auditing library for Entity Framework Core that supports Domain Driven Design.

Resources

License

Releases

No releases published

Packages

No packages published

Languages