backend phase 0: foundation, cross-cutting seams & starter cleanup

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.
This commit is contained in:
hamid
2026-06-30 22:48:41 +03:30
parent 53a40dc51d
commit 765cc632d5
75 changed files with 1539 additions and 1418 deletions
+39 -23
View File
@@ -81,27 +81,36 @@ projects/assemblies, Clean-Architecture layers, and cross-layer dependencies.
```
src/
├── Core/
│ ├── Baya.Domain Entities (User, Order, Role…), BaseEntity, IEntity, ITimeModification
│ └── Baya.Application Features/ (Commands & Queries), Contracts/, Models/, pipeline behaviors (Common/)
│ ├── 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/
│ ├── Baya.Infrastructure.Identity Jwt/, Identity/ (Managers, Stores, PermissionManager, Seed)
│ ├── Baya.Infrastructure.CrossCutting Serilog wiring
│ ├── 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/, appsettings*.json
│ ├── Baya.WebFramework BaseController, Filters/, Middlewares/, Swagger/, Routing/
│ └── Plugins/Baya.Web.Plugins.Grpc gRPC services + .proto models
│ ├── 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.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
@@ -116,16 +125,20 @@ Service registration is composed from per-layer extension methods (each project'
```
ConfigureHealthChecks() · SetupOpenTelemetry()
AddApplicationServices() // Mediator + validators + pipeline behaviors
RegisterIdentityServices(...) // Identity, JWT/JWE, authorization policies
AddPersistenceServices(...) // DbContext, UnitOfWork, repositories
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 → **authentication → authorization**
controllers → metrics → health checks → gRPC.
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.
@@ -137,17 +150,20 @@ never inline registrations there directly.
Features live under `Baya.Application/Features/<Area>/{Commands|Queries}/<Name>/`:
```
Features/Order/
├── Commands/CreateOrderCommand/
│ ├── CreateOrderCommand.cs record : ICommand<OperationResult<T>>
│ ├── CreateOrderCommand.Handler.cs internal sealed class : ICommandHandler<...>
│ └── CreateOrderCommand.Validator.cs
└── Queries/GetUserOrdersQuery/
├── GetUserOrdersQuery.cs
├── GetUserOrdersQuery.Handler.cs
└── GetUserOrdersQuery.Result.cs
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.