# Local multi-instance testing with .NET Aspire

For developers

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=...`.

![The Aspire dashboard Resources page with mongo, Mongodb, redis and two grand-web replicas running, each with its URL](https://grandnode.com/Plugins/Theme.GrandNodeCom/Content/kb/developers/aspire-dashboard.webp)

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](https://grandnode.com/Plugins/Theme.GrandNodeCom/Content/kb/developers/aspire-console-logs.webp)

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](https://grandnode.com/getting-started-docker-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.

### Related

- [Development setup](https://grandnode.com/developers-development-setup)
- [Running in production: Docker and Kubernetes](https://grandnode.com/getting-started-docker-kubernetes)

- [Web page](https://grandnode.com/developers-aspire-local-testing)
