Skip to content

Commit

Permalink
Fix Auditing of Detached entities #4
Browse files Browse the repository at this point in the history
  • Loading branch information
PanyushkinD authored and PanyushkinD committed May 10, 2018
1 parent 6da3019 commit 7cab79e
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 11 deletions.
142 changes: 131 additions & 11 deletions EFCore.CommonTools.Tests/Auditing/AuditableEntitiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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())
{
Expand Down Expand Up @@ -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);
}
}
}
}
16 changes: 16 additions & 0 deletions EFCore.CommonTools/Auditing/AuditableEntitiesExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ private static void UpdateAuditableEntity<TUserId>(
UpdateTrackableEntity(dbEntry, utcNow);
modificationAuditable.UpdaterUserId = editorUserId;
dbEntry.CurrentValues[nameof(IModificationAuditable<TUserId>.UpdaterUserId)] = editorUserId;

if (entity is ICreationAuditable<TUserId>)
{
PreventPropertyOverwrite<TUserId>(
dbEntry, nameof(ICreationAuditable<TUserId>.CreatorUserId));
}
}
break;

Expand Down Expand Up @@ -118,12 +124,22 @@ private static void UpdateAuditableEntity(
UpdateTrackableEntity(dbEntry, utcNow);
modificationAuditable.UpdaterUserId = editorUserId;
dbEntry.CurrentValues[nameof(IModificationAuditable.UpdaterUserId)] = editorUserId;

if (entity is ICreationAuditable)
{
PreventPropertyOverwrite<string>(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<string>(dbEntry, nameof(ICreationAuditableV1.CreatorUser));
}
}
break;

Expand Down
21 changes: 21 additions & 0 deletions EFCore.CommonTools/Auditing/TrackableEntitiesExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DateTime>(dbEntry, nameof(ICreationTrackable.CreatedUtc));
}
}
break;

Expand All @@ -73,5 +78,21 @@ private static void UpdateTrackableEntity(EntityEntry dbEntry, DateTime utcNow)
throw new NotSupportedException();
}
}

/// <summary>
/// If we set <see cref="EntityEntry.State"/> to <see cref="EntityState.Modified"/> on entity with
/// empty <see cref="ICreationTrackable.CreatedUtc"/> or <see cref="ICreationAuditable.CreatorUserId"/>
/// we should not overwrite database values.
/// https://github.com/gnaeus/EntityFramework.CommonTools/issues/4
/// </summary>
private static void PreventPropertyOverwrite<TProperty>(EntityEntry dbEntry, string propertyName)
{
var propertyEntry = dbEntry.Property(propertyName);

if (propertyEntry.IsModified && Equals(dbEntry.CurrentValues[propertyName], default(TProperty)))
{
propertyEntry.IsModified = false;
}
}
}
}

0 comments on commit 7cab79e

Please sign in to comment.