diff --git a/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs b/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs index b20079c..6c1a3d2 100644 --- a/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs @@ -39,7 +39,7 @@ void AddDbContext(DbContextOptionsBuilder options) services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddSingleton(); services.AddTransient(); } diff --git a/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs b/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs index 56b5d57..1dfb3b4 100644 --- a/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs +++ b/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs @@ -7,5 +7,5 @@ public interface ICrystaProgramSyncModuleService Task> GetSyncModulesAsync(CancellationToken cancellationToken); // Save or update a sync module (persist SyncInfo changes) - Task UpdateSyncModuleAsync(CrystaProgramSyncModule module); + Task UpdateSyncModuleAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default); } diff --git a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs index 95e173e..c9c354b 100644 --- a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs +++ b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs @@ -6,70 +6,102 @@ using CrystaLearn.Core.Data; using CrystaLearn.Core.Models.Crysta; using CrystaLearn.Core.Services.Contracts; +using Microsoft.EntityFrameworkCore; namespace CrystaLearn.Core.Services; -public partial class CrystaProgramSyncModuleService : ICrystaProgramSyncModuleService +public partial class CrystaProgramSyncModuleService : ICrystaProgramSyncModuleService, IDisposable { - private static List _modules = new(); - private AppDbContext DbContext { get; set; } = default!; + private List _modules = new(); + private IDbContextFactory DbContextFactory { get; set; } = default!; + private bool _initialized = false; + private readonly SemaphoreSlim _initLock = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _updateLock = new SemaphoreSlim(1, 1); + private bool _disposed = false; - public CrystaProgramSyncModuleService(AppDbContext dbContext) + public CrystaProgramSyncModuleService(IDbContextFactory dbContextFactory) { - this.DbContext = dbContext; - if (_modules.Count == 0) + this.DbContextFactory = dbContextFactory; + } + + private async Task EnsureInitializedAsync(CancellationToken cancellationToken) + { + if (!_initialized) { - _modules = DbContext.Set().Include(f => f.CrystaProgram).ToListAsync().GetAwaiter().GetResult(); + await _initLock.WaitAsync(cancellationToken); + try + { + if (!_initialized) + { + await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken); + _modules = await dbContext.Set().Include(f => f.CrystaProgram).ToListAsync(cancellationToken); + _initialized = true; + } + } + finally + { + _initLock.Release(); + } } } public async Task> GetSyncModulesAsync(CancellationToken cancellationToken) { + await EnsureInitializedAsync(cancellationToken); return _modules; } - public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module) + public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default) { // Try to persist to database if DbContext is available and configured try { - if (DbContext != null) + if (DbContextFactory != null) { - var set = DbContext.Set(); + await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken); + var set = dbContext.Set(); - var existing = await set.FindAsync(new object[] { module.Id }, cancellationToken: CancellationToken.None); + var existing = await set.FindAsync(new object[] { module.Id }, cancellationToken: cancellationToken); if (existing != null) { // Update all scalar properties from incoming module - DbContext.Entry(existing).CurrentValues.SetValues(module); + dbContext.Entry(existing).CurrentValues.SetValues(module); // If SyncInfo is an owned/complex type, ensure its properties are updated as well if (module.SyncInfo != null) { existing.SyncInfo ??= new SyncInfo(); - DbContext.Entry(existing).CurrentValues.SetValues(existing); // ensure entry is tracked - DbContext.Entry(existing).Reference(e => e.SyncInfo).TargetEntry?.CurrentValues.SetValues(module.SyncInfo); + dbContext.Entry(existing).CurrentValues.SetValues(existing); // ensure entry is tracked + dbContext.Entry(existing).Reference(e => e.SyncInfo).TargetEntry?.CurrentValues.SetValues(module.SyncInfo); } - DbContext.Update(existing); + dbContext.Update(existing); } else { - await set.AddAsync(module); + await set.AddAsync(module, cancellationToken); } - await DbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); // keep in-memory copy in sync as well - replace whole object to reflect all fields - var idx = _modules.FindIndex(m => m.Id == module.Id); - if (idx >= 0) + await _updateLock.WaitAsync(cancellationToken); + try { - _modules[idx] = module; + var idx = _modules.FindIndex(m => m.Id == module.Id); + if (idx >= 0) + { + _modules[idx] = module; + } + else + { + _modules.Add(module); + } } - else + finally { - _modules.Add(module); + _updateLock.Release(); } return; @@ -81,14 +113,32 @@ public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module) } // Fallback: update in-memory collection (replace whole object) - var existingInMemoryIndex = _modules.FindIndex(m => m.Id == module.Id); - if (existingInMemoryIndex >= 0) + await _updateLock.WaitAsync(cancellationToken); + try + { + var existingInMemoryIndex = _modules.FindIndex(m => m.Id == module.Id); + if (existingInMemoryIndex >= 0) + { + _modules[existingInMemoryIndex] = module; + } + else + { + _modules.Add(module); + } + } + finally { - _modules[existingInMemoryIndex] = module; + _updateLock.Release(); } - else + } + + public void Dispose() + { + if (!_disposed) { - _modules.Add(module); + _initLock?.Dispose(); + _updateLock?.Dispose(); + _disposed = true; } } } diff --git a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs index 95faa1f..5817979 100644 --- a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs +++ b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs @@ -6,61 +6,100 @@ namespace CrystaLearn.Core.Services; -public partial class CrystaProgramSyncModuleServiceFake : ICrystaProgramSyncModuleService +public partial class CrystaProgramSyncModuleServiceFake : ICrystaProgramSyncModuleService, IDisposable { - private static List _modules = new(); + private List _modules = new(); + private bool _initialized = false; + private readonly SemaphoreSlim _initLock = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _updateLock = new SemaphoreSlim(1, 1); + private bool _disposed = false; private IConfiguration Configuration { get; set; } = default!; public CrystaProgramSyncModuleServiceFake(IConfiguration configuration) { Configuration = configuration; - if (_modules.Count == 0) - { - var pat = Configuration["AzureDevOps:PersonalAccessToken"]; + } - _modules = new List + private async Task EnsureInitializedAsync(CancellationToken cancellationToken = default) + { + if (!_initialized) + { + await _initLock.WaitAsync(cancellationToken); + try { - new CrystaProgramSyncModule + if (!_initialized) { - Id = Guid.NewGuid(), - CrystaProgramId = CrystaProgramServiceFake.FakeProgramCSI.Id, - CrystaProgram = CrystaProgramServiceFake.FakeProgramCSI, - ModuleType = SyncModuleType.AzureBoard, - SyncConfig = - $$""" + var pat = Configuration["AzureDevOps:PersonalAccessToken"]; + + _modules = new List + { + new CrystaProgramSyncModule + { + Id = Guid.NewGuid(), + CrystaProgramId = CrystaProgramServiceFake.FakeProgramCSI.Id, + CrystaProgram = CrystaProgramServiceFake.FakeProgramCSI, + ModuleType = SyncModuleType.AzureBoard, + SyncConfig = + $$""" + { + "Organization": "cs-internship", + "PersonalAccessToken": "{{pat}}", + "Project": "CS Internship Program" + } + """, + SyncInfo = new SyncInfo { - "Organization": "cs-internship", - "PersonalAccessToken": "{{pat}}", - "Project": "CS Internship Program" + LastSyncDateTime = DateTimeOffset.Now.AddDays(-2), + LastSyncOffset = "0" } - """, - SyncInfo = new SyncInfo - { - LastSyncDateTime = DateTimeOffset.Now.AddDays(-2), - LastSyncOffset = "0" - } + } + }; + _initialized = true; } - }; + } + finally + { + _initLock.Release(); + } } } public async Task> GetSyncModulesAsync(CancellationToken cancellationToken) { + await EnsureInitializedAsync(cancellationToken); return _modules; } - public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module) + public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default) { - var existing = _modules.FirstOrDefault(m => m.Id == module.Id); - if (existing != null) + await _updateLock.WaitAsync(cancellationToken); + try { - existing.SyncInfo = module.SyncInfo; - existing.SyncConfig = module.SyncConfig; + var existing = _modules.FirstOrDefault(m => m.Id == module.Id); + if (existing != null) + { + existing.SyncInfo = module.SyncInfo; + existing.SyncConfig = module.SyncConfig; + } + else + { + _modules.Add(module); + } + } + finally + { + _updateLock.Release(); } - else + } + + public void Dispose() + { + if (!_disposed) { - _modules.Add(module); + _initLock?.Dispose(); + _updateLock?.Dispose(); + _disposed = true; } } }