Tour of the codebase
Where everything lives in the AppointMe .NET 10 solution. The API host, four module projects with their contracts and tests, the Aspire AppHost, the React SPA, and the files that pin versions.
- Solution
AppointMe.sln, also asAppointMe.slnx- Projects
- 17 .NET projects, plus the SPA
- Per module
.Contracts, the module,.Tests- Conventions
- Written down in
CLAUDE.md
The solution at a glance
Section titled “The solution at a glance”One API host loads four modules. Everything else in the repo runs, tests, ships or packages that host.
Directory.github/ # CI, CodeQL, gitleaks, Dependabot
- …
Directory.template.config/ # the
dotnet newrename rules- …
Directorydocker/keycloak/ # dev-cert export for Compose
- …
Directorydocs/ # the identity-resolution deep dive
- …
Directoryinfra/ # Bicep for Azure, a Cloudflare Worker
- …
Directorysrc/
DirectoryAppointMe.Api/ # the host: Program.cs and cross-cutting code
- …
DirectoryAppointMe.Api.Tests/
- …
DirectoryAppointMe.Aspire/ # the AppHost and the Keycloak realm
- …
DirectoryAppointMe.Frontend/ # the React 19 SPA
- …
DirectoryAppointMe.Shared/ # the shared kernel
- …
DirectoryAppointMe.Shared.Tests/
- …
DirectoryBooking/ # appointments, attendees, staff, dashboard
- …
DirectoryCRM/ # customers
- …
DirectoryIdentity/ # users, sign-up, login
- …
DirectoryOrganizations/ # companies, employees, invitations, roles
- …
Directorytemplates/ # NuGet packaging and the smoke test
- …
- AppointMe.sln
- CLAUDE.md # architecture and conventions
- compose.yaml # the backing services without Aspire
- Directory.Build.props
- Directory.Packages.props
- global.json
- NuGet.config
| Project | Holds |
|---|---|
AppointMe.Api |
Program.cs, which registers the modules, plus the host’s cross-cutting folders: Authentication, Authorization, MultiTenancy, ErrorHandling, Wolverine, Hangfire, OpenApi, Demo and more |
AppointMe.Aspire |
The AppHost: containers, ports, the realm import (appointme-realm.json) and the dev-certificate check |
AppointMe.Shared |
Types every module uses: CompanyId and ICurrentCompany, Permission, AggregateRoot, value objects, IEndpoint, the migration runner and the job abstractions |
Identity |
Users, sign-up, login and logout, and the Keycloak and Entra identity providers |
Organizations |
Companies, onboarding, employees, invitations, role permission overrides and GET /me |
CRM |
Customers |
Booking |
Appointments, attendees, service providers and the dashboard queries |
AppointMe.Frontend isn’t in the solution. It’s an npm project that the AppHost starts with AddViteApp.
Anatomy of a module
Section titled “Anatomy of a module”Every module is three projects: a public .Contracts project, the module itself, and its .Tests.
| Project | Holds | Referenced by |
|---|---|---|
AppointMe.Crm.Contracts |
Integration events such as CustomerRegisteredEvent, CustomerSnapshot, and interfaces other modules call, such as ICustomerRehydrationSource |
Other modules. Booking references it. |
AppointMe.Crm |
Everything else: aggregates, slices, the DbContext, migrations, permissions |
The API host and AppointMe.Crm.Tests only |
AppointMe.Crm.Tests |
xUnit.net v3 tests, in folders that mirror the slices | — |
Inside the module, CRM is the smallest complete example:
Directorysrc/CRM/AppointMe.Crm/
DirectoryAuthorization/
- CrmDefaultGrantPolicy.cs # which roles get which permissions
DirectoryConfiguration/
- CrmModule.cs #
AddCrmModule()
- CrmModule.cs #
DirectoryCustomers/ # one feature folder
- Customer.cs # the aggregate root
- CustomerId.cs
- CustomerPermissions.cs
DirectoryDatabase/ # EF config, query filters, Dapper repository
- …
DirectoryRegisterCustomer/ # one slice per use case
- RegisterCustomer.cs # the domain factory
- RegisterCustomerCommand.cs
- RegisterCustomerCommandHandler.cs
- RegisterCustomerEndpoint.cs
- RegisterCustomerRequest.cs
- RegisterCustomerResponse.cs
DirectoryGetCustomers/
- …
- …
DirectoryDatabase/
- CrmDbContext.cs # schema
crm DirectoryMigrations/
- …
- CrmDbContext.cs # schema
- ModuleAssembly.cs
| Folder | What goes there |
|---|---|
Authorization/ |
The module’s IDefaultGrantPolicy: the permissions each role starts with |
Configuration/ |
One Add<Module>Module() extension that registers everything the module owns |
Database/ (module level) |
The DbContext, with its own schema and its own __EFMigrationsHistory table, and Migrations/ |
<Feature>/ |
The aggregate, its ID type, its Permissions class, and anything several slices share |
<Feature>/Database/ |
EF type configurations, named query filters, and the Dapper repository and queries |
<Feature>/<Slice>/ |
One use case: command or query, handler, endpoint, request and response |
AddCrmModule() shows the whole registration recipe. Each module repeats it with its own types:
return services .AddDbContext<CrmDbContext>((serviceProvider, options) => { options.UseSqlServer(serviceProvider.GetRequiredService<ConnectionStrings>().AppointMeSql, builder => { builder.MigrationsHistoryTable("__EFMigrationsHistory", CrmDbContext.DefaultSchema); }); }) .AddDatabaseMigration<CrmDbContext>() .AddEndpoints(ModuleAssembly.Instance) .AddSingleton<IDbConnectionFactory, SqlConnectionFactory>(serviceProvider => new SqlConnectionFactory(serviceProvider.GetRequiredService<ConnectionStrings>().AppointMeSql)) .AddScoped<CustomersRepository>() .AddScoped<ICustomerRehydrationSource, CustomerRehydrationSource>() .AddScoped<SeedDemoCustomers>() .AddScoped<TopUpDemoCustomersJob>() .AddPermissions(ModuleAssembly.Instance) .AddRecurringJobs(ModuleAssembly.Instance);Endpoints, permissions and recurring jobs are found by scanning the module’s assembly, so a new endpoint or permission needs no registration line. Vertical slices explains the slice rules, and Add a module builds a fifth one.
I want to change X: look in Y
Section titled “I want to change X: look in Y”Most changes touch one slice folder. The rest have one obvious home, listed here.
| I want to change | Look in |
|---|---|
| An endpoint’s route, verb or response | <Module>/<Feature>/<Slice>/<Slice>Endpoint.cs, an IEndpoint |
| What a permission is called | <Feature>/<Entity>Permissions.cs, for example Customers/CustomerPermissions.cs |
| Which roles get a permission by default | <Module>/Authorization/<Module>DefaultGrantPolicy.cs |
| A table, column or index | <Feature>/Database/<Entity>TypeConfiguration.cs, then add a migration |
| A tenant or soft-delete filter | <Module>/Database/<Module>DbContext.cs, in OnModelCreating |
| A read query | <Feature>/Database/: the Dapper repository and its <Entity>Queries.cs |
| An event other modules consume | <Module>.Contracts/<Feature>/Events/ |
| How an exception maps to an HTTP status | src/AppointMe.Api/ErrorHandling/ |
| The tenant header | src/AppointMe.Api/Program.cs, AddAppointMeMultiTenancy(...) |
| Wolverine’s transport or handler policies | src/AppointMe.Api/Wolverine/WolverineHostBuilderExtensions.cs |
| Local containers, ports or images | src/AppointMe.Aspire/Program.cs, and compose.yaml to match |
| Keycloak clients, users or email settings | src/AppointMe.Aspire/appointme-realm.json |
| Local connection strings, Keycloak URLs or demo mode | src/AppointMe.Api/appsettings.Development.json |
| A frontend route | src/AppointMe.Frontend/src/app/router/router.tsx |
| Lint or format rules | The lint and fmt blocks of src/AppointMe.Frontend/vite.config.ts |
| A package version | Directory.Packages.props |
Build files at the root
Section titled “Build files at the root”Four root files set the SDK, the language, every package version and the package source for all 17 projects.
| File | What it sets |
|---|---|
Directory.Build.props |
net10.0, C# 14, nullable and implicit usings for every project, plus the Company, Authors and RepositoryUrl metadata |
Directory.Packages.props |
Central package management with transitive pinning. Every PackageVersion lives here, so a .csproj has PackageReference items with no version. |
global.json |
SDK 10.0.0, rollForward: latestMajor, allowPrerelease: false |
NuGet.config |
Clears all sources and maps every package (*) to nuget.org. Add a private feed here, with its own mapping. |
<PropertyGroup> <Company>BravoDev</Company> <Authors>BravoDev</Authors> <RepositoryUrl>https://github.com/bravodev-hub/appointme.git</RepositoryUrl> <RepositoryType>Git</RepositoryType> <TargetFramework>net10.0</TargetFramework> <LangVersion>14</LangVersion> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <WarningsAsErrors>false</WarningsAsErrors> <NoWarn>$(NoWarn);NU1608</NoWarn></PropertyGroup>One version lives outside these files. src/AppointMe.Aspire/AppointMe.Aspire.csproj names the Aspire.AppHost.Sdk version (13.6.4), and it has to move by hand with the Aspire.* packages. Directory.Packages.props also holds one security pin, System.Security.Cryptography.Pkcs, with a comment on when to drop it.
Beyond src/
Section titled “Beyond src/”The frontend, the infrastructure, the Docker helpers, the template packaging and CI each have one folder.
| Folder | What’s there | Read more |
|---|---|---|
src/AppointMe.Frontend/ |
The SPA: pages in src/app, the generated client in src/api, the axios mutator in src/lib/axios.ts, vite.config.ts and orval.config.ts |
Frontend overview |
infra/ |
main.bicep with eleven modules in modules/, main.bicepparam, and cloudflare-worker/ |
Azure with Bicep |
docker/keycloak/ |
export-dev-cert.sh and a certs/ folder for its git-ignored output |
Local services & ports |
templates/ and .template.config/ |
The package project, the nuget.org README, the generated project’s README in overlay/, smoke-test.sh, and template.json with its rename design in templates/README.md |
Rename your project |
.github/ |
devtest.yml (build, test, deploy on v*), codeql.yml, secret-scan.yml (gitleaks), template.yml, dependabot.yml |
GitHub Actions CI/CD |
docs/ |
identity-resolution.md, a two-part write-up on identity and principal |
Identity & principal |
.claude/ and CLAUDE.md |
Conventions for AI coding agents, and the regenerate-api and verify skills |
Working with AI agents |
Why a separate .Contracts project for every module?1 min · optional
A module may reference another module’s .Contracts project, never the module itself. Booking references AppointMe.Crm.Contracts and AppointMe.Organizations.Contracts. Only the API host and each module’s own tests reference a module project.
So the compiler stops Booking from reaching into CRM’s DbContext or handlers. What crosses the boundary is events, snapshots and a few interfaces, all in a project small enough to review.
The cost is an extra project per module. Modular monolith covers the rest of the boundary rules.