views:

173

answers:

3

Hi,

Im tring to implement some generic logging in the entityframework 4.0 using the SavingChanges event in the entity context.

I want to record details about any create/ update and deleted records including the id of the record being changed (which is always an int identity field). During the update and delete processes I can get the id for the record using

    Dim AddedItems = Statemanager.GetObjectStateEntries(EntityState.Added)
    For Each entry As ObjectStateEntry In DirectCast(sender, ObjectContext).ObjectStateManager.GetObjectStateEntries(EntityState.Added)

        NewLogItem.RecordId = CInt(entry.EntityKey.EntityKeyValues(0).Value.ToString())
    Next

But I obviously cannot get the id of the about to be inserted record during an insert becuase it hasn't been written to the database yet, is there a simple way around this?

A: 

I think there is no way to get the ID of a new inserted record in the SavingChanges event since the event is simply called before the database gets touched at all and the ID is created. Unfortunately there is no SavedChanges event which gets called after the data are saved.

A possible option might be to leverage the fact that one of the overloads of the ObjectContext's SaveChanges method is virtual (or overridable in VB), namely:

public virtual int SaveChanges(SaveOptions options)

(Note: It's virtual only in EF 4, not in the earlier version! And only this overload is virtual, the other two overloads of SaveChanges are not!)

So you could override this method in your model's ObjectContext. In this overridden method you call SaveChanges of the base class and after that your new entities should have the created IDs from the database which you could log then.

(It's just a rough idea, I never tested or used this virtual overload. I've built a similar logging mechanism in SavingChanges as you're just implementing and had the same problem. But this was in EF 3.5 where this virtual method didn't exist yet. Finally I had logged Inserts only at the important places after calling SaveChanges, but that wasn't then "generic" of course. Perhaps the new virtual method is a good chance to improve that now.)

Edit

A bit more precise what I meant (in C# syntax):

public partial class MyEntitiesContext : ObjectContext
{
    // ...

    public override int SaveChanges(SaveOptions options)
    {
        // Log something BEFORE entities are stored in DB

        int result = base.SaveChanges(options);

        // Log something AFTER entities are stored in DB, especially new entities
        // with auto-incrementing identity key which have been inserted in the DB
        // should have the final primary key value now

        return result;
    }

    // ...
}

Important is to call base.SaveChanges(options). (Calling only SaveChanges(options) without base. ends with a StackOverflow of course.)

Slauma
Hi Slauma,Thanks for your help, couldn't get that to work, it looks like SaveChanges(SaveOptions options) may be called by the parameterless method call SaveChanges() (which I would need to call from my overridden method) because when I called this from within my overridden SaveChanges(SaveOptions options) method it resulted in a StackOverflowExcpetion. It did however make me rethink how to do this, I ended up writing my own SaveAndLogChanges method in the context.
Julian Slater
@Julian: The StackOverflowException you described looks very like you were not calling the SaveChanges method of the base class within your overridden implementation. I've edited my answer and added details to emphasize this.
Slauma
Hi Public Overrides Function SaveChanges(ByVal options As SaveOptions) As IntegerReturn MyBase.SaveChanges(options)End Function
Julian Slater
Hi Slauma,That's more or less what I was doing except calling MyBase.SaveChanges(), have changed to just the code below and it tries to create a duplicate record upon retrieving, editing and then saving an entity.Public Overrides Function SaveChanges(ByVal options As SaveOptions) As Integer Return MyBase.SaveChanges(options) End Functioneven if I were to get this working I would still have to make 2 calls to savechanges() to add the log entries for inserted rows. Thanks for your help, it is really appreciated
Julian Slater
A: 

This is what I ended up with, not ideal in the fact that I ended up with two saveChanges calls and therefore two transactions but cant see any other way around it, if we dont log the deleted items before the save then we cannot access them correctly after the savechanges() (the entities will no longer exist and this is reflected in the State Entry for deleted items)

Public Function SaveAndLogChanges() As Integer

    Dim Statemanager As ObjectStateManager = Me.ObjectStateManager

    Dim AddedItems As IEnumerable(Of ObjectStateEntry) = Statemanager.GetObjectStateEntries(EntityState.Added)
    Dim UpdatedItems As IEnumerable(Of ObjectStateEntry) = Statemanager.GetObjectStateEntries(EntityState.Modified)
    Dim DeletedItems As IEnumerable(Of ObjectStateEntry) = Statemanager.GetObjectStateEntries(EntityState.Deleted)

    For Each entry As ObjectStateEntry In UpdatedItems
        ' dont do anything for any of the log file entries or we will end up in an infinate loop
        If (Not entry.EntitySet.Name = "LogItem" And Not entry.EntitySet.Name = "LogTransaction") Then
            createLogEntry(LogItemType.ItemType.Modify, entry)
        End If
    Next

    ' deleted entities
    For Each entry As ObjectStateEntry In DeletedItems
        ' dont do anything for any of the log file entries or we will end up in an infinate loop
        If (Not entry.EntitySet.Name = "LogItem" And Not entry.EntitySet.Name = "LogTransaction") Then
            createLogEntry(LogItemType.ItemType.Delete, entry)
        End If
    Next

    ' need to save changes here to ensure we have ids for the inserted records
    Me.SaveChanges()

    ' inserted enties
    For Each entry As ObjectStateEntry In AddedItems
        ' dont do anything for any of the log file entries or we will end up in an infinate loop
        If (Not entry.EntitySet.Name = "LogItem" And Not entry.EntitySet.Name = "LogTransaction") Then
            createLogEntry(LogItemType.ItemType.Insert, entry)
        End If
    Next

    Me.SaveChanges()

    Return iRet
End Function
Julian Slater
+2  A: 

hello *,

One option you can do is override the SaveChanges method and call SaveChanges to the base with not accepting changes to the objectcontext. this way you can audit the changes on the ObjectStateManager.

 override void SaveChanges(SaveOptions)
 {
context.SaveChanges(SaveOptions.DetectChangesBeforeSave);
//query the object statemanager

 }

The key is calling the savechanges to the base with right saveoptions. The default would accept all the changes to the ObjectStateManager so all entries would then be in unchaged state and you won't be able to know what got added or deleted or modified. after looping through the statemanager you can then call AcceptChanges explicitly. I cover this concept in my book as well.

12-1 Executing Code When SaveChanges() Is Called

zeeshanhirani
Ah, interesting! I was always wondering what's the purpose of `AcceptAllChanges` in contrast to `SaveChanges`. Now I see a useful application for this method!
Slauma