765cc632d5
Remove the Order demo (entity/feature/repo/config/gRPC/proto) and the three pre-marketplace migrations; regenerate a fresh InitialBaseline migration. Stand up the REST surface (PingController + System/Ping CQRS) proving the Mediator -> behaviors -> OperationResult -> ApiResult envelope end to end. Close wiring gaps: register LoggingBehavior (outermost) and add the built-in rate limiter (per-IP global + otp/auth/sensitive policies), placed before authentication. Add current-user + audit plumbing: ICurrentUser (HttpContext + null impls), rename BaseEntity audit fields to CreatedAt/ModifiedAt (DateTimeOffset) + CreatedById/ModifiedById, stamped by a new AuditFieldInterceptor. Introduce five cross-cutting seams (IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher) with in-memory/local mocks registered via AddCrossCuttingSeams. Add Baya.Test.Foundation (encryptor, audit interceptor, ping handler) and update docs, contracts (swagger.v1.json), handoff, report, and mocks registry.
223 lines
12 KiB
Markdown
223 lines
12 KiB
Markdown
# Balinyaar Server — Claude Code Guidelines
|
|
|
|
The backend API of **Balinyaar**, a trust-first home-nursing marketplace in Iran.
|
|
|
|
- **Coding rules** (the full rule set you must follow) → [CONVENTIONS.md](CONVENTIONS.md). Read it
|
|
before writing any server code.
|
|
- Repo-wide context and the frontend → root [CLAUDE.md](../CLAUDE.md).
|
|
- Product/domain rules (business logic, schema, payments, escrow, verification) → [`product/`](../product/).
|
|
Read the relevant doc before designing an entity, feature, or endpoint — don't infer business rules
|
|
from code.
|
|
|
|
---
|
|
|
|
## Role
|
|
|
|
You are a **senior .NET software engineer** working on this codebase. That means:
|
|
|
|
- You write production-quality code, not demo code. Every file you touch should look like it was
|
|
written by someone who has shipped .NET APIs at scale.
|
|
- You understand the architecture and work _with_ it, not around it. Clean Architecture boundaries
|
|
are non-negotiable.
|
|
- You think before you write. If a task is ambiguous, reason through the design first. If it touches a
|
|
contract other layers depend on, think about downstream impact.
|
|
- You prefer simplicity and clarity over cleverness. The next engineer (or agent) should read your
|
|
code without a guide.
|
|
- You never leave the codebase in a worse state than you found it.
|
|
|
|
---
|
|
|
|
## Stack
|
|
|
|
- **ASP.NET Core / .NET 10** (`net10.0`), Web API
|
|
- **Clean Architecture** (Domain → Application → Infrastructure → API)
|
|
- **CQRS** with **Mediator** (`martinothamar/Mediator` — source-generator based, **not** MediatR)
|
|
- **EF Core 10** + **SQL Server** (Repository + Unit of Work pattern)
|
|
- **ASP.NET Core Identity** with **JWE** (signed + AES-128-encrypted JWT), OTP, and dynamic permission authorization
|
|
- **Mapster** for mapping, **FluentValidation** for validation, **Serilog** for structured logging
|
|
- **OpenTelemetry** + **prometheus-net** for observability, **NSwag** for OpenAPI, **Asp.Versioning** for versioning
|
|
- **xUnit** + **NSubstitute** for tests
|
|
- All NuGet versions are centrally pinned in `Directory.Packages.props`
|
|
|
|
> Note: some prose elsewhere may say "MediatR" — the actual dispatcher is `martinothamar/Mediator`.
|
|
> Use `ISender`/`ICommand`/`IQuery` from that package, not MediatR types.
|
|
|
|
---
|
|
|
|
## Commands (run from `server/`)
|
|
|
|
| Task | Command |
|
|
| ----------------- | ------- |
|
|
| Restore | `dotnet restore Baya.sln` |
|
|
| Build | `dotnet build Baya.sln` |
|
|
| Run API | `dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj` |
|
|
| Test | `dotnet test Baya.sln` |
|
|
| Add migration | `dotnet ef migrations add <Name> --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api` |
|
|
| Update DB | `dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api` |
|
|
|
|
**Default URL:** `https://localhost:5002` — Swagger at `/swagger`.
|
|
On boot, `Program.cs` calls `ApplyMigrationsAsync()` and `SeedDefaultUsersAsync()` — a reachable SQL
|
|
Server is required to start.
|
|
|
|
---
|
|
|
|
## Quality gates — run before declaring work done
|
|
|
|
1. `dotnet build Baya.sln` — zero new warnings introduced. Unused `using`s, locals, parameters,
|
|
private fields, or members count as failures — delete them, don't suppress them
|
|
([CONVENTIONS.md](CONVENTIONS.md) §2 "No unused code").
|
|
2. `dotnet test Baya.sln` — all tests pass.
|
|
3. Read your own diff as if reviewing a PR: would a senior engineer approve it without comment?
|
|
4. If the change alters the architecture, update the **Project map** below in the same change
|
|
(see "Keeping the Project map current").
|
|
|
|
---
|
|
|
|
## Project map
|
|
|
|
This tree is the **canonical description of the server's architecture** — the authoritative list of
|
|
projects/assemblies, Clean-Architecture layers, and cross-layer dependencies.
|
|
|
|
```
|
|
src/
|
|
├── Core/
|
|
│ ├── Baya.Domain Entities (User, Role…), BaseEntity, IEntity, ITimeModification, IAuditableEntity
|
|
│ └── Baya.Application Features/ (Commands & Queries), Contracts/ (incl. Contracts/Common cross-cutting seams), Models/, pipeline behaviors (Common/)
|
|
├── Infrastructure/
|
|
│ ├── Baya.Infrastructure.Persistence ApplicationDbContext, Repositories/, Configuration/, Migrations/, Interceptors/ (AuditFieldInterceptor)
|
|
│ ├── Baya.Infrastructure.Identity Jwt/, Identity/ (Managers, Stores, PermissionManager, Seed, CurrentUser/)
|
|
│ ├── Baya.Infrastructure.CrossCutting Serilog wiring + Seams/ (mock impls of the cross-cutting seams) + AddCrossCuttingSeams
|
|
│ └── Baya.Infrastructure.Monitoring HealthChecks, OpenTelemetry, prometheus-net
|
|
├── API/
|
|
│ ├── Baya.Web.Api Program.cs, Controllers/V1/ (PingController), appsettings*.json
|
|
│ ├── Baya.WebFramework BaseController, Filters/, Middlewares/, Swagger/, Routing/, ServiceConfiguration/ (rate limiting)
|
|
│ └── Plugins/Baya.Web.Plugins.Grpc gRPC services + .proto models (User only)
|
|
├── Shared/Baya.SharedKernel Extensions + validation base
|
|
└── Tests/
|
|
├── Baya.Tests.Setup Shared test infrastructure (SQLite, NSubstitute setup)
|
|
├── Baya.Test.Infrastructure.Identity xUnit identity tests
|
|
└── Baya.Test.Foundation xUnit tests for cross-cutting plumbing (encryptor, audit interceptor, ping)
|
|
```
|
|
|
|
**Dependency direction points inward.** Domain has no dependencies. Application depends only on
|
|
Domain. Infrastructure and API implement/consume Application contracts. Never make Domain or
|
|
Application reference Infrastructure or the API — this is a hard rule.
|
|
|
|
**Cross-cutting seams.** Application defines mock-able external dependencies as interfaces in
|
|
`Contracts/Common/` (`IDateTimeProvider`, `IFieldEncryptor`, `ICacheService`, `IObjectStorage`,
|
|
`INotificationDispatcher`, plus `ICurrentUser`). Their in-memory/local mock implementations live in
|
|
`Baya.Infrastructure.CrossCutting/Seams/` and are registered by `AddCrossCuttingSeams(configuration)`
|
|
(config section `Seams`); `ICurrentUser` is registered in the Identity layer. Swapping a mock for a
|
|
real provider is a registration change — handlers depend only on the contract. Audit fields are
|
|
stamped by `AuditFieldInterceptor` (Persistence), not in handlers.
|
|
|
|
**Keeping the Project map current.** When a change touches the architecture — adds, removes, or
|
|
renames a project/assembly, a Clean-Architecture layer, or a major folder, or changes a cross-layer
|
|
dependency — you **must** update this Project map (and the dependency rule above, if affected) in the
|
|
**same** change. This is the server-specific form of the root "Keep docs honest" rule: the map is
|
|
only canonical if it stays accurate.
|
|
|
|
---
|
|
|
|
## Startup wiring
|
|
|
|
Service registration is composed from per-layer extension methods (each project's `ServiceConfiguration/`):
|
|
|
|
```
|
|
ConfigureHealthChecks() · SetupOpenTelemetry()
|
|
AddApplicationServices() // Mediator + pipeline behaviors (Logging → Metrics → Validate)
|
|
RegisterIdentityServices(...) // Identity, JWT/JWE, authorization policies, ICurrentUser + IHttpContextAccessor
|
|
AddPersistenceServices(...) // DbContext (+ AuditFieldInterceptor), UnitOfWork, repositories
|
|
AddCrossCuttingSeams(config) // IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher (mocks)
|
|
AddWebFrameworkServices() // API versioning + snake_case routing
|
|
AddRateLimitingPolicies() // built-in rate limiter: per-IP global + named (otp/auth/sensitive)
|
|
AddSwagger("v1", "v1.1") · RegisterValidatorsAsServices() · AddMapster()
|
|
ConfigureGrpcPluginServices()
|
|
```
|
|
|
|
Pipeline order: exception handler → Swagger → routing → **rate limiter → authentication →
|
|
authorization** → controllers → metrics → health checks → gRPC. `UseRateLimiter()` is placed
|
|
**before** `UseAuthentication()` so over-limit auth/OTP attempts are rejected (`429`) before hitting
|
|
the auth stack.
|
|
|
|
When adding new infrastructure, expose it as an extension method and call it from `Program.cs` —
|
|
never inline registrations there directly.
|
|
|
|
---
|
|
|
|
## CQRS — how a feature is shaped
|
|
|
|
Features live under `Baya.Application/Features/<Area>/{Commands|Queries}/<Name>/`:
|
|
|
|
```
|
|
Features/<Area>/
|
|
├── Commands/<VerbNoun>Command/
|
|
│ ├── <VerbNoun>Command.cs record : IRequest<OperationResult<T>>
|
|
│ ├── <VerbNoun>Command.Handler.cs internal sealed class : IRequestHandler<...>
|
|
│ └── <VerbNoun>Command.Validator.cs
|
|
└── Queries/<VerbNoun>Query/
|
|
├── <VerbNoun>Query.cs
|
|
├── <VerbNoun>Query.Handler.cs
|
|
└── <VerbNoun>Query.Result.cs
|
|
```
|
|
|
|
A minimal live example shipped in backend-phase-0: `Features/System/Queries/Ping/` (query + handler +
|
|
result), surfaced by `Controllers/V1/PingController`.
|
|
|
|
Handlers are `internal sealed`. Requests are `record` types. Validators use FluentValidation and are
|
|
picked up automatically by the `ValidateCommandBehavior` pipeline behavior. Never throw for expected
|
|
failures — use `OperationResult` factory methods.
|
|
|
|
**To add a feature:** create the folder, implement request + handler + (optional) validator, add any
|
|
new contracts to `Application/Contracts/` and implement them in Infrastructure, then wire a controller
|
|
action to `sender.Send(...)`. Full conventions are in [CONVENTIONS.md](CONVENTIONS.md) §5.
|
|
|
|
---
|
|
|
|
## Persistence
|
|
|
|
- Access the DB through `IUnitOfWork` — not `ApplicationDbContext` directly outside Infrastructure.
|
|
- Commit once per command via `unitOfWork.CommitAsync()`.
|
|
- Use `AsNoTracking()` on all read-only queries.
|
|
- Always project to a DTO in queries — never return entity objects from handlers.
|
|
- Add entity config in `Persistence/Configuration/<Area>Config/` implementing `IEntityTypeConfiguration<T>`.
|
|
- Soft delete is enforced via a global query filter per entity (see [CONVENTIONS.md](CONVENTIONS.md) §6).
|
|
|
|
---
|
|
|
|
## Identity & auth
|
|
|
|
- JWT/JWE issued by `IJwtService` (`Baya.Infrastructure.Identity/Jwt/JwtService.cs`).
|
|
- Dynamic permission system: `DynamicPermissionHandler` reads `[controller]` + `[action]` route
|
|
values and checks role claims. Always use `[controller]`/`[action]` tokens so the keys stay
|
|
consistent (see CONVENTIONS.md §1 Routing).
|
|
- Settings bound from `appsettings.json` → `IdentitySettings`.
|
|
- Auth and OTP endpoints must be rate-limited (CONVENTIONS.md §11).
|
|
|
|
---
|
|
|
|
## Conventions — quick reference
|
|
|
|
Full rules in [CONVENTIONS.md](CONVENTIONS.md). The essentials:
|
|
|
|
- All URL segments are `snake_case` via `SnakeCaseParameterTransformer` — use `[controller]`/`[action]` tokens.
|
|
- Controllers are `sealed`, inherit `BaseController`, inject `ISender`, return `base.OperationResult(result)`.
|
|
Never call `Ok()` / `BadRequest()` / `NotFound()` directly.
|
|
- Handlers are `internal sealed`; never throw for expected failures — return `OperationResult`.
|
|
- `record` for requests/DTOs, `class` for entities (no public setters), `sealed class` for handlers/services.
|
|
- `async`/`await` all the way; pass `CancellationToken` through every async call; never `.Result`/`.Wait()`/`async void`.
|
|
- Mapster for mapping; FluentValidation for validation (validate at the boundary).
|
|
- Package versions live **only** in `Directory.Packages.props` — never `Version=` in a `.csproj`.
|
|
- No unused code (usings, locals, parameters, private fields/members) and no *what*-comments — explain *why*, prefer self-documenting names (§2).
|
|
- Architecture changes (a project/layer/major folder or a cross-layer dependency) must update the **Project map** in the same change.
|
|
- The `Baya.*` namespace is project naming — do not rename without explicit instruction.
|
|
|
|
---
|
|
|
|
## Known build warnings (pre-existing — do not fix unless tasked)
|
|
|
|
| Warning | Project | Note |
|
|
| ------- | ------- | ---- |
|
|
| `NU1510` on `Microsoft.Extensions.Logging.Debug` | `Baya.Web.Api` | Redundant transitive reference, harmless |
|
|
| `NETSDK1057` (preview SDK) | all | .NET 10 SDK is preview on this machine |
|