28

I am building an MVC 4 application, using EF 5. I need to do an audit trail, ie log any changes that end users make.

I have asked this question a few times, but haven't really gotten a satisfying answer before. So I am adding a lot more details in hoping to get somewhere..

currently I have multiple repositories

ie

 public class AuditZoneRepository : IAuditZoneRepository
    {
        private AISDbContext context = new AISDbContext();


        public int Save(AuditZone model, ModelStateDictionary modelState)
        {
            if (model.Id == 0)
            {
                context.AuditZones.Add(model);
            }
            else
            {
                var recordToUpdate = context.AuditZones.FirstOrDefault(x => x.Id == model.Id);
                if (recordToUpdate != null)
                {
                    recordToUpdate.Description = model.Description;
                    recordToUpdate.Valid = model.Valid;
                    recordToUpdate.ModifiedDate = DateTime.Now;
                }
            }

            try
            {
                context.SaveChanges();
                return 1;
            }
            catch (Exception ex)
            {
                modelState.AddModelError("", "Database error has occured.  Please try again later");
                return -1;
            }
        }
    }



    public class PostcodesRepository : IPostcodesRepository
    {
        private AISDbContext context = new AISDbContext();


        public int Save(Postcodes model, ModelStateDictionary modelState)
        {
            if (model.Id == 0)
            {
                context.Postcodes.Add(model);
            }
            else
            {
                var recordToUpdate = context.Postcodes.FirstOrDefault(x => x.Id == model.Id);
                if (recordToUpdate != null)
                {
                    recordToUpdate.Suburb = model.Suburb;
                    recordToUpdate.State = model.State;
                    recordToUpdate.Postcode = model.Postcode;
                    recordToUpdate.AuditZoneId = model.AuditZoneId;
                    recordToUpdate.ModifiedDate = DateTime.Now;
                }
            }

            try
            {
                context.SaveChanges();
                return 1;
            }
            catch (Exception ex)
            {
                modelState.AddModelError("", "Database error has occured.  Please try again later");
                return -1;
            }
        }



    }

Now I know for me to add the code to check to see if there are any changes i need to add it in the try of the save. Before the context.SaveChanges().

But currently I have 10 repos. I don't really want to add code to 10 different places. As this code will do exactly the same thing. I want to somehow have a baseclass that the repos inherit from.

any help? any sample code? any pointers?

would be appreciated. I am sure other people would have done this before

I am mappying my keys, relationships and tables like so

 public class AuditZoneMap : EntityTypeConfiguration<AuditZone>
    {
        public AuditZoneMap()
        {
            // Primary Key
            HasKey(t => t.Id);


            // Properties
            Property(t => t.Description)
                .HasMaxLength(100);


            // Table & Column Mappings
            ToTable("AuditZone");
            Property(t => t.Id).HasColumnName("Id");
            Property(t => t.Description).HasColumnName("Description");
            Property(t => t.Valid).HasColumnName("Valid");          
            Property(t => t.CreatedDate).HasColumnName("CreatedDate");
            Property(t => t.CreatedBy).HasColumnName("CreatedBy");
            Property(t => t.ModifiedDate).HasColumnName("ModifiedDate");
            Property(t => t.ModifiedBy).HasColumnName("ModifiedBy");

            // Relationships        
            HasOptional(t => t.CreatedByUser)
               .WithMany(t => t.CreatedByAuditZone)
               .HasForeignKey(d => d.CreatedBy);

            HasOptional(t => t.ModifiedByUser)
                .WithMany(t => t.ModifiedByAuditZone)
                .HasForeignKey(d => d.ModifiedBy);


        }
    }
user2206329
  • 2,792
  • 10
  • 54
  • 81
  • 1
    Or... you dont create!... just get this nuget package instead. http://www.nuget.org/packages/trackerenableddbcontext – Rosdi Kasim Jan 23 '15 at 03:07
  • 2
    Or this one: [Audit.EntityFramework](https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.EntityFramework#auditentityframework) – thepirat000 Sep 14 '16 at 03:50

8 Answers8

33

What I recommend you is to use the ChangeTracker property in EF.

Inside your DBContext.cs you will have this:

public class DBContext : DbContext
    {

        public DBContext () : base("DatabaseName")
        {

        }



        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {


        }

        public DbSet<YourPocoModelNameHere > YourPocoModelNameHere { get; set; }



        // This is overridden to prevent someone from calling SaveChanges without specifying the user making the change
        public override int SaveChanges()
        {
            throw new InvalidOperationException("User ID must be provided");
        }
        public int SaveChanges(int userId)
        {
            // Get all Added/Deleted/Modified entities (not Unmodified or Detached)
            foreach (var ent in this.ChangeTracker.Entries().Where(p => p.State == System.Data.EntityState.Added || p.State == System.Data.EntityState.Deleted || p.State == System.Data.EntityState.Modified))
            {
                // For each changed record, get the audit record entries and add them
                foreach (AuditLog x in GetAuditRecordsForChange(ent, userId))
                {
                    this.AuditLogs.Add(x);
                }
            }

            // Call the original SaveChanges(), which will save both the changes made and the audit records
            return base.SaveChanges();
        }

        private List<AuditLog> GetAuditRecordsForChange(DbEntityEntry dbEntry, int userId)
        {
            List<AuditLog> result = new List<AuditLog>();

            DateTime changeTime = DateTime.UtcNow;

            // Get the Table() attribute, if one exists
            //TableAttribute tableAttr = dbEntry.Entity.GetType().GetCustomAttributes(typeof(TableAttribute), false).SingleOrDefault() as TableAttribute;

            TableAttribute tableAttr = dbEntry.Entity.GetType().GetCustomAttributes(typeof(TableAttribute), true).SingleOrDefault() as TableAttribute;

            // Get table name (if it has a Table attribute, use that, otherwise get the pluralized name)
            string tableName = tableAttr != null ? tableAttr.Name : dbEntry.Entity.GetType().Name;

            // Get primary key value (If you have more than one key column, this will need to be adjusted)
            var keyNames = dbEntry.Entity.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Count() > 0).ToList();

            string keyName = keyNames[0].Name; //dbEntry.Entity.GetType().GetProperties().Single(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Count() > 0).Name;

            if (dbEntry.State == System.Data.EntityState.Added)
            {
                // For Inserts, just add the whole record
                // If the entity implements IDescribableEntity, use the description from Describe(), otherwise use ToString()

                foreach (string propertyName in dbEntry.CurrentValues.PropertyNames)
                {
                    result.Add(new AuditLog()
                    {
                        AuditLogId = Guid.NewGuid(),
                        UserId = userId,
                        EventDateUTC = changeTime,
                        EventType = "A",    // Added
                        TableName = tableName,
                        RecordId = dbEntry.CurrentValues.GetValue<object>(keyName).ToString(),
                        ColumnName = propertyName,
                        NewValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString()
                    }
                            );
                }
            }
            else if (dbEntry.State == System.Data.EntityState.Deleted)
            {
                // Same with deletes, do the whole record, and use either the description from Describe() or ToString()
                result.Add(new AuditLog()
                {
                    AuditLogId = Guid.NewGuid(),
                    UserId = userId,
                    EventDateUTC = changeTime,
                    EventType = "D", // Deleted
                    TableName = tableName,
                    RecordId = dbEntry.OriginalValues.GetValue<object>(keyName).ToString(),
                    ColumnName = "*ALL",
                    NewValue = (dbEntry.OriginalValues.ToObject() is IDescribableEntity) ? (dbEntry.OriginalValues.ToObject() as IDescribableEntity).Describe() : dbEntry.OriginalValues.ToObject().ToString()
                }
                    );
            }
            else if (dbEntry.State == System.Data.EntityState.Modified)
            {
                foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
                {
                    // For updates, we only want to capture the columns that actually changed
                    if (!object.Equals(dbEntry.OriginalValues.GetValue<object>(propertyName), dbEntry.CurrentValues.GetValue<object>(propertyName)))
                    {
                        result.Add(new AuditLog()
                        {
                            AuditLogId = Guid.NewGuid(),
                            UserId = userId,
                            EventDateUTC = changeTime,
                            EventType = "M",    // Modified
                            TableName = tableName,
                            RecordId = dbEntry.OriginalValues.GetValue<object>(keyName).ToString(),
                            ColumnName = propertyName,
                            OriginalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
                            NewValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString()
                        }
                            );
                    }
                }
            }
            // Otherwise, don't do anything, we don't care about Unchanged or Detached entities

            return result;
        }


    }

This will use the following table in your DB:

USE [databasename]
GO

/****** Object:  Table [dbo].[auditlog]    Script Date: 06/01/2014 05:56:49 p. m. ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[auditlog](
    [auditlogid] [uniqueidentifier] NOT NULL,
    [userid] [int] NOT NULL,
    [eventdateutc] [datetime] NOT NULL,
    [eventtype] [char](1) NOT NULL,
    [tablename] [nvarchar](100) NOT NULL,
    [recordid] [nvarchar](100) NOT NULL,
    [columnname] [nvarchar](100) NOT NULL,
    [originalvalue] [nvarchar](max) NULL,
    [newvalue] [nvarchar](max) NULL,
 CONSTRAINT [PK_AuditLog] PRIMARY KEY NONCLUSTERED 
(
    [auditlogid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO

ALTER TABLE [dbo].[auditlog]  WITH CHECK ADD  CONSTRAINT [FK_auditlog_users] FOREIGN KEY([userid])
REFERENCES [dbo].[users] ([userid])
GO

ALTER TABLE [dbo].[auditlog] CHECK CONSTRAINT [FK_auditlog_users]
GO

With this all set then you will just need to call your dbContext.SaveChanges(here the userId);

Hope this will work for you... I use it in all my applications and works great!

Enjoy it.


Full code found here: https://jmdority.wordpress.com/2011/07/20/using-entity-framework-4-1-dbcontext-change-tracking-for-audit-logging/

James McCormack
  • 9,217
  • 3
  • 47
  • 57
VAAA
  • 14,531
  • 28
  • 130
  • 253
  • VAAA - thanks for that.. this is exactly the sort of thing I was looking for... I will try it out soon and let you know... – user2206329 Jan 07 '14 at 00:59
  • 1
    VAAA - I tried what you suggested, but am having a few issues. I am using a map to configure the primary keys, relationships and table names. I am not explicitly putting the [Key] data annotations on the poco models.. see my updated code above, so when I use your code, it does not know what the key is, to fix it I can simply update my poco models, but then I am duplicating my work, do you know how I can use my existing infrastructure? – user2206329 Jan 13 '14 at 03:51
  • Hi, strange but Im having the same issue when mapping using EntityTypeConfiguration. Did you get it working using EntityTypeConfiguration? Thanks – VAAA Apr 06 '14 at 04:05
  • I'm running into a small issue regarding migrations. I am unable to run updates because the exception being thrown in SaveChanges(). How do you recommend I alter the code so that this is not a problem? – Michael Apr 08 '14 at 03:43
  • did you forget to include AuditLog class? – Toolkit Aug 25 '14 at 06:16
  • I know this is an old question, but I am curious about the performance of the NVARCHAR(MAX) columns you used here. What would be the performance implications here when this table may easily end up with millions of rows? – QuietSeditionist Jan 22 '15 at 20:32
  • For EF6, this should just use EntityState (in the System.Data.Entity namespace instead of the System.Data namespace) – Michiel Cornille Feb 09 '16 at 08:44
  • 1
    Also, the recordid is only known after the save, so for auditing .Added state, the id will always be 0, any solution for this? refreshing the entries after adding them, and then updating the auditlog seems like overkill. – Michiel Cornille Mar 04 '16 at 09:59
  • 1
    there a solution here http://stackoverflow.com/questions/19797820/how-to-get-id-from-entity-for-auditlog-in-entity-framework-6 – Michiel Cornille Mar 04 '16 at 10:06
10

I found this NuGet package (TrackerEnabledDbContext) and followed these 4 steps:

  1. Install package TrackerEnabledDbContext

  2. Inherit my DbContext from TrackerContext in TrackerEnabledDbContext namespace

    public class ApplicationDbContext :  TrackerContext 
        {
            public ApplicationDbContext()
                : base("DefaultConnection")
            {
            }
    

Add a migration and update my database. Two new tables were created for recording changes (AuditLog and AuditLogDetails).

  1. Decide which tables you want to track and apply [TrackChanges] attribute to the classes. In case you want to skip tracking for some specific columns, you can apply [SkipTracking] attribute to those columns (properties).

  2. Whenever you make a change in database, you call DbContext.SaveChanges(). Now you have an overload available for that which takes an integer. This should be the logged-in person's user ID. If you don't pass the user ID, this change will not be recorded into the tracking table.

    databaseContext.SaveChanges(userId);
    

And that's all. Later you can retrieve the logs with this:

var AuditLogs = db.GetLogs<Proyecto>(id).ToList();
Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38
demispb
  • 101
  • 2
  • 8
  • What about getting session userId or userName parameter just inside the SaveChanges() method. Or which method should be applied in order to pass the user's name or id parameter to SaveChanges method as userId? On the other hand, if it is better to create a new table for username and userid, at which step should they retrieved i.e. on controller stage, or DbContext stage in SaveChanges( ) method? Thanks in advance. – Jack Mar 05 '15 at 13:49
  • 1
    I can override SaveChanges() , get the user id and then call SaveChanges(user_id), it think its better than change all the SaceChanges() calls on my controllers, for retrieve the name on my controller i make a helper class History, get all logs for this generic entity, filter or sort the logs, and then get the username from my User tables using the stored Id. var aLogs = _db.GetLogs(id).OrderByDescending(x => x.EventDateUTC).ToList(); foreach (var log in aLogs) { log.UserName = _db.Users.Find(iduser).DisplayName; – demispb Mar 05 '15 at 17:49
  • Thanks for reply. But I meant the phase before saving logs to the database, not monitoring time. So, before saving logs to the database we need to use user id that is stored on my User table. So, what is the best way to get the user's id before saving logs (calling SaveChanges()) to the database? – Jack Mar 06 '15 at 07:36
  • 1
    In my case i use Windows Authentication (Active Directory in an intranet application) and you can call db.SaveChanges(db.Users.First(x=>x.AdName == User.Identity.Name).Id); AdName is the Active directory name for the user ("Domain\user") stored on my DB and you can compare this with User.Identity.Name from the current logged user, then you get the Id... – demispb Mar 10 '15 at 12:45
  • I tried it. But there is a little problem regarding to Identity section. Should I have to install a package to use it? – Jack Mar 10 '15 at 12:57
  • check this link https://msdn.microsoft.com/en-us/library/system.security.principal.iidentity(v=vs.110).aspx its included on System.Security.Principal Namespace – demispb Mar 10 '15 at 13:31
  • 1
    Thanks. But after using that reference another error is encountered: "Xxxx.Domain.Entities.User' does not contain a definition for 'Identity' ... " – Jack Mar 10 '15 at 14:36
  • 1
    My mistake, If your trying to use the Identity outside the controller you can use: HttpContext.Current.User.Identity.Name for getting the current user name, then with this name search in your user table by name and get the Id, the other way its for request the current user name inside a controller. Let me know if this works for u. – demispb Mar 10 '15 at 14:49
6

Disclaimer: I'm the owner of the project Entity Framework Plus

EF+ has auditing features which support EF5, EF6, and EF Core.

// using Z.EntityFramework.Plus; // Don't forget to include this.

var ctx = new EntityContext();
// ... ctx changes ...

var audit = new Audit();
audit.CreatedBy = "ZZZ Projects"; // Optional
ctx.SaveChanges(audit);

// Access to all auditing information
var entries = audit.Entries;
foreach(var entry in entries)
{
    foreach(var property in entry.Properties)
    {
    }
}

A lot of options are available like an AutoSave in the database.

Documentation: EF+ Audit

Jonathan Magnan
  • 10,874
  • 2
  • 38
  • 60
2

In Generic repository pattern, we can write a generic event handler for db context savechanges event.

I googled it and gathered few many information.

  1. I dont want to write a sql server trigger
  2. I dont want to handle savechanges method in each entity.

So i am planning to write generic single method

Db structure that I am using

audit table

CREATE TABLE [dbo].[Audit](
    [Id] [BIGINT] IDENTITY(1,1) NOT NULL,
    [TableName] [nvarchar](250) NULL,
    [Updated By] [nvarchar](100) NULL,
    [Actions] [nvarchar](25) NULL,
    [OldData] [text] NULL,
    [NewData] [text] NULL,
    [Created For] varchar(200) NULL,
    [Updated Date] [datetime] NULL,
 CONSTRAINT [PK_DBAudit] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

2.Update your dbcontext with Audit table entity.

3.Hook generic event handler for Dbcontext savechanges

c# code

    namespace ARMS.Domain      
    {
        using System;
        using System.Collections.Generic;
        using System.Collections.ObjectModel;
        using System.Data;
        using System.Data.Objects;
        using System.Linq;
        using System.Text;
        using System.ComponentModel.DataAnnotations;
        public partial class ARMSContext
        {
            Collection<Audit> auditTrailList = new Collection<Audit>();

            partial void OnContextCreated()
            { 
                this.SavingChanges += new EventHandler(ArmsEntities_SavingChanges);
            }
            public enum AuditActions
            {
                Added,
                Modified,
                Deleted
            }
            void ArmsEntities_SavingChanges(object sender, EventArgs e)
            { 
                auditTrailList.Clear(); 
                IEnumerable<ObjectStateEntry> changes =
                    this.ObjectStateManager.GetObjectStateEntries(
                    EntityState.Added | EntityState.Deleted | EntityState.Modified); 
                foreach (ObjectStateEntry stateEntryEntity in changes)
                {


                        if (!stateEntryEntity.IsRelationship && stateEntryEntity.Entity != null && !(stateEntryEntity.Entity is Audit))
                        {
                            Audit audit = this.GetAudit(stateEntryEntity);
                            auditTrailList.Add(audit);
                        }



                }
                if (auditTrailList.Count > 0)
                {
                    foreach (var audit in auditTrailList)
                    {
                        this.Audits.AddObject(audit);  
                    } 
                }
            }
            public Audit GetAudit(ObjectStateEntry entry)
            {
                Audit audit = new Audit();



                audit.Updated_By ="Test";
                audit.TableName = entry.EntitySet.ToString();
                audit.Updated_Date = DateTime.Now;
                audit.Created_For = Convert.ToString(entry.Entity);
                audit.Actions = Enum.Parse(typeof(AuditActions),entry.State.ToString(), true).ToString();
                StringBuilder newValues = new StringBuilder();
                StringBuilder oldValues = new StringBuilder();
                if (entry.State == EntityState.Added)
                {  
                    SetAddedProperties(entry, newValues);
                    audit.NewData = newValues.ToString();  
                } 
                else if (entry.State == EntityState.Deleted)
                {   SetDeletedProperties(entry, oldValues);
                    audit.OldData = oldValues.ToString(); 
                } 
                else if (entry.State == EntityState.Modified)
                { 
                    SetModifiedProperties(entry, oldValues, newValues);
                    audit.OldData = oldValues.ToString();
                    audit.NewData = newValues.ToString(); 
                } 
                return audit;
            } 
            private void SetAddedProperties(ObjectStateEntry entry, StringBuilder newData)
            {      
                CurrentValueRecord currentValues = entry.CurrentValues;
                for (int i = 0; i < currentValues.FieldCount; i++)
                {  
                    newData.AppendFormat("{0}={1} || ", currentValues.GetName(i), currentValues.GetValue(i));
                } 
            } 
            private void SetDeletedProperties(ObjectStateEntry entry, StringBuilder oldData)
            {
                foreach (var propertyName in entry.GetModifiedProperties())
                {
                    var oldVal = entry.OriginalValues[propertyName];
                    if (oldVal != null)
                    {
                        oldData.AppendFormat("{0}={1} || ", propertyName, oldVal);
                    }
                }
            } 
            private void SetModifiedProperties(ObjectStateEntry entry, StringBuilder oldData, StringBuilder newData)
            {         
                foreach (var propertyName in entry.GetModifiedProperties())
                {
                    var oldVal = entry.OriginalValues[propertyName];
                    var newVal = entry.CurrentValues[propertyName];
                    if (oldVal != null && newVal != null && !Equals(oldVal, newVal))
                    {
                        newData.AppendFormat("{0}={1} || ", propertyName, newVal);
                        oldData.AppendFormat("{0}={1} || ", propertyName, oldVal);
                    }
                } 
            }   
        }
    }
gunr2171
  • 16,104
  • 25
  • 61
  • 88
2

Update:

The most requested feature for EF Core 6.0 was support for SQL Server temporal tables. As of RC1, temporal table support is now here!

SQL Server temporal tables automatically keep track of all the data ever stored in a table, even after that data has been updated or deleted. This is achieved by creating a parallel “history table” into which timestamped historical data is stored whenever a change is made to the main table. This allows historical data to be queried, such as for auditing, or restored, such as for recovery after accidental mutation or deletion.

EF Core 6.0 supports:

  • The creation of temporal tables using EF Core migrations
  • Transformation of existing tables into temporal tables, again using migrations
  • Querying historical data
  • Restoring data from some point in the past

https://devblogs.microsoft.com/dotnet/prime-your-flux-capacitor-sql-server-temporal-tables-in-ef-core-6-0/

Original:

You can do it like this without any external library for Entity Framework Core:

using (var context = new SampleContext())
{
    // Insert a row
    var customer = new Customer();
    customer.FirstName = "John";
    customer.LastName = "doe";
    context.Customers.Add(customer);
    await context.SaveChangesAsync();

    // Update the first customer
    customer.LastName = "Doe";
    await context.SaveChangesAsync();

    // Delete the customer
    context.Customers.Remove(customer);
    await context.SaveChangesAsync();
}

enter image description here

Model:

public class Audit
{
    public int Id { get; set; }
    public string TableName { get; set; }
    public DateTime DateTime { get; set; }
    public string KeyValues { get; set; }
    public string OldValues { get; set; }
    public string NewValues { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class SampleContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Audit> Audits { get; set; }
}

DbContext:

public class SampleContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Audit> Audits { get; set; }

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {
        var auditEntries = OnBeforeSaveChanges();
        var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        await OnAfterSaveChanges(auditEntries);
        return result;
    }

    private List<AuditEntry> OnBeforeSaveChanges()
    {
        ChangeTracker.DetectChanges();
        var auditEntries = new List<AuditEntry>();
        foreach (var entry in ChangeTracker.Entries())
        {
            if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
                continue;

            var auditEntry = new AuditEntry(entry);
            auditEntry.TableName = entry.Metadata.Relational().TableName;
            auditEntries.Add(auditEntry);

            foreach (var property in entry.Properties)
            {
                if (property.IsTemporary)
                {
                    // value will be generated by the database, get the value after saving
                    auditEntry.TemporaryProperties.Add(property);
                    continue;
                }

                string propertyName = property.Metadata.Name;
                if (property.Metadata.IsPrimaryKey())
                {
                    auditEntry.KeyValues[propertyName] = property.CurrentValue;
                    continue;
                }

                switch (entry.State)
                {
                    case EntityState.Added:
                        auditEntry.NewValues[propertyName] = property.CurrentValue;
                        break;

                    case EntityState.Deleted:
                        auditEntry.OldValues[propertyName] = property.OriginalValue;
                        break;

                    case EntityState.Modified:
                        if (property.IsModified)
                        {
                            auditEntry.OldValues[propertyName] = property.OriginalValue;
                            auditEntry.NewValues[propertyName] = property.CurrentValue;
                        }
                        break;
                }
            }
        }

        // Save audit entities that have all the modifications
        foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties))
        {
            Audits.Add(auditEntry.ToAudit());
        }

        // keep a list of entries where the value of some properties are unknown at this step
        return auditEntries.Where(_ => _.HasTemporaryProperties).ToList();
    }

    private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
    {
        if (auditEntries == null || auditEntries.Count == 0)
            return Task.CompletedTask

        foreach (var auditEntry in auditEntries)
        {
            // Get the final value of the temporary properties
            foreach (var prop in auditEntry.TemporaryProperties)
            {
                if (prop.Metadata.IsPrimaryKey())
                {
                    auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
                }
                else
                {
                    auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
                }
            }

            // Save the Audit entry
            Audits.Add(auditEntry.ToAudit());
        }

        return SaveChangesAsync();
    }
}

public class AuditEntry
{
    public AuditEntry(EntityEntry entry)
    {
        Entry = entry;
    }

    public EntityEntry Entry { get; }
    public string TableName { get; set; }
    public Dictionary<string, object> KeyValues { get; } = new Dictionary<string, object>();
    public Dictionary<string, object> OldValues { get; } = new Dictionary<string, object>();
    public Dictionary<string, object> NewValues { get; } = new Dictionary<string, object>();
    public List<PropertyEntry> TemporaryProperties { get; } = new List<PropertyEntry>();

    public bool HasTemporaryProperties => TemporaryProperties.Any();

    public Audit ToAudit()
    {
        var audit = new Audit();
        audit.TableName = TableName;
        audit.DateTime = DateTime.UtcNow;
        audit.KeyValues = JsonConvert.SerializeObject(KeyValues);
        audit.OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues);
        audit.NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues);
        return audit;
    }
}

Source:

https://www.meziantou.net/entity-framework-core-history-audit-table.htm

If you are using Entity Framework 6 or Entity Framework Core you could also use Audit.NET and Audit.EntityFramework written by @thepirat000. This works fine but I like to have a minimum of NuGet dependencies, preferably 0, that are not backed by a large community/corporation and that depends heavily on a single developer.

https://github.com/thepirat000/Audit.NET/graphs/contributors

You can also read more about Slowly changing dimension types and from there create a solution that fits your needs.

If you need entire Entity Framework Snapshot History look at this answer.

Ogglas
  • 62,132
  • 37
  • 328
  • 418
1

Create a class for capture the changes or track the changes when entity added, modifies or deleted.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Web;

namespace MVC_AuditTrail.Models
{
    public class AuditTrailFactory
    {
        private DbContext context;

        public AuditTrailFactory(DbContext context)
        {
            this.context = context;
        }
        public Audit GetAudit(DbEntityEntry entry)
        {
            Audit audit = new Audit();
            // var user = (User)HttpContext.Current.Session[":user"];
            audit.UserId = "swapnil";// user.UserName;
            audit.TableName = GetTableName(entry);
            audit.UpdateDate = DateTime.Now;
            audit.TableIdValue = GetKeyValue(entry);

            //entry is Added 
            if (entry.State == EntityState.Added)
            {
                var newValues = new StringBuilder();
                SetAddedProperties(entry, newValues);
                audit.NewData = newValues.ToString();
                audit.Actions = AuditActions.I.ToString();
            }
            //entry in deleted
            else if (entry.State == EntityState.Deleted)
            {
                var oldValues = new StringBuilder();
                SetDeletedProperties(entry, oldValues);
                audit.OldData = oldValues.ToString();
                audit.Actions = AuditActions.D.ToString();
            }
            //entry is modified
            else if (entry.State == EntityState.Modified)
            {
                var oldValues = new StringBuilder();
                var newValues = new StringBuilder();
                SetModifiedProperties(entry, oldValues, newValues);
                audit.OldData = oldValues.ToString();
                audit.NewData = newValues.ToString();
                audit.Actions = AuditActions.U.ToString();
            }

            return audit;
        }

        private void SetAddedProperties(DbEntityEntry entry, StringBuilder newData)
        {
            foreach (var propertyName in entry.CurrentValues.PropertyNames)
            {
                var newVal = entry.CurrentValues[propertyName];
                if (newVal != null)
                {
                    newData.AppendFormat("{0}={1} || ", propertyName, newVal);
                }
            }
            if (newData.Length > 0)
                newData = newData.Remove(newData.Length - 3, 3);
        }

        private void SetDeletedProperties(DbEntityEntry entry, StringBuilder oldData)
        {
            DbPropertyValues dbValues = entry.GetDatabaseValues();
            foreach (var propertyName in dbValues.PropertyNames)
            {
                var oldVal = dbValues[propertyName];
                if (oldVal != null)
                {
                    oldData.AppendFormat("{0}={1} || ", propertyName, oldVal);
                }
            }
            if (oldData.Length > 0)
                oldData = oldData.Remove(oldData.Length - 3, 3);
        }

        private void SetModifiedProperties(DbEntityEntry entry, StringBuilder oldData, StringBuilder newData)
        {
            DbPropertyValues dbValues = entry.GetDatabaseValues();
            foreach (var propertyName in entry.OriginalValues.PropertyNames)
            {
                var oldVal = dbValues[propertyName];
                var newVal = entry.CurrentValues[propertyName];
                if (oldVal != null && newVal != null && !Equals(oldVal, newVal))
                {
                    newData.AppendFormat("{0}={1} || ", propertyName, newVal);
                    oldData.AppendFormat("{0}={1} || ", propertyName, oldVal);
                }
            }
            if (oldData.Length > 0)
                oldData = oldData.Remove(oldData.Length - 3, 3);
            if (newData.Length > 0)
                newData = newData.Remove(newData.Length - 3, 3);
        }

        public long? GetKeyValue(DbEntityEntry entry)
        {
            var objectStateEntry = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
            long id = 0;
            if (objectStateEntry.EntityKey.EntityKeyValues != null)
                id = Convert.ToInt64(objectStateEntry.EntityKey.EntityKeyValues[0].Value);

            return id;
        }

        private string GetTableName(DbEntityEntry dbEntry)
        {
            TableAttribute tableAttr = dbEntry.Entity.GetType().GetCustomAttributes(typeof(TableAttribute), false).SingleOrDefault() as TableAttribute;
            string tableName = tableAttr != null ? tableAttr.Name : dbEntry.Entity.GetType().Name;
            return tableName;
        }
    }

    public enum AuditActions
    {
        I,
        U,
        D
    }
}

Then create audit table entity and context class.

And Override savechanges method in this method get audit changes and save before base entity saved.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Web;

namespace MVC_AuditTrail.Models
{
    public class Student
    {
        public int StudentID { get; set; }

        public string Name { get; set; }

        public string  mobile { get; set; }
    }

    public  class Audit
    {
        public long Id { get; set; }
        public string TableName { get; set; }
        public string UserId { get; set; }
        public string Actions { get; set; }
        public string OldData { get; set; }
        public string NewData { get; set; }
        public Nullable<long> TableIdValue { get; set; }
        public Nullable<System.DateTime> UpdateDate { get; set; }
    }


    public class StdContext : DbContext
    {
        private AuditTrailFactory auditFactory;
        private List<Audit> auditList = new List<Audit>();
        private List<DbEntityEntry> objectList = new List<DbEntityEntry>();
        public StdContext() : base("stdConnection")
        {
            Database.SetInitializer<StdContext>(new CreateDatabaseIfNotExists<StdContext>());
        }

        public DbSet<Student> Student { get; set; }
        public DbSet<Audit> Audit { get; set; }

        public override int SaveChanges()
        {
            auditList.Clear();
            objectList.Clear();
            auditFactory = new AuditTrailFactory(this);

            var entityList = ChangeTracker.Entries().Where(p => p.State == EntityState.Added || p.State == EntityState.Deleted || p.State == EntityState.Modified);
            foreach (var entity in entityList)
            {
                Audit audit = auditFactory.GetAudit(entity);
                bool isValid = true;
                if (entity.State == EntityState.Modified && string.IsNullOrWhiteSpace(audit.NewData) && string.IsNullOrWhiteSpace(audit.OldData))
                {
                    isValid = false;
                }
                if (isValid)
                {
                    auditList.Add(audit);
                    objectList.Add(entity);
                }
            }

            var retVal = base.SaveChanges();
            if (auditList.Count > 0)
            {
                int i = 0;
                foreach (var audit in auditList)
                {
                    if (audit.Actions == AuditActions.I.ToString())
                        audit.TableIdValue = auditFactory.GetKeyValue(objectList[i]);
                    this.Audit.Add(audit);
                    i++;
                }

                base.SaveChanges();
            }

            return retVal;
        }
    }
}
Swapnil Malap
  • 610
  • 1
  • 7
  • 14
0
  public override int SaveChanges()
        {
            var auditEntries = new List<AuditEntry>();

            var modifiedEntities = ChangeTracker.Entries()
                .Where(p => p.State == EntityState.Modified).ToList();

            foreach (var change in modifiedEntities)
            {
                var auditEntry = new AuditEntry(change);
                var primaryKey = GetPrimaryKeys(change);
                auditEntry.TableName = change.Entity.GetType().Name;//get table name
               // var id = change.CurrentValues.GetValue<object>("Id").ToString();//get current id
                auditEntry.EntityId = primaryKey.ToString();
                auditEntry.EntityTypeId = primaryKey.ToString();
                auditEntries.Add(auditEntry);


                foreach (var prop in change.OriginalValues.PropertyNames)
                {
                    if (prop == "Id")
                    {
                        continue;
                    }

                    switch (change.State)
                    {
                        case EntityState.Modified:
                            if ((change.State & EntityState.Modified) != 0)
                            {
                                auditEntry.OldValues[prop] = change.OriginalValues[prop].ToString();
                                auditEntry.NewValues[prop] = change.CurrentValues[prop].ToString();
                            }
                            break;
                    }

                }
                foreach (var auditEntry1 in auditEntries.Where(_ => !_.HasTemporaryProperties))
                {
                    Audits.Add(auditEntry1.ToAudit());
                }
            }
            return base.SaveChanges();
        }

        private object GetPrimaryKeys(DbEntityEntry entry)
        {
            var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);

            return objectStateEntry.EntityKey.EntityKeyValues[0].Value;

        }
Dimand
  • 1
-1

Another option is to create an audit action attribute to allow methods to be decorated with an attribute describing the audit operation. Just inherit from Attribute and list the information you want to capture in the constructor, then create an interceptor (inherit from castle) to intercept request to a method. The interceptor will invoke the audit service (which is just a simple class that will write the audit message to the database).

meol
  • 1,027
  • 11
  • 7