From 545d43f1f536504ff0b6d49e1660ff382033c84b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 06:24:50 +0000 Subject: [PATCH] Revert persistent file-based cache implementation, restore IMemoryCache usage Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --- .gitignore | 3 - Server/Program.cs | 2 +- Services/CachedTransportService.cs | 12 +- Services/FilePersistentCache.cs | 179 ------------------ Services/IPersistentCache.cs | 10 - .../CachedTransportServiceTests.cs | 36 +--- 6 files changed, 14 insertions(+), 228 deletions(-) delete mode 100644 Services/FilePersistentCache.cs delete mode 100644 Services/IPersistentCache.cs diff --git a/.gitignore b/.gitignore index a592bae..c629877 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,3 @@ nunit-*.xml # mac files .DS_Store - -# Cache directories -cache/ diff --git a/Server/Program.cs b/Server/Program.cs index be58d91..89a6aa1 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -10,7 +10,7 @@ var transportConfig = builder.Configuration.GetSection("Transport").Get(); +builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/Services/CachedTransportService.cs b/Services/CachedTransportService.cs index f5cc9f4..36e7d88 100644 --- a/Services/CachedTransportService.cs +++ b/Services/CachedTransportService.cs @@ -1,8 +1,9 @@ +using Microsoft.Extensions.Caching.Memory; using ShiftScheduler.Shared; namespace ShiftScheduler.Services { - public class CachedTransportService(ITransportApiService transportService, TransportConfiguration config, IPersistentCache cache) : ITransportService + public class CachedTransportService(ITransportApiService transportService, TransportConfiguration config, IMemoryCache cache) : ITransportService { public async Task GetConnectionAsync(DateTime shiftStartTime) { @@ -13,8 +14,7 @@ namespace ShiftScheduler.Services var cacheKey = GenerateCacheKey(searchDate, searchTime); // Try to get from cache first - var cachedConnection = await cache.GetAsync(cacheKey); - if (cachedConnection != null) + if (cache.TryGetValue(cacheKey, out TransportConnection? cachedConnection) && cachedConnection != null) { return cachedConnection; } @@ -25,7 +25,11 @@ namespace ShiftScheduler.Services // Cache the connection result if valid if (connection != null) { - await cache.SetAsync(cacheKey, connection, TimeSpan.FromDays(config.CacheDurationDays)); + var cacheOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(config.CacheDurationDays) + }; + cache.Set(cacheKey, connection, cacheOptions); } return connection; diff --git a/Services/FilePersistentCache.cs b/Services/FilePersistentCache.cs deleted file mode 100644 index dfc4c3c..0000000 --- a/Services/FilePersistentCache.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Text.Json; - -namespace ShiftScheduler.Services -{ - public class FilePersistentCache : IPersistentCache - { - private readonly string _cacheDirectory; - private readonly string _indexFilePath; - private readonly object _lockObject = new object(); - - public FilePersistentCache(string cacheDirectory = "cache") - { - _cacheDirectory = Path.Combine(Directory.GetCurrentDirectory(), cacheDirectory); - _indexFilePath = Path.Combine(_cacheDirectory, "cache_index.json"); - EnsureCacheDirectoryExists(); - } - - public async Task GetAsync(string key) where T : class - { - try - { - var cacheFilePath = GetCacheFilePath(key); - if (!File.Exists(cacheFilePath)) - return null; - - var cacheEntryJson = await File.ReadAllTextAsync(cacheFilePath); - var cacheEntry = JsonSerializer.Deserialize(cacheEntryJson); - - if (cacheEntry == null || cacheEntry.ExpiresAt < DateTime.UtcNow) - { - // Cache entry expired, remove it - await RemoveAsync(key); - return null; - } - - return JsonSerializer.Deserialize(cacheEntry.Value); - } - catch - { - // If there's any error reading the cache, return null - return null; - } - } - - public async Task SetAsync(string key, T value, TimeSpan expiration) where T : class - { - try - { - var cacheEntry = new CacheEntry - { - Key = key, - Value = JsonSerializer.Serialize(value), - ExpiresAt = DateTime.UtcNow.Add(expiration), - CreatedAt = DateTime.UtcNow - }; - - var cacheFilePath = GetCacheFilePath(key); - var cacheEntryJson = JsonSerializer.Serialize(cacheEntry); - - lock (_lockObject) - { - EnsureCacheDirectoryExists(); - File.WriteAllText(cacheFilePath, cacheEntryJson); - } - - await UpdateIndexAsync(key, cacheEntry.ExpiresAt); - } - catch - { - // Fail silently if cache write fails - } - } - - public async Task RemoveAsync(string key) - { - try - { - var cacheFilePath = GetCacheFilePath(key); - if (File.Exists(cacheFilePath)) - { - File.Delete(cacheFilePath); - } - await RemoveFromIndexAsync(key); - } - catch - { - // Fail silently if removal fails - } - } - - public async Task ClearExpiredAsync() - { - try - { - var index = await LoadIndexAsync(); - var expiredKeys = index.Where(kvp => kvp.Value < DateTime.UtcNow).Select(kvp => kvp.Key).ToList(); - - foreach (var key in expiredKeys) - { - await RemoveAsync(key); - } - } - catch - { - // Fail silently if cleanup fails - } - } - - private string GetCacheFilePath(string key) - { - var safeKey = key.Replace(':', '_').Replace('/', '_').Replace('\\', '_'); - return Path.Combine(_cacheDirectory, $"{safeKey}.json"); - } - - private void EnsureCacheDirectoryExists() - { - if (!Directory.Exists(_cacheDirectory)) - { - Directory.CreateDirectory(_cacheDirectory); - } - } - - private async Task UpdateIndexAsync(string key, DateTime expiresAt) - { - try - { - var index = await LoadIndexAsync(); - index[key] = expiresAt; - - var indexJson = JsonSerializer.Serialize(index); - await File.WriteAllTextAsync(_indexFilePath, indexJson); - } - catch - { - // Fail silently if index update fails - } - } - - private async Task RemoveFromIndexAsync(string key) - { - try - { - var index = await LoadIndexAsync(); - index.Remove(key); - - var indexJson = JsonSerializer.Serialize(index); - await File.WriteAllTextAsync(_indexFilePath, indexJson); - } - catch - { - // Fail silently if index update fails - } - } - - private async Task> LoadIndexAsync() - { - try - { - if (!File.Exists(_indexFilePath)) - return new Dictionary(); - - var indexJson = await File.ReadAllTextAsync(_indexFilePath); - return JsonSerializer.Deserialize>(indexJson) ?? new Dictionary(); - } - catch - { - return new Dictionary(); - } - } - - private class CacheEntry - { - public string Key { get; set; } = string.Empty; - public string Value { get; set; } = string.Empty; - public DateTime ExpiresAt { get; set; } - public DateTime CreatedAt { get; set; } - } - } -} \ No newline at end of file diff --git a/Services/IPersistentCache.cs b/Services/IPersistentCache.cs deleted file mode 100644 index e9159a6..0000000 --- a/Services/IPersistentCache.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ShiftScheduler.Services -{ - public interface IPersistentCache - { - Task GetAsync(string key) where T : class; - Task SetAsync(string key, T value, TimeSpan expiration) where T : class; - Task RemoveAsync(string key); - Task ClearExpiredAsync(); - } -} \ No newline at end of file diff --git a/ShiftScheduler.Services.Tests/CachedTransportServiceTests.cs b/ShiftScheduler.Services.Tests/CachedTransportServiceTests.cs index 5e77c20..aee4fd2 100644 --- a/ShiftScheduler.Services.Tests/CachedTransportServiceTests.cs +++ b/ShiftScheduler.Services.Tests/CachedTransportServiceTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Caching.Memory; using Moq; using ShiftScheduler.Services; using ShiftScheduler.Shared; @@ -8,14 +9,14 @@ namespace ShiftScheduler.Services.Tests; public class CachedTransportServiceTests { private readonly Mock _transportServiceMock; - private readonly Mock _cacheMock; + private readonly IMemoryCache _memoryCache; private readonly CachedTransportService _cachedTransportService; private readonly TransportConfiguration _config; public CachedTransportServiceTests() { _transportServiceMock = new Mock(); - _cacheMock = new Mock(); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); _config = new TransportConfiguration { @@ -29,7 +30,7 @@ public class CachedTransportServiceTests CacheDurationDays = 1 }; - _cachedTransportService = new CachedTransportService(_transportServiceMock.Object, _config, _cacheMock.Object); + _cachedTransportService = new CachedTransportService(_transportServiceMock.Object, _config, _memoryCache); } [Fact] @@ -45,10 +46,6 @@ public class CachedTransportServiceTests Platform = "5" }; - _cacheMock - .Setup(x => x.GetAsync(It.IsAny())) - .ReturnsAsync((TransportConnection?)null); // First call returns null (not cached) - _transportServiceMock .Setup(x => x.GetConnectionAsync(shiftStartTime)) .ReturnsAsync(connection); @@ -56,11 +53,6 @@ public class CachedTransportServiceTests // Act - First call should hit the transport service var result1 = await _cachedTransportService.GetConnectionAsync(shiftStartTime); - // Setup cache to return the connection for second call - _cacheMock - .Setup(x => x.GetAsync(It.IsAny())) - .ReturnsAsync(connection); - // Act - Second call should use cache var result2 = await _cachedTransportService.GetConnectionAsync(shiftStartTime); @@ -70,12 +62,8 @@ public class CachedTransportServiceTests result1.ArrivalTime.ShouldBe(result2.ArrivalTime); result1.DepartureTime.ShouldBe(result2.DepartureTime); - // Verify that transport service was called only once for the first call + // Verify that transport service was called only once _transportServiceMock.Verify(x => x.GetConnectionAsync(shiftStartTime), Times.Once); - - // Verify that cache was accessed and set was called - _cacheMock.Verify(x => x.GetAsync(It.IsAny()), Times.AtLeastOnce); - _cacheMock.Verify(x => x.SetAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -84,10 +72,6 @@ public class CachedTransportServiceTests // Arrange var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - _cacheMock - .Setup(x => x.GetAsync(It.IsAny())) - .ReturnsAsync((TransportConnection?)null); - _transportServiceMock .Setup(x => x.GetConnectionAsync(shiftStartTime)) .ReturnsAsync((TransportConnection?)null); @@ -104,9 +88,6 @@ public class CachedTransportServiceTests // Verify that transport service was called twice (no caching for null) _transportServiceMock.Verify(x => x.GetConnectionAsync(shiftStartTime), Times.Exactly(2)); - - // Verify that cache set was never called for null values - _cacheMock.Verify(x => x.SetAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] @@ -119,10 +100,6 @@ public class CachedTransportServiceTests var connection1 = new TransportConnection { ArrivalTime = "2023-12-15T07:30:00" }; var connection2 = new TransportConnection { ArrivalTime = "2023-12-16T07:30:00" }; - _cacheMock - .Setup(x => x.GetAsync(It.IsAny())) - .ReturnsAsync((TransportConnection?)null); // Cache misses for both calls - _transportServiceMock .Setup(x => x.GetConnectionAsync(shiftStartTime1)) .ReturnsAsync(connection1); @@ -143,8 +120,5 @@ public class CachedTransportServiceTests // Verify that transport service was called twice (different cache keys) _transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny()), Times.Exactly(2)); - - // Verify that cache set was called twice (for different cache keys) - _cacheMock.Verify(x => x.SetAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } } \ No newline at end of file