Home > database >  Correct way to Trigger Parent Entity UpdatedAt Refresh when Child Entity is Updated
Correct way to Trigger Parent Entity UpdatedAt Refresh when Child Entity is Updated

Time:09-23

In this scenario using EF I have an Author who has a 1 to N relationship with Articles. The requirement is that when an Article is updated the Author's UpdatedAt timestamp should reflect the change. Now the brute force way would be to load the Author and do a dummy Save when I'm updating an Article. That seems very inefficient. Is there a best practice approach?

CodePudding user response:

Don't now if it is best practice, this is my way to do that. I use a Interface in Entity for that and override DbContext SaveChangesAsync Methode. You can also write Interfaces for insert, delete.

    public interface IBeforeUpdate
    {        
        void BeforeUpdate();
    }
    public class TestDbContext : DbContext
    {        
        public TestDbContext(DbContextOptions options) : base(options)
        {
        }

        public List<IBeforeUpdate> GetModifiedBeforeUpdateEntites() => ChangeTracker
            .Entries()
            .Where(it => it.State == EntityState.Modified && it.Entity is IBeforeUpdate)
            .Select(it => (IBeforeUpdate)it.Entity)
            .ToList();

        public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) 
        {            
            DoBeforeUpdate();
            return base.SaveChanges();
        }

        private void DoBeforeUpdate()
        {
            List<IBeforeUpdate> modifiedEntites = GetModifiedBeforeUpdateEntites();
            foreach (IBeforeUpdate entity in modifiedEntites)
            {
                entity.BeforeUpdate();
            }
        }
    }
    public class Article : IBeforeUpdate
    {
        public long Id { get; set; }

        public Author Author { get; set; }

        public BeforeUpdate()
        {
            Author.UpdatedAt = DateTime.UtcNow;
        }
    }

CodePudding user response:

Assuming we have the following Model:

public class Author
{
    public int Id { get; set; }
    public string AuthorValue { get; set; } = null!;

    public DateTime? UpdatedAt { get; set; }

    public List<Article> Articles { get; set; } = new();
}

public class Article
{
    public int Id { get; set; }
    public string ArticleValue { get; set; } = null!;

    public int AuthorId { get; set; }
    public Author Author { get; set; } = null!;
}

Then we can override SaveChanges/Async and intercept modification of Article table. If Author is not loaded, new entry will be attached and single property marked as modified.

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Author> Authors => Set<Author>();
    public DbSet<Article> Articles => Set<Article>();

    private void HandleChanges()
    {
        var authorsToUpdate = ChangeTracker.Entries<Article>()
            .Where(a => a.State == EntityState.Added || a.State == EntityState.Modified || a.State == EntityState.Deleted)
            .Select(a => (a.Entity.AuthorId, a.Entity.Author))
            .Distinct();

        foreach (var pair in authorsToUpdate)
        {
            if (pair.Author != null)
            {
                // Author already loaded and we can update this record
                pair.Author.UpdatedAt = DateTime.Now;
            }
            else
            {
                // add fake record just for update
                var author = new Author { Id = pair.AuthorId, UpdatedAt = DateTime.Now };
                Attach(author);
                Entry(author).Property(x => x.UpdatedAt).IsModified = true;
            }
        }
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        HandleChanges();
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }

    public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
    {
        HandleChanges();
        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }
}
  • Related