Search code examples
c#entity-frameworktemporal

Implementing a "generic" mechanism to handle temporal data in Entity Framework


I'm trying to accomplish a "generic" mechanism for updating temporal data in my SQL Server database, using Entity Framework.

What I did is create a "marker" interface called ITemporalData that defines two properties that need to be present - DateTime ValidFrom and DateTime? ValidTo.

public interface ITemporalData
{
    DateTime ValidFrom { get; set; }
    DateTime? ValidTo { get; set; }
}

I was hoping to implement a "generic" approach in my DbContext.SaveChanges() override to:

  • clone any ITemporalData object, which would give me a new object to store (EntityState.Added), and set its ValidFrom value to the current date&time
  • reset the original, modified entry to its database values (calling .Reset() on the entity) and then setting the ValidTo for that "old" record to the current date&time

While I can easily filter out the modified ITemporalData objects in the SaveChanges() override like this:

public partial class MyDbContext
{
    // override the "SaveChanges" method
    public override int SaveChanges()
    {
        DateTime currentDateTime = DateTime.Now;

        // get the modified entities that implement the ITemporalData interface
        IEnumerable<DbEntityEntry<ITemporalData>> temporalEntities = ChangeTracker.Entries<ITemporalData>().Where(e => e.State == EntityState.Modified);

        foreach (var temporalEntity in temporalEntities)
        {
            // how would I do that, really? I only have an interface - can't clone an interface...... 
            var cloned = temporalEntity.Entity.Clone();

            // and once it's cloned, I would need to add the new record to the correct DbSet<T> to store it

            // set the "old" records "ValidTo" property to the current date&time
            temporalEntity.Entity.ValidTo = currentDateTime;
        }

        return base.SaveChanges();
    }
}

I'm struggling with the "clone the modified record" approach - I only have a ITemporalData interface, really - but the cloning (using AutoMapper or other approaches) always depends on the actual, underlying concrete datatype.....


Solution

  • To clone entity you might just create new instance via reflection (Activator.CreateInstance) and copy all primitive (non-navigation) properties to it via reflection. Better not use auto-mapper tools for that, since they will access navigation properties too, which might cause lazy-loading (or at least ensure lazy-loading is disabled).

    If you don't like reflection (note that auto-mappers will use it anyway) - you can also inherit your interface from ICloneable and implement Clone method for every ITemporalData entity (if your entities are autogenerated - use partial class for that). Then each entity decides itself how to clone, without any reflection. This way also has benefits in case your clone logic is complex (for example involves cloning related objects from navigation properties).

    To add entity to correct DbSet, use untyped Set method of DbContext:

    this.Set(temporalEntity.GetType()).Add(temporalEntity);