41

If i have the following entity:

public class PocoWithDates
{
   public string PocoName { get; set; }
   public DateTime CreatedOn { get; set; }
   public DateTime LastModified { get; set; }
}

Which corresponds to a SQL Server 2008 table with the same name/attributes...

How can i automatically:

  1. Set the CreatedOn/LastModified field for the record to now (when doing INSERT)
  2. Set the LastModified field for the record to now (when doing UPDATE)

When i say automatically, i mean i want to be able to do this:

poco.Name = "Changing the name";
repository.Save(); 

Not this:

poco.Name = "Changing the name";
poco.LastModified = DateTime.Now;
repository.Save();

Behind the scenes, "something" should automatically update the datetime fields. What is that "something"?

I'm using Entity Framework 4.0 - is there a way that EF can do that automatically for me? (a special setting in the EDMX maybe?)

From the SQL Server side, i can use DefaultValue, but that will only work for INSERT's (not UPDATE's).

Similarly, i can set a default value using a constructor on the POCO's, but again this will only work when instantiating the object.

And of course i could use Triggers, but it's not ideal.

Because i'm using Entity Framework, i can hook into the SavingChanges event and update the date fields here, but the problem is i need to become "aware" of the POCO's (at the moment, my repository is implemented with generics). I would need to do some sort of OO trickery (like make my POCO's implement an interface, and call a method on that). I'm not adversed to that, but if i have to do that, i would rather manually set the fields.

I'm basically looking for a SQL Server 2008 or Entity Framework 4.0 solution. (or a smart .NET way)

Any ideas?

EDIT

Thanks to @marc_s for his answer, but i went with a solution which is better for my scenario.

RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • I just asked this question again, but then for EF6, since I love this question and the offered solutions, but the supplied code examples don't work for EF6.0 as they are currently. – Bart Feb 20 '14 at 15:23

7 Answers7

53

I know I'm a little late to the party, but I just solved this for a project I'm working on and thought I'd share my solution.

First, to make the solution more re-usable, I created a base class with the timestamp properties:

public class EntityBase
{
    public DateTime? CreatedDate { get; set; }
    public DateTime? LastModifiedDate { get; set; }
}

Then I overrode the SaveChanges method on my DbContext:

public class MyContext : DbContext
{
    public override int SaveChanges()
    {
        ObjectContext context = ((IObjectContextAdapter)this).ObjectContext;

        //Find all Entities that are Added/Modified that inherit from my EntityBase
        IEnumerable<ObjectStateEntry> objectStateEntries =
            from e in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified)
            where
                e.IsRelationship == false &&
                e.Entity != null &&
                typeof(EntityBase).IsAssignableFrom(e.Entity.GetType())
            select e;

        var currentTime = DateTime.Now;

        foreach (var entry in objectStateEntries)
        {
            var entityBase = entry.Entity as EntityBase;

            if (entry.State == EntityState.Added)
            {
                entityBase.CreatedDate = currentTime;
            }

            entityBase.LastModifiedDate = currentTime;
        }

        return base.SaveChanges();
    }
}
Nick
  • 693
  • 7
  • 11
  • 2
    Yep, i thought of this, but then IMO the POCO's are no longer "POCOs" since they now inherit from a base class whose only purpose is to serve timestamp operations (eg a database concern). – RPM1984 Jun 08 '11 at 22:34
  • great solution! Added this tracking in one minute to my site :) – Gluip Oct 28 '11 at 09:55
  • This is very neat. One small question though: how can you add on a LastModifiedBy field? – mpora Feb 28 '13 at 21:54
  • 3
    If you don't want to use a base class for your POCOs you can use entry.CurrentValues["CreatedDate"] to set the value. – Rowan Jun 03 '13 at 05:04
  • 1
    Very nice. I modified it slightly by moving the EntityBase stuff out into an interface, and because I'm using Database first, I added it to the T4 template that generates the custom DBContext. But very handy, thanks. – Ken Smith Jun 07 '13 at 22:56
  • 2
    Be sure to also override `SaveChangesAsync`. E.g. `public override Task SaveChangesAsync() { /* do something */ return base.SaveChangesAsync(); }` – Shaun Luttin Feb 17 '14 at 08:45
  • 1
    @ShaunLuttin Good catch - SaveChangesAsync didn't exist when I wrote this :-) – Nick Mar 05 '14 at 21:29
  • 6
    Code above does not work in all cases. I wind up using a different logic to get entries: var entries = ChangeTracker.Entries(); – Echiban Mar 08 '14 at 14:09
  • @RPM1984 that is easily fixable(through attributes for instance) and given solution would still apply. – Arek Bal Sep 09 '14 at 18:06
9

As i have a service layer mediating between my controllers (im using ASP.NET MVC), and my repository, i have decided to auto-set the fields here.

Also, my POCO's have no relationships/abstractions, they are completely independant. I would like to keep it this way, and not mark any virtual properties, or create base classes.

So i created an interface, IAutoGenerateDateFields:

public interface IAutoGenerateDateFields
{
   DateTime LastModified { get;set; }
   DateTime CreatedOn { get;set; }
}

For any POCO's i wish to auto-generate these fields, i implement this inteface.

Using the example in my question:

public class PocoWithDates : IAutoGenerateDateFields
{
   public string PocoName { get; set; }
   public DateTime CreatedOn { get; set; }
   public DateTime LastModified { get; set; }
}

In my service layer, i now check if the concrete object implements the interface:

public void Add(SomePoco poco)
{
   var autoDateFieldsPoco = poco as IAutoGenerateDateFields; // returns null if it's not.

   if (autoDateFieldsPoco != null) // if it implements interface
   {
      autoDateFieldsPoco.LastModified = DateTime.Now;
      autoDateFieldsPoco.CreatedOn = DateTime.Now;
   }

   // ..go on about other persistence work.
}

I will probably break that code in the Add out to a helper/extension method later on.

But i think this is a decent solution for my scenario, as i dont want to use virtuals on the Save (as i'm using Unit of Work, Repository, and Pure POCO's), and don't want to use triggers.

If you have any thoughts/suggestions, let me know.

RPM1984
  • 72,246
  • 58
  • 225
  • 350
6

here is edited version of previous response. Previous one didn't work for updates for me.

public override int SaveChanges()
    {
        var objectStateEntries = ChangeTracker.Entries()
            .Where(e => e.Entity is TrackedEntityBase && (e.State == EntityState.Modified || e.State == EntityState.Added)).ToList();
        var currentTime = DateTime.UtcNow;
        foreach (var entry in objectStateEntries)
        {
            var entityBase = entry.Entity as TrackedEntityBase;
            if (entityBase == null) continue;
            if (entry.State == EntityState.Added)
            {
                entityBase.CreatedDate = currentTime;
            }
            entityBase.LastModifiedDate = currentTime;
        }

        return base.SaveChanges();
    }
the_joric
  • 11,986
  • 6
  • 36
  • 57
serg.salo
  • 580
  • 1
  • 8
  • 14
6

I would also like to put forward a late solution. This one is only applicable to the .NET Framework 4 but makes this sort of task trivial.

var vs = oceContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
foreach (var v in vs)
{
    dynamic dv = v.Entity;
    dv.DateLastEdit = DateTime.Now;
}
famousgarkin
  • 13,687
  • 5
  • 58
  • 74
chinupson
  • 6,117
  • 1
  • 16
  • 8
1

You have two options:

  • have a base class for all your business entity classes that does the

    poco.LastModified = DateTime.Now;
    

    in a virtual .Save() method that all others would have to call

  • use a trigger in the database

I don't think there's any other reasonably safe and easy method to achieve this.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • Can't do option 1. Save happens on the UnitOfWork (not the POCO's - i should have mentioned that, sorry). Hence me mentioning "SavingChanges" as a possibility. I dont really want to go down the trigger route (makes debugging a nightmare). Think i would rather manually set the fields in my service layer. – RPM1984 Oct 07 '10 at 08:52
1

had to add base entity extend it from my database first partial class (which is not ideal)

    public override int SaveChanges()
    {
        AddTimestamps();
        return base.SaveChanges();
    }

    public override async Task<int> SaveChangesAsync()
    {
        AddTimestamps();
        return await base.SaveChangesAsync();
    }

    private void AddTimestamps()
    {
        //var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));

        //ObjectiveContext context = ((IObjectContextAdapter)this).ObjectContext;

        var entities = ChangeTracker.Entries().Where(e => e.Entity is BaseEntity && (e.State == EntityState.Modified || e.State == EntityState.Added)).ToList();

        var currentUsername = !string.IsNullOrEmpty(System.Web.HttpContext.Current?.User?.Identity?.Name)
            ? HttpContext.Current.User.Identity.Name
            : "Anonymous";

        foreach (var entity in entities)
        {
            if (entity.State == EntityState.Added)
            {
                ((BaseEntity)entity.Entity).CREATEDON = DateTime.UtcNow;
                ((BaseEntity)entity.Entity).CREATEDBY = currentUsername;
            }

            ((BaseEntity)entity.Entity).MODIFIEDON = DateTime.UtcNow;
            ((BaseEntity)entity.Entity).MODIFIEDBY = currentUsername;
        }
    }
cagedwhale
  • 51
  • 4
0

We can use partial class and override SaveChanges method to achieve this.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;


namespace TestDatamodel
{
    public partial class DiagnosisPrescriptionManagementEntities
    {
        public override int SaveChanges()
        {
            ObjectContext context = ((IObjectContextAdapter)this).ObjectContext;

            foreach (ObjectStateEntry entry in
                     (context.ObjectStateManager
                       .GetObjectStateEntries(EntityState.Added | EntityState.Modified)))
            {                    
                if (!entry.IsRelationship)
                {
                    CurrentValueRecord entryValues = entry.CurrentValues;
                    if (entryValues.GetOrdinal("ModifiedBy") > 0)
                    {
                        HttpContext currentContext = HttpContext.Current;
                        string userId = "nazrul";
                        DateTime now = DateTime.Now;

                        if (currContext.User.Identity.IsAuthenticated)
                        {
                            if (currentContext .Session["userId"] != null)
                            {
                                userId = (string)currentContext .Session["userId"];
                            }
                            else
                            {                                    
                                userId = UserAuthentication.GetUserId(currentContext .User.Identity.UserCode);
                            }
                        }

                        if (entry.State == EntityState.Modified)
                        {
                           entryValues.SetString(entryValues.GetOrdinal("ModifiedBy"), userId);
                           entryValues.SetDateTime(entryValues.GetOrdinal("ModifiedDate"), now);
                        }

                        if (entry.State == EntityState.Added)
                        {
                            entryValues.SetString(entryValues.GetOrdinal("CreatedBy"), userId);
                            entryValues.SetDateTime(entryValues.GetOrdinal("CreatedDate"), now);
                        }
                    }
                }
            }

            return base.SaveChanges();
        }
    }
}
Md. Nazrul Islam
  • 2,809
  • 26
  • 31