Lay the cross-cutting platform backbone every later phase reads from. Adds the first marketplace EF migration baseline (new `ops` schema) and the mechanisms b2..b15 reuse: typed runtime config, an append-only audit trail, an analytics event log, the holiday/bank-closure calendar, in-app notifications, and the internal support-alert worklist. Schema & migration - New `ops` schema + migration InitialMarketplaceBaseline with 6 tables: PlatformConfigs (IAuditable), AuditLogs (append-only), SystemEvents, IranianHolidays, Notifications, SupportAlerts — with indexes/uniques and FKs to usr.Users. Seeded 12 config keys + 7 sample holidays via HasData. Domain / Application - IAuditable marker + [AuditRedacted] attribute; entities + string-code constant holders (config data_type, holiday type, alert type/severity/status). - Facade contracts: IPlatformConfig, IHolidayCalendar, IAnalyticsSink, IAuditLogger, INotificationService, ISupportAlertService; DTOs + PagedResult<T>; evolved the INotificationDispatcher.Notification record to carry Type + DataJson; Pagination helper. - 14 CQRS commands/queries (+ validators) wiring the endpoints to the facades. Infrastructure - DB-backed facade implementations in Persistence/Services/; real in-app INotificationDispatcher (removes the b0 log stub); notification-retention hosted service (purge is_read=1 AND age>90d). - Extended AuditFieldInterceptor to also append an old/new-diff audit_logs row for every IAuditable change in the same transaction (PII redacted). - Registered all facades + hosted service in AddPersistenceServices; removed the dispatcher registration from AddCrossCuttingSeams. API - 5 controllers: admin PlatformConfig/Holidays/Audit/SupportAlerts ([Authorize(DynamicPermission)]) + current-user Notifications ([Authorize]), all tenant-scoped and paginated. 16 Swagger paths total. Money-correctness & safety rules honoured - Config read at compute time (cached, parsed by data_type), never hardcoded; every config change is audited in the same transaction; audit_logs is append-only (no update/delete path); support alerts are admin-only; notifications are tenant-scoped; analytics is fire-and-forget. Tests & docs - 18 new foundation tests over in-memory SQLite (config typing + audit, holidays, notifications + tenancy + retention, support alerts, analytics); build clean (0 new code warnings), 22 tests green; migration applied to the dev DB and swagger.v1.json refreshed. - Updated server Project map + CONVENTIONS, product data-model doc 12 (seeded config defaults), config-reference contract, mock registry, backend handoff/ STATUS/report. Follow-ups: add FK constraints for SupportAlerts.BookingId (b9) and ReviewId (b14) when those tables land. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 KiB
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. Read it before writing any server code.
- Repo-wide context and the frontend → root CLAUDE.md.
- Product/domain rules (business logic, schema, payments, escrow, verification) →
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. UseISender/ICommand/IQueryfrom 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
dotnet build Baya.sln— zero new warnings introduced. Unusedusings, locals, parameters, private fields, or members count as failures — delete them, don't suppress them (CONVENTIONS.md §2 "No unused code").dotnet test Baya.sln— all tests pass.- Read your own diff as if reviewing a PR: would a senior engineer approve it without comment?
- 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…, + Configuration/Audit/Analytics/Holidays/Notifications/SupportAlerts), BaseEntity, IEntity, ITimeModification, IAuditableEntity, IAuditable (audit-row marker)
│ └── Baya.Application Features/ (Commands & Queries; + Configuration/Audit/Analytics/Holidays/Notifications/SupportAlerts areas), Contracts/ (incl. Contracts/Common cross-cutting seams + the platform-signal facade contracts), Models/, pipeline behaviors (Common/)
├── Infrastructure/
│ ├── Baya.Infrastructure.Persistence ApplicationDbContext, Repositories/, Configuration/ (per-area EF config), Migrations/, Interceptors/ (AuditFieldInterceptor — audit-fields + audit-log rows), Services/ (DB-backed platform-signal facades + notification-retention hosted service)
│ ├── 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 + admin PlatformConfig/Holidays/Audit/SupportAlerts + current-user Notifications), 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.
Platform-signal facades (backend-phase-1). The cross-cutting marketplace tables live in a dedicated
ops schema (mirroring how Identity uses usr): PlatformConfigs, AuditLogs, SystemEvents,
IranianHolidays, Notifications, SupportAlerts. Because they are DB-backed, their Application
contracts — IPlatformConfig (typed cached config), IHolidayCalendar (bank-closure calendar),
IAnalyticsSink (fire-and-forget system_events), IAuditLogger (explicit append-only writes +
trail), INotificationService (per-user notification reads/commands), ISupportAlertService (internal
worklist) — are implemented in Baya.Infrastructure.Persistence/Services/ and registered by
AddPersistenceServices, not in CrossCutting. The real INotificationDispatcher (in-app
notifications write) also lives there and supersedes the b0 log stub. The
NotificationRetentionHostedService (the retention/IJobScheduler seam) is registered as a hosted
service there too. Other domains call these contracts; they never re-create the tables. The
AuditFieldInterceptor additionally writes an append-only audit_logs row for any IAuditable entity
(currently PlatformConfig) in the same transaction as the change.
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 §5.
Persistence
- Access the DB through
IUnitOfWork— notApplicationDbContextdirectly 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/implementingIEntityTypeConfiguration<T>. - Soft delete is enforced via a global query filter per entity (see CONVENTIONS.md §6).
Identity & auth
- JWT/JWE issued by
IJwtService(Baya.Infrastructure.Identity/Jwt/JwtService.cs). - Dynamic permission system:
DynamicPermissionHandlerreads[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. The essentials:
- All URL segments are
snake_caseviaSnakeCaseParameterTransformer— use[controller]/[action]tokens. - Controllers are
sealed, inheritBaseController, injectISender, returnbase.OperationResult(result). Never callOk()/BadRequest()/NotFound()directly. - Handlers are
internal sealed; never throw for expected failures — returnOperationResult. recordfor requests/DTOs,classfor entities (no public setters),sealed classfor handlers/services.async/awaitall the way; passCancellationTokenthrough 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— neverVersion=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 |