Создание снимка ChangeTracker

Редактировал(а) Alexandr Fokin 2024/01/04 20:21

 Создание и восстановление снимка сущностей из ChangeTracker.
Create and restore an entity snapshot from Entity Framework Core ChangeTracker.
 
 
Компонент, позволяющий создать снимок контекста с возможностью выполнить восстановление состояния.
Позволяет делать нечто похожее на вложенную/виртуальную транзакцию в рамках одной транзакции БД.
Также позволяет отметить сущность, для которых не требуется выполнять откат изменений (Например если есть сущность процесса с фиксацией статуса и ошибки операции).
Также можно сказать, что это некий аналог InMemory SavePoint. Он позволяет создавать снимки отслеживаемых сущностей, модифицировать их и сбрасывать изменений.
При этом не выполняя забросов к БД (уменьшая количество запросов к БД).
 internal class ChangeTrackerSnapshotService<TDbContext>
    : IChangeTrackerSnapshotService
   where TDbContext : DbContext
{
   private readonly TDbContext _dbContext;


   public ChangeTrackerSnapshotService(
        TDbContext dbContext
        )
    {
        _dbContext = dbContext;
    }


   #region public

   public ISubscribe CaptureState()
    {
        Dictionary<Type, List<EntitySnapshot>> data = new ();

       foreach (var elem in _dbContext.ChangeTracker.Entries())
        {
           if (!data.TryGetValue(elem.Metadata.ClrType, out var store))
            {
                store = new List<EntitySnapshot>();
                data.Add(elem.Metadata.ClrType, store);
            }

           var item = new EntitySnapshot()
            {
                Entity = elem.Entity,
                State = elem.State,
                Properties = elem.Properties.ToDictionary(
                    e => e.Metadata.Name,
                    e => new PropertySnapshot()
                    {
                        CurrentValue = e.CurrentValue,
                        OriginalValue = e.OriginalValue,
                       //IsModified = e.IsModified
                   }
                    ),
                RestoreCurrentValue = true
            };
            store.Add(item);
        }

       var snapshot = new DbContextSnapshot()
        {
            Entities = data,
            IgnoreRestoreEntities = Array.Empty<EntitySnapshot>(),
        };
       return new Subscribe(this, snapshot);
    }

   public ISubscribe CaptureState<TKey, TEntity>(
        TEntity ignoreRestoreObject
        )
       where TEntity : Entity<TKey>
    {
        Dictionary<Type, List<EntitySnapshot>> data = new ();
        List<EntitySnapshot> ignoreRestoreEntities = new(1);

       foreach (var elem in _dbContext.ChangeTracker.Entries())
        {
           if (!data.TryGetValue(elem.Metadata.ClrType, out var store))
            {
                store = new List<EntitySnapshot>();
                data.Add(elem.Metadata.ClrType, store);
            }

            EntitySnapshot item;
           if (ReferenceEquals(elem.Entity, ignoreRestoreObject))
            {
                item = new EntitySnapshot()
                {
                    Entity = elem.Entity,
                    State = elem.State,
                    Properties = elem.Properties.ToDictionary(
                    e => e.Metadata.Name,
                    e => new PropertySnapshot()
                    {
                        CurrentValue = e.CurrentValue,
                        OriginalValue = e.OriginalValue
                    }
                    ),
                    RestoreCurrentValue = false
                };
                ignoreRestoreEntities.Add(item);
            }
           else
            {
                item = new EntitySnapshot()
                {
                    Entity = elem.Entity,
                    State = elem.State,
                    Properties = elem.Properties.ToDictionary(
                    e => e.Metadata.Name,
                    e => new PropertySnapshot()
                    {
                        CurrentValue = e.CurrentValue,
                        OriginalValue = e.OriginalValue,
                       //IsModified = e.IsModified
                   }
                    ),
                    RestoreCurrentValue = true
                };
            }
           
            store.Add(item);
        }

       var snapshot = new DbContextSnapshot()
        {
            Entities = data,
            IgnoreRestoreEntities = ignoreRestoreEntities
        };
       return new Subscribe(this, snapshot);
    }

   #endregion

   #region restore

   private void RestoreState(
       in DbContextSnapshot snapshot
        )
    {
       // 1) Фиксируем текущих значений для сущностей, которые не должны откатываться
       (EntitySnapshot snapshot, EntityEntry currentData)[] currentData;
        {
            currentData = snapshot.IgnoreRestoreEntities
                .Select(e => (e, _dbContext.Entry(e.Entity)))
                .ToArray();
        }

       // 2) Отчистка текущего состояния
           _dbContext.ChangeTracker.Clear();

       // 3) Отчистка навигационных коллекций в сущностях (не отчищаются автоматически).
       {
           var clearCollectionActions = snapshot.Entities.Keys
                .Select(e => (type: e, clearData: GetClearCollections(e)))
                .Where(e => e.clearData.needClear)
                .ToDictionary(e => e.type, e => e.clearData.clearAction);

           foreach (var elem in clearCollectionActions)
            {
               foreach (var elem2 in snapshot.Entities[elem.Key])
                {
                    elem.Value(elem2.Entity);
                }
            }
        }

       //4) Присоединяем все сущности
       foreach (var elem in snapshot.Entities.Values.SelectMany(e => e))
        {
           var entry = _dbContext.Attach(elem.Entity);                
        }

       // 5) Заполняем откатываемые сущности (фреймворк дозаполняет навигационные свойства)
       foreach (var elem in snapshot.Entities.Values.SelectMany(e => e).Where(e => e.RestoreCurrentValue))
        {
           var entry = _dbContext.Entry(elem.Entity);
            entry.State = elem.State;

           foreach (var elem2 in elem.Properties)
            {
               var prop = entry.Property(elem2.Key);
                prop.OriginalValue = elem2.Value.OriginalValue;
                prop.CurrentValue = elem2.Value.CurrentValue;
            }
        }

       // 6) Заполняем не откатываемые сущности (фреймворк дозаполняет навигационные свойства)
       foreach (var elem in currentData)
        {
           var entry = _dbContext.Entry(elem.snapshot.Entity);
            entry.State = elem.currentData.State;

           foreach (var elem2 in elem.currentData.Properties)
            {
               var prop = entry.Property(elem2.Metadata.Name);
                prop.OriginalValue = elem2.OriginalValue;
                prop.CurrentValue = elem2.CurrentValue;
            }
        }

       //_dbContext.ChangeTracker.DetectChanges();
   }

   private static (bool needClear, Action<object> clearAction) GetClearCollections(
        Type type
        )
    {
       var clearAction = _clearCollectionActions.GetOrAdd(
            type,
            valueFactory: (k) =>
            {
               var genericType = typeof(ICollection<>);
                List<Action<object>> clearActions = new();

               foreach (var elem in k.GetProperties())
                {
                   if (
                        elem.PropertyType.IsClass
                        && TypeHelper.TryGetGenericInterfaceParameter(elem.PropertyType, genericType, 0, out var collectionItemType)
                        )
                    {
                       var typedCollectionType = genericType.MakeGenericType(collectionItemType);

                       var method = typedCollectionType
                            .GetMethod(nameof(ICollection<object>.Clear));

                       var objectParameter = Expression.Parameter(typeof(object));
                       var callExpression = Expression.Call(
                            instance: Expression.Convert(
                                expression: Expression.Property(
                                    expression: Expression.Convert(
                                        expression: objectParameter,
                                        type: k
                                        ),
                                    property: elem
                                    ),
                                type: typedCollectionType
                                ),
                            method: method!
                            );

                       var clearAction = Expression
                            .Lambda<Action<object>>(callExpression, objectParameter)
                            .Compile();

                        clearActions.Add(clearAction);
                    }
                }

               if (clearActions.Count == 0)
                {
                   return (false, (e) => { });
                }
               else
                {
                   return (
                       true,
                        (e) =>
                        {
                           foreach (var elem in clearActions)
                            {
                                elem(e);
                            }
                        }
                    );
                }
            }
            );
       return clearAction;
    }

   private static readonly ConcurrentDictionary<Type, (bool needClear, Action<object>)> _clearCollectionActions
        = new ();

   #endregion


   #region Types

   private record Subscribe
        : ISubscribe
    {
       private readonly ChangeTrackerSnapshotService<TDbContext> _captureStateService;
       private readonly DbContextSnapshot _snapshot;
       private bool IsUsed { get; set; }


       public Subscribe(
            ChangeTrackerSnapshotService<TDbContext> captureStateService,
           in DbContextSnapshot snapshot
            )
        {
            _captureStateService = captureStateService;
            _snapshot = snapshot;
            IsUsed = false;
        }            


       public void NoRestore()
        {
           if (IsUsed)
            {
               return;
            }

            SetUsed();
        }

       public void Restore()
        {
           if (IsUsed)
            {
               return;
            }

            _captureStateService.RestoreState(_snapshot);
            SetUsed();
        }

       public void Dispose()
        {
            GC.SuppressFinalize(this);
           if (IsUsed)
            {
               return;
            }

            Restore();
        }

       private void SetUsed()
        {
            IsUsed = true;

           //Уменьшаем нагрузку на GC, очищаем коллекции                
           if (_snapshot.Entities is Dictionary<Type, List<EntitySnapshot>> entityCollection)
            {
               foreach (var elem in entityCollection.Values)
                {
                   foreach (var elem2 in elem)
                    {
                       if (elem2.Properties is Dictionary<string, PropertySnapshot> propertyCollection)
                        {
                            propertyCollection.Clear();
                            propertyCollection.TrimExcess();
                        }
                    }
                    elem.Clear();
                    elem.TrimExcess();
                }
                entityCollection.Clear();
                entityCollection.TrimExcess();
            }
           if (_snapshot.IgnoreRestoreEntities is List<EntitySnapshot> ignoreCollection)
            {
                ignoreCollection.Clear();
                ignoreCollection.TrimExcess();
            }
        }
    }

   #endregion
}


public readonly record struct DbContextSnapshot
{
   public IReadOnlyCollection<EntitySnapshot> IgnoreRestoreEntities { get; init; }
   public IReadOnlyDictionary<Type, List<EntitySnapshot>> Entities { get; init; }


   #region Types

   public readonly record struct EntitySnapshot
    {
       public object Entity { get; init; }
       public EntityState State { get; init; }
       /// <summary>
       /// Нужно ли сбрасывать сущность
       /// </summary>
       public bool RestoreCurrentValue { get; init; }
       public IReadOnlyDictionary<string, PropertySnapshot> Properties { get; init; }
    }

   public readonly record struct PropertySnapshot
    {
       public object? CurrentValue { get; init; }
       public object? OriginalValue { get; init; }
       //public bool IsModified { get; init; }
   }

   #endregion
}

 

Теги: