Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework - Auditing activity

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();
}
like image 449
Biggle10 Avatar asked Sep 27 '10 21:09

Biggle10


2 Answers

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.

like image 159
Morteza Manavi Avatar answered Nov 06 '22 03:11

Morteza Manavi


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

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

like image 42
Bilal Fazlani Avatar answered Nov 06 '22 02:11

Bilal Fazlani