From 7cab79eb016d506f1f5df61ae7ab455d0297a49a Mon Sep 17 00:00:00 2001 From: PanyushkinD Date: Thu, 10 May 2018 11:33:27 +0300 Subject: [PATCH] Fix Auditing of Detached entities #4 --- .../Auditing/AuditableEntitiesTests.cs | 142 ++++++++++++++++-- .../Auditing/AuditableEntitiesExtensions.cs | 16 ++ .../Auditing/TrackableEntitiesExtensions.cs | 21 +++ 3 files changed, 168 insertions(+), 11 deletions(-) diff --git a/EFCore.CommonTools.Tests/Auditing/AuditableEntitiesTests.cs b/EFCore.CommonTools.Tests/Auditing/AuditableEntitiesTests.cs index 132e461..102beee 100644 --- a/EFCore.CommonTools.Tests/Auditing/AuditableEntitiesTests.cs +++ b/EFCore.CommonTools.Tests/Auditing/AuditableEntitiesTests.cs @@ -3,8 +3,12 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; #if EF_CORE +using Microsoft.EntityFrameworkCore; + namespace EntityFrameworkCore.CommonTools.Tests #elif EF_6 +using System.Data.Entity; + namespace EntityFramework.CommonTools.Tests #endif { @@ -16,44 +20,44 @@ public void TestAuditableEntitiesGeneric() { using (var context = CreateInMemoryDbContext()) { - var author = new User(); - context.Users.Add(author); + var user = new User(); + context.Users.Add(user); + context.SaveChanges(); // insert - var post = new Post { Title = "first", Author = author }; + var post = new Post { Title = "first" }; context.Posts.Add(post); - - context.SaveChanges(author.Id); + context.SaveChanges(user.Id); context.Entry(post).Reload(); Assert.AreEqual(DateTime.UtcNow.Date, post.CreatedUtc.ToUniversalTime().Date); - Assert.AreEqual(author.Id, post.CreatorUserId); + Assert.AreEqual(user.Id, post.CreatorUserId); // update post.Title = "second"; - context.SaveChanges(author.Id); + context.SaveChanges(user.Id); context.Entry(post).Reload(); Assert.IsNotNull(post.UpdatedUtc); Assert.AreEqual(DateTime.UtcNow.Date, post.UpdatedUtc?.ToUniversalTime().Date); - Assert.AreEqual(author.Id, post.UpdaterUserId); + Assert.AreEqual(user.Id, post.UpdaterUserId); // delete context.Posts.Remove(post); - context.SaveChanges(author.Id); + context.SaveChanges(user.Id); context.Entry(post).Reload(); Assert.AreEqual(true, post.IsDeleted); Assert.IsNotNull(post.DeletedUtc); Assert.AreEqual(DateTime.UtcNow.Date, post.DeletedUtc?.ToUniversalTime().Date); - Assert.AreEqual(author.Id, post.DeleterUserId); + Assert.AreEqual(user.Id, post.DeleterUserId); } } [TestMethod] - public async Task TestAuditableEntities() + public async Task TestAuditableEntitiesString() { using (var context = CreateSqliteDbContext()) { @@ -89,5 +93,121 @@ public async Task TestAuditableEntities() Assert.AreEqual("admin", settings.DeleterUserId); } } + + // https://github.com/gnaeus/EntityFramework.CommonTools/issues/4 + [TestMethod] + public void TestAuditableEntitiesUpdateExisting() + { + using (var context = CreateSqliteDbContext()) + { + var firstUser = new User(); + var secondUser = new User(); + context.Users.Add(firstUser); + context.Users.Add(secondUser); + context.SaveChanges(); + + // insert + var post = new Post { Title = "first" }; + context.Posts.Add(post); + context.SaveChanges(firstUser.Id); + context.Entry(post).Reload(); + DateTime createdUtc = post.CreatedUtc; + + // set empty CreatedUtc and CreatorUserId + post.CreatedUtc = default(DateTime); + post.CreatorUserId = default(int); + post.Title = "second"; +#if EF_CORE + context.Posts.Update(post); +#elif EF_6 + context.Entry(post).State = EntityState.Modified; +#endif + context.SaveChanges(firstUser.Id); + + // CreatedUtc and CreatorUserId should not be changed + context.Entry(post).Reload(); + Assert.AreEqual(createdUtc, post.CreatedUtc); + Assert.AreEqual(firstUser.Id, post.CreatorUserId); + + // explicitely change CreatedUtc and CreatorUserId + post.CreatedUtc = new DateTime(2018, 01, 01); + post.CreatorUserId = secondUser.Id; + post.Title = "third"; +#if EF_CORE + context.Posts.Update(post); +#elif EF_6 + context.Entry(post).State = EntityState.Modified; +#endif + context.SaveChanges(firstUser.Id); + + // CreatedUtc and CreatorUserId should equals to explicitely passed values + context.Entry(post).Reload(); + Assert.AreEqual(new DateTime(2018, 01, 01), post.CreatedUtc); + Assert.AreEqual(secondUser.Id, post.CreatorUserId); + } + } + + // https://github.com/gnaeus/EntityFramework.CommonTools/issues/4 + [TestMethod] + public void TestAuditableEntitiesUpdateDetached() + { + using (var context = CreateSqliteDbContext()) + { + var firstUser = new User(); + var secondUser = new User(); + context.Users.Add(firstUser); + context.Users.Add(secondUser); + context.SaveChanges(); + + // insert + var post = new Post { Title = "first" }; + context.Posts.Add(post); + context.SaveChanges(firstUser.Id); + context.Entry(post).Reload(); + DateTime createdUtc = post.CreatedUtc; + + // attach modified entity with empty CreatedUtc and CreatorUserId + context.Entry(post).State = EntityState.Detached; + post = new Post + { + Id = post.Id, + Title = "second", + RowVersion = post.RowVersion, + }; +#if EF_CORE + context.Posts.Update(post); +#elif EF_6 + context.Entry(post).State = EntityState.Modified; +#endif + context.SaveChanges(firstUser.Id); + + // CreatedUtc and CreatorUserId should not be changed + context.Entry(post).Reload(); + Assert.AreEqual(createdUtc, post.CreatedUtc); + Assert.AreEqual(firstUser.Id, post.CreatorUserId); + + // attach modified entity with explicitely set CreatedUtc and CreatorUserId + context.Entry(post).State = EntityState.Detached; + post = new Post + { + Id = post.Id, + Title = "third", + RowVersion = post.RowVersion, + CreatedUtc = new DateTime(2018, 01, 01), + CreatorUserId = secondUser.Id, + }; +#if EF_CORE + context.Posts.Update(post); +#elif EF_6 + context.Entry(post).State = EntityState.Modified; +#endif + context.SaveChanges(firstUser.Id); + + // CreatedUtc and CreatorUserId should equals to explicitely passed values + context.Entry(post).Reload(); + Assert.AreEqual(new DateTime(2018, 01, 01), post.CreatedUtc); + Assert.AreEqual(secondUser.Id, post.CreatorUserId); + } + } } } diff --git a/EFCore.CommonTools/Auditing/AuditableEntitiesExtensions.cs b/EFCore.CommonTools/Auditing/AuditableEntitiesExtensions.cs index 00c096b..4494000 100644 --- a/EFCore.CommonTools/Auditing/AuditableEntitiesExtensions.cs +++ b/EFCore.CommonTools/Auditing/AuditableEntitiesExtensions.cs @@ -74,6 +74,12 @@ public static void UpdateAuditableEntities(this DbContext context, string editor UpdateTrackableEntity(dbEntry, utcNow); modificationAuditable.UpdaterUserId = editorUserId; dbEntry.CurrentValues[nameof(IModificationAuditable.UpdaterUserId)] = editorUserId; + + if (entity is ICreationAuditable) + { + PreventPropertyOverwrite( + dbEntry, nameof(ICreationAuditable.CreatorUserId)); + } } break; @@ -118,12 +124,22 @@ public static void UpdateAuditableEntities(this DbContext context, string editor UpdateTrackableEntity(dbEntry, utcNow); modificationAuditable.UpdaterUserId = editorUserId; dbEntry.CurrentValues[nameof(IModificationAuditable.UpdaterUserId)] = editorUserId; + + if (entity is ICreationAuditable) + { + PreventPropertyOverwrite(dbEntry, nameof(ICreationAuditable.CreatorUserId)); + } } else if (entity is IModificationAuditableV1 modificationAuditableV1) { UpdateTrackableEntity(dbEntry, utcNow); modificationAuditableV1.UpdaterUser = editorUserId; dbEntry.CurrentValues[nameof(IModificationAuditableV1.UpdaterUser)] = editorUserId; + + if (entity is ICreationAuditableV1) + { + PreventPropertyOverwrite(dbEntry, nameof(ICreationAuditableV1.CreatorUser)); + } } break; diff --git a/EFCore.CommonTools/Auditing/TrackableEntitiesExtensions.cs b/EFCore.CommonTools/Auditing/TrackableEntitiesExtensions.cs index eedb77c..3cfea4a 100644 --- a/EFCore.CommonTools/Auditing/TrackableEntitiesExtensions.cs +++ b/EFCore.CommonTools/Auditing/TrackableEntitiesExtensions.cs @@ -51,6 +51,11 @@ private static void UpdateTrackableEntity(EntityEntry dbEntry, DateTime utcNow) { modificatonTrackable.UpdatedUtc = utcNow; dbEntry.CurrentValues[nameof(IModificationTrackable.UpdatedUtc)] = utcNow; + + if (entity is ICreationTrackable) + { + PreventPropertyOverwrite(dbEntry, nameof(ICreationTrackable.CreatedUtc)); + } } break; @@ -73,5 +78,21 @@ private static void UpdateTrackableEntity(EntityEntry dbEntry, DateTime utcNow) throw new NotSupportedException(); } } + + /// + /// If we set to on entity with + /// empty or + /// we should not overwrite database values. + /// https://github.com/gnaeus/EntityFramework.CommonTools/issues/4 + /// + private static void PreventPropertyOverwrite(EntityEntry dbEntry, string propertyName) + { + var propertyEntry = dbEntry.Property(propertyName); + + if (propertyEntry.IsModified && Equals(dbEntry.CurrentValues[propertyName], default(TProperty))) + { + propertyEntry.IsModified = false; + } + } } }