From df8712fda58d4f618dcd8a409992d02c1f977b1f Mon Sep 17 00:00:00 2001 From: aligneddev Date: Mon, 6 Apr 2026 16:33:49 +0000 Subject: [PATCH 1/3] apphost and urls --- .../Rides/WeatherLookupServiceTests.cs | 12 ++++++++--- .../Application/Rides/WeatherLookupService.cs | 12 +++++------ src/BikeTracking.Api/Program.cs | 21 +++++++++++++++++-- src/BikeTracking.AppHost/AppHost.cs | 20 ++++++++++++++++++ .../playwright.config.ts | 4 ++++ 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/BikeTracking.Api.Tests/Application/Rides/WeatherLookupServiceTests.cs b/src/BikeTracking.Api.Tests/Application/Rides/WeatherLookupServiceTests.cs index 6ddc2c1..354d47b 100644 --- a/src/BikeTracking.Api.Tests/Application/Rides/WeatherLookupServiceTests.cs +++ b/src/BikeTracking.Api.Tests/Application/Rides/WeatherLookupServiceTests.cs @@ -65,7 +65,9 @@ public async Task GetOrFetchAsync_CacheHit_DoesNotCallHttp() { Content = new StringContent("{}", Encoding.UTF8, "application/json"), }); - var factory = new StubHttpClientFactory(new HttpClient(handler)); + var factory = new StubHttpClientFactory( + new HttpClient(handler) { BaseAddress = new Uri("https://api.open-meteo.com") } + ); var config = new ConfigurationBuilder().AddInMemoryCollection().Build(); var service = new OpenMeteoWeatherLookupService( @@ -101,7 +103,9 @@ public async Task GetOrFetchAsync_SecondCall_UsesCacheAndCallsHttpOnce() ), }); - var factory = new StubHttpClientFactory(new HttpClient(handler)); + var factory = new StubHttpClientFactory( + new HttpClient(handler) { BaseAddress = new Uri("https://api.open-meteo.com") } + ); var config = new ConfigurationBuilder().AddInMemoryCollection().Build(); var service = new OpenMeteoWeatherLookupService( @@ -164,7 +168,9 @@ public async Task GetOrFetchAsync_AfterServiceRestart_UsesPersistedCacheWithoutH "application/json" ), }); - var factory = new StubHttpClientFactory(new HttpClient(handler)); + var factory = new StubHttpClientFactory( + new HttpClient(handler) { BaseAddress = new Uri("https://api.open-meteo.com") } + ); var config = new ConfigurationBuilder().AddInMemoryCollection().Build(); await using var restartedContext = CreateSqliteContext(connection); diff --git a/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs b/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs index c861507..8034f15 100644 --- a/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs +++ b/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs @@ -101,9 +101,8 @@ ILogger logger // Determine which API to call (forecast vs. archive) var daysDiff = (int)(DateTime.UtcNow.Date - dateTimeUtc.Date).TotalDays; var isHistorical = daysDiff > 92; - var endpoint = isHistorical - ? "https://archive-api.open-meteo.com/v1/archive" - : "https://api.open-meteo.com/v1/forecast"; + var clientName = isHistorical ? "OpenMeteoArchive" : "OpenMeteoForecast"; + var requestPath = isHistorical ? "/v1/archive" : "/v1/forecast"; var apiKey = configuration["WeatherLookup:ApiKey"]; var apiKeyParam = string.IsNullOrWhiteSpace(apiKey) @@ -111,7 +110,8 @@ ILogger logger : $"&apikey={Uri.EscapeDataString(apiKey)}"; // Build query parameters - var pastDaysParam = isHistorical ? "" : $"&past_days={Math.Min(daysDiff + 1, 92)}"; + var pastDaysParam = + isHistorical || daysDiff < 0 ? "" : $"&past_days={Math.Min(daysDiff + 1, 92)}"; var queryParams = $"?latitude={Uri.EscapeDataString(latitude.ToString(CultureInfo.InvariantCulture))}" + $"&longitude={Uri.EscapeDataString(longitude.ToString(CultureInfo.InvariantCulture))}" @@ -126,9 +126,9 @@ ILogger logger try { - var client = httpClientFactory.CreateClient("OpenMeteo"); + var client = httpClientFactory.CreateClient(clientName); using var response = await client.GetAsync( - $"{endpoint}{queryParams}", + $"{requestPath}{queryParams}", cancellationToken ); diff --git a/src/BikeTracking.Api/Program.cs b/src/BikeTracking.Api/Program.cs index 95b919b..7bd3ec8 100644 --- a/src/BikeTracking.Api/Program.cs +++ b/src/BikeTracking.Api/Program.cs @@ -15,6 +15,14 @@ var connectionString = builder.Configuration.GetConnectionString("BikeTracking") ?? "Data Source=biketracking.local.db"; +var eiaGasPriceBaseUrl = + builder.Configuration["ExternalApis:EiaGasPriceBaseUrl"] ?? "https+http://eia-gas-price"; +var openMeteoForecastBaseUrl = + builder.Configuration["ExternalApis:OpenMeteoForecastBaseUrl"] + ?? "https+http://open-meteo-forecast"; +var openMeteoArchiveBaseUrl = + builder.Configuration["ExternalApis:OpenMeteoArchiveBaseUrl"] + ?? "https+http://open-meteo-archive"; builder.Services.Configure(builder.Configuration.GetSection("Identity")); builder.Services.AddDbContext(options => @@ -48,14 +56,23 @@ "EiaGasPrice", client => { - client.BaseAddress = new Uri("https://api.eia.gov"); + client.BaseAddress = new Uri(eiaGasPriceBaseUrl); client.Timeout = TimeSpan.FromSeconds(10); } ); builder.Services.AddHttpClient( - "OpenMeteo", + "OpenMeteoForecast", client => { + client.BaseAddress = new Uri(openMeteoForecastBaseUrl); + client.Timeout = TimeSpan.FromSeconds(5); + } +); +builder.Services.AddHttpClient( + "OpenMeteoArchive", + client => + { + client.BaseAddress = new Uri(openMeteoArchiveBaseUrl); client.Timeout = TimeSpan.FromSeconds(5); } ); diff --git a/src/BikeTracking.AppHost/AppHost.cs b/src/BikeTracking.AppHost/AppHost.cs index 2db7464..ea01664 100644 --- a/src/BikeTracking.AppHost/AppHost.cs +++ b/src/BikeTracking.AppHost/AppHost.cs @@ -1,5 +1,19 @@ var builder = DistributedApplication.CreateBuilder(args); +var openMeteoForecast = builder.AddExternalService( + "open-meteo-forecast", + "https://api.open-meteo.com/" +); +var openMeteoArchive = builder.AddExternalService( + "open-meteo-archive", + "https://archive-api.open-meteo.com/" +); +var eiaGasPrice = builder.AddExternalService("eia-gas-price", "https://api.eia.gov/"); + +const string eiaGasPriceBaseUrl = "https+http://eia-gas-price"; +const string openMeteoForecastBaseUrl = "https+http://open-meteo-forecast"; +const string openMeteoArchiveBaseUrl = "https+http://open-meteo-archive"; + // Local SQL Server database for development // var database = builder.AddSqlServer("sql").AddDatabase("biketracking"); // TODO https://aspire.dev/integrations/databases/sqlite/sqlite-get-started/?lang=csharp @@ -10,6 +24,12 @@ var apiService = builder .AddProject("api") //.WithReference(database) + .WithReference(openMeteoForecast) + .WithReference(openMeteoArchive) + .WithReference(eiaGasPrice) + .WithEnvironment("ExternalApis__EiaGasPriceBaseUrl", eiaGasPriceBaseUrl) + .WithEnvironment("ExternalApis__OpenMeteoForecastBaseUrl", openMeteoForecastBaseUrl) + .WithEnvironment("ExternalApis__OpenMeteoArchiveBaseUrl", openMeteoArchiveBaseUrl) .WithHttpHealthCheck("/health") .WithExternalHttpEndpoints(); diff --git a/src/BikeTracking.Frontend/playwright.config.ts b/src/BikeTracking.Frontend/playwright.config.ts index d0e575d..cca0d59 100644 --- a/src/BikeTracking.Frontend/playwright.config.ts +++ b/src/BikeTracking.Frontend/playwright.config.ts @@ -45,6 +45,10 @@ export default defineConfig({ env: { ASPNETCORE_URLS: e2eApiUrl, PLAYWRIGHT_E2E: "1", + ExternalApis__EiaGasPriceBaseUrl: "https://api.eia.gov", + ExternalApis__OpenMeteoForecastBaseUrl: "https://api.open-meteo.com", + ExternalApis__OpenMeteoArchiveBaseUrl: + "https://archive-api.open-meteo.com", // Use a dedicated E2E database so test runs never touch the local dev DB. // ASP.NET Core maps ConnectionStrings__ to ConnectionStrings[name]. ConnectionStrings__BikeTracking: `Data Source=${e2eDbPath}`, From 7826135e0f28598d83ba07357d64a72bc266330d Mon Sep 17 00:00:00 2001 From: aligneddev Date: Mon, 6 Apr 2026 16:59:16 +0000 Subject: [PATCH 2/3] weather call fix --- .../Application/Rides/WeatherLookupService.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs b/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs index 8034f15..1fb4752 100644 --- a/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs +++ b/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs @@ -110,20 +110,17 @@ ILogger logger : $"&apikey={Uri.EscapeDataString(apiKey)}"; // Build query parameters - var pastDaysParam = - isHistorical || daysDiff < 0 ? "" : $"&past_days={Math.Min(daysDiff + 1, 92)}"; + // Note: past_days is mutually exclusive with start_date/end_date on the Open-Meteo API. var queryParams = $"?latitude={Uri.EscapeDataString(latitude.ToString(CultureInfo.InvariantCulture))}" + $"&longitude={Uri.EscapeDataString(longitude.ToString(CultureInfo.InvariantCulture))}" + $"&start_date={dateTimeUtc.Date:yyyy-MM-dd}" + $"&end_date={dateTimeUtc.Date:yyyy-MM-dd}" - + $"{pastDaysParam}" + "&hourly=temperature_2m,wind_speed_10m,wind_direction_10m,relative_humidity_2m,cloud_cover,precipitation,weather_code" + "&temperature_unit=fahrenheit" + "&wind_speed_unit=mph" + "&timezone=auto" + apiKeyParam; - try { var client = httpClientFactory.CreateClient(clientName); From 1ee617b08ad01a36d578a3b4768d0189058b257f Mon Sep 17 00:00:00 2001 From: aligneddev Date: Mon, 6 Apr 2026 17:03:51 +0000 Subject: [PATCH 3/3] error logging --- .../Application/Rides/WeatherLookupService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs b/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs index 1fb4752..3604567 100644 --- a/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs +++ b/src/BikeTracking.Api/Application/Rides/WeatherLookupService.cs @@ -132,11 +132,12 @@ ILogger logger if (!response.IsSuccessStatusCode) { logger.LogWarning( - "Open-Meteo lookup failed for rounded {LatitudeRounded},{LongitudeRounded} at {UtcHour} with status {StatusCode}", + "Open-Meteo lookup failed for rounded {LatitudeRounded},{LongitudeRounded} at {UtcHour} with status {StatusCode}: {ResponseContent}", latRounded, lonRounded, dateTimeUtc, - response.StatusCode + response.StatusCode, + await response.Content.ReadAsStringAsync(cancellationToken) ); // Cache failure for short period to avoid hammering API