I'm giving a look to the Clean Architecture Solution Template for C#, available on Github here.
The class for which I'm looking for a clarification is the AuditableEntityInterceptor
class, whose source code is available here.
The code of the interceptor is the following one:
public class AuditableEntityInterceptor : SaveChangesInterceptor
{
private readonly IUser _user;
private readonly TimeProvider _dateTime;
public AuditableEntityInterceptor(
IUser user,
TimeProvider dateTime)
{
_user = user;
_dateTime = dateTime;
}
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
UpdateEntities(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
UpdateEntities(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
public void UpdateEntities(DbContext? context)
{
if (context == null) return;
foreach (var entry in context.ChangeTracker.Entries<BaseAuditableEntity>())
{
if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())
{
var utcNow = _dateTime.GetUtcNow();
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedBy = _user.Id;
entry.Entity.Created = utcNow;
}
entry.Entity.LastModifiedBy = _user.Id;
entry.Entity.LastModified = utcNow;
}
}
}
}
public static class Extensions
{
public static bool HasChangedOwnedEntities(this EntityEntry entry) =>
entry.References.Any(r =>
r.TargetEntry != null &&
r.TargetEntry.Metadata.IsOwned() &&
(r.TargetEntry.State == EntityState.Added || r.TargetEntry.State == EntityState.Modified));
}
This EF core interceptor is designed to run some logic when methods SaveChanges()
and SaveChangesAsync()
are called on the db context instance. Basically, the idea is being able to automatically set the values for the audit fields CreatedBy
, Created
, LastModifiedBy
and LastModified
defined by the base class BaseAuditableEntity
(which is used as a base class for any entity which needs auditing capabilities).
The piece of code that I'm not able to fully understand is the method named HasChangedOwnedEntities()
.
Is that really needed ?
Why the condition expressed by entry.State is EntityState.Added or EntityState.Modified
is not enough to identify any entity which has been added or modified during the current unit of work ?
My understanding is that the purpose of the HasChangedOwnedEntities()
method is to detect entities which must be considered modified because at least one of their owned entities has been modified during the current unit of work.
Is my understanding correct ?
Why simply checking the condition entry.State == EntityState.Modified
is not enough to identify these entities ?
Why simply checking the condition
entry.State == EntityState.Modified
is not enough to identify these entities ?
Because if only the owned entity is changed then the owner will be considered as unchanged, only the owned one itself will be marked as modified. This is quite simple to check:
public class Owner
{
public int Id { get; set; }
public Owned Owned { get; set; }
}
[Owned]
public class Owned
{
public string? Data { get; set; }
}
// setup context ctx which has DbSet<Owner> Owners
var owner = ctx.Owners.First();
owner.Owned.Data = "Updated" + Guid.NewGuid();
Console.WriteLine(ctx.Owners.Entry(owner).State); // prints "Unchanged"
Console.WriteLine(ctx.Entry(owner.Owned).State); // prints "Modified"
Owned entities are still entities with separate change tracking etc. That is the conceptual difference between them and Complex Types introduced with EF Core 8 (see What is difference between ComplexType and OwnsOne in Entity Framework Core?)