marc_s
marc_s

Reputation: 755207

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:

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.....

Upvotes: 3

Views: 727

Answers (2)

Gert Arnold
Gert Arnold

Reputation: 109255

You can add this Clone method to your context:

T Clone<T>(DbEntityEntry<T> entry)
    where T : class
{
    var proxyCreationEnabled = this.Configuration.ProxyCreationEnabled;
    try
    {
        this.Configuration.ProxyCreationEnabled = false;
        var clone = (T)entry.CurrentValues.ToObject();
        Set(clone.GetType()).Add(clone);
        return clone;
    }
    finally
    {
        this.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
    }
}

And use it as follows:

var cloned = Clone(temporalEntity);

clone.GetType will return the actual type of the cloned object, whereas T will be the compile-time type, ITemporalData.

This uses EF's own infrastructure to create a clone, which no doubt is faster than reflection.

Although the clone's state is immediately set to Added, it won't perform lazy loading. But it may be safer to ensure that the clone will never be a proxy and, hence, will never trigger lazy loading in case you decide to do other things with a clone. (Thanks Evk for his keen comments).

Upvotes: 1

Evk
Evk

Reputation: 101583

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);

Upvotes: 2

Related Questions