From 909d6ebb0e5958ffa78cca859b9e408d88d38bac Mon Sep 17 00:00:00 2001 From: AdrianHarwood Date: Tue, 23 Jun 2026 07:55:04 +0100 Subject: [PATCH 01/13] Added a program call and service layer wrapper for service layer update --- PPMTool.Data/Entities/Project.cs | 1 + PPMTool/Program.cs | 4 +++ PPMTool/Services/ProjectService.cs | 41 +++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/PPMTool.Data/Entities/Project.cs b/PPMTool.Data/Entities/Project.cs index c1fbe84d..8b7223a5 100644 --- a/PPMTool.Data/Entities/Project.cs +++ b/PPMTool.Data/Entities/Project.cs @@ -325,6 +325,7 @@ public bool HasResourcesWithNoFundingSourceOnRunningTask() /// Whether to update the subtask costs and save to database /// If necessary a set of financial references /// The percentage of top slice to apply from the settings + /// When no financial references are available in the DB with which to update the costs public void UpdateProjectMetaData(bool recomputeSubTaskCosts, IEnumerable financialReferences, float indirectsPercentage) { // Check conditions for cost update diff --git a/PPMTool/Program.cs b/PPMTool/Program.cs index 67c0b6ee..63497b90 100644 --- a/PPMTool/Program.cs +++ b/PPMTool/Program.cs @@ -410,6 +410,10 @@ await settingsService.IntialiseServiceCacheAsync(context); var featureService = app.Services.GetRequiredService(); await featureService.IntialiseServiceCacheAsync(context); + + // Update the project meta data for all projects in the database + var projectService = app.Services.GetRequiredService(); + await projectService.UpdateAllProjectMetaDataAsync(context); } // Run the app diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index 07c1b51b..d70fc731 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -12,10 +12,18 @@ namespace PPMTool.Services public class ProjectService : BaseEntityService { private readonly SettingsService settingsService; + private readonly FinancialReferenceService financialReferenceService; + private readonly ILogger logger; - public ProjectService(SettingsService settingsService) + public ProjectService( + SettingsService settingsService, + FinancialReferenceService financialReferenceService, + ILogger logger + ) { this.settingsService = settingsService; + this.financialReferenceService = financialReferenceService; + this.logger = logger; } /// @@ -214,5 +222,36 @@ internal School GetSchoolAndFaculty(PPMToolContext context, int projectId) .ThenInclude(x => x.Faculty) .FirstOrDefault(x => x.ProjectId == projectId)?.School; } + + /// + /// Updates the metadata for all projects in the database context. + /// This method iterates through all projects, invoking the UpdateProjectMetaData method on each project to refresh its metadata. + /// The changes are then persisted to the database. + /// + /// + /// + internal async Task UpdateAllProjectMetaDataAsync(PPMToolContext context) + { + var prefix = settingsService.GetSetting(SettingType.ProjectAbbreviation, string.Empty); + var indirects = settingsService.GetSetting(SettingType.BAUTopSliceFractionDefault, 0); + var finrefs = financialReferenceService.GetAll(context); + var projects = await context.Projects.ToListAsync(); + foreach (var project in projects) + { + try + { + project.UpdateProjectMetaData( + true, + finrefs, + indirects + ); + Update(context, project); + } + catch (Exception ex) + { + logger.LogWarning($"Error occurred while updating project metadata for project {project.ProjectId} ({prefix}-{project.RTP}): {ex.Message}"); + } + } + } } } From 79b8c1a7388e78ce283ffcb620fd1d1c9226c08c Mon Sep 17 00:00:00 2001 From: AdrianHarwood Date: Tue, 23 Jun 2026 07:55:35 +0100 Subject: [PATCH 02/13] Rearrange the startup logic to get the call in the scope --- PPMTool/Program.cs | 75 +++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/PPMTool/Program.cs b/PPMTool/Program.cs index 63497b90..65fc61d6 100644 --- a/PPMTool/Program.cs +++ b/PPMTool/Program.cs @@ -345,6 +345,11 @@ } } +// Set default culture +var cultureInfo = new CultureInfo("en-GB"); +CultureInfo.DefaultThreadCurrentCulture = cultureInfo; +CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + // Set dummy data seed flag // This is intended for development and testing purposes only and should be used with caution as it will delete existing data. var shouldSeed = builder.Configuration.GetValue("DeveloperSettings:SeedDummyData"); @@ -364,50 +369,41 @@ // execution (throws InvalidCastException: PoolingDataSource -> MultiplexingDataSource), // so the synchronous overload silently fails on PostgreSQL, leaving the schema out of date. await context.Database.MigrateAsync(); -} -// Seed the default superuser from the settings if it doesn't already exist -SeedHelper.SeedSuperUserIfNotExist(scope.ServiceProvider); + // Seed the default superuser from the settings if it doesn't already exist + SeedHelper.SeedSuperUserIfNotExist(scope.ServiceProvider); -// Seed features -SeedHelper.SeedFeatures(scope.ServiceProvider); + // Seed features + SeedHelper.SeedFeatures(scope.ServiceProvider); -// If seeding run the dummy data seeding methods -if (shouldSeed) -{ - // Seed tables with suitable values -- Note that competencies are already seeded by migrations - SeedHelper.SeedPeople(scope.ServiceProvider); - SeedHelper.SeedAbsences(scope.ServiceProvider); - SeedHelper.SeedUsers(scope.ServiceProvider); - SeedHelper.SeedWorkloadModelChanges(scope.ServiceProvider); - SeedHelper.SeedSkillTags(scope.ServiceProvider); - SeedHelper.SeedOwnedSkillsForPeople(scope.ServiceProvider); - SeedHelper.SeedCompetencyAssessments(scope.ServiceProvider); - SeedHelper.SeedInnateCodesAndTasks(scope.ServiceProvider); - SeedHelper.SeedFinancialReferences(scope.ServiceProvider); - SeedHelper.SeedOrganisationalUnits(scope.ServiceProvider); - SeedHelper.SeedProjects(scope.ServiceProvider); - SeedHelper.SeedFundingSources(scope.ServiceProvider); - SeedHelper.SeedSubTasks(scope.ServiceProvider); - SeedHelper.SeedResources(scope.ServiceProvider); - SeedHelper.SeedNotes(scope.ServiceProvider); - SeedHelper.SeedInvoicesAndPayments(scope.ServiceProvider); - SeedHelper.SeedTimesheets(scope.ServiceProvider); -} - -// Set default culture -var cultureInfo = new CultureInfo("en-GB"); -CultureInfo.DefaultThreadCurrentCulture = cultureInfo; -CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; - -// Clean local application file path -FileHelper.CleanLocalApplicationFilePath(logger); + // If seeding run the dummy data seeding methods + if (shouldSeed) + { + // Seed tables with suitable values -- Note that competencies are already seeded by migrations + SeedHelper.SeedPeople(scope.ServiceProvider); + SeedHelper.SeedAbsences(scope.ServiceProvider); + SeedHelper.SeedUsers(scope.ServiceProvider); + SeedHelper.SeedWorkloadModelChanges(scope.ServiceProvider); + SeedHelper.SeedSkillTags(scope.ServiceProvider); + SeedHelper.SeedOwnedSkillsForPeople(scope.ServiceProvider); + SeedHelper.SeedCompetencyAssessments(scope.ServiceProvider); + SeedHelper.SeedInnateCodesAndTasks(scope.ServiceProvider); + SeedHelper.SeedFinancialReferences(scope.ServiceProvider); + SeedHelper.SeedOrganisationalUnits(scope.ServiceProvider); + SeedHelper.SeedProjects(scope.ServiceProvider); + SeedHelper.SeedFundingSources(scope.ServiceProvider); + SeedHelper.SeedSubTasks(scope.ServiceProvider); + SeedHelper.SeedResources(scope.ServiceProvider); + SeedHelper.SeedNotes(scope.ServiceProvider); + SeedHelper.SeedInvoicesAndPayments(scope.ServiceProvider); + SeedHelper.SeedTimesheets(scope.ServiceProvider); + } -// Initialise feature service cache -using (var context = dbContextFactory.CreateDbContext()) -{ + // Initialise the settings cache var settingsService = app.Services.GetRequiredService(); await settingsService.IntialiseServiceCacheAsync(context); + + // Initialise the feature cache var featureService = app.Services.GetRequiredService(); await featureService.IntialiseServiceCacheAsync(context); @@ -416,5 +412,8 @@ await projectService.UpdateAllProjectMetaDataAsync(context); } +// Clean local application file path +FileHelper.CleanLocalApplicationFilePath(logger); + // Run the app app.Run(); \ No newline at end of file From 4855fc2ddc2d77b3f5c79fb60787af3441dd26eb Mon Sep 17 00:00:00 2001 From: AdrianHarwood Date: Tue, 23 Jun 2026 07:57:10 +0100 Subject: [PATCH 03/13] Actually get the service from the scope not the root provider --- PPMTool/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PPMTool/Program.cs b/PPMTool/Program.cs index 65fc61d6..dbdc7342 100644 --- a/PPMTool/Program.cs +++ b/PPMTool/Program.cs @@ -408,7 +408,7 @@ await featureService.IntialiseServiceCacheAsync(context); // Update the project meta data for all projects in the database - var projectService = app.Services.GetRequiredService(); + var projectService = scope.ServiceProvider.GetRequiredService(); await projectService.UpdateAllProjectMetaDataAsync(context); } From 2e9ba87ff4fc3c39d4438741e039061798f24205 Mon Sep 17 00:00:00 2001 From: AdrianHarwood Date: Tue, 23 Jun 2026 08:13:04 +0100 Subject: [PATCH 04/13] Fixed up to use the existing GetAll so it deep loads the subtasks and resources --- PPMTool/Program.cs | 2 +- PPMTool/Services/ProjectService.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/PPMTool/Program.cs b/PPMTool/Program.cs index dbdc7342..815167a1 100644 --- a/PPMTool/Program.cs +++ b/PPMTool/Program.cs @@ -409,7 +409,7 @@ // Update the project meta data for all projects in the database var projectService = scope.ServiceProvider.GetRequiredService(); - await projectService.UpdateAllProjectMetaDataAsync(context); + projectService.UpdateAllProjectMetaData(context); } // Clean local application file path diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index d70fc731..523b7152 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -230,12 +230,12 @@ internal School GetSchoolAndFaculty(PPMToolContext context, int projectId) /// /// /// - internal async Task UpdateAllProjectMetaDataAsync(PPMToolContext context) + internal void UpdateAllProjectMetaData(PPMToolContext context) { var prefix = settingsService.GetSetting(SettingType.ProjectAbbreviation, string.Empty); var indirects = settingsService.GetSetting(SettingType.BAUTopSliceFractionDefault, 0); var finrefs = financialReferenceService.GetAll(context); - var projects = await context.Projects.ToListAsync(); + var projects = GetAll(context); foreach (var project in projects) { try @@ -246,6 +246,7 @@ internal async Task UpdateAllProjectMetaDataAsync(PPMToolContext context) indirects ); Update(context, project); + logger.LogInformation($"Updated project metadata for project {project.ProjectId} ({prefix}-{project.RTP})"); } catch (Exception ex) { From 8879385862c22882c9d1bc811513515402eb01b7 Mon Sep 17 00:00:00 2001 From: Adrian Harwood Date: Wed, 1 Jul 2026 09:03:59 +0100 Subject: [PATCH 05/13] Fixed docstring Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- PPMTool/Services/ProjectService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index a82a2f3f..a19e91a4 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -226,8 +226,7 @@ internal School GetSchoolAndFaculty(PPMToolContext context, int projectId) /// This method iterates through all projects, invoking the UpdateProjectMetaData method on each project to refresh its metadata. /// The changes are then persisted to the database. /// - /// - /// + /// The database context used to access and update projects. internal void UpdateAllProjectMetaData(PPMToolContext context) { var prefix = settingsService.GetSetting(SettingType.ProjectAbbreviation, string.Empty); From 2c49f35c3c9a0e86d39554620effc4103a6e18ea Mon Sep 17 00:00:00 2001 From: Adrian Harwood Date: Wed, 1 Jul 2026 09:27:14 +0100 Subject: [PATCH 06/13] Fixed incorrect type for indirects setting Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- PPMTool/Services/ProjectService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index a19e91a4..b294b1aa 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -230,7 +230,7 @@ internal School GetSchoolAndFaculty(PPMToolContext context, int projectId) internal void UpdateAllProjectMetaData(PPMToolContext context) { var prefix = settingsService.GetSetting(SettingType.ProjectAbbreviation, string.Empty); - var indirects = settingsService.GetSetting(SettingType.BAUTopSliceFractionDefault, 0); + var indirects = settingsService.GetSetting(SettingType.BAUTopSliceFractionDefault, 0f); var finrefs = financialReferenceService.GetAll(context); var projects = GetAll(context); foreach (var project in projects) From 07a6a957fc79162e97a0c0de65bb1183dd28dfc8 Mon Sep 17 00:00:00 2001 From: AdrianHarwood Date: Wed, 1 Jul 2026 17:51:35 +0100 Subject: [PATCH 07/13] Improved performance of the financial references during meta data update --- PPMTool/Services/ProjectService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index b294b1aa..b82da54c 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -231,7 +231,7 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) { var prefix = settingsService.GetSetting(SettingType.ProjectAbbreviation, string.Empty); var indirects = settingsService.GetSetting(SettingType.BAUTopSliceFractionDefault, 0f); - var finrefs = financialReferenceService.GetAll(context); + var finrefs = financialReferenceService.GetAll(context).ToList(); var projects = GetAll(context); foreach (var project in projects) { From b16a9f0c4295a0b0c0eb1940e8557aef7de62bef Mon Sep 17 00:00:00 2001 From: AdrianHarwood Date: Wed, 1 Jul 2026 17:53:48 +0100 Subject: [PATCH 08/13] Add exception to logger --- PPMTool/Services/ProjectService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index b82da54c..26794207 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -247,7 +247,7 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) } catch (Exception ex) { - logger.LogWarning($"Error occurred while updating project metadata for project {project.ProjectId} ({prefix}-{project.RTP}): {ex.Message}"); + logger.LogWarning(ex, $"Error occurred while updating project metadata for project {project.ProjectId} ({prefix}-{project.RTP}): {ex.Message}"); } } } From 9f79b001c480e5ca40241d31ea6b4bc18b9a516f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:56:50 +0000 Subject: [PATCH 09/13] Initial plan From 59c908e13e8ffb9b5c15ed92e8c1b9980cd57ca8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:02:19 +0000 Subject: [PATCH 10/13] Optimize project metadata startup update path --- PPMTool/Services/ProjectService.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index 26794207..63880b3a 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -232,7 +232,19 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) var prefix = settingsService.GetSetting(SettingType.ProjectAbbreviation, string.Empty); var indirects = settingsService.GetSetting(SettingType.BAUTopSliceFractionDefault, 0f); var finrefs = financialReferenceService.GetAll(context).ToList(); - var projects = GetAll(context); + var projects = context.Projects + .Include(p => p.School) + .ThenInclude(s => s.Faculty) + .Include(p => p.SubTasks) + .ThenInclude(s => s.AssignedResources) + .ThenInclude(r => r.Person) + .ThenInclude(r => r.WorkloadModelChanges) + .Include(p => p.SubTasks) + .ThenInclude(s => s.AssignedResources) + .ThenInclude(r => r.FundedFrom) + .Include(p => p.ProjectManager) + .Include(p => p.FundingSources) + .ToList(); foreach (var project in projects) { try @@ -242,7 +254,6 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) finrefs, indirects ); - Update(context, project); logger.LogInformation($"Updated project metadata for project {project.ProjectId} ({prefix}-{project.RTP})"); } catch (Exception ex) @@ -250,6 +261,7 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) logger.LogWarning(ex, $"Error occurred while updating project metadata for project {project.ProjectId} ({prefix}-{project.RTP}): {ex.Message}"); } } + CommitChanges(context); } } } From 2d75373b036b8018384facd719119705cdb8d26d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:07:35 +0000 Subject: [PATCH 11/13] Improve project metadata refresh efficiency at startup --- PPMTool/Services/ProjectService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index 63880b3a..34246feb 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -254,6 +254,7 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) finrefs, indirects ); + CommitChanges(context); logger.LogInformation($"Updated project metadata for project {project.ProjectId} ({prefix}-{project.RTP})"); } catch (Exception ex) @@ -261,7 +262,6 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) logger.LogWarning(ex, $"Error occurred while updating project metadata for project {project.ProjectId} ({prefix}-{project.RTP}): {ex.Message}"); } } - CommitChanges(context); } } } From de210c37f396dd2d7fd93ff4aef258898bb0a226 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:14:55 +0000 Subject: [PATCH 12/13] Trim metadata refresh includes to required minimum --- PPMTool/Services/ProjectService.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/PPMTool/Services/ProjectService.cs b/PPMTool/Services/ProjectService.cs index 34246feb..27bbcec9 100644 --- a/PPMTool/Services/ProjectService.cs +++ b/PPMTool/Services/ProjectService.cs @@ -233,16 +233,10 @@ internal void UpdateAllProjectMetaData(PPMToolContext context) var indirects = settingsService.GetSetting(SettingType.BAUTopSliceFractionDefault, 0f); var finrefs = financialReferenceService.GetAll(context).ToList(); var projects = context.Projects - .Include(p => p.School) - .ThenInclude(s => s.Faculty) .Include(p => p.SubTasks) .ThenInclude(s => s.AssignedResources) .ThenInclude(r => r.Person) .ThenInclude(r => r.WorkloadModelChanges) - .Include(p => p.SubTasks) - .ThenInclude(s => s.AssignedResources) - .ThenInclude(r => r.FundedFrom) - .Include(p => p.ProjectManager) .Include(p => p.FundingSources) .ToList(); foreach (var project in projects) From feba19bf223d2e62aafb16759e579f7accbdfee1 Mon Sep 17 00:00:00 2001 From: AdrianHarwood Date: Thu, 2 Jul 2026 08:57:58 +0100 Subject: [PATCH 13/13] Added handling for missing school and faculty info which is not required for meta data update --- PPMTool.Data/Helpers/AssignmentHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PPMTool.Data/Helpers/AssignmentHelper.cs b/PPMTool.Data/Helpers/AssignmentHelper.cs index dc9a2628..fda63fc1 100644 --- a/PPMTool.Data/Helpers/AssignmentHelper.cs +++ b/PPMTool.Data/Helpers/AssignmentHelper.cs @@ -134,8 +134,8 @@ public static IEnumerable GetAssignmentChunks( ProjectId = project.RTP, ProjectName = project.Name, LeadRSE = project.ProjectManager?.Name ?? "Unknown", - UpperOrgUnit = project.School.Faculty.Name, - LowerOrgUnit = project.School.Name, + UpperOrgUnit = project.School?.Faculty?.Name ?? "Unknown", + LowerOrgUnit = project.School?.Name ?? "Unknown", PI = project.PI, TaskName = task.Name, StartDate = adjustedTaskStart,