From be741ca7875b044762f9b720688973d63c7b0f7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 Aug 2025 06:52:55 +0000 Subject: [PATCH] Implement caching for transport connection results with IMemoryCache Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --- Server/Program.cs | 1 + Server/appsettings.json | 3 +- Services/ShiftScheduler.Services.csproj | 1 + Services/TransportService.cs | 33 +++++++- Shared/TransportModels.cs | 1 + .../TransportServiceTests.cs | 75 +++++++++++++++++-- 6 files changed, 104 insertions(+), 10 deletions(-) diff --git a/Server/Program.cs b/Server/Program.cs index 5f549ff..63d0c2d 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -10,6 +10,7 @@ var transportConfig = builder.Configuration.GetSection("Transport").Get(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/Server/appsettings.json b/Server/appsettings.json index a32e56e..295ba16 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -14,7 +14,8 @@ "SafetyBufferMinutes": 30, "MinBreakMinutes": 60, "MaxEarlyArrivalMinutes": 60, - "MaxLateArrivalMinutes": 15 + "MaxLateArrivalMinutes": 15, + "CacheDurationMinutes": 15 }, "Shifts": [ diff --git a/Services/ShiftScheduler.Services.csproj b/Services/ShiftScheduler.Services.csproj index ba281ba..fb248fc 100644 --- a/Services/ShiftScheduler.Services.csproj +++ b/Services/ShiftScheduler.Services.csproj @@ -13,6 +13,7 @@ + diff --git a/Services/TransportService.cs b/Services/TransportService.cs index 63de945..4007bc5 100644 --- a/Services/TransportService.cs +++ b/Services/TransportService.cs @@ -1,9 +1,10 @@ using System.Text.Json; +using Microsoft.Extensions.Caching.Memory; using ShiftScheduler.Shared; namespace ShiftScheduler.Services { - public class TransportService(HttpClient httpClient, TransportConfiguration config) + public class TransportService(HttpClient httpClient, TransportConfiguration config, IMemoryCache cache) { public async Task GetConnectionAsync(DateTime shiftStartTime) { @@ -14,6 +15,16 @@ namespace ShiftScheduler.Services // and request more connections to cover the full range var searchTime = shiftStartTime.AddMinutes(config.MaxLateArrivalMinutes).ToString("HH:mm"); + // Generate cache key based on request parameters + var cacheKey = GenerateCacheKey(searchDate, searchTime); + + // Try to get from cache first + if (cache.TryGetValue(cacheKey, out TransportApiResponse? cachedResponse) && cachedResponse != null) + { + return ProcessApiResponse(cachedResponse, shiftStartTime); + } + + // Not in cache, make API call var url = $"{config.ApiBaseUrl}/connections?from={Uri.EscapeDataString(config.StartStation)}&to={Uri.EscapeDataString(config.EndStation)}&date={searchDate}&time={searchTime}&isArrivalTime=1&limit=5"; var response = await httpClient.GetStringAsync(url); var apiResponse = JsonSerializer.Deserialize(response, new JsonSerializerOptions @@ -21,6 +32,26 @@ namespace ShiftScheduler.Services PropertyNameCaseInsensitive = true }); + // Cache the response if valid + if (apiResponse != null) + { + var cacheOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(config.CacheDurationMinutes) + }; + cache.Set(cacheKey, apiResponse, cacheOptions); + } + + return ProcessApiResponse(apiResponse, shiftStartTime); + } + + private string GenerateCacheKey(string searchDate, string searchTime) + { + return $"transport_{config.StartStation}_{config.EndStation}_{searchDate}_{searchTime}"; + } + + private TransportConnection? ProcessApiResponse(TransportApiResponse? apiResponse, DateTime shiftStartTime) + { if (apiResponse?.Connections.Count > 0) { var allConnections = apiResponse.Connections.Select(MapToTransportConnection).ToList(); diff --git a/Shared/TransportModels.cs b/Shared/TransportModels.cs index b93f83c..1dca09a 100644 --- a/Shared/TransportModels.cs +++ b/Shared/TransportModels.cs @@ -9,6 +9,7 @@ namespace ShiftScheduler.Shared public int MinBreakMinutes { get; set; } = 60; public int MaxEarlyArrivalMinutes { get; set; } = 60; public int MaxLateArrivalMinutes { get; set; } = 15; + public int CacheDurationMinutes { get; set; } = 15; } public class TransportConnection diff --git a/ShiftScheduler.Services.Tests/TransportServiceTests.cs b/ShiftScheduler.Services.Tests/TransportServiceTests.cs index 63c2c19..7dcb77a 100644 --- a/ShiftScheduler.Services.Tests/TransportServiceTests.cs +++ b/ShiftScheduler.Services.Tests/TransportServiceTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.Json; +using Microsoft.Extensions.Caching.Memory; using Moq; using Moq.Protected; using ShiftScheduler.Services; @@ -12,12 +13,14 @@ public class TransportServiceTests { private readonly Mock _httpMessageHandlerMock; private readonly HttpClient _httpClient; + private readonly IMemoryCache _memoryCache; private readonly TransportService _transportService; public TransportServiceTests() { _httpMessageHandlerMock = new Mock(); _httpClient = new HttpClient(_httpMessageHandlerMock.Object); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); var config = new TransportConfiguration { @@ -27,10 +30,11 @@ public class TransportServiceTests SafetyBufferMinutes = 30, MinBreakMinutes = 60, MaxEarlyArrivalMinutes = 60, - MaxLateArrivalMinutes = 15 + MaxLateArrivalMinutes = 15, + CacheDurationMinutes = 15 }; - _transportService = new TransportService(_httpClient, config); + _transportService = new TransportService(_httpClient, config, _memoryCache); } [Fact] @@ -115,19 +119,74 @@ public class TransportServiceTests result.ArrivalTime.ShouldBe("2023-12-15T07:25:00"); } + [Fact] + public async Task GetConnectionAsync_WithCaching_ShouldCacheApiResponse() + { + // Arrange + var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); + var apiResponse = CreateValidApiResponse(); + var jsonResponse = JsonSerializer.Serialize(apiResponse); + + SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse); + + // Act - First call should hit the API + var result1 = await _transportService.GetConnectionAsync(shiftStartTime); + + // Act - Second call should use cache + var result2 = await _transportService.GetConnectionAsync(shiftStartTime); + + // Assert + result1.ShouldNotBeNull(); + result2.ShouldNotBeNull(); + result1.ArrivalTime.ShouldBe(result2.ArrivalTime); + result1.DepartureTime.ShouldBe(result2.DepartureTime); + + // Verify that HTTP call was made only once + _httpMessageHandlerMock.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.IsAny(), + ItExpr.IsAny()); + } + + [Fact] + public async Task GetConnectionAsync_WithDifferentDates_ShouldMakeSeparateApiCalls() + { + // Arrange + var shiftStartTime1 = new DateTime(2023, 12, 15, 8, 0, 0); + var shiftStartTime2 = new DateTime(2023, 12, 16, 8, 0, 0); + var apiResponse = CreateValidApiResponse(); + var jsonResponse = JsonSerializer.Serialize(apiResponse); + + SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse); + + // Act - Different dates should result in different cache keys + var result1 = await _transportService.GetConnectionAsync(shiftStartTime1); + var result2 = await _transportService.GetConnectionAsync(shiftStartTime2); + + // Assert + result1.ShouldNotBeNull(); + result2.ShouldNotBeNull(); + + // Verify that HTTP calls were made twice (different cache keys) + _httpMessageHandlerMock.Protected().Verify( + "SendAsync", + Times.Exactly(2), + ItExpr.IsAny(), + ItExpr.IsAny()); + } + private void SetupHttpMockResponse(HttpStatusCode statusCode, string content) { - var response = new HttpResponseMessage(statusCode) - { - Content = new StringContent(content) - }; - _httpMessageHandlerMock.Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(response); + .ReturnsAsync(() => new HttpResponseMessage(statusCode) + { + Content = new StringContent(content) + }); } private static TransportApiResponse CreateValidApiResponse()