using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Base; using PARR.DAL.Context; using PARR.Domain.Common.Pagination; using PARR.Domain.Entities.Attributes; using PARR.Domain.Entities.Base; using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History.Base; using System.Reflection; namespace PARR.DAL.Repositories.Base { internal abstract class BaseRepository : IBaseRepository where T : class, IBaseEntity { protected readonly ILogger _logger; protected readonly DbSet EntitySet; protected readonly DataContext EntityContext; protected BaseRepository(ILogger logger, DataContext dataContext) { this._logger = logger; this.EntityContext = dataContext; this.EntitySet = dataContext.Set(); } public virtual async Task AddRangeAsync(List objs) { _logger.LogDebug("Начинаю добавление диапазона объектов типа {EntityType}, количество: {Count}", typeof(T).Name, objs.Count); objs.ForEach(item => item.DateCreated = DateTimeOffset.UtcNow); try { await EntitySet.AddRangeAsync(objs); _logger.LogDebug("Успешно добавлено {Count} объектов типа {EntityType}", objs.Count, typeof(T).Name); return true; } catch (Exception ex) { _logger.LogError(ex, "Ошибка при добавлении диапазона объектов типа {EntityType}", typeof(T).Name); return false; } } public async Task CommitAsync(IHistoryInitiator? initiator = null) { _logger.LogDebug("Начинаю сохранение изменений в БД для объектов типа {EntityType}", typeof(T).Name); #region Изменения var modifiedEntrities = EntityContext.ChangeTracker.Entries() .Where(t => t.State == EntityState.Modified/* || t.State == EntityState.Deleted*/); _logger.LogDebug("Найдено {Count} измененных сущностей для обработки истории", modifiedEntrities.Count()); foreach (var obj in modifiedEntrities) { // Если нужно, обновляем дату изменения DateModifiedResolver(obj); //Если нужно, пишем историю TableHistoryResolver(obj); } #endregion SetInitiator(initiator); try { var changedCount = await EntityContext.SaveChangesAsync(); _logger.LogDebug("Успешно сохранено {ChangedCount} изменений в БД для объектов типа {EntityType}", changedCount, typeof(T).Name); return true; } catch (Exception ex) { _logger.LogError(ex, "Ошибка при сохранении изменений в БД для объектов типа {EntityType}", typeof(T).Name); return false; } } /// /// Установить инициатора /// /// private void SetInitiator(IHistoryInitiator? initiator) { if (initiator == null) return; _logger.LogDebug("Устанавливаю инициатора для изменений"); // Задаем инициатора только для новых и измененных записей var entrities = EntityContext.ChangeTracker.Entries() .Where(t => t.State == EntityState.Modified || t.State == EntityState.Added); var entityCount = entrities.Count(); _logger.LogDebug("Найдено {Count} сущностей для установки инициатора", entityCount); // смотрим есть ли у объекта интерфейс IHistoryInitiator, если есть, задаём значения foreach (var obj in entrities) { // для всех объектов которые наследуются от IHistoryInitiator и не являются наследниками IHistoryTable if (obj.Entity is IHistoryInitiator && obj.Entity is IHistoryTable == false) { (obj.Entity as IHistoryInitiator)!.InitiatorIp = initiator?.InitiatorIp ?? null; (obj.Entity as IHistoryInitiator)!.InitiatorParrComponentId = initiator?.InitiatorParrComponentId ?? null; (obj.Entity as IHistoryInitiator)!.InitiatorComment = initiator?.InitiatorComment ?? null; _logger.LogDebug("Установлен инициатор для сущности типа {EntityType}", obj.Entity.GetType().Name); } } } /// /// Обновления DateModified у таблиц с IBase /// /// private void DateModifiedResolver(EntityEntry obj) { if (obj.Entity is IBaseEntityDateModified entity) { //logger.LogDebug("Обновляю DateModified для сущности типа {EntityType}", obj.Entity.GetType().Name); //(obj.Entity as IBaseEntityDateModified)!.DateModified = DateTimeOffset.UtcNow; var entityType = entity.GetType(); //Ищем DateModified var properyInfo = entityType.GetProperty(nameof(IBaseEntityDateModified.DateModified)); // Проверяем наличие атрибута ManualControlAttribute на этом свойстве bool isManual = properyInfo?.GetCustomAttributes(typeof(ManualControlAttribute), false).Any() ?? false; if (!isManual) { _logger.LogDebug("Обновляю DateModified для сущности типа {EntityType}", obj.Entity.GetType().Name); entity.DateModified = DateTimeOffset.UtcNow; } else { _logger.LogDebug("Пропуск обновления DateModified (ManualControl) для {EntityType}", entityType.Name); } } } /// /// При необходимости, записывать историю таблиц /// /// private void TableHistoryResolver(EntityEntry obj) { _logger.LogDebug("Проверяю необходимость создания истории для сущности типа {EntityType}", obj.Entity.GetType().Name); var myHistoryInterface = obj.Entity.GetType().GetInterfaces() .Where(t => t.IsGenericType) .Where(t => t.GetGenericTypeDefinition() == typeof(IMyHistory<>)) .FirstOrDefault(); // у этого объекта нет интерфейса IMyHistory<>. Не ведем историю if (myHistoryInterface == null) { _logger.LogDebug("Сущность типа {EntityType} не требует ведения истории", obj.Entity.GetType().Name); return; } // !!! Эта таблица хочет хранить историю !!! // Получаем тип таблицы где хранится история var historyType = myHistoryInterface.GetGenericArguments().First(); var historyProps = historyType.GetProperties(/*BindingFlags.DeclaredOnly | */ /*BindingFlags.Public*/).ToList(); _logger.LogDebug("Создаю историю для сущности типа {EntityType}, тип истории: {HistoryType}", obj.Entity.GetType().Name, historyType.Name); var historyInstance = Activator.CreateInstance(historyType); if (historyInstance == null) { _logger.LogError("Не смог создать инстанс для ведения истории {HistoryType}", historyType.Name); return; } // Заполняем поля истории, полями которые есть в исходной таблице FillHistoryProps(obj, ref historyInstance, historyProps); // Заполняем поля интерфейса IHistoryTable FillHistoryProp(ref historyInstance, nameof(IHistoryTable.DateAddedToHistory), DateTimeOffset.UtcNow); // Заполняем Id FillHistoryProp(ref historyInstance, nameof(IBaseEntity.Id), Guid.NewGuid()); try { EntityContext.Add(historyInstance); _logger.LogDebug("История добавлена для сущности типа {EntityType}", obj.Entity.GetType().Name); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при добавлении объекта в историю {HistoryType}", historyType.Name); } } /// /// Заполнить объект истории /// /// /// /// private void FillHistoryProps(EntityEntry originalObj, ref object historyInstance, List propsList) { _logger.LogDebug("Заполняю историю для сущности типа {EntityType}", originalObj.Entity.GetType().Name); foreach (var prop in propsList) { if (originalObj.OriginalValues.Properties.FirstOrDefault(t => t.Name == prop.Name) == null) continue; var sourceObjProp = originalObj.Property(prop.Name); if (sourceObjProp == null) return; //Смотрим, если это Id, записываем его в ParentId var histPropName = prop.Name == nameof(IBaseEntity.Id) ? nameof(IHistoryTable.ParentId) : prop.Name; var histProp = historyInstance.GetType().GetProperty(histPropName); if (histProp == null) continue; //сравним типы if (sourceObjProp.Metadata.PropertyInfo?.PropertyType != histProp.PropertyType) continue; var origValues = originalObj.Property(prop.Name).OriginalValue; histProp.SetValue(historyInstance, origValues); } _logger.LogDebug("Завершено заполнение истории для сущности типа {EntityType}", originalObj.Entity.GetType().Name); } /// /// Заполнить поле объекта /// /// /// /// /// private void FillHistoryProp(ref object instanceObj, string propName, TValue value) { var histProp = instanceObj.GetType().GetProperty(propName); if (histProp == null) { _logger.LogError("При изменении объекта для БД, не найдено свойство {PropertyName}", propName); return; } // сравним типы if (histProp.PropertyType != typeof(TValue)) { _logger.LogError("При изменении объекта для БД, не совпадают типы у свойства {PropertyName}, {PropertyType}!={ValueType}", propName, histProp.PropertyType.Name, typeof(TValue).Name); return; } histProp.SetValue(instanceObj, value); } #region test, comments //private void SaveHistory(EntityEntry obj) //{ // // пока просто попробуем для шаблонов // if (obj.Entity is Template == false) // return; // var template = (Template)obj.Entity; // // https://stackoverflow.com/questions/15012621/how-to-get-original-entity-from-changetracker // var history = new TemplateHistory(); // // { // history.Id = Guid.NewGuid(); // history.ParentId = template.Id; // history.DateAddedToHistory = DateTimeOffset.UtcNow; // history.InitiatorIp = null; // history.InitiatorParrComponentId = null; // history.InitiatorComment = null; // //--- // //DateModified="", // //Name ="", // //IsActiveTemplate = null, // //IsActiveSchedule = null, // //LastRun = null, // //NextRun = null // // DateModified = template.DateModified, // // Name = template.Name, // // IsActiveTemplate = template.IsActiveTemplate, // // IsActiveSchedule = template.IsActiveSchedule, // // LastRun = template.LastRun, // // NextRun = template.NextRun // //}; // // obj.OriginalValues.GetValue // //ITemplateBase хочу получить все поля из интерфейса, потом заполнить по ним в хистори // var interfaceType = typeof(ITemplateGeneralProps); // foreach (var prop in interfaceType.GetProperties()) // { // var propName = prop.Name; // //var propType = prop.PropertyType; // var originalPropValue = obj.OriginalValues.GetValue(propName); // var histProp = history.GetType().GetProperty(propName); // histProp.SetValue(histProp, originalPropValue); // } // EntitiContext.Add(history); //} #endregion public virtual async Task CreateAsync(T obj) { _logger.LogDebug("Начинаю создание объекта типа {EntityType}", typeof(T).Name); if (obj.DateCreated == DateTimeOffset.MinValue) obj.DateCreated = DateTimeOffset.UtcNow; try { await EntitySet.AddAsync(obj); _logger.LogDebug("Объект типа {EntityType} добавлен в контекст", typeof(T).Name); return true; } catch (Exception ex) { _logger.LogError(ex, "Ошибка при добавлении объекта типа {EntityType} в БД", typeof(T).Name); return false; } } public virtual bool Delete(T obj) { _logger.LogDebug("Начинаю удаление объекта типа {EntityType}", obj.GetType().Name); try { EntitySet.Remove(obj); _logger.LogDebug("Объект типа {EntityType} удален из контекста", obj.GetType().Name); return true; } catch (Exception ex) { _logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД", obj.GetType().Name); return false; } } public virtual async Task DeleteAsync(Guid id) { _logger.LogDebug("Начинаю удаление объекта типа {EntityType} по ID: {Id}", typeof(T).Name, id); try { var exist = await GetAsync(id); if (exist == null) { _logger.LogError("Ошибка при удалении из БД. Не найдена запись в БД типа {EntityType} с id: {Id}", typeof(T).Name, id); return false; } EntitySet.Remove(exist); _logger.LogDebug("Объект типа {EntityType} с ID {Id} удален из контекста", typeof(T).Name, id); return true; } catch (Exception ex) { _logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД по ID: {Id}", typeof(T).Name, id); return false; } } public virtual IQueryable Get() { _logger.LogDebug("Получаю набор объектов типа {EntityType}", typeof(T).Name); return EntitySet; } public virtual async Task GetAsync(Guid id) { _logger.LogDebug("Получаю объект типа {EntityType} по ID: {Id}", typeof(T).Name, id); return await EntitySet.FirstOrDefaultAsync(t => t.Id == id); } public virtual IQueryable GetPage(IQueryable query, PaginationFilter paginationFilter) { _logger.LogDebug("Получаю страницу объектов типа {EntityType}, страница: {PageNumber}, размер: {PageSize}", typeof(T).Name, paginationFilter.PageNumber, paginationFilter.PageSize); int skip = (paginationFilter.PageNumber - 1) * paginationFilter.PageSize; return query.Skip(skip).Take(paginationFilter.PageSize); } } }