Imported from dotnet/nuget-trends (
AGENTS.md). Install upstream withnpx skills add dotnet/nuget-trends. Copyright stays with the author.
AI Agent Instructions for NuGet Trends
This document provides guidance for AI agents working on the NuGet Trends codebase.
Project Overview
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)
Code Style
EditorConfig
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
Important Style Notes
- 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.
Project Structure
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)
Scripts
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
Common Tasks
Running the Application
# 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.cs
Running Tests
dotnet test NuGetTrends.slnx
Building
dotnet build NuGetTrends.slnx
Technology Stack
- 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)
Adding New Pages
When adding a new routable page, follow this checklist:
1. SEO — Sitemap, Titles, Meta Tags, Canonical URL
- 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.
2. IL Trimming Compatibility
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.
3. Playwright E2E Tests
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.
4. Static Asset References
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.
5. Blazor SSR + WASM Considerations
- 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().
Sentry Observability
Browser JS SDK
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.
Scheduler Metrics and Spans
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.
API Controllers
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).