Создание снимка ChangeTracker
Редактировал(а) Alexandr Fokin 2024/01/04 20:21
Создание и восстановление снимка сущностей из ChangeTracker. Create and restore an entity snapshot from Entity Framework Core ChangeTracker. | |||
| |||
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 } |