Local multi-instance testing with .NET Aspire

The repository contains a .NET Aspire host (src/Aspire/Aspire.AppHost) that starts a production-like setup on your machine with one command: MongoDB and Redis in containers and two replicas of the store sharing them. It is the easiest way to see how GrandNode behaves with several instances - the situation of a Kubernetes cluster - before you deploy, and to test cache invalidation through Redis for real.

What it starts

ResourceWhat it is
mongo / MongodbA MongoDB container and the Mongodb database. The connection string is passed to the store as ConnectionStrings__Mongodb - the same environment variable you use in production.
redisA Redis container for the cache invalidation bus.
grand-web × 2Two processes of Grand.Web, each on its own port, behind one shared address. Configured with Redis__RedisPubSubEnabled=true, the channel grandnode-cache, Extensions__PluginShadowCopy=false (both processes use the same folder), and Debug logging for Grand.Infrastructure.Caching.Redis.

The containers have a persistent lifetime: they keep running, with their data, after you stop the host, so the next start is fast and the store stays installed. The project definitions are in Program.cs and ProjectConfiguration.cs of the AppHost - change the replica count there.

Prerequisites

  • The .NET 10 SDK and a trusted development certificate (dotnet dev-certs https --trust).
  • Docker Desktop (or another container runtime Aspire supports) running.
  • The solution built once, so the plugins are in src/Web/Grand.Web/Plugins: dotnet build GrandNode.slnx.

No Aspire workload has to be installed; the AppHost uses the Aspire SDK packages.

Run it

dotnet run --project src/Aspire/Aspire.AppHost --launch-profile https

Or set Aspire.AppHost as the startup project in Visual Studio or Rider. The console prints the dashboard address with a one-time login link, for example https://localhost:17196/login?t=....

The Aspire dashboard Resources page with mongo, Mongodb, redis and two grand-web replicas running, each with its URL
The Aspire dashboard: MongoDB, Redis and two replicas of the store.
  1. Open the store through the URL shown for grand-web. On the first start the database is empty and you get the installation wizard - without the database fields, because the connection string comes from Aspire. Install (with sample data if you like).
  2. Stop the host and start it again (or restart both replicas from the dashboard), as the wizard asks.
  3. Each replica's console log, under Console in the dashboard, shows its own address (Now listening on: https://localhost:...) and Subscribed to Redis pub/sub channel grandnode-cache.

Test cache invalidation between instances

Use the two replicas' own addresses (from their console logs), not the shared one, so you know which instance answers.

  1. Open a product page on replica A and on replica B - both now have the product in their memory cache.
  2. Sign in to the admin on replica A, rename the product and save.
  3. Reload the product on replica B. It must show the new name immediately.
  4. In the dashboard, replica A logs Published cache invalidation message (type: RemoveByPrefix, key: ...), delivered to 2 subscriber(s) - two, because both replicas subscribe. Replica B logs Received cache invalidation message ... from client ... for the same keys.
Console logs of a replica in the Aspire dashboard with Received cache invalidation message lines from the other replica and a Published message delivered to 2 subscribers
A replica's console: messages received from the other replica, and its own message delivered to 2 subscribers.

To see what goes wrong without Redis, set Redis__RedisPubSubEnabled to false in ProjectConfiguration.cs and repeat: replica B keeps showing the old name until its cache expires. This is exactly what happens in a cluster without Redis - see Running in production: Docker and Kubernetes.

Logs, traces and metrics

Grand.Web calls AddServiceDefaults() from Aspire.ServiceDefaults, which wires OpenTelemetry. Under Aspire, the dashboard's Structured logs, Traces and Metrics show every request, database call and log entry of both replicas, correlated - useful for finding slow pages or chatty code. Outside Aspire the same telemetry is exported when OTEL_EXPORTER_OTLP_ENDPOINT is set.

Start from scratch

Because the containers are persistent, the database survives restarts. To start again with an empty store, remove the containers: docker ps shows them as mongo-... and redis-...; docker rm -f them and run the host again.

What it does not simulate

  • Both replicas run from the same project folder, so they share App_Data, Plugins and wwwroot - like a shared volume in a cluster. Problems caused by unshared folders (sitemap.xml, custom CSS) do not show up here.
  • Because both processes load plugins from one folder, a replica may log a one-off PluginManager access error for a plugin's .deps.json at start-up while the other replica holds the file.
  • There is no ingress or TLS termination; forwarded headers are not exercised.

Question not answered here? Ask the community on GitHub Discussions ↗