Monday 13 February 2012

Basic Auditing for Dynamic Data with Entity Framework 4.x

This is my first article of 2012 and I thought I had published an article on this previously but apparently not Crying face so I will now rectify that oversight.

The first this we need out audit fields, these are added to every entity we need to audit, next we need an Interface to allow us to fine entities with Audit fields.

AuditFields

Figure 1 – Audit Fields

public interface IAuditable
{
String CreatedByUserID { get; set; }
String CreatedDateTime { get; set; }
String UpdatedByUserID { get; set; }
String UpdatedDateTime { get; set; }
}

Listing 1 – IAuditable interface

We then need to add this to each entity that will be audited, this is just applied to you metadata classes on the Partial Class NOT the buddy class.

[MetadataType(typeof(AppAlterMetadata))]
public partial class AppAlter : IAuditable
{
internal class AppAlterMetadata
{
public Object ID { get; set; }

// other field deleted for brevety

// auditing fields
public Object CreatedByUserID { get; set; }
public Object CreatedDateTime { get; set; }
public Object UpdatedByUserID { get; set; }
public Object UpdatedDateTime { get; set; }
}
}

Listing 2 – the interface applied to each entity that requires auditing

Now for the code that does the auditing automatically,

public partial class MyEntities
{
/// <summary>
/// Called when [context created].
/// </summary>
partial void OnContextCreated()
{
// Register the handler for the SavingChanges event.
this.SavingChanges += new EventHandler(context_SavingChanges);
}


/// <summary>
/// Handles the SavingChanges event of the context control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private static void context_SavingChanges(object sender, EventArgs e)
{
var objects = ((ObjectContext)sender).ObjectStateManager;

// handle auditing
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Added));
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Modified), InsertMode: false);
}


/// <summary>
/// Auditing helper utility class
/// </summary>
internal static class AuditingHelperUtility
{
internal static void ProcessAuditFields(IEnumerable<Object> list, bool InsertMode = true)
{
foreach (ObjectStateEntry item in list)
{
var appUserID = GetUserId();
// deal with insert and update entities
var auditEntity = item.Entity as IAuditable;
if (auditEntity != null)
{

if (InsertMode)
{
auditEntity.CreatedByUserID = appUserID;
auditEntity.CreatedDateTime = DateTime.Now;
}

auditEntity.UpdatedByUserID = appUserID;
auditEntity.UpdatedDateTime = DateTime.Now;
}

}
}
}


public static String GetUserId()
{
return System.Web.HttpContext.Current.User.Identity.Name;
}
}

Listing 3 – the Audit code

Lets break this down into three sections

Section 1

Here we wire-up the SavingChanges handler in the OnContextCreated() partial method to do this we first need to create a partial class from out entities for you look in the EDMX code behind file you will see something like this;

EFClasses

Figure 2 – Entities classes

so we add a new class to the the project, make sure it has the same namespace as the EDMX code behind file (this is pretty much the same as for out metadata classes) and then we add the partial class same as the MyEntities (this will be the name you gave it when creating but it is there in the code behind you can’t miss it) class, see Listing 3.

The method is wired up with this line of code:

this.SavingChanges += new EventHandler(context_SavingChanges);

Section 2

Now in the context_SavingChanges method we simply get the ObjectStateManager  which has all the objects that are being added, updated and deleted, here we are only interested in the Added and Modified items. All we do is call our helper with each collection of objects.

Section 3

Looking at Listing 3 you will see the AuditingHelperUtility and it’s ProcessAuditFields method, here we first of all cast the each entity to the IAuditable interface and check for null if it isn't then set the appropriate properties and exit.

Finally

This can be expanded to cover many different requirements, I have maintained a separate audit table using this method with the addition of a little reflection.

24 comments:

Anonymous said...

Hi Steve,

Thanks for the wonderful article. But I am facing a small problem. Since I do not want to display these fields on UI, I have them as ScaffoldColumn = false. What I am forced to do is do 1 more select and getting creation fields and adding it to value object. Else database throws exception these to fields as it tries to update them to NULL.

Can you please let me know if there is more sophisticated way for doing things?

Thanks.

Anonymous said...

Hi Steve.

It's possible with Linq to SQL?
within of OnContextCreated don't exist SavingChanges event.
My entity inherits only of INotifyPropertyChanging, INotifyPropertyChanged

Stephen J. Naughton said...

yes it is see this link here http://weblogs.asp.net/stevesheldon/archive/2008/02/23/a-method-to-handle-audit-fields-using-linq-to-sql.aspx

have fun

Steve

paul said...

Steve,

I have a issue with DD which i have posted in StackOverflow:
http://stackoverflow.com/questions/10176911/asp-net-dynamic-data-web-site-passing-default-value-while-inserting-updating-r
BUT not got solution yet. Wud you please help me??

thanks,
Paul

Stephen J. Naughton said...

This article does what you need, you can have any column names you wan t you just need to change the interface appropriatly.

Steve

Unknown said...
This comment has been removed by the author.
Anonymous said...

Hi Steve,

nice articel. Thats what I need, but if I try to implement it, the follwoing error occures:
Compilation Error
Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.

Compiler Error Message: CS0759: No defining declaration found for implementing declaration of partial method 'MyNamespace.MyEntities.OnContextCreated()'

Source Error:

Line 15: ///
Line 16:
Line 17: partial void OnContextCreated()
Line 18: {
Line 19: // Register the handler for the SavingChanges event.

Could you please help me or uplaod a sample so I can compare the solutions.

Thank you verry much.

Florian

Stephen J. Naughton said...

it looks to me like your partial class for you entity model is not correctly named you can find out the correct name and namespce by opening the .designer.cs file of you entity model and looking there.

Steve
P.S. feel fre to email me directly my address is at the top of the page.

ScottK said...

Steve, I've run into an odd issue and I'm hoping you can provide some insight. I implemented a similar process, but I get the following error when I debug or run it locally: Compiler Error Message: CS0759: No defining declaration found for implementing declaration of partial method OnContextCreated

After hours of research, testing and fighting, I decided to publish it to our test web server. On that box, it works flawlessly. No compiler error and it functions as expected.

Meanwhile, on my development machine, it builds without issue, but produces the compiler error every time I try to debug the site or run it locally.

Have you seen this before? Any idea what is causing this?

Thanks, SK

Stephen J. Naughton said...

your partial methods are not defined in the same CLASS as the the data model (not that is not the same file) they both must be in the same CLASS and namespace and the same assembly.

Steve

ScottK said...

They are in the same class, namespace and assembly. They work perfectly when published to the web server, but generate the compiler error when debugging or running on my development machine.

Very odd and frustrating...SK

Anonymous said...

Hi Steve!

I try to customice the delete button. I want to set a "deleted flag" instead of deleting the item. So i can also save the auditing.
But I don't find a solution. I tried to apply the orginal values and to change the delete button into a customiced edit button, which updated the flag instead of deleting the data.

Could you please help me, finding a solution? Thank you verry much!!

Unknown said...

Hi,

This is a really helpful atrical, I am currently struggling implementing an audit on my first MVC application. I am using a generic repository and unitofwork and just wondered how I can adapt this so that the Insert, Update and delete actions call your audit utility to populate the audit fields for each table?

I also wanted to change the audit fields slightly as I want to save this to a seperate table so I have a log. So I wanted to do something like you described using reflection? So I can keep an audit trail.

Can you give me some pointers on how I can achieve this?

Many thanks,

Andy

Stephen J. Naughton said...

see http://www.codeproject.com/Articles/34491/Implementing-Audit-Trail-using-Entity-Framework-Pa

and

http://www.codeproject.com/Articles/34627/Implementing-Audit-Trail-using-Entity-Framework-Pa

they may be what you want.

Steve

Tim Cartwright said...

Instead of using a interface (good idea btw) I implemented some generic methods:

private static void SetRecordValue(CurrentValueRecord currentValues, string valueName, T value)
{
int ordinal = GetRecordOrdinal(currentValues, valueName);
if (ordinal > 0)
currentValues.SetValue(ordinal, value);
}

private static T GetRecordValue(CurrentValueRecord currentValues, string valueName, T defaultValue)
{
T ret = defaultValue;
int ordinal = GetRecordOrdinal(currentValues, valueName);
if (ordinal > 0)
ret = (T)currentValues.GetValue(ordinal);
return ret;
}

private static int GetRecordOrdinal(CurrentValueRecord currentValues, string valueName)
{
int ret = 0;
//getordinal and the string indexer throw an exception if the property does not exist....
if (currentValues.DataRecordInfo.FieldMetadata.Any(x => x.FieldType.Name == valueName))
ret = currentValues.GetOrdinal(valueName);
return ret;
}

Stephen J. Naughton said...

Hi Tim, I am looking a new way of auditing see Implementing Audit Trail using Entity Framework Part - 1 this looks to me the way to go, once I have a working sample I will do an article.

Steve

Tim Cartwright said...

Steve, I had a question. Whenever using the currentvalues setvalue, the data gets written to the database, but the base object does not get updated. The reason behind my question is that I was trying to implement a version number for the objects that increments when changes occur. I even tried setting the value on the propertyinfo.

What happens is that it increments in the database, but it does not do so in the object. So if they save a second time without refetching it the object from the database it remains the original value. EX:

Version
1st data pull: 1
Save to database: DB value 2; object value 1
Save 2 to database: DB value 2; object value 1
etc...

Do you know of a way around this? I have googled on and off for a couple of days with no luck.

Tim Cartwright said...

Steve, forget my last question. It has something to do with the way we are doing our tr transforms. When i try this method with a regular edmx, the base object gets updated.

BTW, I have read those code project articles before. However they dont quite suit our needs. We have a need to display the object hierarchy in a semi readable manner when reporting the audit trail. Currently I have been struggling to figure out how to figure out the hiearachy inside the saving changes from object to parent. The reason is some of our objects could have one parent or another depending upon the context. Clear as mud I know, but to lay it out in full detail would take too long in a comment.

ewoox said...

Hi, Steve, any news on audit trail using entity framework?

Stephen J. Naughton said...

Hi it's on my list, but I had a heart attack back in January and then a triple bypass operation and was in hospital for about a month. so I'm only just getting back to work. but I do need to get it working and will post it on my blog once it's done.

Steve

ewoox said...

Sorry to hear...
I'll be waiting for your post, in case I won't be able to figure it out by myself.
Had a little trouble making "Basic Auditing" to work. In my case Dynamic Data works with the DbContext's ObjectContext. if anyone interested here's link: https://coderwall.com/p/3k-pwg

Stephen J. Naughton said...

Hi yes there are some changes for DBContext and I will have to take that into account also.

Steve

Anonymous said...

Hi, Iam new in DynamicDAta.

Could you please give me a example of how to add logging (in a audit table) for insert/delete/update without SP?

I tried usign FormView1_ItemUpdated/FormView1_ItemInserted at PageTemplates, but I would know another way to add logging.

The tables's business has not field (CreatedDate/CreateBy/CreateBy User...)

Stephen J. Naughton said...

At the moment this is the only example I have zorry :(

Steve