namespace EnVisage { using Code; using Code.Integration; using Code.ThreadedProcessing; using Code.BLL; using Models.Entities; using NLog; using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Core.EntityClient; using System.Data.Entity.Core.Objects; using System.Data.Entity.Infrastructure; using System.Linq; using System.Threading.Tasks; using System.Web; using static Code.AuditProxy; public partial class EnVisageEntities //: DbContext { #region Private Variables private readonly Logger _Logger = LogManager.GetCurrentClassLogger(); #endregion #region Private Classes private class TransactionInformation { public bool IsLocalTransaction { get; set; } public string TransactionId { get; set; } public string ExecutorId { get; set; } } #endregion #region Constructors public EnVisageEntities(string sConnectionString) : base(sConnectionString) { } #endregion public static EnVisageEntities PrevuEntity(string sConnectionString, bool isAdo) { if (isAdo) { var entityBuilder = new EntityConnectionStringBuilder { Provider = "System.Data.SqlClient", ProviderConnectionString = sConnectionString, Metadata = @"res://*/DataModel.csdl|res://*/DataModel.ssdl|res://*/DataModel.msl" }; sConnectionString = entityBuilder.ConnectionString; } return new EnVisageEntities(sConnectionString); } public ObjectContext ObjectContext() { return (this as IObjectContextAdapter).ObjectContext; } public override int SaveChanges() { var trackChangesTransaction = TrackChanges(); var result = base.SaveChanges(); if (trackChangesTransaction.IsLocalTransaction) Task.Run(() => CommitHistoryChanges(trackChangesTransaction.TransactionId, trackChangesTransaction.ExecutorId)); return result; } /// Performs bulk save changes with history tracking public void ExecuteBulkSaveChanges() { var trackChangesTransaction = TrackChanges(); this.BulkSaveChanges(); if (trackChangesTransaction.IsLocalTransaction) Task.Run(() => CommitHistoryChanges(trackChangesTransaction.TransactionId, trackChangesTransaction.ExecutorId)); } #region Private Methods /// Sets timestamps for created and changed entities as well as for their parent container entities private void SetTimestamps() { var stateManager = ObjectContext().ObjectStateManager; var addedEntities = stateManager.GetObjectStateEntries(EntityState.Added); var modifiedEntities = stateManager.GetObjectStateEntries(EntityState.Modified); var removedEntities = stateManager.GetObjectStateEntries(EntityState.Deleted); addedEntities.Where(x => x.Entity is ITimestampEntity).ToList().ForEach(t => { var timestampEntity = t.Entity as ITimestampEntity; timestampEntity?.SetCreatedTimestamp(); }); modifiedEntities.Where(x => x.Entity is ITimestampEntity).ToList().ForEach(t => (t.Entity as ITimestampEntity).SetUpdatedTimestamp()); #region Update scenario timestamps by relations int modifiedCount = modifiedEntities.Count(); int addedCount = addedEntities.Count(); int removedCount = removedEntities.Count(); if ((modifiedCount > 0) || (addedCount > 0) || (removedCount > 0)) { List allChangedEntities = new List(modifiedCount + addedCount + removedCount); allChangedEntities.AddRange(addedEntities); allChangedEntities.AddRange(modifiedEntities); allChangedEntities.AddRange(removedEntities); List changedScenarios = GetScenarios(allChangedEntities); if (changedScenarios.Count > 0) { // Exclude recently created scenarios List excludableScenarios = addedEntities.Where(x => x.Entity is Scenario).Select(x => (x.Entity as Scenario).Id).ToList(); Scenarios.Where(x => !excludableScenarios.Contains(x.Id) && changedScenarios.Contains(x.Id)) .ToList() .ForEach(s => s.SetUpdatedTimestamp()); } allChangedEntities = null; addedEntities = null; modifiedEntities = null; removedEntities = null; } #endregion } private List GetScenarios(List changedOrCreatedItems) { List scenarios = new List(); if ((changedOrCreatedItems == null) || (changedOrCreatedItems.Count < 1)) return scenarios; scenarios.AddRange( changedOrCreatedItems.Where(x => (x.Entity is ScenarioDetail) && (x.Entity as ScenarioDetail).ParentID.HasValue && !(x.Entity as ScenarioDetail).ParentID.Value.Equals(Guid.Empty)) .Select(t => (t.Entity as ScenarioDetail).ParentID.Value)); scenarios.AddRange( changedOrCreatedItems.Where(x => (x.Entity is TeamAllocation) && !(x.Entity as TeamAllocation).ScenarioId.Equals(Guid.Empty)) .Select(t => (t.Entity as TeamAllocation).ScenarioId)); scenarios.AddRange( changedOrCreatedItems.Where(x => (x.Entity is PeopleResourceAllocation) && !(x.Entity as PeopleResourceAllocation).ScenarioId.Equals(Guid.Empty)) .Select(t => (t.Entity as PeopleResourceAllocation).ScenarioId)); scenarios.AddRange( changedOrCreatedItems.Where(x => (x.Entity is CostSaving) && !(x.Entity as CostSaving).ScenarioId.Equals(Guid.Empty)) .Select(t => (t.Entity as CostSaving).ScenarioId)); scenarios.AddRange( changedOrCreatedItems.Where(x => (x.Entity is Rate) && (x.Entity as Rate).ParentId.HasValue && !(x.Entity as Rate).ParentId.Value.Equals(Guid.Empty) && !(x.Entity as Rate).ParentId.Value.Equals((x.Entity as Rate).ExpenditureCategoryId)) .Select(t => (t.Entity as Rate).ParentId.Value)); // For rates now return all the parents (not only scenarios) scenarios.AddRange( changedOrCreatedItems.Where(x => (x.Entity is Note) && (x.Entity as Note).ParentId.HasValue && !(x.Entity as Note).ParentId.Value.Equals(Guid.Empty)) .Select(t => (t.Entity as Note).ParentId.Value)); return scenarios.Distinct().ToList(); } private void PushUpdates(IEnumerable entries) { if (entries == null || !entries.Any()) return; var trackingEntries = entries.Where(x => !x.IsRelationship && x.Entity != null && (x.Entity is Scenario || x.Entity is ScenarioDetail || x.Entity is Project)) .ToArray(); var changedScenarios = trackingEntries.Where(x => x.Entity is Scenario) .Select(x => (x.Entity as Scenario)).ToList(); var changedScenariosIds = changedScenarios.Select(x => x.Id); var changedScenariosViaDetailsIds = trackingEntries.Where(x => x.Entity is ScenarioDetail) .Where(x => (x.Entity as ScenarioDetail).ParentID.HasValue) .Select(x => (x.Entity as ScenarioDetail).ParentID.Value) .Distinct(); var changedScenariosViaDetails = changedScenariosViaDetailsIds.Any() ? Scenarios.AsNoTracking() .Where(x => x.Status == (int)ScenarioStatus.Active && !changedScenariosIds.Contains(x.Id) && changedScenariosViaDetailsIds.Contains(x.Id)) .ToList() : new List(); if (changedScenariosViaDetails.Any()) changedScenarios.AddRange(changedScenariosViaDetails); var changedProjects = trackingEntries.Where(x => x.Entity is Project) .Select(x => (x.Entity as Project)).ToList(); var changedProjectsIds = changedProjects.Select(x => x.Id); var changedProjectsViaScenariosIds = changedScenarios.Where(x => x.ParentId.HasValue) .Select(x => x.ParentId.Value) .Distinct(); var changedProjectsViaScenarios = changedProjectsViaScenariosIds.Any() ? Projects.AsNoTracking() .Where(x => !changedProjectsIds.Contains(x.Id) && changedProjectsViaScenariosIds.Contains(x.Id)) .ToList() : new List(); if (changedProjectsViaScenarios.Any()) changedProjects.AddRange(changedProjectsViaScenarios); var parentProjectsIds = changedProjects.Where(x => x.ParentProjectId.HasValue) .Select(x => x.ParentProjectId.Value) .Distinct(); var parentProjects = parentProjectsIds.Any() ? Projects.AsNoTracking() .Where(x => !changedProjectsIds.Contains(x.Id) && !changedProjectsViaScenariosIds.Contains(x.Id) && parentProjectsIds.Contains(x.Id)) .ToList() : new List(); if (parentProjects.Any()) changedProjects.AddRange(parentProjects); var requiredStatuses = changedProjects.Select(x => x.StatusId).Distinct(); var statusesDict = requiredStatuses.Any() ? Status.Where(x => requiredStatuses.Contains(x.Id)).ToArray().ToDictionary(x => x.Id) : new Dictionary(); var requiredTypes = changedProjects.Select(x => x.TypeId).Distinct(); var typesDict = requiredTypes.Any() ? Types.Where(x => requiredTypes.Contains(x.Id)).ToArray().ToDictionary(x => x.Id) : new Dictionary(); var changedScenariosDict = changedScenarios.ToDictionary(x => x.Id); var changedProjectsDict = changedProjects.ToDictionary(x => x.Id); foreach (var entry in trackingEntries) { var captureForNotification = false; Project project = null; Scenario scenario = null; Status _status = null; Type _type = null; if (entry.Entity is ScenarioDetail) { var scenarioDetail = entry.Entity as ScenarioDetail; if (scenarioDetail.ParentID.HasValue && changedScenariosDict.ContainsKey(scenarioDetail.ParentID.Value)) scenario = changedScenariosDict[scenarioDetail.ParentID.Value]; if (scenario == null) return; if (scenario.Type == (int)ScenarioType.Portfolio && scenario.ParentId.HasValue) if (changedProjectsDict.ContainsKey(scenario.ParentId.Value)) project = changedProjectsDict[scenario.ParentId.Value]; } else if (entry.Entity is Scenario) { scenario = entry.Entity as Scenario; if (scenario.Type == (int)ScenarioType.Portfolio && scenario.ParentId.HasValue) if (changedProjectsDict.ContainsKey(scenario.ParentId.Value)) project = changedProjectsDict[scenario.ParentId.Value]; } else if (entry.Entity is Project) { var projectId = (entry.Entity as Project).Id; if (changedProjectsDict.ContainsKey(projectId)) project = changedProjectsDict[projectId]; } if (project != null) { _status = statusesDict.ContainsKey(project.StatusId) ? statusesDict[project.StatusId] : null; _type = typesDict.ContainsKey(project.TypeId) ? typesDict[project.TypeId] : null; if (_status != null && _status.Id != Guid.Empty && _type != null && _type.Id != Guid.Empty) { if (_status.NotifyOnProjectDelete.HasValue) if (_status.NotifyOnProjectDelete.Value) captureForNotification = true; if (_status.NotifyOnProjectCreate.HasValue) if (_status.NotifyOnProjectCreate.Value) captureForNotification = true; if (_status.NotifyOnProjectChange.HasValue) if (_status.NotifyOnProjectChange.Value) captureForNotification = true; if (_type.NotifyOnProjectDelete.HasValue) if (_type.NotifyOnProjectDelete.Value) captureForNotification = true; if (_type.NotifyOnProjectCreate.HasValue) if (_type.NotifyOnProjectCreate.Value) captureForNotification = true; if (_type.NotifyOnProjectChange.HasValue) if (_type.NotifyOnProjectChange.Value) captureForNotification = true; } } // if we are not capturing data, and the scenario is not active we do not // do any push changes. if (entry.Entity is Scenario || entry.Entity is ScenarioDetail) if (!captureForNotification) return; string action = entry.State == EntityState.Added ? "Add" : "Update"; _Logger.Log(LogLevel.Debug, "CRM " + action + " for " + (entry.Entity is Scenario ? "Scenario" : "Project")); var crmDal = IntergrationHelper.GetIntergrationClass(IntergrationAccessType.ProjectExport, null); Dictionary> pushToCrmCollection = new Dictionary>(); string tableName = entry.Entity.GetType().Name.Split('_')[0]; Dictionary colcollection = new Dictionary(); pushToCrmCollection.Add(tableName, colcollection); // TODO: do we realy need to track each property or just changed ones? var props = entry.Entity.GetType().GetProperties(); if (entry.State == EntityState.Modified || entry.State == EntityState.Added) { foreach (var elem in props) { string fieldName = elem.Name; try { var val = Convert.ToString(elem.GetValue(entry.Entity)); if (entry.State == EntityState.Modified || entry.State == EntityState.Added) { _Logger.Log(LogLevel.Debug, "CRM Field Update for " + (entry.Entity is Scenario ? "Scenario" : "Project") + fieldName + "," + val); colcollection.Add(fieldName, val); } } catch { } } } if (pushToCrmCollection.Any()) { Guid parentKey = Guid.Empty; var entity = entry.Entity as Scenario; if (entity != null) { Scenario s = entity; try { parentKey = Guid.Parse(s.Project.ProjectNumber); } catch { } } else if (entry.Entity is Project) { Project p = (Project)entry.Entity; try { parentKey = Guid.Parse(p.ProjectNumber); } catch { } } if (parentKey != Guid.Empty) { BackgroundProcessManager bpm = new BackgroundProcessManager(); bpm.UpdateCRMAsync(pushToCrmCollection, parentKey, crmDal, action); } } try { if (captureForNotification) { if (entry.Entity is Project) { if (project.ParentProjectId.HasValue) if (changedProjectsDict.ContainsKey(project.ParentProjectId.Value)) project = changedProjectsDict[project.ParentProjectId.Value]; string url = Code.Session.AbsoluteUrl.EditProjectUrl(project.Id, this); (new NotificationManager(null)).SendProjectChangeNotifications(project, _type, _status, entry.State, url); } if (entry.Entity is Scenario || entry.Entity is ScenarioDetail) { if (scenario.Type == (int)ScenarioType.Portfolio) { string url = Code.Session.AbsoluteUrl.EditScenarioUrl(scenario.Id, this); (new NotificationManager(null)).SendScenarioChangeNotifications(scenario, _type, _status, entry.State, url); } } } } catch (Exception dds) { _Logger.Error(dds, "error in Capture change notifications!!!"); } } } private List ResolveChanges(ObjectStateEntry stateEntry) { #region Arguments Validation if (stateEntry == null) throw new ArgumentNullException(nameof(stateEntry)); #endregion if (stateEntry.State == EntityState.Deleted) return new List(); var properties = stateEntry.CurrentValues.DataRecordInfo.FieldMetadata.Select(x => x.FieldType.Name).ToList(); var changedProperties = new List(properties.Count); foreach (var propertyName in properties) { var oldValue = stateEntry.State == EntityState.Added ? DBNull.Value : stateEntry.OriginalValues[propertyName]; var newValue = stateEntry.CurrentValues[propertyName]; // we may not check for null value because if field has null in the database it will be presented as DBNull value here if (!oldValue.Equals(newValue)) { changedProperties.Add(new HistorySaveItemPropertyModel { Name = propertyName, OldValue = oldValue.ToString(), NewValue = newValue.ToString(), }); } } return changedProperties; } private string ResolveGroupKey(object entity) { if (entity == null) throw new ArgumentNullException(nameof(entity)); if (entity is CostSaving) return ((CostSaving)entity).ScenarioId.ToString(); if (entity is HolidayAllocation) return ((HolidayAllocation)entity).HolidayId.ToString(); if (entity is NonProjectTimeResourceAllocation) return ((NonProjectTimeResourceAllocation)entity).NonProjectTime2ResourceId.ToString(); if (entity is NonProjectTimeTeamAllocation) return ((NonProjectTimeTeamAllocation)entity).NonProjectTime2TeamId.ToString(); if (entity is PeopleResourceActual) return ((PeopleResourceActual)entity).PeopleResourceId.ToString(); if (entity is PeopleResourceAllocation) return ((PeopleResourceAllocation)entity).ScenarioId.ToString(); if (entity is PeopleResourceExpCatChange) return ((PeopleResourceExpCatChange)entity).PeopleResourceId.ToString(); if (entity is PeopleResource2Team) return ((PeopleResource2Team)entity).PeopleResourceId.ToString(); if (entity is ScenarioDetail) return ((ScenarioDetail)entity).ParentID?.ToString(); if (entity is TeamAllocation) return ((TeamAllocation)entity).ScenarioId.ToString(); if (entity is Security) return ((Security)entity).PrincipalId.ToString(); if (entity is ProjectAccess) return ((ProjectAccess)entity).PrincipalId.ToString(); if (entity is Holiday2ExpenditureCategory) return ((Holiday2ExpenditureCategory)entity).HolidayId.ToString(); if (entity is Holiday2PeopleResource) return ((Holiday2PeopleResource)entity).HolidayId.ToString(); if (entity is Holiday2Team) return ((Holiday2Team)entity).HolidayId.ToString(); if (entity is Team2Project) return (entity as Team2Project).ProjectId.ToString(); if (entity is TagLink) return (entity as TagLink).ParentID.ToString(); if (entity is StrategicGoal2Project) return (entity as StrategicGoal2Project).ProjectId.ToString(); return null; } private string ResolveEntityId(ObjectStateEntry stateEntryEntity, List properties) { if (stateEntryEntity == null) throw new ArgumentNullException(nameof(stateEntryEntity)); if (stateEntryEntity.EntityKey != null) if (stateEntryEntity.EntityKey.EntityKeyValues != null) if (stateEntryEntity.EntityKey.EntityKeyValues.Any()) return stateEntryEntity.EntityKey.EntityKeyValues[0].Value.ToString(); if (properties == null || !properties.Any()) return null; var propertyKey = properties.FirstOrDefault(x => x.Name.ToLower() == "id"); return propertyKey?.NewValue; } private string ResolveEntityType(ObjectStateEntry stateEntryEntity) { if (stateEntryEntity == null) throw new ArgumentNullException(nameof(stateEntryEntity)); if (stateEntryEntity.EntitySet != null) { if (stateEntryEntity.EntitySet.ElementType != null) return stateEntryEntity.EntitySet.ElementType.Name; return stateEntryEntity.EntitySet.Name; } return null; } private TransactionInformation TrackChanges() { #region Refresh Changes // DetectChanges is called as part of the implementation of the SaveChanges. // This means that if you override SaveChanges in your context, then DetectChanges will not have been called before your SaveChanges method is called. // This can sometimes catch people out, especially when checking if an entity has been modified or not since its state may not be set to Modified until DetectChanges is called. // So we need to call it right before we'll try to get changed entities to not skip some changes ChangeTracker.DetectChanges(); #endregion #region Automatic changes TimeStamping SetTimestamps(); #endregion // TODO: AK, review why there is no user var executorId = Utils.CurrentUserId(); var stateManager = ObjectContext().ObjectStateManager; var changes = stateManager.GetObjectStateEntries(EntityState.Modified | EntityState.Added | EntityState.Deleted) .Where(x => !x.IsRelationship && x.Entity != null && !(x.Entity is History) && !(x.Entity is supt_ImportMessages) && !(x.Entity is supt_tbl_MongoDBBackup) && !(x.Entity is supt_tbl_ProjectIds) && !(x.Entity is supt_tbl_RecParser) && !(x.Entity is FiscalCalendar)); var isLocalTransaction = false; var transactionId = this.GetClientConnectionId(); if (transactionId == null) { transactionId = Guid.NewGuid().ToString(); isLocalTransaction = true; } foreach (var stateEntryEntity in changes) { if (stateEntryEntity.Entity == null) continue; #region Prepare History Items and Log History var groupKey = ResolveGroupKey(stateEntryEntity.Entity); var properties = ResolveChanges(stateEntryEntity); var entityId = ResolveEntityId(stateEntryEntity, properties); var entityType = ResolveEntityType(stateEntryEntity); var modificationType = stateEntryEntity.State.ToString(); // we do not need to save entity if no properties were changed if (stateEntryEntity.State == EntityState.Deleted || properties.Any()) LogHistory(transactionId, groupKey, entityId, entityType, modificationType, properties); #endregion } #region Capture updates for CRM/3rd party systems //capture changes to push to crm PushUpdates(changes); #endregion var transactionInfo = new TransactionInformation { IsLocalTransaction = isLocalTransaction, TransactionId = transactionId, ExecutorId = executorId }; return transactionInfo; } #endregion } }