Reputation: 755207
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:
ITemporalData
object, which would give me a new object to store (EntityState.Added
), and set its ValidFrom
value to the current date&time.Reset()
on the entity) and then setting the ValidTo
for that "old" record to the current date&timeWhile 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
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
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