32

I am building in a Change History / Audit Log to my MVC app which is using the Entity Framework.

So specifically in the edit method public ActionResult Edit(ViewModel vm), we find the object we are trying to update, and then use TryUpdateModel(object) to transpose the values from the form on to the object that we are trying to update.

I want to log a change when any field of that object changes. So basically what I need is a copy of the object before it is edited and then compare it after the TryUpdateModel(object) has done its work. i.e.

[HttpPost]
public ActionResult Edit(ViewModel vm)
{
    //Need to take the copy here
    var object = EntityFramework.Object.Single(x=>x.ID = vm.ID);

    if (ModelState.IsValid)
    {
        //Form the un edited view model
        var uneditedVM = BuildViewModel(vm.ID); //this line seems to confuse the EntityFramework (BuildViewModel() is used to build the model when originally displaying the form)
        //Compare with old view model
        WriteChanges(uneditedVM, vm);
        ...
        TryUpdateModel(object);
    }
    ...
}

But the problem is when the code retrieves the "unedited vm", this is causing some unexpected changes in the EntityFramework - so that TryUpdateModel(object); throws an UpdateException.

So the question is - in this situation - how do I create a copy of the object outside of EntityFramework to compare for change/audit history, so that it does not affect or change the EntityFramework at all

edit: Do not want to use triggers. Need to log the username who did it.

edit1: Using EFv4, not too sure how to go about overriding SaveChanges() but it may be an option


This route seems to be going nowhere, for such a simple requirement! I finally got it to override properly, but now I get an exception with that code:

public partial class Entities
{
    public override int SaveChanges(SaveOptions options)
    {
        DetectChanges();
        var modifiedEntities = ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
        foreach (var entry in modifiedEntities)
        {
            var modifiedProps = ObjectStateManager.GetObjectStateEntry(entry).GetModifiedProperties(); //This line throws exception The ObjectStateManager does not contain an ObjectStateEntry with a reference to an object of type 'System.Data.Objects.EntityEntry'.
            var currentValues = ObjectStateManager.GetObjectStateEntry(entry).CurrentValues;
            foreach (var propName in modifiedProps)
            {
                var newValue = currentValues[propName];
                //log changes
            }
        }

        //return base.SaveChanges();
        return base.SaveChanges(options);
    }
}
baron
  • 10,563
  • 20
  • 51
  • 86
  • 1
    Try this http://pastebin.com/puygwzyb – Omar Jul 29 '11 at 02:19
  • 1
    Or... you dont create!... just get this nuget package instead. http://www.nuget.org/packages/trackerenableddbcontext – Rosdi Kasim Jan 23 '15 at 03:08
  • @Rosdi Yeh that'll solve the problem by adding a ton more bloat to your solution when this problem can be solved with very little code. I don't get why people feel the need to pull in packages for literally everything lately instead of just writing a few lines. – War Feb 03 '16 at 10:40

3 Answers3

24

IF you are using EF 4 you can subscribe to the SavingChanges event.

Since Entities is a partial class you can add additional functionality in a separate file. So create a new file named Entities and there implement the partial method OnContextCreated to hook up the event

public partial class Entities
{
    partial void OnContextCreated()
    {
        SavingChanges += OnSavingChanges;
    }

    void OnSavingChanges(object sender, EventArgs e)
    {

        var modifiedEntities = ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
        foreach (var entry in modifiedEntities)
        {
            var modifiedProps = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).GetModifiedProperties();
            var currentValues = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).CurrentValues;
            foreach (var propName in modifiedProps)
            {
                var newValue = currentValues[propName];
                //log changes
            }
        }
    }
}

If you are using EF 4.1 you can go through this article to extract changes

Eranga
  • 31,383
  • 5
  • 88
  • 92
  • "There is no suitable method for override" ? – baron Jul 29 '11 at 01:25
  • @baron you can handle the [SavingChanges](http://msdn.microsoft.com/en-us/library/system.data.objects.objectcontext.savingchanges.aspx) event – Eranga Jul 29 '11 at 01:35
  • Also "base class ObjectContext doesn't contain parameterless constructor". Did you have a blog where you got that code from ? – baron Jul 29 '11 at 01:35
  • I really don't understand how to set this up. I create a class "Test" which extends `ObjectContext` do I set up the event handler in this class ? – baron Jul 29 '11 at 01:38
  • @baron no you don't need to create a separate "Test" which extends `ObjectContext`. instead you need to subscribe in your `Entities` constructor. I have edited my answer – Eranga Jul 29 '11 at 01:58
  • @Eranga, keep in mind that the Entities is auto-generated, it's better that he create a new class that inherits from the generated context than to edit the generated code. – Omar Jul 29 '11 at 02:19
  • Yes this is the problem. Thank you Omar. Although I tried the code you provided, when I perform `Entities.SaveChanges();` and have breakpoint on the `context_SavingChanges()` method - it never enters! – baron Jul 29 '11 at 03:52
15

See FrameLog, an Entity Framework logging library that I wrote for this purpose. It is open-source, including for commercial use.

I know that you would rather just see a code snippet showing how to do this, but to properly handle all the cases for logging, including relationship changes and many-to-many changes, the code gets quite large. Hopefully the library will be a good fit for your needs, but if not you can freely adapt the code.

FrameLog can log changes to all scalar and navigation properties, and also allows you to specify a subset that you are interested in logging.

Martin Eden
  • 5,785
  • 3
  • 26
  • 33
  • 3
    I've implemented FrameLog in my project and it's working fine with only a few hours work doing the integration. I'm using it now for a single table in my database that I needed to create an audit log for. – Boggin Aug 19 '13 at 08:14
  • 1
    It's a real nice framework. Very easy to use and customize. In less than 3 hours we have implemented this on our project. Thank you. You did a good job Martin =) – fabriciorissetto Feb 11 '14 at 12:28
  • 1
    Anyone looking for a simple audit log, there is one here: https://github.com/loresoft/EntityFramework.Extended Otherwise FrameLog rocks – flux Sep 30 '15 at 09:19
  • Here is another one: [Audit.EntityFramework](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.EntityFramework/README.md) – thepirat000 Sep 10 '17 at 18:02
1

There is an article with high rating here at the codeproject: Implementing Audit Trail using Entity Framework . It seems to do what you want. I have started to use this solution in a project. I first wrote triggers in T-SQL in the database but it was too hard to maintain them with changes in the object model happening all the time.

Patrik Lindström
  • 978
  • 3
  • 12
  • 20