A small ASP.NET Core Web API that returns the top n best stories ordered by score from an in-memory snapshot refreshed in the background.
- .NET 10 SDK
dotnet restore
dotnet run --project .\src\StoryProvider.WebAPI\StoryProvider.WebAPI.csprojBy default the API listens on the local URLs shown by ASP.NET Core at startup.
With the included development launch settings, the app runs on:
http://localhost:5156https://localhost:7031
In the Development environment the app exposes:
- OpenAPI document:
/openapi/v1.json - Swagger UI:
/swagger
Examples:
http://localhost:5156/swaggerhttps://localhost:7031/swagger
GET /api/stories/best?n=10
Example response:
[
{
"title": "Example Hacker News story",
"uri": "https://example.com/article",
"postedBy": "johndoe",
"time": "2025-01-15T09:30:00+00:00",
"score": 245,
"commentCount": 81
},
{
"title": "Another top story",
"uri": "https://example.com/another-article",
"postedBy": "janedoe",
"time": "2025-01-15T08:10:00+00:00",
"score": 198,
"commentCount": 54
}
]The application is configured in src/StoryProvider.WebAPI/appsettings.json.
BaseUrl: the Hacker News API base URLBestStoriesEndpoint: the upstream endpoint used to retrieve best story IDsItemEndpointFormat: the upstream endpoint template used to retrieve story details by IDStoryCacheDurationSeconds: how long individual story details are cached in memoryMaxConcurrentRequests: the maximum number of concurrent upstream story-detail requests across the application
MaxStoriesPerRequest: the maximum number of stories accepted per API request
RefreshIntervalSeconds: how often the background worker refreshes the snapshotCacheLifetimeSeconds: how long the best stories are retained in the in-memory cache
| Project | Purpose / intent |
|---|---|
StoryProvider.WebAPI |
ASP.NET Core host that exposes the HTTP API, Swagger/OpenAPI, configuration binding, and DI composition root. |
StoryProvider.Workers |
Background processing layer that decides when best-story snapshot refreshes run. |
StoryProvider.Services |
Application use-case layer that refreshes and queries the cached best-story snapshot. |
StoryProvider.Services.Abstractions |
Contracts for application services and shared response models used across layers. |
StoryProvider.Models |
Shared internal models. |
StoryProvider.Sources |
Provider adapter layer that validates and maps external story data into the internal story model. |
StoryProvider.Sources.Abstractions |
Contracts and shared story model used by source adapters and services. |
StoryProvider.Clients |
Low-level upstream HTTP client layer for talking to Hacker News. |
StoryProvider.Clients.Abstractions |
Contracts and DTOs for upstream client communication. |
- The API exposes a single REST endpoint for the requested best stories scenario.
- Stories without the required data for the response contract are skipped.
- In-memory caching is sufficient for this exercise to reduce repeated upstream calls within one app instance.
- A background worker refreshes the best-stories cache so requests are served from memory.
- The story cache is the only in-memory cache layer used by the application flow.
- Outbound requests for story details are throttled with bounded concurrency through a SemaphoreSlim.
- Add static code analysis with SonarQube in the CI/CD pipeline.
- Publish a versioned service artifact and Swagger/OpenAPI package to GitHub Releases from CI/CD.
- Add file logging and decentralized logging integration.
- Add distributed caching (e.g. Redis) for multi-instance deployments.
- Add circuit breaker resilience policies for upstream dependencies (e.g. Polly).
- Increase unit test coverage and add integration tests.
- Add more story sources behind
IStorySource. - Add pagination or streaming options for larger result sets (currently limiting n to a configurable value).
- Add Dependency Injection specific project components to interface with WebApi.
- Add a Better wrapper around caching layer
- Add Exception filters and more tests around api call failure.