Search code examples
serializationentity-framework-corejson.net

Overriding SaveChanges to Create Copy of Entity for Versioning


We have an entity DataClass and we want to keep the changes to that entity by creating copies of the object and reference the changed object to parent with ParentVersionId.

public class DataClass : IVersionedEntity
{
    public int Id { get; set; }
    public string Adi { get; set; }

    public int? ParentVersionId { get; set; }
    public virtual DataClass? ParentVersion { get; set; }

    public virtual ICollection<DataClass> ParentVersions { get; set; }
}

SerializeDeserialize method below is used to create the copy of the objects.

public static T SerializeDeserialize<T>(T obj, bool convertComplexProperties = false)
{
    JsonSerializerSettings settings = new JsonSerializerSettings { Converters = { new XXXXDateTimeConverter() } };

    var strSource = JsonConvert.SerializeObject(obj, settings);    
    var destination = JsonConvert.DeserializeObject<T>(strSource, settings);
    return destination;
}

When we call SerializeDeserialize with an parameter object with DataClass type, everything works fine. However we want to override DbContext.SaveChangesAsync and create copies and add them to context while saving changes.

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
    var modifiedEntries = this.ChangeTracker.Entries().Where(e => e.State == EntityState.Modified);

    foreach (var modifiedEntry in modifiedEntries)
    {
        var clonedEntity = ObjectUtility.SerializeDeserialize(modifiedEntry.Entity) as IVersionedEntity;

        clonedEntity.Id = 0;
        cloned.ParentVersionId = (modifiedEntry.Entity as IVersionedEntity).Id;

        this.Add(clonedEntity);
    }
    return base.SaveChangesAsync(cancellationToken);
}

Above, modifiedEntry.Entity is type of System.Object and when SerializeDeserialize method is called with it, it returns System.Object (clonedEntity) instead of the real type, which throws exception.

As far as I see, only generic way to add an entity to DBContext is Add method. Is there any way to solve this issue?

Update:

I'm thinking of implementing a IVersionable interface on my entity classes:

public interface IVersionable
{
    int Id { get; set; }
    int? ParentVersionId { get; set; }
}

and cast the modifiedEntry.OriginalValues.ToObject() to that interface, setting the related properties and add to DBContext.

private void VersionEntity()
{
    var modifiedEntries = this.ChangeTracker.Entries().Where(e => e.State == EntityState.Modified && VersionedTypes.Contains(e.Entity.GetType()) && e.Entity is IVersionable);

    foreach (var modifiedEntry in modifiedEntries)
    {
        if (modifiedEntry.OriginalValues.ToObject() is not IVersionable clonedTypedEntity) 
            continue;

        clonedTypedEntity.Id = 0;
        clonedTypedEntity.ParentVersionId = (modifiedEntry.Entity as IVersionable)?.Id;

        Add(clonedTypedEntity);
    }
}
public override int SaveChanges()
{
    VersionEntity();
    return base.SaveChanges();
}

Solution

  • You have to use reflection to call generic method. I have added additional method CloneObject to use it instead of invoking SerializeDeserialize directly.

    public static class ObjectUtility
    {
        private static readonly MethodInfo _serializeDeserialize =
            typeof(ObjectUtility).GetMethod(nameof(SerializeDeserialize));
    
        public static T SerializeDeserialize<T>(T obj, bool convertComplexProperties = false)
        {
            JsonSerializerSettings settings = new JsonSerializerSettings { Converters = { new XXXXDateTimeConverter() } };
    
            var strSource = JsonConvert.SerializeObject(obj, settings);    
            var destination = JsonConvert.DeserializeObject<T>(strSource, settings);
            return destination;
        }
    
        public static object CloneObject(object obj, bool convertComplexProperties = false)
        {
            var method = _serializeDeserialize.MakeGenericMethod(obj.GetType());
            return method.Invoke(null, new[] { obj, convertComplexProperties });
        }
    }
    

    But I also can propose variant without such cloning:

    private void VersionEntity()
    {
        ChangeTracker.DetectChanges();
        var modifiedEntries = ChangeTracker.Entries().Where(e => e.State == EntityState.Modified && e.Entity is IVersionable);
    
        foreach (var modifiedEntry in modifiedEntries)
        {
            var cloned = (IVersionable)Activator.CreateInstance(modifiedEntry.Entity.GetType());
            modifiedEntry.CurrentValues.SetValues(cloned);
    
            // rollback
            modifiedEntry.CurrentValues.SetValues(modifiedEntry.OriginalValues);
    
            cloned.Id = 0;
            cloned.ParentVersionId = ((IVersionable)(modifiedEntry).Entity).Id;
    
            Add((object)cloned);
        }
    }