1

I am using the entity framework to create an audit trail. Rather than audit every property, I thought I would create a custom attribute

  [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class DoNotAudit : Attribute
    {
    }

Then I would apply this to my model

 [Table("AuditZone")]
    public class AuditZone
    {
        public AuditZone()
        {
            AuditZoneUploadedCOESDetails = new List<UploadedCOESDetails>();
            AuditZonePostcode = new List<Postcodes>();
        }

        [Key]
        public int Id { get; set; }
        public string Description { get; set; }     
        public bool Valid { get; set; }       

        public DateTime CreatedDate { get; set; }
        public int? CreatedBy { get; set; }
        [DoNotAudit]
        public DateTime? ModifiedDate { get; set; }
        public int? ModifiedBy { get; set; }

        public virtual UserProfile CreatedByUser { get; set; }
        public virtual UserProfile ModifiedByUser { get; set; }

        public virtual ICollection<UploadedCOESDetails> AuditZoneUploadedCOESDetails { get; set; }
        public virtual ICollection<Postcodes> AuditZonePostcode { get; set; }
    }

Then in my code for the audit trail

  // 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;

            var test = dbEntry.Entity.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Count() > 0).ToList();



            // //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)
                {

                    var doNotAUditDefined = dbEntry.Property(propertyName).GetType().GetCustomAttributes(typeof(DoNotAudit), false);

                   // var test1 = dbEntry.Property(propertyName).GetType().Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Count() > 0).ToList();

                //    var test = dbEntry.Entity.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Count() > 0).ToList();


                    // 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;
        }

In the modified section I have the following line of code

  var doNotAUditDefined = dbEntry.Property(propertyName).GetType().GetCustomAttributes(typeof(DoNotAudit), false);

WHen I step through the code, even for the modifiedDate property this is shown as empty. How can that be? any help is appreciated

Thanks

user2206329
  • 2,792
  • 10
  • 54
  • 81

1 Answers1

2

What you get with the following code:

dbEntry.Property(propertyName).GetType()

is the type of the modified property, like DateTime? in the case of ModifiedType. So there is no attribute defined on the DateTime? class. (As the attribute is defined in your AuditZone class)

What I would do is to save the list of properties that should not be audited before entering into the modified part of your audit code (at least before looping the list of modified properties). Then as looping through the modified properties, check if the property name is in the list of properties excluded from audit. Something like this:

var auditExcludedProps = dbEntry.Entity.GetType()
                                       .GetProperties()
                                       .Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Any())
                                       .Select(p => p.Name)
                                       .ToList();

foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{

    var doNotAUditDefined = auditExcludedProps.Contains(propertyName);

    ...
}

You may want to double check that dbEntry.Entity.GetType() returns your class AuditZone and the list auditExcludedProps contains the ModifiedDate property.

Hope it helps!

Daniel J.G.
  • 34,266
  • 9
  • 112
  • 112
  • Yes that would work.. But there is another method called dbEntry.member(property name) and again that gives nothing.. So just to be clear by using a custom attribute on a property there is no way to check it? – user2206329 Jan 13 '14 at 11:47
  • In the case of `ModifiedDate`, that would give you a `DbPropertyEntry` object, see [mdsn](http://msdn.microsoft.com/en-us/library/system.data.entity.infrastructure.dbentityentry.member(v=vs.113).aspx). So you will be in the same scenario as when using `dbEntry.Property(propertyName)` – Daniel J.G. Jan 13 '14 at 12:06
  • The way to check for the custom attribute is to get the type of the class (`AuditZone`) declaring the property (`ModifiedDate`) with the custom attribute. That means the reflection needs to start getting the type info of AuditZone, and then looking at its properties. EF doesn't seem to provide a way of getting the `PropertyInfo` (which would allow you to check for custom attributes) given a `DbPropertyEntry` object. – Daniel J.G. Jan 13 '14 at 12:17
  • Daniel - I was trying to work out a way to contact you, as I have another question for you.. but its not related to this question. but the link is here http://stackoverflow.com/questions/21085406/entitytypeconfiguration-how-to-get-the-key-property – user2206329 Jan 15 '14 at 05:26
  • Daniel - one more question? http://stackoverflow.com/questions/21214179/audit-trail-in-ef5-and-mvc-4-how-to-get-the-actual-foreign-key-value – user2206329 Jan 21 '14 at 13:31