Remove a module
Remove a module from the AppointMe .NET 10 modular monolith. A dependency map, then the exact steps that took Booking out with a green build and tests, from projects to SPA routes, migrations and Hangfire jobs.
- You'll remove
- Booking, the least-coupled module
- Time
- About an hour
- Touches
- Solutions, API host, SPA, database, Hangfire
- Can't remove
- Identity and Organizations
Who depends on whom
Section titled “Who depends on whom”Booking is a leaf: no other module references it. CRM is next, because only Booking uses its contracts. Identity and Organizations hold up everything else.
This map comes from every <ProjectReference> and every cross-module using at the documented commit.
| Module | Its projects reference | Used by other modules | Used by the API host |
|---|---|---|---|
| Shared | Nothing | All of them | All of it |
| Organizations | Shared | Identity handles EmployeeInvited and EmployeeInvitationResent. CRM’s demo top-up job reads ICompanyRehydrationSource. Booking projects companies and employees. |
CurrentPrincipalResolver and UserPrincipalFactory build every principal. CompanyRegistered starts DemoSeedingSaga. |
| Identity | Shared, Organizations.Contracts | None | HttpIdentityFactory turns every sign-in into a UserIdentity through IUserIdentityRegistry |
| CRM | Shared, Organizations.Contracts | Booking handles the customer events and reads ICustomerRehydrationSource |
DemoSeedingSaga sends SeedDemoCustomersCommand |
| Booking | Shared, CRM.Contracts, Organizations.Contracts | None | DemoSeedingSaga waits for ServiceProviderProjected and AttendeeProjected, then sends SeedDemoAppointmentsCommand |
To remove CRM, remove Booking first, or rewrite Booking without attendees. Every step below was run on a fresh checkout. The result built with 0 Error(s), all 187 remaining tests passed, Wolverine’s codegen test passed, and the SPA passed npm run check and npm run build.
What you can’t remove
Section titled “What you can’t remove”Identity and Organizations are the platform the other modules run on. Remove either one and no request gets past authentication or a permission check.
| Module | What depends on it | What breaks without it |
|---|---|---|
| Identity | HttpIdentityFactory in the API host, the /login, /logout and /signup endpoints, and EmployeeInvitedHandler, which creates invited users in Keycloak or Entra |
The host’s HttpIdentityFactory stops compiling. Without a replacement, nothing turns a sign-in into a UserIdentity, so no caller gets past anonymous. |
| Organizations | AddAppointMeAuthorization() registers its CurrentPrincipalResolver. CRM, Booking and Identity reference its contracts. |
No principal, no companies to be a tenant of, no GET /api/v1/me or /me/access, no onboarding and no permissions screen |
Both are also referenced from inside the host, not only from Program.cs. To swap the identity provider instead, see Entra External ID. The modules themselves are on Identity and Organizations.
Revert the database first
Section titled “Revert the database first”Undo the module’s migrations while its project still exists, because dotnet ef needs the migration classes to know what to drop.
-
Drop the module’s tables. Locally, revert every Booking migration against the Aspire database:
Terminal window dotnet ef database update 0 --project src/Booking/AppointMe.Booking --startup-project src/AppointMe.Api --context BookingDbContext --connection "Server=localhost,60740;Database=AppointMe;User ID=sa;Password=Password1;TrustServerCertificate=True"For a shared or production database, write the same steps to a script and run it through your usual change process.
20260813125851_AddDashboardStatsIndexesis Booking’s last migration:Terminal window dotnet ef migrations script 20260813125851_AddDashboardStatsIndexes 0 --project src/Booking/AppointMe.Booking --startup-project src/AppointMe.Api --context BookingDbContext -o drop-booking.sqlEither way,
Appointments,Attendees,ServiceProvidersandBookingCompaniesare dropped, and thebookingschema and an empty history table stay behind. -
Drop what EF leaves. Then remove the stored overrides for the module’s permissions:
DROP TABLE [booking].[__EFMigrationsHistory];DROP SCHEMA [booking];DELETE FROM [organizations].[RolePermissionOverrides] WHERE [PermissionCode] LIKE 'appointments%';The
DELETEisn’t optional. When a user’s role has an override for a code that no module declares any more, building their principal throws:src/AppointMe.Shared/Authorization/Permissions/PermissionRegistry.csView source ↗ public Permission Get(string code) => _permissionsByCode.TryGetValue(code, out var permission)? permission: throw new InvalidOperationException($"Unknown permission '{code}'");
Remove the backend
Section titled “Remove the backend”Delete the three projects, take them out of both solution files and the host, and replace the one host class that orchestrates Booking.
-
Remove the projects from both solutions.
dotnet slnalso drops the emptyBookingsolution folder.Terminal window dotnet sln AppointMe.sln remove src/Booking/AppointMe.Booking.Contracts/AppointMe.Booking.Contracts.csproj src/Booking/AppointMe.Booking/AppointMe.Booking.csproj src/Booking/AppointMe.Booking.Tests/AppointMe.Booking.Tests.csprojdotnet sln AppointMe.slnx remove src/Booking/AppointMe.Booking.Contracts/AppointMe.Booking.Contracts.csproj src/Booking/AppointMe.Booking/AppointMe.Booking.csproj src/Booking/AppointMe.Booking.Tests/AppointMe.Booking.Tests.csprojgit rm -r src/BookingThe migrations live inside the module project, so they go with it.
-
Unreference it from the API. Delete the
AppointMe.Booking.csprojline fromsrc/AppointMe.Api/AppointMe.Api.csproj. The API references only the module project, not its contracts. -
Unregister it in
Program.cs. Deleteusing AppointMe.Booking.Configuration;and the last call in this chain, and move the semicolon up:src/AppointMe.Api/Program.csView source ↗ builder.Services.AddSharedModule(builder.Configuration).AddIdentityModule(builder.Configuration).AddOrganizationsModule().AddCrmModule().AddBookingModule(); -
Replace
DemoSeedingSaga. It’s the only other host code that breaks, with 7 compile errors. The saga seeds customers only after Booking projects the company’s first service provider, and seeds appointments as attendees appear:src/AppointMe.Api/Demo/DemoSeedingSaga.csView source ↗ public object? Handle([SagaIdentityFrom(nameof(ServiceProviderProjected.CompanyId))]ServiceProviderProjected @event){if (CustomerSeedingTriggered){return null;}CustomerSeedingTriggered = true;return new SeedDemoCustomersCommand(CustomersExpected).WithCompany(@event.CompanyId);}Without Booking, nothing sends
ServiceProviderProjected, so demo companies would get no customers. Delete the saga and seed customers straight fromCompanyRegistered:src/AppointMe.Api/Demo/DemoSeedingHandler.cs using AppointMe.Crm.Customers.SeedDemoCustomers;using AppointMe.Organizations.Contracts.Companies.Events;namespace AppointMe.Api.Demo;public static class DemoSeedingHandler{private const int CustomerCount = 150;public static object? Handle(CompanyRegistered @event, IConfiguration configuration){if (!configuration.GetValue<bool>("Demo:Enabled")){return null;}return new SeedDemoCustomersCommand(CustomerCount).WithCompany(@event.CompanyId);}}WithCompanyputs the company on the cascaded command, soSeedDemoCustomersCommandHandlergets itsCompanyId. -
Build, test and check the handlers.
codegen previewshould listDemoSeedingHandlerand nothing fromAppointMe.Booking.Terminal window dotnet build AppointMe.slndotnet test AppointMe.slndotnet build AppointMe.sln -c Releasedotnet run --project src/AppointMe.Api/AppointMe.Api.csproj -c Release --no-build --no-launch-profile -- codegen test
Nothing else in the backend changes. Aspire, compose.yaml and infra/ have nothing per module, and CRM still publishes its customer events: with no handler left, nothing consumes them. README.md and CLAUDE.md still describe Booking, so edit them to match.
Remove the frontend
Section titled “Remove the frontend”Regenerate the client first. The type checker then lists every file that used the module, and you delete or edit each one.
-
Regenerate and check. From
src/AppointMe.Frontend:Terminal window npm run generate:apinpm run checkcheckfails withTS2305(has no exported member 'AppointmentDto','useGetAppointments','TrendBucketDto', …) andTS2322(Type '"appointments:schedule"' is not assignable to type 'Permission[] | Permission') in the files below. -
Delete the module’s pages.
src/app/appointments/is all Booking. The dashboard insrc/app/dashboard/is built on Booking’s statistics endpoints, so it goes too:Terminal window git rm -r src/app/appointments src/app/dashboardThe dashboard’s new-customers chart reads CRM. Deleting the whole dashboard is what was verified here. To keep that chart, rebuild it on
useGetNewCustomerStatsalone. -
Fix the router. Remove the two imports and the two routes, and point the index route somewhere that still exists:
src/AppointMe.Frontend/src/app/router/router.tsxView source ↗ {index: true,element: <Navigate to='/appointments' replace />,},{path: 'dashboard',element: <Dashboard />,handle: { breadcrumb: 'Dashboard', navId: 'dashboard' },},{path: 'appointments',element: <Appointments />,handle: { breadcrumb: 'Appointments', navId: 'appointments' },},This run sent the index route to
/customers. -
Fix the sidebar and the labels. In
src/app/layouts/sidebar/nav-data.ts, delete theAppointmentsandDashboardentries and the unusedCalendarIconandChartPieIconimports. Insrc/app/settings/permissions/permission-labels.ts, delete the twoappointmentsresource labels and the fiveappointmentspermission labels. -
Check again. Both pass after the edits above:
Terminal window npm run checknpm run buildThe role descriptions in
src/app/team/roles.tsstill mention appointments. They’re plain text, so edit them to suit what’s left.
After you deploy
Section titled “After you deploy”Hangfire still holds Booking’s recurring jobs, and nothing in the code removes them. Delete them once the new version is running.
| Job ID | Schedule | Registered when |
|---|---|---|
booking:company-reconciliation |
Hourly, on the hour | Always |
booking:service-provider-reconciliation |
Hourly, on the hour | Always |
booking:attendee-reconciliation |
Daily, 03:00 UTC | Always |
booking:top-up-demo-appointments |
Daily, 03:00 UTC | Demo:Enabled is true |
When one comes due, the scheduler logs Recurring job 'booking:company-reconciliation' can't be scheduled due to an error and will be retried in 00:00:15. with Could not resolve assembly 'AppointMe.Booking', and tries again 15 seconds later. Hangfire 1.8 gives up after five retries: it logs … can't be scheduled due to an error and will be disabled. and stops scheduling the job, but the job stays in its storage. Sign in as a super admin, open the Hangfire dashboard at /admin/jobs, and delete them under Recurring Jobs. In code, IRecurringJobManager.RemoveIfExists("booking:company-reconciliation") does the same for one job.
Verify it works
Section titled “Verify it works”The same checks CI runs, plus a database check.
| Check | Command | Expect |
|---|---|---|
| Solution builds | dotnet build AppointMe.sln |
0 Error(s) |
| Tests pass | dotnet test AppointMe.sln |
5 test projects pass: Shared 118, Identity 9, Organizations 28, CRM 7, Api 25 |
| Handlers wire | codegen test, as above |
Success! |
| SPA is clean | npm run check and npm run build |
No type errors, and ✓ built |
| No Booking left in the database | SELECT name FROM sys.schemas WHERE name = 'booking' |
No rows |
| No stale overrides | SELECT COUNT(*) FROM [organizations].[RolePermissionOverrides] WHERE [PermissionCode] LIKE 'appointments%' |
0 |
Troubleshooting
Section titled “Troubleshooting”Each symptom points at one step you skipped.
| Symptom | Cause | Fix |
|---|---|---|
CS0234: The type or namespace name 'Booking' does not exist in DemoSeedingSaga.cs |
The saga still uses Booking’s events and commands | Replace it with the handler above |
500 with Unknown permission 'appointments:view' in the log, for some users only |
Stored overrides still name Booking’s permissions | Run the DELETE on RolePermissionOverrides |
Recurring job 'booking:…' can't be scheduled due to an error in the log |
Hangfire still has Booking’s recurring jobs | Delete them on /admin/jobs |
dotnet ef database update 0 can’t find the project or BookingDbContext |
The project, or the host’s reference and AddBookingModule() call, is already deleted |
Run the revert from a checkout of the commit before the removal, then come back |
booking tables are still there after a deploy |
Nothing drops a deleted module’s schema | Run the revert script and the DROP statements |
| New demo companies get no customers | DemoSeedingSaga was deleted and not replaced |
Add DemoSeedingHandler |
Why is removing a module mostly deleting a folder?2 min · optional
Other modules reach a module only through its contracts project, and the host through its Add*Module() call and project reference. So once the projects are gone, the compiler shows every remaining use. For Booking there were two, both in the host: Program.cs and DemoSeedingSaga.
What the compiler can’t see is state outside the code: tables, permission overrides and Hangfire job definitions keep the module’s names after the code is gone. That’s why the database and Hangfire steps are the ones most worth reading twice.