mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 15:01:14 +00:00
[AZ-310] [AZ-311] Route tile endpoints through ITileService
Move cache+DB+download logic for /tiles/{z}/{x}/{y} and
/api/satellite/tiles/latlon out of Program.cs into TileService.
Endpoints now inject only ITileService + ILogger. Service owns
IMemoryCache (1h absolute / 30min sliding preserved). Added
TileBytes DTO; ITileService gains GetOrDownloadTileAsync and
DownloadAndStoreSingleTileAsync. 5 new unit tests cover cache
hit, repo hit, downloader fallback, and AZ-311 happy + error.
Build clean (0/0), unit suite 40/40. Resolves architecture
baseline F3 in code (docs handled by AZ-315).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
# Refactor: route ServeTile through ITileService
|
||||
|
||||
**Task**: AZ-310_refactor_servetile_via_tileservice
|
||||
**Name**: ServeTile via ITileService.GetOrDownloadTileAsync
|
||||
**Description**: Move tile cache+DB+download logic from the `/tiles/{z}/{x}/{y}` endpoint into `ITileService` and make the endpoint a thin handler.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: None
|
||||
**Component**: TileDownloader (currently in SatelliteProvider.Services)
|
||||
**Tracker**: AZ-310
|
||||
**Epic**: AZ-309
|
||||
|
||||
## Problem
|
||||
|
||||
`Program.cs:141` (`ServeTile`) directly injects `ISatelliteDownloader`, `ITileRepository`, and `IMemoryCache`. It re-implements cache-or-DB-or-download logic that overlaps with `TileService` but is not delegated. This is architecture baseline finding F3 (Medium).
|
||||
|
||||
## Outcome
|
||||
|
||||
- All tile-serving logic for `/tiles/{z}/{x}/{y}` is owned by `TileService`.
|
||||
- `ServeTile` handler is a thin function: validate route, call service, write headers, return bytes.
|
||||
- `IMemoryCache` is no longer injected at the endpoint level for tile serving.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- New method `Task<TileBytes> GetOrDownloadTileAsync(int z, int x, int y, CancellationToken ct = default)` on `ITileService`. `TileBytes` is a record `(byte[] Bytes, string ContentType, string ETag, TimeSpan MaxAge)`.
|
||||
- Implementation in `TileService` owns the in-memory cache (DI-injected).
|
||||
- New unit tests for `GetOrDownloadTileAsync` (cache hit, repo hit, downloader fallback).
|
||||
- `Program.cs` ServeTile handler thinned.
|
||||
|
||||
### Excluded
|
||||
- Adding a new integration test for `/tiles/{z}/{x}/{y}` (existing smoke does not cover it; out of scope here).
|
||||
- Renaming any database tables, columns, or DTOs.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Endpoint route preserved**
|
||||
Given the API is running
|
||||
When a client calls `GET /tiles/{z}/{x}/{y}`
|
||||
Then the response shape (image/jpeg bytes, ETag header, Cache-Control header) is unchanged from before the refactor.
|
||||
|
||||
**AC-2: Cache hit path**
|
||||
Given a tile has previously been served and is still in the in-memory cache
|
||||
When `GetOrDownloadTileAsync` is called for the same `(z,x,y)`
|
||||
Then the result comes from cache and neither the repository nor the downloader is invoked.
|
||||
|
||||
**AC-3: Repo hit path**
|
||||
Given a tile is not in cache but exists in the database with a valid file path
|
||||
When `GetOrDownloadTileAsync` is called
|
||||
Then the file is read from disk, cached, and returned without invoking the downloader.
|
||||
|
||||
**AC-4: Downloader fallback path**
|
||||
Given a tile is neither in cache nor in the database
|
||||
When `GetOrDownloadTileAsync` is called
|
||||
Then `ISatelliteDownloader.DownloadSingleTileAsync` is invoked, the downloaded tile is inserted into the repository, the bytes are read, cached, and returned.
|
||||
|
||||
**AC-5: Endpoint no longer injects downloader/repo/cache**
|
||||
Given the post-refactor `Program.cs`
|
||||
When the `ServeTile` handler is inspected
|
||||
Then it injects only `ITileService`, `HttpContext`, and `ILogger<Program>` (not `ISatelliteDownloader`, `ITileRepository`, or `IMemoryCache`).
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|--------------|------------------|
|
||||
| AC-2 | Cache hit | Mock cache returns bytes; repo + downloader mocks asserted unused |
|
||||
| AC-3 | Repo hit | Mock repo returns tile entity with existing file path; downloader mock asserted unused |
|
||||
| AC-4 | Downloader fallback | Mock repo returns null; downloader returns DownloadedTileInfoV2; assert insert + return |
|
||||
|
||||
## Constraints
|
||||
|
||||
- Public route, query/body shape, response shape preserved.
|
||||
- No new external libraries.
|
||||
- Cache lifetime (1h absolute, 30min sliding) preserved exactly.
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: Cache singleton lifetime semantics change**
|
||||
- *Risk*: Moving `IMemoryCache` from endpoint scope into service scope might alter cache key collision behavior.
|
||||
- *Mitigation*: TileService is registered as `Singleton`; `IMemoryCache` is also Singleton. Cache keys remain `tile_{z}_{x}_{y}`.
|
||||
@@ -1,66 +0,0 @@
|
||||
# Refactor: route GetTileByLatLon through ITileService
|
||||
|
||||
**Task**: AZ-311_refactor_gettilebylatlon_via_tileservice
|
||||
**Name**: GetTileByLatLon via ITileService.DownloadAndStoreSingleTileAsync
|
||||
**Description**: Move single-tile download+insert logic from `/api/satellite/tiles/latlon` into `ITileService` and thin the endpoint handler.
|
||||
**Complexity**: 2 points
|
||||
**Dependencies**: AZ-310
|
||||
**Component**: TileDownloader (currently in SatelliteProvider.Services)
|
||||
**Tracker**: AZ-311
|
||||
**Epic**: AZ-309
|
||||
|
||||
## Problem
|
||||
|
||||
`Program.cs:206` (`GetTileByLatLon`) injects `ISatelliteDownloader` and `ITileRepository` and re-implements download-then-insert. Same root cause as AZ-310 (architecture baseline F3).
|
||||
|
||||
## Outcome
|
||||
|
||||
- `GetTileByLatLon` handler is thin: call service, project to response, return.
|
||||
- `TileService` owns the download+insert flow for single-tile requests.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- New method `Task<TileMetadata> DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken ct = default)` on `ITileService`.
|
||||
- Implementation calls `ISatelliteDownloader.DownloadSingleTileAsync` then `ITileRepository.InsertAsync`.
|
||||
- New unit tests for the new method.
|
||||
- `Program.cs` `GetTileByLatLon` handler thinned.
|
||||
|
||||
### Excluded
|
||||
- Changes to `DownloadTileResponse` shape.
|
||||
- Changes to `ISatelliteDownloader` (zoom validation chain is unchanged).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Endpoint contract preserved**
|
||||
Given the API is running
|
||||
When a client calls `GET /api/satellite/tiles/latlon?Latitude=...&Longitude=...&ZoomLevel=...`
|
||||
Then the response is the same `DownloadTileResponse` JSON shape as before the refactor.
|
||||
|
||||
**AC-2: Service owns the flow**
|
||||
Given valid lat/lon/zoom
|
||||
When `DownloadAndStoreSingleTileAsync` is called
|
||||
Then `ISatelliteDownloader.DownloadSingleTileAsync` is invoked once, `ITileRepository.InsertAsync` is invoked once, and the resulting `TileMetadata` is returned.
|
||||
|
||||
**AC-3: Endpoint no longer injects downloader/repo**
|
||||
Given the post-refactor `Program.cs`
|
||||
When the `GetTileByLatLon` handler is inspected
|
||||
Then it injects `ITileService` and `ILogger<Program>` only (no `ISatelliteDownloader` or `ITileRepository`).
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|--------------|------------------|
|
||||
| AC-2 | Happy path | Mocks for downloader + repo wired; assert one call each; assert TileMetadata fields match the downloaded tile |
|
||||
| AC-2 | Downloader throws | Service propagates the exception; repo `InsertAsync` is not called |
|
||||
|
||||
## Constraints
|
||||
|
||||
- HTTP route, query parameter names, and response JSON shape preserved exactly.
|
||||
- Zoom validation in `GoogleMapsDownloaderV2.DownloadSingleTileAsync` keeps firing.
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: TileMetadata projection drift**
|
||||
- *Risk*: The endpoint currently constructs `DownloadTileResponse` directly from a `TileEntity`. After refactor it must do so from `TileMetadata`.
|
||||
- *Mitigation*: Compare every field one-to-one in the new code; existing field-level mapping is straightforward.
|
||||
Reference in New Issue
Block a user