Revert persistent file-based cache implementation, restore IMemoryCache usage
Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>
This commit is contained in:
parent
b5f052f4fb
commit
545d43f1f5
6 changed files with 14 additions and 228 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -59,6 +59,3 @@ nunit-*.xml
|
||||||
|
|
||||||
# mac files
|
# mac files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Cache directories
|
|
||||||
cache/
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ var transportConfig = builder.Configuration.GetSection("Transport").Get<Transpor
|
||||||
// Register services
|
// Register services
|
||||||
builder.Services.AddSingleton(shifts);
|
builder.Services.AddSingleton(shifts);
|
||||||
builder.Services.AddSingleton(transportConfig);
|
builder.Services.AddSingleton(transportConfig);
|
||||||
builder.Services.AddSingleton<IPersistentCache, FilePersistentCache>();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddHttpClient<TransportService>();
|
builder.Services.AddHttpClient<TransportService>();
|
||||||
builder.Services.AddSingleton<ShiftService>();
|
builder.Services.AddSingleton<ShiftService>();
|
||||||
builder.Services.AddSingleton<IcsExportService>();
|
builder.Services.AddSingleton<IcsExportService>();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using ShiftScheduler.Shared;
|
using ShiftScheduler.Shared;
|
||||||
|
|
||||||
namespace ShiftScheduler.Services
|
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<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime)
|
public async Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime)
|
||||||
{
|
{
|
||||||
|
|
@ -13,8 +14,7 @@ namespace ShiftScheduler.Services
|
||||||
var cacheKey = GenerateCacheKey(searchDate, searchTime);
|
var cacheKey = GenerateCacheKey(searchDate, searchTime);
|
||||||
|
|
||||||
// Try to get from cache first
|
// Try to get from cache first
|
||||||
var cachedConnection = await cache.GetAsync<TransportConnection>(cacheKey);
|
if (cache.TryGetValue(cacheKey, out TransportConnection? cachedConnection) && cachedConnection != null)
|
||||||
if (cachedConnection != null)
|
|
||||||
{
|
{
|
||||||
return cachedConnection;
|
return cachedConnection;
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,11 @@ namespace ShiftScheduler.Services
|
||||||
// Cache the connection result if valid
|
// Cache the connection result if valid
|
||||||
if (connection != null)
|
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;
|
return connection;
|
||||||
|
|
|
||||||
|
|
@ -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<T?> GetAsync<T>(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<CacheEntry>(cacheEntryJson);
|
|
||||||
|
|
||||||
if (cacheEntry == null || cacheEntry.ExpiresAt < DateTime.UtcNow)
|
|
||||||
{
|
|
||||||
// Cache entry expired, remove it
|
|
||||||
await RemoveAsync(key);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(cacheEntry.Value);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// If there's any error reading the cache, return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetAsync<T>(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<Dictionary<string, DateTime>> LoadIndexAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(_indexFilePath))
|
|
||||||
return new Dictionary<string, DateTime>();
|
|
||||||
|
|
||||||
var indexJson = await File.ReadAllTextAsync(_indexFilePath);
|
|
||||||
return JsonSerializer.Deserialize<Dictionary<string, DateTime>>(indexJson) ?? new Dictionary<string, DateTime>();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return new Dictionary<string, DateTime>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
namespace ShiftScheduler.Services
|
|
||||||
{
|
|
||||||
public interface IPersistentCache
|
|
||||||
{
|
|
||||||
Task<T?> GetAsync<T>(string key) where T : class;
|
|
||||||
Task SetAsync<T>(string key, T value, TimeSpan expiration) where T : class;
|
|
||||||
Task RemoveAsync(string key);
|
|
||||||
Task ClearExpiredAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Moq;
|
using Moq;
|
||||||
using ShiftScheduler.Services;
|
using ShiftScheduler.Services;
|
||||||
using ShiftScheduler.Shared;
|
using ShiftScheduler.Shared;
|
||||||
|
|
@ -8,14 +9,14 @@ namespace ShiftScheduler.Services.Tests;
|
||||||
public class CachedTransportServiceTests
|
public class CachedTransportServiceTests
|
||||||
{
|
{
|
||||||
private readonly Mock<ITransportApiService> _transportServiceMock;
|
private readonly Mock<ITransportApiService> _transportServiceMock;
|
||||||
private readonly Mock<IPersistentCache> _cacheMock;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly CachedTransportService _cachedTransportService;
|
private readonly CachedTransportService _cachedTransportService;
|
||||||
private readonly TransportConfiguration _config;
|
private readonly TransportConfiguration _config;
|
||||||
|
|
||||||
public CachedTransportServiceTests()
|
public CachedTransportServiceTests()
|
||||||
{
|
{
|
||||||
_transportServiceMock = new Mock<ITransportApiService>();
|
_transportServiceMock = new Mock<ITransportApiService>();
|
||||||
_cacheMock = new Mock<IPersistentCache>();
|
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
|
||||||
_config = new TransportConfiguration
|
_config = new TransportConfiguration
|
||||||
{
|
{
|
||||||
|
|
@ -29,7 +30,7 @@ public class CachedTransportServiceTests
|
||||||
CacheDurationDays = 1
|
CacheDurationDays = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
_cachedTransportService = new CachedTransportService(_transportServiceMock.Object, _config, _cacheMock.Object);
|
_cachedTransportService = new CachedTransportService(_transportServiceMock.Object, _config, _memoryCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -45,10 +46,6 @@ public class CachedTransportServiceTests
|
||||||
Platform = "5"
|
Platform = "5"
|
||||||
};
|
};
|
||||||
|
|
||||||
_cacheMock
|
|
||||||
.Setup(x => x.GetAsync<TransportConnection>(It.IsAny<string>()))
|
|
||||||
.ReturnsAsync((TransportConnection?)null); // First call returns null (not cached)
|
|
||||||
|
|
||||||
_transportServiceMock
|
_transportServiceMock
|
||||||
.Setup(x => x.GetConnectionAsync(shiftStartTime))
|
.Setup(x => x.GetConnectionAsync(shiftStartTime))
|
||||||
.ReturnsAsync(connection);
|
.ReturnsAsync(connection);
|
||||||
|
|
@ -56,11 +53,6 @@ public class CachedTransportServiceTests
|
||||||
// Act - First call should hit the transport service
|
// Act - First call should hit the transport service
|
||||||
var result1 = await _cachedTransportService.GetConnectionAsync(shiftStartTime);
|
var result1 = await _cachedTransportService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
// Setup cache to return the connection for second call
|
|
||||||
_cacheMock
|
|
||||||
.Setup(x => x.GetAsync<TransportConnection>(It.IsAny<string>()))
|
|
||||||
.ReturnsAsync(connection);
|
|
||||||
|
|
||||||
// Act - Second call should use cache
|
// Act - Second call should use cache
|
||||||
var result2 = await _cachedTransportService.GetConnectionAsync(shiftStartTime);
|
var result2 = await _cachedTransportService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
|
|
@ -70,12 +62,8 @@ public class CachedTransportServiceTests
|
||||||
result1.ArrivalTime.ShouldBe(result2.ArrivalTime);
|
result1.ArrivalTime.ShouldBe(result2.ArrivalTime);
|
||||||
result1.DepartureTime.ShouldBe(result2.DepartureTime);
|
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);
|
_transportServiceMock.Verify(x => x.GetConnectionAsync(shiftStartTime), Times.Once);
|
||||||
|
|
||||||
// Verify that cache was accessed and set was called
|
|
||||||
_cacheMock.Verify(x => x.GetAsync<TransportConnection>(It.IsAny<string>()), Times.AtLeastOnce);
|
|
||||||
_cacheMock.Verify(x => x.SetAsync(It.IsAny<string>(), It.IsAny<TransportConnection>(), It.IsAny<TimeSpan>()), Times.Once);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -84,10 +72,6 @@ public class CachedTransportServiceTests
|
||||||
// Arrange
|
// Arrange
|
||||||
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
|
|
||||||
_cacheMock
|
|
||||||
.Setup(x => x.GetAsync<TransportConnection>(It.IsAny<string>()))
|
|
||||||
.ReturnsAsync((TransportConnection?)null);
|
|
||||||
|
|
||||||
_transportServiceMock
|
_transportServiceMock
|
||||||
.Setup(x => x.GetConnectionAsync(shiftStartTime))
|
.Setup(x => x.GetConnectionAsync(shiftStartTime))
|
||||||
.ReturnsAsync((TransportConnection?)null);
|
.ReturnsAsync((TransportConnection?)null);
|
||||||
|
|
@ -104,9 +88,6 @@ public class CachedTransportServiceTests
|
||||||
|
|
||||||
// Verify that transport service was called twice (no caching for null)
|
// Verify that transport service was called twice (no caching for null)
|
||||||
_transportServiceMock.Verify(x => x.GetConnectionAsync(shiftStartTime), Times.Exactly(2));
|
_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<string>(), It.IsAny<TransportConnection>(), It.IsAny<TimeSpan>()), Times.Never);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -119,10 +100,6 @@ public class CachedTransportServiceTests
|
||||||
var connection1 = new TransportConnection { ArrivalTime = "2023-12-15T07:30:00" };
|
var connection1 = new TransportConnection { ArrivalTime = "2023-12-15T07:30:00" };
|
||||||
var connection2 = new TransportConnection { ArrivalTime = "2023-12-16T07:30:00" };
|
var connection2 = new TransportConnection { ArrivalTime = "2023-12-16T07:30:00" };
|
||||||
|
|
||||||
_cacheMock
|
|
||||||
.Setup(x => x.GetAsync<TransportConnection>(It.IsAny<string>()))
|
|
||||||
.ReturnsAsync((TransportConnection?)null); // Cache misses for both calls
|
|
||||||
|
|
||||||
_transportServiceMock
|
_transportServiceMock
|
||||||
.Setup(x => x.GetConnectionAsync(shiftStartTime1))
|
.Setup(x => x.GetConnectionAsync(shiftStartTime1))
|
||||||
.ReturnsAsync(connection1);
|
.ReturnsAsync(connection1);
|
||||||
|
|
@ -143,8 +120,5 @@ public class CachedTransportServiceTests
|
||||||
|
|
||||||
// Verify that transport service was called twice (different cache keys)
|
// Verify that transport service was called twice (different cache keys)
|
||||||
_transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny<DateTime>()), Times.Exactly(2));
|
_transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny<DateTime>()), Times.Exactly(2));
|
||||||
|
|
||||||
// Verify that cache set was called twice (for different cache keys)
|
|
||||||
_cacheMock.Verify(x => x.SetAsync(It.IsAny<string>(), It.IsAny<TransportConnection>(), It.IsAny<TimeSpan>()), Times.Exactly(2));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue