Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ dotnet run --project template/SimpleModule.Host # runs on https://localhost:
```bash
npm install # install all workspace dependencies
npm run dev # start development (dotnet + all module watches, unminified JS)
npm run dev:build # build all modules in dev mode (unminified, with source maps)
npm run build:dev # build all modules in dev mode (unminified, with source maps)
npm run build # production build (minified, optimized)
npm run check # biome lint + format check
npm run check:fix # auto-fix lint + formatting
Expand Down Expand Up @@ -146,7 +146,7 @@ See [docs/CONSTITUTION.md](docs/CONSTITUTION.md) for the authoritative reference
- Module boundaries, dependencies, and data ownership
- Communication patterns (contracts and events)
- Endpoint, frontend, and authorization rules
- Compiler-enforced diagnostics (SM0001-SM0054)
- Compiler-enforced diagnostics (SM0001-SM0061)
- Framework contributor guidelines

## Key Constraints
Expand All @@ -169,8 +169,8 @@ When you add a new `IViewEndpoint`, you **must** register it in your module's `P
**Why:** The C# source generator discovers your new endpoint and validates it's properly decorated, but React needs a corresponding entry in the page registry. If you forget:

- The endpoint compiles and runs fine on the server
- Navigating to that page in React silently 404s client-side (no error in console, no error response shown to user)
- The developer won't know for hours or until QA finds it
- Navigating to that page throws a descriptive client-side error — the page resolver names the missing page and lists the available ones, the ClientApp shows an error toast, and the error is logged to the browser console
- `npm run validate-pages` catches the mismatch at build time (and in CI) before it reaches the browser

**Pattern:**

Expand Down
54 changes: 29 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,30 @@ This starts the .NET backend and Vite watchers for all modules and the ClientApp
SimpleModule.AppHost # .NET Aspire orchestration (app + PostgreSQL + pgAdmin)
SimpleModule.ServiceDefaults # Aspire service defaults (OpenTelemetry, health checks)
framework/
SimpleModule.Core # IModule, IEndpoint, IViewEndpoint, [Dto], [Module], IEventBus
SimpleModule.Core # IModule, IEndpoint, IViewEndpoint, [Dto], [Module], events, menus
SimpleModule.Generator # Roslyn IIncrementalGenerator (netstandard2.0)
SimpleModule.Database # Multi-provider DB (SQLite, PostgreSQL, SQL Server)
SimpleModule.Hosting # ASP.NET host builder extensions, Inertia page rendering
SimpleModule.DevTools # Developer tooling and diagnostics
SimpleModule.Storage # File storage abstraction with Local, Azure Blob, and S3 providers
SimpleModule.Storage(.Local/.Azure/.S3) # File storage abstraction + provider implementations
SimpleModule.Testing # Shared testing utilities for module test projects
modules/
Admin, Agents, AuditLogs, BackgroundJobs, Dashboard, Email,
FeatureFlags, FileStorage, Localization, Marketplace, Orders,
PageBuilder, Products, Rag, RateLimiting, Settings, Tenants, Users
Admin, AuditLogs, BackgroundJobs, Dashboard, Email, FeatureFlags,
FileStorage, Identity, Keycloak, Localization, Notifications,
RateLimiting, Settings, Tenants, Users
OpenIddict # OpenID Connect / OAuth 2.0 via OpenIddict
Permissions # RBAC and access control
packages/
SimpleModule.Client # Vite plugin + page resolution for module frontends
SimpleModule.UI # Radix UI component library with Tailwind
SimpleModule.Theme.Default # Tailwind CSS base theme
SimpleModule.Echo # Real-time client for the broadcasting hub (wraps SignalR)
SimpleModule.TsConfig # Shared TypeScript configuration
template/
SimpleModule.Host # Host app (net10.0) — wires modules via generated code
cli/
SimpleModule.Cli # `sm` CLI tool for scaffolding and validation
tools/
SimpleModule.DevTools # Developer tooling and diagnostics
tests/ # Framework tests, shared test infrastructure, and e2e tests
scripts/ # Build/dev orchestrators, type extraction, component scaffolding
```
Expand All @@ -83,41 +87,41 @@ scripts/ # Build/dev orchestrators, type extraction, compo

```
Browser request
→ ASP.NET route handler calls Inertia.Render("Products/Browse", { products })
→ ASP.NET route handler calls Inertia.Render("Tenants/Browse", { tenants })
→ Inertia middleware renders static HTML shell with JSON props
→ Client loads React, dynamically imports module's Products.pages.js bundle
→ Client loads React, dynamically imports module's Tenants.pages.js bundle
→ React component hydrates with server-provided props
```

Subsequent navigations are XHR — Inertia fetches JSON props and swaps the React component client-side without a full page reload.

### Module Anatomy

Each module follows the same structure. Using Products as an example:
Each module follows the same structure. Using the Tenants module as an example:

```
modules/Products/
modules/Tenants/
src/
Products.Contracts/ # Public interface + DTOs (what other modules depend on)
IProductContracts.cs # Service interface
Product.cs # [Dto] — generates TypeScript types
ProductId.cs # Strongly-typed ID
CreateProductRequest.cs # Request DTO
Products/ # Implementation (never referenced by other modules)
ProductsModule.cs # [Module("Products")] + IModule — DI, menus, permissions
ProductsDbContext.cs # Module-scoped EF Core context
ProductService.cs # Implements IProductContracts
Endpoints/Products/ # IEndpoint classes (auto-discovered, auto-mapped)
Views/ # React .tsx pages
Pages/index.ts # Page registry — maps route names to lazy component imports
SimpleModule.Tenants.Contracts/ # Public interface + DTOs (what other modules depend on)
ITenantContracts.cs # Service interface
Tenant.cs # [Dto] — generates TypeScript types
TenantId.cs # Strongly-typed ID
CreateTenantRequest.cs # Request DTO
SimpleModule.Tenants/ # Implementation (never referenced by other modules)
TenantsModule.cs # [Module("Tenants")] + IModule — DI, menus, permissions
TenantsDbContext.cs # Module-scoped EF Core context
TenantService.cs # Implements ITenantContracts
Endpoints/Tenants/ # IEndpoint classes (auto-discovered, auto-mapped)
Pages/ # IViewEndpoint classes + co-located React .tsx pages
Pages/index.ts # Page registry — maps route names to lazy component imports
tests/
Products.Tests/ # xUnit tests for this module
SimpleModule.Tenants.Tests/ # xUnit tests for this module
```

### Module Communication

- **Contracts** — Modules expose a `.Contracts` project with a public interface (e.g., `IProductContracts`) and `[Dto]` types. Other modules depend on contract interfaces, never on implementation projects.
- **Event bus** — `IEventBus.PublishAsync<T>()` broadcasts to all `IEventHandler<T>` implementations. Handlers execute sequentially; failures are isolated and collected into an `AggregateException`.
- **Contracts** — Modules expose a `.Contracts` project with a public interface (e.g., `ITenantContracts`) and `[Dto]` types. Other modules depend on contract interfaces, never on implementation projects.
- **Event bus** — Modules publish domain events via Wolverine's `IMessageBus.PublishAsync<T>()`. Handlers are discovered by naming convention (a class ending in `Handler`/`Consumer` with a `Handle`/`Consume` method) and dispatched in-process through a durable inbox/outbox — no manual registration needed.

### Database

Expand Down
4 changes: 2 additions & 2 deletions docs/CONSTITUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ All hooks are optional. All have default no-op implementations.
7. **ConfigureFeatureFlags** -- register feature flag definitions
8. **ConfigureAgents** -- register AI agent definitions
9. **ConfigureRateLimits** -- register rate limit policies
10. **ConfigureHost** -- configure host-level integrations (e.g., TickerQ, database initialization) after the host is built
10. **ConfigureHost** -- configure host-level integrations (e.g., database initialization and schema creation) after the host is built
11. **OnStartAsync** -- one-time initialization after all services are registered
12. **OnStopAsync** -- graceful shutdown cleanup
13. **CheckHealthAsync** -- report module health status (Healthy, Degraded, Unhealthy)
Expand Down Expand Up @@ -316,7 +316,7 @@ public sealed class CreateProductRequest : FormRequest<CreateProductRequest>

- Every `IViewEndpoint` must have a matching entry in `Pages/index.ts`.
- The key must match the component name passed to `Inertia.Render()`.
- Missing entries cause silent client-side 404s with no error in the console.
- Missing entries throw a descriptive client-side error (the resolver names the missing page and lists the available ones) and surface an error toast -- they do not fail silently.
- Run `npm run validate-pages` to verify all endpoints have matching page entries.

### Dynamic Imports
Expand Down
2 changes: 1 addition & 1 deletion docs/site/frontend/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Each module compiles its React pages into a single ES module bundle (`{ModuleNam
| Technology | Version | Purpose |
|---|---|---|
| React | 19 | UI rendering |
| Inertia.js | 2.x | SPA-like navigation without a client-side router |
| Inertia.js | 3.x | SPA-like navigation without a client-side router |
| Vite | 6.x | Build tooling and dev server |
| Tailwind CSS | 4.x | Utility-first styling |
| TypeScript | 5.8 | Type safety |
Expand Down
2 changes: 1 addition & 1 deletion docs/site/getting-started/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ You don't write registration code, startup configuration, or reflection-based di
| Testing | xUnit.v3, FluentAssertions, Bogus, Playwright, BenchmarkDotNet, NBomber |
| Database | SQLite, PostgreSQL, SQL Server via EF Core |
| File storage | Local, AWS S3, Azure Blob Storage |
| Job scheduling | TickerQ with CRON support |
| Job scheduling | Database-backed queue with CRON (Cronos) support |
| CLI | System.CommandLine |

## Next Steps
Expand Down
8 changes: 4 additions & 4 deletions docs/site/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export const pages: Record<string, unknown> = {
```

::: danger Don't Forget the Page Registry
Every `IViewEndpoint` that calls `Inertia.Render("Customers/Something", ...)` **must** have a matching entry in `Pages/index.ts`. If you forget, the endpoint works on the server but silently 404s on the client with no error message.
Every `IViewEndpoint` that calls `Inertia.Render("Customers/Something", ...)` **must** have a matching entry in `Pages/index.ts`. If you forget, the endpoint works on the server but the client throws a descriptive error (and shows an error toast) instead of rendering the page.

Run `npm run validate-pages` to catch mismatches.
:::
Expand Down Expand Up @@ -285,7 +285,7 @@ This starts:
- The SimpleModule host app on **http://localhost:8080**
- A **PostgreSQL** instance for production-like database behavior

::: tip Development vs Customerion Database
::: tip Development vs Production Database
During local development with `npm run dev`, the app uses SQLite by default -- no database server needed. Docker Compose switches to PostgreSQL to match production behavior.
:::

Expand All @@ -294,8 +294,8 @@ During local development with `npm run dev`, the app uses SQLite by default -- n
| Command | What it does |
| ------------------------ | ----------------------------------------------- |
| `npm run dev` | Start backend + all frontend watchers |
| `npm run build` | Customerion build (minified, optimized) |
| `npm run dev:build` | Dev build (unminified, source maps) |
| `npm run build` | Production build (minified, optimized) |
| `npm run build:dev` | Dev build (unminified, source maps) |
| `npm run check` | Lint + format check (Biome) |
| `npm run check:fix` | Auto-fix lint + formatting |
| `npm run validate-pages` | Verify all endpoints have page registry entries |
Expand Down
2 changes: 1 addition & 1 deletion docs/site/guide/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ public class EditEndpoint : IViewEndpoint
```

::: warning
When adding a new `IViewEndpoint`, you **must** also register the corresponding component in your module's `Pages/index.ts`. The Inertia component name in `Inertia.Render("Customers/Edit", ...)` must have a matching key in the pages record. If you forget, the page will silently 404 on the client side with no error.
When adding a new `IViewEndpoint`, you **must** also register the corresponding component in your module's `Pages/index.ts`. The Inertia component name in `Inertia.Render("Customers/Edit", ...)` must have a matching key in the pages record. If you forget, the client throws a descriptive error (and shows an error toast) instead of rendering the page.

Run `npm run validate-pages` to verify all endpoints have matching frontend entries.
:::
Expand Down
2 changes: 1 addition & 1 deletion docs/site/guide/inertia.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export const pages: Record<string, unknown> = {
```

::: danger Critical
Every `IViewEndpoint` that calls `Inertia.Render("Module/Page", ...)` **must** have a matching entry in the module's `Pages/index.ts`. Missing entries silently fail with no error in the console. Run `npm run validate-pages` to catch mismatches.
Every `IViewEndpoint` that calls `Inertia.Render("Module/Page", ...)` **must** have a matching entry in the module's `Pages/index.ts`. Missing entries throw a descriptive client-side error (the resolver lists the available pages) and surface an error toast — they do not fail silently. Run `npm run validate-pages` to catch mismatches at build time.
:::

### Writing a Page Component
Expand Down
3 changes: 2 additions & 1 deletion docs/site/guide/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ public interface IModule
| `ConfigurePermissions` | Define module-specific permissions for authorization |
| `ConfigureSettings` | Register configurable settings for the module |
| `ConfigureFeatureFlags` | Register feature flag definitions |
| `ConfigureAgents` | Register AI agent definitions |
| `ConfigureRateLimits` | Register rate limit policies |
| `ConfigureHost` | Configure host-level integrations after the host is built (e.g., TickerQ, database initialization) |
| `ConfigureHost` | Configure host-level integrations after the host is built (e.g., database initialization and schema creation) |
| `OnStartAsync` | One-time async initialization after all services are registered |
| `OnStopAsync` | Graceful shutdown cleanup |
| `CheckHealthAsync` | Report per-module health status |
Expand Down
3 changes: 2 additions & 1 deletion docs/site/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ public interface IModule
| `ConfigurePermissions` | Application startup | Register permission definitions |
| `ConfigureSettings` | Application startup | Register settings definitions |
| `ConfigureFeatureFlags` | Application startup | Register feature flag definitions |
| `ConfigureAgents` | Application startup | Register AI agent definitions |
| `ConfigureRateLimits` | Application startup | Register rate limit policies |
| `ConfigureHost` | After host is built | Configure host-level integrations (TickerQ, DB init) |
| `ConfigureHost` | After host is built | Configure host-level integrations (database init, schema creation) |
| `OnStartAsync` | After services registered | One-time async initialization |
| `OnStopAsync` | Graceful shutdown | Cleanup, flush buffers, drain work |
| `CheckHealthAsync` | Health check endpoint | Report module health status |
Expand Down
2 changes: 1 addition & 1 deletion framework/SimpleModule.Core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public sealed class ProductsModule : IModule
- **IModule** interface for defining self-contained modules
- **IEndpoint** interface for auto-discovered Minimal API endpoints
- **[Dto]** attribute for cross-module data transfer types with TypeScript generation
- **IEventBus** for decoupled module-to-module communication
- **Domain events** (`IEvent` / `DomainEvent`) dispatched in-process via Wolverine's `IMessageBus` for decoupled module-to-module communication
- **IMenuRegistry** for dynamic navigation menu registration
- **Inertia.js integration** for server-driven React page rendering

Expand Down
Loading