Search code examples
c#entity-framework-coreclean-architecture.net-8.0

Need clarification on the AuditableEntityInterceptor class of the Clean Architecture solution template for C#


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 ?


Solution

  • 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?)