Skip to content

Commit 7cab79e

Browse files
PanyushkinDPanyushkinD
authored andcommitted
Fix Auditing of Detached entities #4
1 parent 6da3019 commit 7cab79e

File tree

3 files changed

+168
-11
lines changed

3 files changed

+168
-11
lines changed

EFCore.CommonTools.Tests/Auditing/AuditableEntitiesTests.cs

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
using Microsoft.VisualStudio.TestTools.UnitTesting;
44

55
#if EF_CORE
6+
using Microsoft.EntityFrameworkCore;
7+
68
namespace EntityFrameworkCore.CommonTools.Tests
79
#elif EF_6
10+
using System.Data.Entity;
11+
812
namespace EntityFramework.CommonTools.Tests
913
#endif
1014
{
@@ -16,44 +20,44 @@ public void TestAuditableEntitiesGeneric()
1620
{
1721
using (var context = CreateInMemoryDbContext())
1822
{
19-
var author = new User();
20-
context.Users.Add(author);
23+
var user = new User();
24+
context.Users.Add(user);
25+
context.SaveChanges();
2126

2227
// insert
23-
var post = new Post { Title = "first", Author = author };
28+
var post = new Post { Title = "first" };
2429
context.Posts.Add(post);
25-
26-
context.SaveChanges(author.Id);
30+
context.SaveChanges(user.Id);
2731

2832
context.Entry(post).Reload();
2933
Assert.AreEqual(DateTime.UtcNow.Date, post.CreatedUtc.ToUniversalTime().Date);
30-
Assert.AreEqual(author.Id, post.CreatorUserId);
34+
Assert.AreEqual(user.Id, post.CreatorUserId);
3135

3236
// update
3337
post.Title = "second";
3438

35-
context.SaveChanges(author.Id);
39+
context.SaveChanges(user.Id);
3640

3741
context.Entry(post).Reload();
3842
Assert.IsNotNull(post.UpdatedUtc);
3943
Assert.AreEqual(DateTime.UtcNow.Date, post.UpdatedUtc?.ToUniversalTime().Date);
40-
Assert.AreEqual(author.Id, post.UpdaterUserId);
44+
Assert.AreEqual(user.Id, post.UpdaterUserId);
4145

4246
// delete
4347
context.Posts.Remove(post);
4448

45-
context.SaveChanges(author.Id);
49+
context.SaveChanges(user.Id);
4650

4751
context.Entry(post).Reload();
4852
Assert.AreEqual(true, post.IsDeleted);
4953
Assert.IsNotNull(post.DeletedUtc);
5054
Assert.AreEqual(DateTime.UtcNow.Date, post.DeletedUtc?.ToUniversalTime().Date);
51-
Assert.AreEqual(author.Id, post.DeleterUserId);
55+
Assert.AreEqual(user.Id, post.DeleterUserId);
5256
}
5357
}
5458

5559
[TestMethod]
56-
public async Task TestAuditableEntities()
60+
public async Task TestAuditableEntitiesString()
5761
{
5862
using (var context = CreateSqliteDbContext())
5963
{
@@ -89,5 +93,121 @@ public async Task TestAuditableEntities()
8993
Assert.AreEqual("admin", settings.DeleterUserId);
9094
}
9195
}
96+
97+
// https://github.com/gnaeus/EntityFramework.CommonTools/issues/4
98+
[TestMethod]
99+
public void TestAuditableEntitiesUpdateExisting()
100+
{
101+
using (var context = CreateSqliteDbContext())
102+
{
103+
var firstUser = new User();
104+
var secondUser = new User();
105+
context.Users.Add(firstUser);
106+
context.Users.Add(secondUser);
107+
context.SaveChanges();
108+
109+
// insert
110+
var post = new Post { Title = "first" };
111+
context.Posts.Add(post);
112+
context.SaveChanges(firstUser.Id);
113+
context.Entry(post).Reload();
114+
DateTime createdUtc = post.CreatedUtc;
115+
116+
// set empty CreatedUtc and CreatorUserId
117+
post.CreatedUtc = default(DateTime);
118+
post.CreatorUserId = default(int);
119+
post.Title = "second";
120+
#if EF_CORE
121+
context.Posts.Update(post);
122+
#elif EF_6
123+
context.Entry(post).State = EntityState.Modified;
124+
#endif
125+
context.SaveChanges(firstUser.Id);
126+
127+
// CreatedUtc and CreatorUserId should not be changed
128+
context.Entry(post).Reload();
129+
Assert.AreEqual(createdUtc, post.CreatedUtc);
130+
Assert.AreEqual(firstUser.Id, post.CreatorUserId);
131+
132+
// explicitely change CreatedUtc and CreatorUserId
133+
post.CreatedUtc = new DateTime(2018, 01, 01);
134+
post.CreatorUserId = secondUser.Id;
135+
post.Title = "third";
136+
#if EF_CORE
137+
context.Posts.Update(post);
138+
#elif EF_6
139+
context.Entry(post).State = EntityState.Modified;
140+
#endif
141+
context.SaveChanges(firstUser.Id);
142+
143+
// CreatedUtc and CreatorUserId should equals to explicitely passed values
144+
context.Entry(post).Reload();
145+
Assert.AreEqual(new DateTime(2018, 01, 01), post.CreatedUtc);
146+
Assert.AreEqual(secondUser.Id, post.CreatorUserId);
147+
}
148+
}
149+
150+
// https://github.com/gnaeus/EntityFramework.CommonTools/issues/4
151+
[TestMethod]
152+
public void TestAuditableEntitiesUpdateDetached()
153+
{
154+
using (var context = CreateSqliteDbContext())
155+
{
156+
var firstUser = new User();
157+
var secondUser = new User();
158+
context.Users.Add(firstUser);
159+
context.Users.Add(secondUser);
160+
context.SaveChanges();
161+
162+
// insert
163+
var post = new Post { Title = "first" };
164+
context.Posts.Add(post);
165+
context.SaveChanges(firstUser.Id);
166+
context.Entry(post).Reload();
167+
DateTime createdUtc = post.CreatedUtc;
168+
169+
// attach modified entity with empty CreatedUtc and CreatorUserId
170+
context.Entry(post).State = EntityState.Detached;
171+
post = new Post
172+
{
173+
Id = post.Id,
174+
Title = "second",
175+
RowVersion = post.RowVersion,
176+
};
177+
#if EF_CORE
178+
context.Posts.Update(post);
179+
#elif EF_6
180+
context.Entry(post).State = EntityState.Modified;
181+
#endif
182+
context.SaveChanges(firstUser.Id);
183+
184+
// CreatedUtc and CreatorUserId should not be changed
185+
context.Entry(post).Reload();
186+
Assert.AreEqual(createdUtc, post.CreatedUtc);
187+
Assert.AreEqual(firstUser.Id, post.CreatorUserId);
188+
189+
// attach modified entity with explicitely set CreatedUtc and CreatorUserId
190+
context.Entry(post).State = EntityState.Detached;
191+
post = new Post
192+
{
193+
Id = post.Id,
194+
Title = "third",
195+
RowVersion = post.RowVersion,
196+
CreatedUtc = new DateTime(2018, 01, 01),
197+
CreatorUserId = secondUser.Id,
198+
};
199+
#if EF_CORE
200+
context.Posts.Update(post);
201+
#elif EF_6
202+
context.Entry(post).State = EntityState.Modified;
203+
#endif
204+
context.SaveChanges(firstUser.Id);
205+
206+
// CreatedUtc and CreatorUserId should equals to explicitely passed values
207+
context.Entry(post).Reload();
208+
Assert.AreEqual(new DateTime(2018, 01, 01), post.CreatedUtc);
209+
Assert.AreEqual(secondUser.Id, post.CreatorUserId);
210+
}
211+
}
92212
}
93213
}

EFCore.CommonTools/Auditing/AuditableEntitiesExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ private static void UpdateAuditableEntity<TUserId>(
7474
UpdateTrackableEntity(dbEntry, utcNow);
7575
modificationAuditable.UpdaterUserId = editorUserId;
7676
dbEntry.CurrentValues[nameof(IModificationAuditable<TUserId>.UpdaterUserId)] = editorUserId;
77+
78+
if (entity is ICreationAuditable<TUserId>)
79+
{
80+
PreventPropertyOverwrite<TUserId>(
81+
dbEntry, nameof(ICreationAuditable<TUserId>.CreatorUserId));
82+
}
7783
}
7884
break;
7985

@@ -118,12 +124,22 @@ private static void UpdateAuditableEntity(
118124
UpdateTrackableEntity(dbEntry, utcNow);
119125
modificationAuditable.UpdaterUserId = editorUserId;
120126
dbEntry.CurrentValues[nameof(IModificationAuditable.UpdaterUserId)] = editorUserId;
127+
128+
if (entity is ICreationAuditable)
129+
{
130+
PreventPropertyOverwrite<string>(dbEntry, nameof(ICreationAuditable.CreatorUserId));
131+
}
121132
}
122133
else if (entity is IModificationAuditableV1 modificationAuditableV1)
123134
{
124135
UpdateTrackableEntity(dbEntry, utcNow);
125136
modificationAuditableV1.UpdaterUser = editorUserId;
126137
dbEntry.CurrentValues[nameof(IModificationAuditableV1.UpdaterUser)] = editorUserId;
138+
139+
if (entity is ICreationAuditableV1)
140+
{
141+
PreventPropertyOverwrite<string>(dbEntry, nameof(ICreationAuditableV1.CreatorUser));
142+
}
127143
}
128144
break;
129145

EFCore.CommonTools/Auditing/TrackableEntitiesExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ private static void UpdateTrackableEntity(EntityEntry dbEntry, DateTime utcNow)
5151
{
5252
modificatonTrackable.UpdatedUtc = utcNow;
5353
dbEntry.CurrentValues[nameof(IModificationTrackable.UpdatedUtc)] = utcNow;
54+
55+
if (entity is ICreationTrackable)
56+
{
57+
PreventPropertyOverwrite<DateTime>(dbEntry, nameof(ICreationTrackable.CreatedUtc));
58+
}
5459
}
5560
break;
5661

@@ -73,5 +78,21 @@ private static void UpdateTrackableEntity(EntityEntry dbEntry, DateTime utcNow)
7378
throw new NotSupportedException();
7479
}
7580
}
81+
82+
/// <summary>
83+
/// If we set <see cref="EntityEntry.State"/> to <see cref="EntityState.Modified"/> on entity with
84+
/// empty <see cref="ICreationTrackable.CreatedUtc"/> or <see cref="ICreationAuditable.CreatorUserId"/>
85+
/// we should not overwrite database values.
86+
/// https://github.com/gnaeus/EntityFramework.CommonTools/issues/4
87+
/// </summary>
88+
private static void PreventPropertyOverwrite<TProperty>(EntityEntry dbEntry, string propertyName)
89+
{
90+
var propertyEntry = dbEntry.Property(propertyName);
91+
92+
if (propertyEntry.IsModified && Equals(dbEntry.CurrentValues[propertyName], default(TProperty)))
93+
{
94+
propertyEntry.IsModified = false;
95+
}
96+
}
7697
}
7798
}

0 commit comments

Comments
 (0)