This document provides guidance for AI agents working on the NuGet Trends codebase.
NuGet Trends is a web application that tracks NuGet package download statistics and displays trends over time. The project consists of:
- Frontend: Blazor SSR + WebAssembly hybrid (two-project pattern)
- Backend: ASP.NET Core Web API (.NET)
- Scheduler: Hangfire background jobs for catalog import and download tracking
- Databases: PostgreSQL (metadata, search) + ClickHouse (time-series download data)
- Message Queue: RabbitMQ
- Orchestration: .NET Aspire (AppHost)
The project uses .editorconfig files to define code formatting rules. Always follow these conventions:
- Indentation: 4 spaces for C# files
- Charset: UTF-8
- See root
.editorconfigfor complete C# style rules
- Never use
#region/#endregionin C# code. Keep code organized through proper class structure and small, focused methods instead. - Blazor components in the Client project (
NuGetTrends.Web.Client) are interactive WASM components. The Server project (NuGetTrends.Web) only hasApp.razorandRoutes.razor.
src/
├── NuGet.Protocol.Catalog/ # NuGet catalog protocol client
├── NuGetTrends.Data/ # EF Core data layer + ClickHouse service
│ └── ClickHouse/
│ └── migrations/ # ClickHouse schema migrations (SQL)
├── NuGetTrends.Scheduler/ # Hangfire background jobs
├── NuGetTrends.AppHost/ # .NET Aspire orchestration
├── NuGetTrends.ServiceDefaults/ # Shared Aspire service defaults
├── NuGetTrends.Web/ # Server project (API controllers, SSR host)
│ └── Components/ # App.razor, Routes.razor only
├── NuGetTrends.Web.Client/ # Client project (all interactive Blazor components)
│ ├── Pages/ # Routable pages (Home, Packages)
│ ├── Layout/ # MainLayout
│ ├── Shared/ # Shared components
│ ├── Models/ # DTOs and models
│ └── Services/ # State services (PackageState, ThemeState, LoadingState)
├── NuGetTrends.Web.Tests/ # Unit tests
├── NuGetTrends.IntegrationTests/ # Integration tests
└── scripts/ # Utility scripts (C# scripts preferred)
Utility and maintenance scripts live in the scripts/ directory. Prefer C# scripts (.cs files using #!/usr/bin/env dotnet shebang with #:package directives) over shell scripts. This keeps tooling consistent with the rest of the codebase and works cross-platform.
Existing scripts:
seed-clickhouse-test-data.cs— Seeds ClickHouse with test data for a single packageseed-local-test-data.cs— Seeds both PostgreSQL and ClickHouse with test data for local development
# Full stack via Aspire (PostgreSQL, ClickHouse, RabbitMQ, Web, Scheduler)
dotnet run --project src/NuGetTrends.AppHost
# Seed test data for local development
PG_CONNECTION_STRING="..." CH_CONNECTION_STRING="..." ./scripts/seed-local-test-data.csdotnet test NuGetTrends.slnxdotnet build NuGetTrends.slnx- Backend: .NET 10, ASP.NET Core, Entity Framework Core, Hangfire, Sentry
- Frontend: Blazor SSR + WebAssembly hybrid, Blazored.Toast, Blazor-ApexCharts
- Databases: PostgreSQL (Npgsql), ClickHouse (ClickHouse.Driver)
- Infrastructure: .NET Aspire, Docker, Kubernetes (GKE)
When adding a new routable page, follow this checklist:
- Sitemap: Add the new URL to
src/NuGetTrends.Web/wwwroot/sitemap.xml. - Use the
<SeoHead>component (Shared/SeoHead.razor) which sets<PageTitle>,<meta description>, canonical URL, Open Graph, and Twitter Card tags in one place. Usage:<SeoHead Title="NuGet Trends - Your Page" Description="A unique description for this page." Path="/your-page" />
- The
<HeadOutlet>component is already configured inApp.razorwithInteractiveWebAssemblyrender mode, so<PageTitle>and<HeadContent>work from client-side page components. - Shared defaults (og:image, og:site_name, twitter:card, twitter:image) are set in
App.razorand don't need to be repeated per page.
The WASM client project has IL trimming enabled (PublishTrimmed=true, TrimMode=full) in Release builds. This aggressively strips unused code and will break the app in staging/production if not handled:
- JSON deserialization: All HTTP response DTOs must be registered in
NuGetTrendsJsonContext(src/NuGetTrends.Web.Client/NuGetTrendsJsonContext.cs) as[JsonSerializable(typeof(YourType))]. AllGetFromJsonAsynccalls must use the trim-safeJsonTypeInfo<T>overload (e.g.,GetFromJsonAsync(url, NuGetTrendsJsonContext.Default.YourType)). Never use the generic overload withoutJsonTypeInfo— it relies on reflection that the trimmer removes. - Trimmer roots: If a new component or third-party library is only referenced indirectly (e.g., via DI injection or cross-project
@rendermode), the trimmer may strip it. Add it tosrc/NuGetTrends.Web.Client/TrimmerRoots.xml. See existing entries forRoutesandBlazored.Toastas examples. - Test in Release mode: Trimmer issues only surface in Release builds. Always verify with
dotnet publish -c Releasewhen adding new dependencies or DI registrations.
Every new page must have Playwright tests. Tests live in src/NuGetTrends.PlaywrightTests/:
- Page health: Add the new route to the
[InlineData]list inPageHealthTests.csso it's checked for HTTP 4xx errors and JS console errors. - Functional tests: Add a test class for page-specific behavior (e.g.,
FrameworkPageTests.cs,ThemeToggleTests.cs). - Test patterns:
- All test classes use
[Collection("Playwright")]and injectPlaywrightFixture. - Use
PlaywrightFixture.WaitForWasmAsync(page)to wait for Blazor WASM hydration instead of arbitraryWaitForTimeoutAsyncdelays. - After WASM hydration, wait for specific elements with
WaitForSelectorAsyncrather than blanket timeouts. - Use
page.GotoAsync(url, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle })for initial navigation.
- All test classes use
- Fixture registration: If the new page needs a new API controller, cache service, or DI registration, also add it to
PlaywrightFixture.ConfigureServices(). - Seed data: If the page displays data, ensure
DevelopmentDataSeederprovides appropriate test data so tests have something to assert against.
Use @Assets["path"] syntax in App.razor for all CSS/JS references so MapStaticAssets() serves fingerprinted, cache-busted URLs. Never use raw /css/app.css paths in the server-rendered HTML.
- New pages go in
src/NuGetTrends.Web.Client/Pages/(the Client project), not the Server project. - Pages are prerendered on the server (
prerender: true) then hydrated on the client. Be aware that code inOnInitializedAsyncruns twice (server + client). Use[SupplyParameterFromQuery]for URL state and checkOperatingSystem.IsBrowser()if something should only run client-side. - When modifying response headers in middleware, use
OnStartingcallbacks — Blazor SSR streams the response body, so headers are read-only afterawait next().
The Sentry Browser JS SDK is loaded in App.razor (conditional on Sentry:Dsn config being set) to catch errors even when WASM fails to boot. It includes Session Replay (100% on error, 0% normal). The Blazor.start() promise has a .catch() that reports WASM initialization failures to Sentry.
All Hangfire jobs and background workers should be instrumented with Sentry:
- Metrics: Use
hub.Metrics.EmitCounter,hub.Metrics.EmitGauge, andhub.Metrics.EmitDistributionto track key operational data (packages processed, queue sizes, job completions/failures). UseSentrySdk.Experimental.Metricsin classes that use the static API (e.g.,DailyDownloadWorker). - Transactions and spans: Wrap significant operations in Sentry transactions (
SentrySdk.StartTransaction) with child spans (transaction.StartChild/span.StartChild) for sub-operations like DB queries, API calls, queue processing, and serialization. Finish spans and set status toSpanStatus.OkorSpanStatus.InternalErroras appropriate. - Naming conventions: Use dot-separated hierarchical names —
scheduler.job.completed,scheduler.daily_download.packages_queued,worker.queue_latency. Tag metrics withjob_namefor filtering. - When adding a new Hangfire job, follow the pattern in
TfmAdoptionSnapshotRefresher.csandDailyDownloadPackageIdPublisher.csfor metrics emission.
New API endpoints backing pages should consider adding Sentry breadcrumbs or spans for operations that could be slow or fail (e.g., ClickHouse queries, external API calls).