| Clean Architecture Toolkit for .NET | |||||
|
Clean Architecture Toolkit is a clean, modular architecture foundation for .NET applications. It embraces SOLID principles and focuses on maintainability, testability, and scalability through clear boundaries and pluggable design. |
|
||||
|
| HOME | MODULE OVERVIEW | DEMO | |
|||||
Aspire plan
This plan covers the first Aspire AppHost of the demo.
It is part of Demo.md. The demo plan says what Aspire hosts. This plan says how we build it, in which order.
Contents
- The goal
- Decisions
- Starting point
- Projects
- How the AppHost works
- Swapping the price provider
- Phases
- Later
- Open questions
The goal
One command starts the whole demo, and the Aspire dashboard shows it.
- Catalog and Orders run as projects.
- They get their connection strings and service URLs from the AppHost.
- Logs, traces, metrics and health checks are in the dashboard.
- Orders asks Catalog for the current price of every book, instead of using the fake.
React, the gateway, Keycloak, the Service Bus and Inventory are not part of this build. See Later.
Decisions
| Topic | Decision |
|---|---|
| Names | Cat.Demo.AppHost and Cat.Demo.ServiceDefaults, each in its own folder in demo/. |
| SQL Server | Two modes, switched by one AppHost setting: Container and Existing. See How the AppHost works. |
| First mode to build | Existing, because Docker and WSL were not installed on the owner’s machine at the start. Container followed in its own phase. |
| Standalone start | Every service still starts with dotnet run and its own appsettings. The AppHost is a convenience, not a requirement. |
| Price provider | The first version is a plain HttpClient implementation in Orders.Infrastructure. Cat.Api.Client replaces it in the future. See Swapping the price provider. |
| Service to service calls | Orders calls Catalog directly for now, because the gateway does not exist yet. |
| Contracts | Orders keeps its own small model of the price response and does not reference Catalog.Contracts. This is a choice, not a rule: Orders needs only the price, so it stays independent of the rest of the Catalog contract. A later service to service call shares contracts, so the demo shows both. See Sharing contracts. |
| Aspire version | The latest stable Aspire that supports .NET 10. Pin it in Directory.Packages.props when the projects are created. |
Starting point
- Catalog and Orders are code first and apply their EF Core migrations at startup in
Development. - Both read their database from the connection strings
catalogandorders. These are the names the AppHost will use as well, so the services do not change for it. - Both have an
IsDevelopmentseeder and fixed launch profile ports: Catalog 5101, Orders 5102. - The
.httpfiles use those ports. - Orders registers
FakePriceProviderinOrders.Infrastructure. - Docker Desktop and WSL2 are installed on the owner’s machine, so both modes run here. WSL2 is limited by
.wslconfigto 8 GB, 4 processors and 2 GB swap.
Projects
Follow the demo-structure skill. It also keeps CleanArchitectureToolkit.slnx in sync.
demo/
Cat.Demo.AppHost/
Cat.Demo.AppHost/
Cat.Demo.ServiceDefaults/
Cat.Demo.ServiceDefaults/
- ServiceDefaults. The standard Aspire project: telemetry, health checks, service discovery and the resilience handler for
HttpClient. - AppHost. Describes the system. It references the Api project of each service.
- Service changes. Each Api project references ServiceDefaults and calls
AddServiceDefaults()andMapDefaultEndpoints(). Nothing else in the services changes. - Modules. No module may reference Aspire or ServiceDefaults. Only the demo does.
How the AppHost works
SQL Server modes
A setting in the AppHost configuration, Sql:Mode, picks one of two modes.
| Mode | What happens | Needs |
|---|---|---|
Existing |
The AppHost passes the connection strings catalog and orders from its own configuration to the services. |
A SQL Server the services can reach, such as the local instance they use today. |
Container |
The AppHost starts a SQL Server container with a data volume, creates the databases catalog and orders and passes their connection strings. |
Docker. |
- In
Existingmode the AppHost readsConnectionStrings:catalogandConnectionStrings:orders. The defaults are the strings that sit in theappsettings.Development.jsonof the services. - The default mode is
Container, because a clone should need one command. A developer without Docker setsSql:ModetoExistingin the user secrets of the AppHost. - The services see no difference. They always read
GetConnectionString("catalog")andGetConnectionString("orders"). - In
Containermode the AppHost limits the memory of SQL Server withMSSQL_MEMORY_LIMIT_MB, as described in the demo plan. Decided: 2048. An idle container uses about 620 MiB. - Decided: the container uses a named data volume,
cat-demo-sql-data, and the default lifetime, so it stops with the AppHost. Aspire generates thesapassword and keeps it in the user secrets. - The database resources are called
catalog-dbandorders-db, because a resource name must differ from the project namescatalogandorders. The databases themselves and the connection names staycatalogandorders. - Reset is
docker volume rm cat-demo-sql-datawith the AppHost stopped.
Start order
- In
Containermode the services wait for their database. The AppHost makes that explicit. - Each service applies its migrations and seeds in
Development, as it does today. The databases exist by then. - Orders waits for Catalog, because it asks Catalog for prices.
Ports
Decided: the AppHost keeps the fixed ports 5101 and 5102, with WithHttpEndpoint(port: ...) and no Aspire proxy. The .http files, the Orders to Catalog address and standalone runs all use the same ports. The dashboard runs on 15101 (OTLP on 19101 and the resource service on 20101), set in the launch profile of the AppHost.
Swapping the price provider
IPriceProvider stays as it is. Only the class behind it changes.
Phase 1: plain HttpClient. This is the version we build now.
- A new class
CatalogPriceProviderinOrders.Infrastructure, using a typedHttpClient. - The base address comes from Aspire service discovery. The AppHost gives Orders a reference to Catalog.
- It calls
GET /catalog/books/prices?bookIds=<id>with onebookIdsvalue per book. - It reads the response into a small model that belongs to Orders. Orders does not reference
Catalog.Contracts. That is a choice for this call, see Sharing contracts. - Responses map as follows:
| Catalog answers | IPriceProvider returns |
|---|---|
| 200 | An Ok response with the price of each book. |
| 404 | A failed response. The step already turns that into PriceUnavailable. |
| 400 or any other status | An error response. Orders never trusts a half answer. |
| Network error or timeout | An error response. |
- The resilience handler from ServiceDefaults adds retries and a circuit breaker to the call.
- Catalog reads are public, so no token is sent. Once Keycloak arrives, the call forwards the token.
- Registration picks the real provider only when the AppHost gives Orders a Catalog address. Without it,
dotnet runkeeps working withFakePriceProvider. - A unit test per row of the table above, with a fake
HttpMessageHandler. These tests need no database and no running service. - The
IPriceProvidersummary and the demo plan still say thatApi.Clientimplements it. Adjust both while building.
In the future: Cat.Api.Client.
- When the
Cat.Api.Clientmodule exists,CatalogPriceProvideris replaced by a typed client from the module, and the call goes through the gateway. - Build the module from this working code, not up front. Then the design follows what we learned: how a client is declared, how the token is forwarded, how error responses map to
ICatResponseand how it works withCat.Resilience. - Only the registration in
Orders.Infrastructurechanges.IPriceProvider, the workflow step and the tests stay.
Phases
- ServiceDefaults. Create the project. Both Api projects call it. Build and run both services standalone to check that nothing broke.
- AppHost,
Existingmode. (Built. The.httprequests pass and the traces show in the dashboard.) Create the AppHost with Catalog and Orders. Both show up in the dashboard with logs, traces and health. Run every request of both.httpfiles against the AppHost. - Price provider. (Built. A
POST /orderstrace shows the call to Catalog and theGET /catalog/books/pricesspan.) AddCatalogPriceProviderand its tests. Place an order through the AppHost and check in the dashboard that the Catalog call is part of the trace. - AppHost,
Containermode. (Built. A fresh start from an empty volume gives empty orders and seeded books. A restart keeps a placed order.) Needs Docker on the machine. Add the SQL Server container, the data volume and the databases. Check a fresh start from an empty volume and a restart that keeps the data. - Documents. (Built.
demo/README.mdhas the run steps for both modes, the Docker memory notes and the AppHost chapter. The drawing shows the dashboard box as built.) Updatedemo/README.mdwith the run steps for both modes and the Docker memory notes. Update Demo.md.
Phases 1 to 3 do not need Docker. Phase 4 does.
Later
These come after the first AppHost build, in the order of the demo plan.
- The
Cat.Api.Clientmodule, replacingCatalogPriceProvider. Cat.Demo.PaymentService, a separate fake payment service that the AppHost starts and that can fail on purpose. See the payment provider decision in Demo.md.- The API gateway, Keycloak, JWT validation in the services and the real
ICurrentUser. - The React client, started by the AppHost.
- The Service Bus emulator, with
Cat.Messaging. - Inventory, database first.
Open questions
- Tracing sources. Settled in phase 1. ServiceDefaults adds the wildcard source
Cat.*, which coversCat.ServiceRequestsandCat.FluentWorkFlows. Check in phase 2 that the activities show up in the dashboard. - Phase 2 check. Done for the requests: every request of both
.httpfiles gives the expected status under the AppHost, and both health checks answer. Seen in the dashboard: traces with theCat.ServiceRequestsandCat.FluentWorkFlowssources. - ASPIRE010 warning. Settled. The AppHost builds with
AspireUseCliBundlefalse, the SDK default, so the dashboard and DCP come from NuGet anddotnet runneeds no extra tool. The warning is suppressed in the AppHost project. Setting it to true would need the Aspire CLI on every machine. - Dashboard login. Settled. The
httplaunch profile of the AppHost setsASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS, so the dashboard opens without a login token. The token changes on every start, which made scripted checks (Playwright) impossible. Anyone on the machine can open the dashboard on localhost, which is fine for a local demo. - Development certificate. The dashboard shows “No trusted development certificate was found” when the machine has not run
dotnet dev-certs https --trust. It is harmless now, because the AppHost, the dashboard and the services all run on HTTP. It matters once HTTPS is used, for example with the gateway or Keycloak. Decide whether the run steps indemo/README.md(phase 5) tell a developer to trust the certificate, and whether the AppHost keepsASPIRE_ALLOW_UNSECURED_TRANSPORT. - Existing mode and Trusted_Connection. The default strings use Windows authentication. That works for a local process but not for a container. Fine for
Existingmode, since nothing runs in a container there.