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
| Resource | What it is |
|---|---|
mongo / Mongodb | A 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. |
redis | A Redis container for the cache invalidation bus. |
grand-web × 2 | Two 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=....

- 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). - Stop the host and start it again (or restart both replicas from the dashboard), as the wizard asks.
- Each replica's console log, under Console in the dashboard, shows its own address (
Now listening on: https://localhost:...) andSubscribed 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.
- Open a product page on replica A and on replica B - both now have the product in their memory cache.
- Sign in to the admin on replica A, rename the product and save.
- Reload the product on replica B. It must show the new name immediately.
- 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 logsReceived cache invalidation message ... from client ...for the same keys.

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,Pluginsandwwwroot- 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
PluginManageraccess error for a plugin's.deps.jsonat start-up while the other replica holds the file. - There is no ingress or TLS termination; forwarded headers are not exercised.
Related
Question not answered here? Ask the community on GitHub Discussions ↗