Jasinski Technical Wiki

Navigation

Home Page
Index
All Pages

Quick Search
»
Advanced Search »

Contributor Links

Create a new Page
Administration
File Management
Login/Logout
Your Profile

Other Wiki Sections

Software

PoweredBy

AuditListener - Entity Framework

RSS
Modified on Tue, Jun 23, 2020, 7:05 AM by Administrator Categorized as Entity Framework Code First

Overview

This article provides a mechanism to track changes to database entities accessed, created, and updated via Entity Framework - both EF6 and EF Core

Overriding the DataModel's SaveChanges Method

public override int SaveChanges()
{
    var username = WebUser.Current?.Username ?? "(unknown)";
    new AuditListener(this, ChangeTracker).LogChanges(username);
    return base.SaveChanges();
}

AuditListener Class

The AuditListener class used above is provided below.

using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Data.Entity;
using System.Text.Json;
using DentaCAD.DAL.Queues;

//  USAGE
//  Override your DbContext.SaveChanges method, and add the following code before 
//  before the "return base.SaveChanges();" line
/*
        var username = WebUser.Current.Username; // TODO: Change this as appropriate
        new AuditListener(this, ChangeTracker).LogChanges(username);
    */
//

class AuditListener
{
    private DbContext _dbContext;
    private DbChangeTracker _changeTracker;

    public AuditListener(DbContext dbContext, DbChangeTracker changeTracker)
    {
        _dbContext = dbContext;
        _changeTracker = changeTracker;
    }
    public void LogChanges(string byUsername)
    {
        var txn = new AuditTransaction
        {
            ByUsername = WebUser.Current.Username
        };

        var persist = false;

        /* 06/15/20 - Removed EntityState.Added, because for these entities we have no values for the PK field(s). */
        /*  It doesn't matter though, because we'll either have them in the database table (if the row is unchanged), */
        /*  or in a subsequent audit record. */
        var watchFor = new List<EntityState>() { /*EntityState.Added,*/ EntityState.Modified, EntityState.Deleted };

        var ignoreEntities = new List<string>() { "SystemLog" };

        foreach (var entry in _changeTracker.Entries())
        {                
            if (!watchFor.Contains(entry.State))
            {
                continue;
            }

            persist = true;

            var ec = new EntityChange(_dbContext, entry);

            /* 06/15/20 - Added the ignoring of specific entities and those without a primary key. */
            if (ignoreEntities.Contains(ec.Name) || ec.PrimaryKey.Length == 0)
            {
                continue;
            }
            txn.EntityChanges.Add(ec);

            foreach (var prop in entry.OriginalValues.PropertyNames)
            {
                var oldValue = $"{entry.OriginalValues[prop]}";
                var newValue = $"{entry.CurrentValues[prop]}";

                if (oldValue == newValue)
                {
                    continue;
                }

                var pc = new PropertyChange
                {
                    Name = prop,
                    OldValue = oldValue,
                    NewValue = newValue
                };

                ec.PropertyChanges.Add(pc);
            }                 
        }

        if (!persist)
        {
            return;
        }

        QueueEngine.AddToAuditTrailQueue(txn);
    }
}

AuditTransaction Class

The AuditTransaction class used by the AuditListener class is provided below.

public class AuditTransaction
{
    public AuditTransaction()
    {
        EntityChanges = new List<EntityChange>();
        OccurredOn = DateTime.Now;
    }

    public string ByUsername { get; set; }
    public DateTime OccurredOn { get; set; }
    public List<EntityChange> EntityChanges { get; set; }
}

EntityChange Class

The EntityChange class used by the AuditTransaction class is provided below.

public class EntityChange
{
    public EntityChange(DbContext dbContext, DbEntityEntry entry)
    {
        PropertyChanges = new List<PropertyChange>();

        Name = entry.Entity.GetType().Name;

        Action = entry.State.ToString();

        var objectStateEntry = ((IObjectContextAdapter)dbContext).ObjectContext
            .ObjectStateManager
            .GetObjectStateEntry(entry.Entity);

        var keys = objectStateEntry.EntityKey.EntityKeyValues;

        /* 06/15/20 - Added code to handle nulls. This is expected to happend only when entities are added */
        if (keys == null)
        {
            PrimaryKey = string.Empty;
        }
        else if (keys.Count() == 1)
        {
            PrimaryKey = keys[0].Value.ToString();
        }
        else
        {
            PrimaryKey = string.Join("|", keys.Select(key => $"{key.Key}={key.Value}").ToList());                
        }
    }

    public string Name { get; set; }
    public string PrimaryKey { get; set; }
    public string Action { get; set; }
    public List<PropertyChange> PropertyChanges { get; set; }
}

PropertyChange Class

The PropertyChange class used by the EntityChange class is provided below.

public class PropertyChange
{
    public string Name { get; set; }
    public string OldValue { get; set; }
    public string NewValue { get; set; }
}

ScrewTurn Wiki version 3.0.1.400. Some of the icons created by FamFamFam. Except where noted, all contents Copyright © 1999-2020, Patrick Jasinski.