Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish EntityUpdatedEvent when navigation changes. #19079

Merged
merged 5 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Volo.Abp.Domain.Entities.Events;

public class AbpEntityChangeOptions
{
/// <summary>
/// Default: true.
/// Publish the EntityUpdatedEvent when any navigation property changes.
/// </summary>
public bool PublishEntityUpdatedEventWhenNavigationChanges { get; set; } = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Auditing;
Expand All @@ -20,6 +21,7 @@
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EntityFrameworkCore.ChangeTrackers;
using Volo.Abp.EntityFrameworkCore.EntityHistory;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
Expand Down Expand Up @@ -53,6 +55,8 @@

public IEntityChangeEventHelper EntityChangeEventHelper => LazyServiceProvider.LazyGetService<IEntityChangeEventHelper>(NullEntityChangeEventHelper.Instance);

public IOptions<AbpEntityChangeOptions> EntityChangeOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpEntityChangeOptions>>();

public IAuditPropertySetter AuditPropertySetter => LazyServiceProvider.LazyGetRequiredService<IAuditPropertySetter>();

public IEntityHistoryHelper EntityHistoryHelper => LazyServiceProvider.LazyGetService<IEntityHistoryHelper>(NullEntityHistoryHelper.Instance);
Expand All @@ -69,6 +73,8 @@

public ILogger<AbpDbContext<TDbContext>> Logger => LazyServiceProvider.LazyGetService<ILogger<AbpDbContext<TDbContext>>>(NullLogger<AbpDbContext<TDbContext>>.Instance);

public AbpEfCoreNavigationHelper AbpEfCoreNavigationHelper => LazyServiceProvider.LazyGetRequiredService<AbpEfCoreNavigationHelper>();

private static readonly MethodInfo ConfigureBasePropertiesMethodInfo
= typeof(AbpDbContext<TDbContext>)
.GetMethod(
Expand Down Expand Up @@ -160,6 +166,7 @@
List<EntityChangeInfo>? entityChangeList = null;
if (auditLog != null)
{
EntityHistoryHelper.InitializeNavigationHelper(AbpEfCoreNavigationHelper);
entityChangeList = EntityHistoryHelper.CreateChangeList(ChangeTracker.Entries().ToList());
}

Expand Down Expand Up @@ -251,12 +258,14 @@

protected virtual void ChangeTracker_Tracked(object? sender, EntityTrackedEventArgs e)
{
AbpEfCoreNavigationHelper.ChangeTracker_Tracked(ChangeTracker, sender, e);
FillExtraPropertiesForTrackedEntities(e);
PublishEventsForTrackedEntity(e.Entry);
}

protected virtual void ChangeTracker_StateChanged(object? sender, EntityStateChangedEventArgs e)
{
AbpEfCoreNavigationHelper.ChangeTracker_StateChanged(ChangeTracker, sender, e);
PublishEventsForTrackedEntity(e.Entry);
}

Expand Down Expand Up @@ -306,17 +315,18 @@
}
}

private void PublishEventsForTrackedEntity(EntityEntry entry)
protected virtual void PublishEventsForTrackedEntity(EntityEntry entry)
{
switch (entry.State)
{
case EntityState.Added:
ApplyAbpConceptsForAddedEntity(entry);
EntityChangeEventHelper.PublishEntityCreatedEvent(entry.Entity);
break;

case EntityState.Modified:
ApplyAbpConceptsForModifiedEntity(entry);
if (entry.Properties.Any(x => x.IsModified && x.Metadata.ValueGenerated == ValueGenerated.Never))
if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
{
if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
{
Expand All @@ -327,13 +337,28 @@
EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity);
}
}

break;

case EntityState.Deleted:
ApplyAbpConceptsForDeletedEntity(entry);
EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
break;
}

if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges)
{
foreach (var entityEntry in ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged && AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(x)))
{
if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As<ISoftDelete>().IsDeleted)
{
EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity);
}

Check warning on line 355 in framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

View check run for this annotation

Codecov / codecov/patch

framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs#L353-L355

Added lines #L353 - L355 were not covered by tests
else
{
EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity);
}
}
}
}

protected virtual void HandlePropertiesBeforeSave()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;

namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers;

/// <summary>
/// Refactor this class after EF Core supports this case.
/// https://github.com/dotnet/efcore/issues/24076#issuecomment-1996623874
/// </summary>
public class AbpEfCoreNavigationHelper : ITransientDependency
{
private Dictionary<string, List<AbpEntityEntryNavigationProperty>> EntityEntryNavigationProperties { get; } = new ();

public virtual void ChangeTracker_Tracked(ChangeTracker changeTracker, object? sender, EntityTrackedEventArgs e)
{
foreach (var entry in changeTracker.Entries())
{
EntityEntryTrackedOrStateChanged(entry);
}
}

public virtual void ChangeTracker_StateChanged(ChangeTracker changeTracker, object? sender, EntityStateChangedEventArgs e)
{
foreach (var entry in changeTracker.Entries())
{
EntityEntryTrackedOrStateChanged(entry);
}
}

private void EntityEntryTrackedOrStateChanged(EntityEntry entityEntry)
{
if (entityEntry.State != EntityState.Unchanged)
{
return;
}

var entryId = GetEntityId(entityEntry);
if (entryId == null)
{
return;
}

var navigationProperties = EntityEntryNavigationProperties.GetOrAdd(entryId, () => new List<AbpEntityEntryNavigationProperty>());
var index = 0;
foreach (var navigationEntry in entityEntry.Navigations.Where(navigation => !navigation.IsModified))
{
if (!navigationEntry.IsLoaded)
{
index++;
continue;
}

var currentValue = navigationEntry.CurrentValue;
if (navigationEntry.CurrentValue is ICollection collection)
{
currentValue = collection.Cast<object?>().ToList();
}

var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index);
if (navigationProperty != null)
{
if (!navigationProperty.IsChanged && (navigationProperty.Value == null || IsCollectionAndEmpty(navigationProperty.Value)))
{
navigationProperty.Value = currentValue;
navigationProperty.IsChanged = currentValue != null && !IsCollectionAndEmpty(currentValue);
}

if (!navigationProperty.IsChanged && navigationProperty.Value != null && !IsCollectionAndEmpty(navigationProperty.Value))
{
navigationProperty.Value = currentValue;
navigationProperty.IsChanged = currentValue == null || IsCollectionAndEmpty(currentValue);
}
}
else
{
navigationProperties.Add(new AbpEntityEntryNavigationProperty(index, navigationEntry.Metadata.Name, currentValue, false));
}

index++;
}
}

public bool IsEntityEntryNavigationChanged(EntityEntry entityEntry)
{
if (entityEntry.State == EntityState.Modified)
{
return true;

Check warning on line 93 in framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs

View check run for this annotation

Codecov / codecov/patch

framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs#L92-L93

Added lines #L92 - L93 were not covered by tests
}

var entryId = GetEntityId(entityEntry);
if (entryId == null)
{
return false;
}

var index = 0;
foreach (var navigationEntry in entityEntry.Navigations)
{
if (navigationEntry.IsModified || (navigationEntry is ReferenceEntry && navigationEntry.As<ReferenceEntry>().TargetEntry?.State == EntityState.Modified))
{
return true;
}

EntityEntryTrackedOrStateChanged(entityEntry);

if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties))
{
var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index);
if (navigationProperty != null && navigationProperty.IsChanged)
{
return true;
}
}

index++;
}

return false;
}

public bool IsEntityEntryNavigationChanged(NavigationEntry navigationEntry, int index)
{
if (navigationEntry.IsModified || (navigationEntry is ReferenceEntry && navigationEntry.As<ReferenceEntry>().TargetEntry?.State == EntityState.Modified))
{
return true;
}

var entryId = GetEntityId(navigationEntry.EntityEntry);
if (entryId == null)
{
return false;

Check warning on line 137 in framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs

View check run for this annotation

Codecov / codecov/patch

framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs#L136-L137

Added lines #L136 - L137 were not covered by tests
}

if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties))
{
var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index);
if (navigationProperty != null && navigationProperty.IsChanged)
{
return true;
}
}

return false;
}

private string? GetEntityId(EntityEntry entityEntry)
{
return entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1
? entryEntity.GetKeys().FirstOrDefault()?.ToString()
: null;
}

private bool IsCollectionAndEmpty(object? value)
{
return value is ICollection && value is ICollection collection && collection.Count == 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers;

public class AbpEntityEntryNavigationProperty
{
public int Index { get; set; }

public string Name { get; set; }

public object? Value { get; set; }

public bool IsChanged { get; set; }

public AbpEntityEntryNavigationProperty(int index, string name, object? value, bool isChanged)
{
Index = index;
Name = name;
Value = value;
IsChanged = isChanged;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;
Expand All @@ -13,6 +12,7 @@
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Values;
using Volo.Abp.EntityFrameworkCore.ChangeTrackers;
using Volo.Abp.Json;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection;
Expand All @@ -30,6 +30,8 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
protected IAuditingHelper AuditingHelper { get; }
protected IClock Clock { get; }

protected AbpEfCoreNavigationHelper? AbpEfCoreNavigationHelper { get; set; }

public EntityHistoryHelper(
IAuditingStore auditingStore,
IOptions<AbpAuditingOptions> options,
Expand All @@ -46,6 +48,11 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
Logger = NullLogger<EntityHistoryHelper>.Instance;
}

public void InitializeNavigationHelper(AbpEfCoreNavigationHelper abpEfCoreNavigationHelper)
{
AbpEfCoreNavigationHelper = abpEfCoreNavigationHelper;
}

public virtual List<EntityChangeInfo> CreateChangeList(ICollection<EntityEntry> entityEntries)
{
var list = new List<EntityChangeInfo>();
Expand Down Expand Up @@ -186,18 +193,21 @@ protected virtual List<EntityPropertyChangeInfo> GetPropertyChanges(EntityEntry
}
}

if (Options.SaveEntityHistoryWhenNavigationChanges && entityEntry.State == EntityState.Unchanged)
if (entityEntry.State == EntityState.Unchanged && Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null)
{
var index = 0;
foreach (var navigation in entityEntry.Navigations)
{
if (navigation.IsModified || (navigation is ReferenceEntry && navigation.As<ReferenceEntry>().TargetEntry?.State == EntityState.Modified))
if (navigation.IsModified || AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(navigation, index))
{
propertyChanges.Add(new EntityPropertyChangeInfo
{
PropertyName = navigation.Metadata.Name,
PropertyTypeFullName = navigation.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!
});
}

index++;
}
}

Expand Down Expand Up @@ -245,9 +255,7 @@ protected virtual bool ShouldSaveEntityHistory(EntityEntry entityEntry, bool def

protected virtual bool HasNavigationPropertiesChanged(EntityEntry entityEntry)
{
return Options.SaveEntityHistoryWhenNavigationChanges && entityEntry.State == EntityState.Unchanged &&
(entityEntry.Navigations.Any(navigationEntry => navigationEntry.IsModified) ||
entityEntry.Navigations.Where(x => x is ReferenceEntry).Cast<ReferenceEntry>().Any(x => x.TargetEntry != null && x.TargetEntry.State == EntityState.Modified));
return entityEntry.State == EntityState.Unchanged && Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null && AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(entityEntry);
}

protected virtual bool ShouldSavePropertyHistory(PropertyEntry propertyEntry, bool defaultValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Volo.Abp.Auditing;
using Volo.Abp.EntityFrameworkCore.ChangeTrackers;

namespace Volo.Abp.EntityFrameworkCore.EntityHistory;

public interface IEntityHistoryHelper
{
void InitializeNavigationHelper(AbpEfCoreNavigationHelper abpEfCoreNavigationHelper);

List<EntityChangeInfo> CreateChangeList(ICollection<EntityEntry> entityEntries);

void UpdateChangeList(List<EntityChangeInfo> entityChanges);
Expand Down