-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathAuditableEntitySaveChangesInterceptor.cs
More file actions
112 lines (99 loc) · 3.82 KB
/
AuditableEntitySaveChangesInterceptor.cs
File metadata and controls
112 lines (99 loc) · 3.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
using FSH.Framework.Core.Context;
using FSH.Framework.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace FSH.Framework.Persistence.Inteceptors;
/// <summary>
/// Interceptor that automatically populates audit metadata for entities implementing <see cref="IAuditableEntity"/>
/// and handles soft delete for entities implementing <see cref="ISoftDeletable"/>.
/// </summary>
public sealed class AuditableEntitySaveChangesInterceptor : SaveChangesInterceptor
{
private readonly ICurrentUser _currentUser;
private readonly TimeProvider _timeProvider;
[ThreadStatic]
private static bool _isSaving;
public AuditableEntitySaveChangesInterceptor(ICurrentUser currentUser, TimeProvider timeProvider)
{
_currentUser = currentUser;
_timeProvider = timeProvider;
}
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (_isSaving)
{
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
try
{
_isSaving = true;
UpdateAuditEntities(eventData.Context);
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
finally
{
_isSaving = false;
}
}
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
if (_isSaving)
{
return base.SavingChanges(eventData, result);
}
try
{
_isSaving = true;
UpdateAuditEntities(eventData.Context);
return base.SavingChanges(eventData, result);
}
finally
{
_isSaving = false;
}
}
private void UpdateAuditEntities(DbContext? context)
{
if (context == null) return;
var userId = _currentUser.IsAuthenticated() ? _currentUser.GetUserId().ToString() : null;
var now = _timeProvider.GetUtcNow();
foreach (var entry in context.ChangeTracker.Entries())
{
// Auditable Entities
if (entry.Entity is IAuditableEntity auditable)
{
if (entry.State == EntityState.Added)
{
entry.Property(nameof(IAuditableEntity.CreatedOnUtc)).CurrentValue = now;
entry.Property(nameof(IAuditableEntity.CreatedBy)).CurrentValue = userId;
}
else if (entry.State == EntityState.Modified || entry.HasChangedOwnedEntities())
{
entry.Property(nameof(IAuditableEntity.LastModifiedOnUtc)).CurrentValue = now;
entry.Property(nameof(IAuditableEntity.LastModifiedBy)).CurrentValue = userId;
}
}
// Soft Deletable Entities
if (entry.Entity is ISoftDeletable softDeletable && entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Property(nameof(ISoftDeletable.IsDeleted)).CurrentValue = true;
entry.Property(nameof(ISoftDeletable.DeletedOnUtc)).CurrentValue = now;
entry.Property(nameof(ISoftDeletable.DeletedBy)).CurrentValue = userId;
}
}
}
}
public static class Extensions
{
public static bool HasChangedOwnedEntities(this Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry) =>
entry.References.Any(r =>
r.TargetEntry != null &&
r.TargetEntry.Metadata.IsOwned() &&
(r.TargetEntry.State == EntityState.Added || r.TargetEntry.State == EntityState.Modified));
}