Entity Framework - Auditing activity
Asked Answered
T

2

11

My database has a 'LastModifiedUser' column on every table in which I intend to collect the logged in user from an application who makes a change. I am not talking about the database user so essentially this is just a string on each entity. I would like to find a way to default this for each entity so that other developers don't have to remember to assign it any time they instantiate the entity.

So something like this would occur:

using (EntityContext ctx = new EntityContext())
{
    MyEntity foo = new MyEntity();

    // Trying to avoid having the following line every time
    // a new entity is created/added.
    foo.LastModifiedUser = Lookupuser(); 

    ctx.Foos.Addobject(foo);
    ctx.SaveChanges();
}
Theaterintheround answered 27/9, 2010 at 21:56 Comment(0)
D
25

There is a perfect way to accomplish this in EF 4.0 by leveraging ObjectStateManager

First, you need to create a partial class for your ObjectContext and subscribe to ObjectContext.SavingChanges Event. The best place to subscribe to this event is inside the OnContextCreated Method. This method is called by the context object’s constructor and the constructor overloads which is a partial method with no implementation:

partial void OnContextCreated() {
    this.SavingChanges += Context_SavingChanges;
}


Now the actual code that will do the job:

void Context_SavingChanges(object sender, EventArgs e) {

    IEnumerable<ObjectStateEntry> objectStateEntries = 
        from ose 
        in this.ObjectStateManager.GetObjectStateEntries(EntityState.Added 
                                                         | EntityState.Modified)
        where ose.Entity != null
        select ose;

    foreach (ObjectStateEntry entry in objectStateEntries) {

        ReadOnlyCollection<FieldMetadata> fieldsMetaData = entry.CurrentValues
                .DataRecordInfo.FieldMetadata;

        FieldMetadata modifiedField = fieldsMetaData
            .Where(f => f.FieldType.Name == "LastModifiedUser").FirstOrDefault();

        if (modifiedField.FieldType != null) {

            string fieldTypeName = modifiedField.FieldType.TypeUsage.EdmType.Name;                    
            if (fieldTypeName == PrimitiveTypeKind.String.ToString()) {
                entry.CurrentValues.SetString(modifiedField.Ordinal, Lookupuser());
            }
        }
    }
}

Code Explanation:
This code locates any Added or Modified entries that have a LastModifiedUser property and then updates that property with the value coming from your custom Lookupuser() method.

In the foreach block, the query basically drills into the CurrentValues of each entry. Then, using the Where method, it looks at the names of each FieldMetaData item for that entry, picking up only those whose Name is LastModifiedUser. Next, the if statement verifies that the LastModifiedUser property is a String field; then it updates the field's value.

Another way to hook up this method (instead of subscribing to SavingChanges event) is by overriding the ObjectContext.SaveChanges Method.

By the way, the above code belongs to Julie Lerman from her Programming Entity Framework book.


EDIT for Self Tracking POCO Implementation:

If you have self tracking POCOs then what I would do is that I first change the T4 template to call the OnContextCreated() method. If you look at your ObjectContext.tt file, there is an Initialize() method that is called by all constructors, therefore a good candidate to call our OnContextCreated() method, so all we need to do is to change ObjectContext.tt file like this:

private void Initialize()
{
    // Creating proxies requires the use of the ProxyDataContractResolver and
    // may allow lazy loading which can expand the loaded graph during serialization.
    ContextOptions.ProxyCreationEnabled = false;
    ObjectMaterialized += new ObjectMaterializedEventHandler(HandleObjectMaterialized);
    // We call our custom method here:
    OnContextCreated();
}

And this will cause our OnContextCreated() to be called upon creation of the Context.

Now if you put your POCOs behind the service boundary, then it means that the ModifiedUserName must come with the rest of data from your WCF service consumer. You can either expose this LastModifiedUser property to them to update or if it stores in another property and you wish to update LastModifiedUser from that property, then you can modify the 2nd code as follows:


foreach (ObjectStateEntry entry in objectStateEntries) {

    ReadOnlyCollection fieldsMetaData = entry.CurrentValues
            .DataRecordInfo.FieldMetadata;

    FieldMetadata sourceField = fieldsMetaData
            .Where(f => f.FieldType.Name == "YourPropertyName").FirstOrDefault();             

    FieldMetadata modifiedField = fieldsMetaData
        .Where(f => f.FieldType.Name == "LastModifiedUser").FirstOrDefault();

    if (modifiedField.FieldType != null) {

        string fieldTypeName = modifiedField.FieldType.TypeUsage.EdmType.Name;
        if (fieldTypeName == PrimitiveTypeKind.String.ToString()) {
            entry.CurrentValues.SetString(modifiedField.Ordinal,
                    entry.CurrentValues[sourceField.Ordinal].ToString());
        }
    }
}


Hope this helps.

Dorree answered 27/9, 2010 at 23:54 Comment(5)
Thanks for the answer. I had looked into this pattern before but could never get the onContextCreated event to fire.Theaterintheround
No problem. Like I said, OnContextCreated is not an Event, it just is a partial method with no implementation which is called by the context object’s constructor and the constructor overloads. All you need to do is to create a partial class for your context and put my 1st code snippet inside it. Please do so and let me know how it works for you, and if it still is not working, we'll make it work!Dorree
Actually, I was thinking about this the wrong way and thus described it wrong. Let me refine my question... We use self-trackign entities and they are served up via a WCF service to the front end. When they are returned back we reattach to the context and save. If they were modified while in possession of the client, the lastModifiedUser needs to reflect that. The server doesn't have access to know who the currentUser is so it can't be the one responsible for setting the user. I suppose in theory that I want to do something like a oncreated event/partial method at the entity level?Theaterintheround
Ok, now I see what you mean. I update the answer to address the POCO situation. Please give it a try.Dorree
There is a little error in the above code. The test if (modifiedField.FieldType != null) should be replaced by if (modifiedField != null)Kristy
O
2

There is a nuget package for this now : https://www.nuget.org/packages/TrackerEnabledDbContext

Github: https://github.com/bilal-fazlani/tracker-enabled-dbcontext

Oneiromancy answered 2/11, 2014 at 8:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.