Last active
April 1, 2020 07:20
-
-
Save spaasis/7db8cfb489b828f133542991574a8438 to your computer and use it in GitHub Desktop.
EF Core audit logging
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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