Skip to content
StarLive demo →
OverviewChecked against main · 31 Oct 20267 min read

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 as AppointMe.slnx
Projects
17 .NET projects, plus the SPA
Per module
.Contracts, the module, .Tests
Conventions
Written down in CLAUDE.md

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 new rename 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.

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()
    • 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/
        • …
    • 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:

src/CRM/AppointMe.Crm/Configuration/CrmModule.csView source ↗
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.

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

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.
Directory.Build.propsView source ↗
<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.

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.