Skip to content

Instantly share code, notes, and snippets.

@spaasis
Last active April 1, 2020 07:20
Show Gist options
  • Save spaasis/7db8cfb489b828f133542991574a8438 to your computer and use it in GitHub Desktop.
Save spaasis/7db8cfb489b828f133542991574a8438 to your computer and use it in GitHub Desktop.
EF Core audit logging
using System;
using System.Collections.Generic;
namespace Infra {
public class AuditableEntity {
public string CreatedBy { get; set; } = null!; //user name, could be id etc.
public DateTime Created { get; set; }
public string? LastModifiedBy { get; set; } //user name, could be id etc.
public DateTime? LastModified { get; set; }
public virtual ICollection<EntityEvent> LogEntries { get; set; } = new HashSet<EntityEvent>();
}
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) {
//disable Updates/Deletes on our history data. This is done in order to increase the trust in the event log.
//in addition to this, the UPDATE/DELETE grants should be disabled in the database
foreach (var entry in changeTracker.Entries<EntityEvent>()) {
switch (entry.State) {
case EntityState.Modified:
throw new InvalidOperationException("Updating Log Entries is not allowed");
case EntityState.Deleted:
throw new InvalidOperationException("Deleting Log Entries is not allowed");
}
}
var ts = DateTime.Now;
var currentUserName = _userService.UserName;
foreach (var entry in changeTracker.Entries<AuditableEntity>()) {
switch (entry.State) {
case EntityState.Added:
entry.Entity.CreatedBy = currentUserFullName;
entry.Entity.Created = ts;
break;
case EntityState.Modified:
var originalValues = new Dictionary<string, object?>();
var currentValues = new Dictionary<string, object?>();
//note: Owned Properties will not work. See https://stackoverflow.com/questions/60946496/ef-core-how-to-audit-trail-with-value-objects
foreach (var prop in entry.Properties.Where(p => p.IsModified)) {
var name = prop.Metadata.Name;
originalValues.Add(name, prop.OriginalValue);
currentValues.Add(name, prop.CurrentValue);
}
entry.Entity.LastModifiedBy = currentUserFullName;
entry.Entity.LastModified = ts;
entry.Entity.LogEntries.Add(
new EntityEvent(
ts,
JsonConvert.SerializeObject(originalValues),
JsonConvert.SerializeObject(currentValues),
currentUserFullName));
break;
}
}
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Domain.ValueObjects {
public class EmailAddress { //example of a value object
public string Value { get; private set; } = null!;
private EmailAddress() {
}
public static EmailAddress Create(string value) {
if (!IsValidEmail(value)) {
throw new ArgumentException("Incorrect email address format", nameof(value));
}
return new EmailAddress {
Value = value
};
}
}
}
using Infra;
namespace Domain.Entities {
public class Entity : AuditableEntity { //all you need to do in order to enable logging
public int EnityId { get; set; }
public string Name { get; set; }
public EmailAddress Email { get; set; }
}
}
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Persistence.EfConfiguration {
internal class EntityConfiguration : IEntityTypeConfiguration<Entity> {
public void Configure(EntityTypeBuilder<Entity> entity) {
//convert value object into a property in order to allow EF ChangeTracker to spot changes to the Entity
entity.Property(e => e.Email).HasConversion(Converters.GetEmailConverter());
}
}
}
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace Infra {
public class EntityEvent {
public int EntityEventId { get; private set; }
public DateTime DateTime { get; private set; }
public string PreviousValuesJson { get; private set; } //store values as JSON in the database
public string NewValuesJson { get; private set; }
public string EditedBy { get; private set; }
public Dictionary<string, object> PreviousValues {
get {
return JsonConvert.DeserializeObject<Dictionary<string, object>>(PreviousValuesJson);
}
}
public Dictionary<string, object> NewValues {
get {
return JsonConvert.DeserializeObject<Dictionary<string, object>>(NewValuesJson);
}
}
public EntityEvent(DateTime dateTime, string previousValuesJson, string newValuesJson, string editedBy) {
DateTime = dateTime;
PreviousValuesJson = previousValuesJson;
NewValuesJson = newValuesJson;
EditedBy = editedBy;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment