Why this little demo is worth a look
Real apps are rarely a single API and a database. This demo shows how .NET Aspire orchestrates a small but realistic distributed app locally, while Dapr provides event-driven glue (pub/sub, state) and keeps your code focused on domain logic. You get a smooth developer experience: one AppHost spins up multiple services, Dapr sidecars, a Node.js client, Redis, and Azurite—and you can watch the whole thing from the Aspire dashboard.
Repo: https://github.com/nikneem/aspire-dapr-chat-service-demo
And yes obviously, for a chat service this small, you would never create three separate backend services that run the application distributed, but hey, this is an Aspire demo so we need multiple services here. So for the sake of the demo, I ripped the app apart into multiple independently deployable distributed services.
A quick primer on .NET Aspire
.NET Aspire is a code-first application model and toolchain for composing, running, and observing distributed apps. At the center is the AppHost: a small C# project where you declare everything your app needs: projects, containers, scripts/executables, and cloud resources. Then let Aspire orchestrate startup order, service discovery, configuration, and diagnostics for local development.
How it works (technically):
You call DistributedApplication.CreateBuilder
, add resources (for example, AddProject
, AddContainer
, AddPostgres
), wire dependencies with WithReference
and WaitFor
, and then Build().Run()
to start the whole topology with one command. Aspire resolves connection strings and network addresses for you and injects them via configuration and environment variables so services “just know” how to talk to each other. Aspire ships NuGet integrations for common services. “Hosting” integrations represent the thing being run (a database container, Azurite, Redis, etc.), while “client” integrations configure consumers (e.g., add an HttpClient with service discovery enabled, health checks, and telemetry). Calling builder.AddServiceDefaults()
applies opinionated defaults like OpenTelemetry logging/tracing/metrics, health endpoints, and HttpClient configuration, so cross-cutting concerns are consistent without boilerplate. When the app runs, the Aspire dashboard launches to show resource status, logs, traces, metrics, and dependency graphs, and lets you manage resources from one place.
And yes you read that right, Aspire is opinionated, by default… But… The defaults that Aspire configures, are just
.cs
code files and therefore adjustable and customizable. It is generally encouraged to assess the defaults and change them to make them fit your needs.
Why developers like it? They can now spin up the entire system—APIs, front-end, sidecars, and emulators—without bespoke scripts. Everyone on the team runs the same composition resulting in fewer “works on my machine” problems. Logs, traces, and metrics are wired in by default and visible in a single UI.
Why Dapr is a game‑changer here
Dapr adds a lightweight sidecar next to each service that exposes common building blocks. For this chat service, pub/sub and state are used. Your code talks to the local sidecar using simple APIs while Dapr handles the provider specifics. So it adds a layer of abstraction between your code, and the underlying service for (in my case) a state store and pub/sub.
This allows me to easily run the service localhost with state and pub/sub components running in containers. And when I deploy the application, configure the same dapr components, but they now communicate with (for example) cloud services. This is all configuration, so you don’t change a single line of code to switch pub/sub from local Redis, to cloud enabled Azure Service Bus.
How the app works
There are four services configured in Aspire. One is a client app, which is a web application running in NodeJs, for the UX/UI. Then there are four backend API’s. A Members API for users to be able to register as a member of the chat. A Messages API that allows users to send messages to the server. A Realtime API that broadcasts messages to clients using SignalR. Yes again, this is completely over-engineered, but only for the sake of this demo.
A user registers in the Chat Client, the Members API persists the profile and publishes a join event, the Messages API accepts and stores chat messages and emits message events, and the Realtime API subscribes to those events to broadcast updates to connected browsers via SignalR. Dapr provides the event bus and state abstractions; Aspire orchestrates the services, sidecars, and emulators so everything starts together and can be observed from one dashboard.
The demo also includes background services to keep the system tidy and responsive:
- Member inactivity cleanup: periodically scans stored members and removes those inactive beyond a threshold (for example, 1 hour). When a member is removed, an event is published so the UI can update presence state accordingly.
- Message retention cleanup: purges messages older than the configured retention window (for example, 24 hours) to keep storage lean during local development. This can run on a simple timer and operate in small batches.
These jobs are designed to be idempotent and safe to rerun; in production, the same responsibilities can scale out as scheduled workers or durable jobs while the event contracts stay unchanged.
Some key features of the application
The Messages API just publishes to a topic; the Realtime API subscribes. They don’t depend on each other’s URLs, schemas, or client SDKs. Start with Redis locally, later switch to Azure Service Bus or another broker by changing Dapr component files instead of rewriting code. Persist conversation or member info through Dapr’s state API. Swap the backing store (Redis, database, cloud service) with configuration changes. Dapr adds timeouts, retries, circuit‑breaker policies, and emits traces/metrics that show up in the Aspire dashboard.
Where Aspire amplifies this is orchestration and discoverability. Aspire composes the whole environment—services, emulators/containers, and Dapr components. You can launch everything with one command and get consistent wiring via configuration. Service names become the contract (not hard‑coded URLs), the dashboard shows cross‑service flows, and changing environments (local vs. cloud) is largely a matter of swapping component/config files, not changing code. The result is fast local feedback with a clean path to production options.
Note that for the state store, I deliberately removed the key prefix. By default, Dapr adds a prefix ensuring to keep data within a single service. By removing the prefix, you can now use the state store as a cache mechanism to share data between services. So when a member enters the chat, the ID & Name combination is stored in cache with a sliding expiration so that the messages service can easily access the name of a chat member by his ID, without having the make a request to the members service.
Getting started
To get started, head to this GitHub repo and find the Prerequisites in the readme file. Make sure to install the required component before you continue. Then clone the repo:
git clone https://github.com/nikneem/aspire-dapr-chat-service-demo.git
There you go, that’s all there is to it. Now open the solution in Visual Studio or whatever your favorite developer IDE is and fire the thing up.
Wrap-up
The Aspire + Dapr chat app is a compact example of a local-first, cloud-ready architecture: Aspire orchestrates, Dapr de-couples, and SignalR delivers real-time UX. Clone the repo, run the AppHost, open a couple browser tabs, and start chatting—then iterate by adding a new service or swapping infrastructure with configuration-only changes.
Last modified on 2025-09-25