Documentation Index
Fetch the complete documentation index at: https://docs.aspfox.com/llms.txt
Use this file to discover all available pages before exploring further.
Layer diagram
┌──────────────────────────────────────────────────────┐
│ Acme.Api │
│ Controllers · Middleware · DI wiring · Swagger │
│ References: Application, Infrastructure │
├──────────────────────────────────────────────────────┤
│ Acme.Infrastructure │
│ EF Core · PostgreSQL · Redis · Stripe · Resend │
│ Hangfire · TenantContext · Migrations │
│ References: Application │
├──────────────────────────────────────────────────────┤
│ Acme.Application │
│ Commands · Queries · Handlers · DTOs · Validators │
│ Pipeline behaviours · Interfaces │
│ References: Domain │
├──────────────────────────────────────────────────────┤
│ Acme.Domain │
│ Entities · Value objects · Enums · Constants │
│ References: nothing │
└──────────────────────────────────────────────────────┘
Dependency direction: inward only →→→
What each layer does
Domain is the core. Entities like User, Tenant, Subscription, Invitation, RefreshToken, and Notification live here. So do the Permissions constants, EmailTemplate enum, and all domain exceptions. Domain has no NuGet dependencies beyond logging abstractions. Nothing in Domain knows about the database, HTTP, or any external service.
Application defines what the system does. Every operation is either a Command (writes data) or a Query (reads data). Handlers contain the actual logic. Application defines interfaces (IApplicationDbContext, IEmailService, IStripeService, ICacheService, INotificationService) and the Infrastructure layer implements them. This inversion is what makes Infrastructure replaceable in tests.
Infrastructure implements the Application interfaces. ApplicationDbContext implements IApplicationDbContext. ResendEmailService implements IEmailService. StripeService implements IStripeService. All EF Core migrations live here. Infrastructure can reference NuGet packages freely because nothing depends on it except the Api host.
Api wires everything together. Controllers receive HTTP requests, immediately call MediatR.Send(), and map the result to an HTTP response. Middleware handles authentication, tenant resolution, and subscription status. The startup registers all dependencies.
Every operation flows through MediatR. Controllers never contain business logic.
HTTP POST /api/v1/tenants/current/invitations
│
▼
InvitationsController.Invite(InviteMemberRequest request)
│
├─ maps request to InviteMemberCommand
▼
MediatR.Send(InviteMemberCommand)
│
▼
LoggingBehaviour<InviteMemberCommand, Result<InvitationDto>>
│ logs the command name and execution time
▼
ValidationBehaviour<InviteMemberCommand, Result<InvitationDto>>
│ runs InviteMemberCommandValidator (FluentValidation)
│ returns Result.Failure(ValidationError) if invalid
▼
InviteMemberCommandHandler.Handle()
│ checks permissions, creates invitation, sends email,
│ creates notification, saves to database
│ returns Result<InvitationDto>
▼
InvitationsController maps Result<T> to ApiResponse<T>
│ Result.IsSuccess → 200 OK with data
│ Result.IsFailure → appropriate HTTP status with error code
▼
HTTP 200 OK / 4xx / 5xx
The Result<T> type is a discriminated union — either success with a value, or failure with an error code and message. Controllers map results to HTTP responses in a single switch. No exceptions are thrown for expected business failures.
API response envelope
Every response from the API uses the same shape:
Success:
{
"success": true,
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"name": "Jane Smith"
}
}
Validation failure (400):
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"email": ["'Email' must not be empty.", "'Email' is not a valid email address."],
"name": ["'Name' must not be empty."]
}
}
}
Business failure (401, 403, 404, 409, etc.):
{
"success": false,
"error": {
"code": "TOKEN_REUSE_DETECTED",
"message": "This refresh token has already been used. Your session has been revoked."
}
}
The code field is a machine-readable string. See the Error Codes reference for every possible value.
Technical decisions
PostgreSQL over SQL Server — no licensing cost, excellent EF Core support, row-level security features, and the most common database choice for .NET on Linux. SQL Server requires a license in production.
RS256 (asymmetric JWT) over HS256 (symmetric) — with HS256, every service that verifies tokens needs the secret key. With RS256, services verify using only the public key. When you add a microservice, an edge function, or a third-party integration that needs to validate AspFox tokens, it needs only the public key — never the private key.
Mapster over AutoMapper — Mapster is faster, requires no profile registration, and handles most mappings with zero configuration. AutoMapper’s profile system adds boilerplate that provides no benefit for a codebase this size.
Resend over SendGrid — cleaner API, better developer experience, generous free tier (100 emails/day), and a dashboard that shows the full rendered HTML of every send. SendGrid’s API is significantly more complex for the same operations.
Zustand over Redux — Redux is right for complex shared state with many consumers and strict update rules. Zustand’s store surface in AspFox (auth state, tenant context, theme preference) does not justify Redux’s ceremony.
TanStack Query — best-in-class cache invalidation and mutation handling. The stale-while-revalidate model and query key-based invalidation eliminate most of the async state management complexity from React components.