skip to Main Content

I have created a simple one-to-many hierarchy using the code first approach. All entities in the hierarchy have an Id, which is a Guid, and timestamps for CreatedOn and LastUpdate. These are contained in the BaseEntity class. Each entity may have other properties, but they are unimportant with regard to this problem. Each entity in the hierarchy additionally has a foreign key to the entity that is it’s parent, as well as a collection of children entities.

For example:

public class BaseEntity
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; set; }
        public DateTime CreatedOn { get; set; }
        public DateTime LastUpdate { get; set; }
    }

 public class RootDirectory : BaseEntity
    {
        public string RootName { get; set; }
        public ICollection<SubDirectory> SubDirectories { get; set; }
    }

Subdirectory class is as follows:

public class Subdirectory : BaseEntity
    {
        public string SubDirectoryName { get; set; }
        public Guid? RootId { get; set; }
        public RootDirectory Root { get; set; }
        public ICollection<File> Files { get; set; }
    }

The File class is then as follows:

public class File : BaseEntity
    {
        public string FileName { get; set; }
        public Guid? SubDirectoryId { get; set; }
        public SubDirectory SubDirectory { get; set; }
    }

This produces the appropriate Foreign key relationship which I was expecting. All operations are working great for this system, except for the LastUpdate.

LastUpdate updates as expected when I use the following generic method in a BaseRepo:

public virtual int Update(TEntity entity, bool persist = true)
    {
        Table.Update(entity);
        return persist ? SaveChanges() : 0;
    }

When I perform this on a File, for example, LastUpdate changes as expected to the time of the Update call. However, when the File is updated, the SubDirectory does not additionally get updated, and in turn, the RootDirectory does not get updated.

Essentially, is there a way in EF to make parents update if a child updates? For lack of better terms, a reverse cascade update? (Cascade delete removes all children for each entity when deleted at a higher level, can I update at a lower level and update all parents above?)

Note that this being generic is very important, as I want to utilize this BaseRepo on other entity hierarchies other than this one, and these hierarchies may have a different number of levels and other such differences.

I figure this has to be possible, as repositories on GitHub and DevOps do this when a file is changed or added. The file that was modified has the Last change, but so do all of it’s parent folders, as they know that something within them updated.

Let me know if anything isn’t clear. Like I said, everything else is working swimmingly for me with EF, and this is just a minor wrinkle that I couldn’t find any documentation or posts on, as most people seem to want to go the other direction, updating children when a parent is updated.

2

Answers


  1. Chosen as BEST ANSWER

    It appears that there is no current, out-of-the-box solution for doing this, so for now I am implementing in a recursive update where I manually go through and update all parents of the entities that have been updated or added. If this changes down the road, feel free to place a new answer saying so.


  2. It’s not as automatic as you want, but I add a SavingChanges event handler to the context. The handler gets the list of added or updated entities and passes them to a processing routine. You can see special code for the cases where the UpdatedDateTime attribute exists in a parent entity. I think you could probably automate much of this by recursively looking for parent entities, but I only have a few cases so it was simpler to include the specific code for those cases.

    /// <summary>
    /// SavingChanges event handler to update UpdatedDateTime and UpdatedBy properties.
    /// </summary>
    /// <param name="sender">The ConferenceDbContext whose changes are about to be saved.</param>
    /// <param name="e">Standard SavingChanges event arg object, not used here.</param>
    private void AssignUpdatedByAndTime(object? sender, SavingChangesEventArgs e)
    {
        //Get added or modified entities.
        List<EntityEntry> changedEntities = ChangeTracker.Entries()
            .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
            .ToList();
    
        //Assign UpdatedDateTime and UpdatedBy properties if they exist.
        foreach (EntityEntry entityEntry in changedEntities)
        {
            //Some subcategory entities have the updated date/by attributes in the parent entity.
            EntityEntry? parentEntry = null;
            string entityTypeName = entityEntry.Metadata.Name;  //Full class name, e.g., ConferenceEF.Models.Meeting
            entityTypeName = entityTypeName.Split('.').Last();
            switch (entityTypeName)
            {
                case nameof(Paper):
                case nameof(FlashPresentation):
                    parentEntry = entityEntry.Reference(nameof(SessionItem)).TargetEntry;
                    break;
                default:
                    break;
            }
            AssignUpdatedByUserAndTime(parentEntry ?? entityEntry);
        }
    }
    
    private void AssignUpdatedByUserAndTime(EntityEntry entityEntry)
    {
        if (entityEntry.Entity is BaseEntityWithUpdatedAndRowVersion
            || entityEntry.Entity is PaperRating)
        {
            PropertyEntry updatedDateTime = entityEntry.Property("UpdatedDateTime");
            DateTimeOffset? currentValue = (DateTimeOffset?)updatedDateTime.CurrentValue;
            //Avoid possible loops by only updating time when it has changed by at least 1 minute.
            //Is this necessary?
            if (!currentValue.HasValue || currentValue < DateTimeOffset.Now.AddMinutes(-1))
                updatedDateTime.CurrentValue = DateTimeOffset.Now;
    
            if (entityEntry.Entity is BaseEntityWithUpdatedAndRowVersion
                || entityEntry.Properties.Any(p => p.Metadata.Name == "UpdatedBy"))
            {
                PropertyEntry updatedBy = entityEntry.Property("UpdatedBy");
                string? newValue = CurrentUserName;  //ClaimsPrincipal.Current?.Identity?.Name;
                if (newValue == null && !updatedBy.Metadata.IsColumnNullable())
                    newValue = string.Empty;
                if (updatedBy.CurrentValue?.ToString() != newValue)
                    updatedBy.CurrentValue = newValue;
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search