Development View
Describes the system’s source code organisation, module boundaries, dependency rules, and the conventions used by developers working on the codebase.
Module / Project Structure
The backend is split into four .NET projects that follow Clean Architecture. Dependency arrows always point inward — API and Infrastructure depend on Application, which depends only on Domain. The Domain project has zero external NuGet dependencies: it contains only pure C# business logic, aggregates, and value objects. The three portal projects (AdminPortal, VendorPortal, CustomerApp) are fully separate ASP.NET applications that communicate with the backend exclusively via the REST API over HTTP — they hold no direct project references to any backend assembly.
flowchart TD
subgraph Backend["src/backend/"]
API["FlowerShop.API\n─────────────\nControllers (23)\nMiddleware pipeline\nProgram.cs / DI root\nDTO request/response types"]
APP["FlowerShop.Application\n─────────────\nCommands + Handlers\nQueries + Handlers\nDomain Services\nDTOs (application)\nFluentValidation validators\nMediatR behaviours\nEvent handlers"]
DOM["FlowerShop.Domain\n─────────────\nAggregates (14)\nValue Objects\nDomain Events\nRepository interfaces\nIDs (strongly-typed)\nResult<T>"]
INF["FlowerShop.Infrastructure\n─────────────\nEF Core DbContext\nEntity configurations\nRepositories\nKeycloak client\nMQTT services\nSignalR hub\nRedis cache\nEvent bus\nDI extensions"]
end
subgraph Portals["src/portals/"]
ADM["FlowerShop.AdminPortal\n(Razor Pages :5001)"]
VND["FlowerShop.VendorPortal\n(Razor Pages :5002)"]
CUS["FlowerShop.CustomerApp\n(ASP.NET MVC :5003)"]
end
subgraph Tests["tests/"]
UT["FlowerShop.Tests.Unit"]
IT["FlowerShop.Tests.Integration"]
DT["FlowerShop.Domain.Tests"]
end
API --> APP
API --> DOM
APP --> DOM
INF --> APP
INF --> DOM
ADM --> API
VND --> API
CUS --> API
Sub-domain Mapping — Projects
| Project | Business Sub-domain(s) | What lives there |
|---|---|---|
FlowerShop.Domain |
All sub-domains | Pure business logic: aggregates (Bouquet, SmartVase, Order, Vendor, Customer, …), value objects (Money, Location, FreshnessScore), domain events, strongly-typed IDs |
FlowerShop.Application |
All sub-domains | Use-case layer: commands/queries per sub-domain (Vendor, Bouquet, Order, Cart, Customer, IoT, Analytics, Notification), MediatR pipeline behaviours |
FlowerShop.Infrastructure |
Identity & Tenancy, IoT/MQTT, Payments, Vendor Management | EF Core repositories, Keycloak client, MQTT services, Redis cache, Stripe stub, FCM stub, SignalR hub, RabbitMQ/InMemory event bus |
FlowerShop.API |
All sub-domains | Controllers (23), middleware pipeline, DI root — thin HTTP adapter over Application layer |
FlowerShop.AdminPortal (:5001) |
Platform Administration | Razor Pages UI for vendor approval/suspension, vase registration, platform analytics |
FlowerShop.VendorPortal (:5002) |
Vendor Management / Vendor Order Fulfilment | Razor Pages UI for bouquet catalogue, vase management, order fulfilment |
FlowerShop.CustomerApp (:5003) |
Customer Purchasing | ASP.NET MVC UI for bouquet browsing, cart, checkout, Leaflet map |
FlowerShop.Tests.Unit |
All sub-domains | Handler and behaviour unit tests (mocked repositories) |
FlowerShop.Tests.Integration |
All sub-domains | Full pipeline tests against real PostgreSQL (TestContainers) |
FlowerShop.Domain.Tests |
All sub-domains | Aggregate invariant and state-machine tests (no infrastructure) |
Dependency rule: arrows flow inward only.
Domainhas zero external dependencies.Applicationdepends only onDomain.InfrastructureandAPIdepend onApplication+Domain. Portals call the REST API over HTTP — they do not reference backend projects directly.
Sub-domain Mapping — MediatR Pipeline
| Behaviour | Business Sub-domain | What it enforces |
|---|---|---|
LoggingBehaviour |
All sub-domains | Traces every command/query with request ID, duration, and result |
ValidationBehaviour |
All sub-domains | Runs FluentValidation rules — rejects malformed bouquet prices, missing vendor IDs, invalid coordinates |
TenantValidationBehavior |
Identity & Tenancy | Ensures ITenantScopedRequest commands carry a valid tenant context before the handler runs |
IdempotencyKeyBehaviour |
Customer Purchasing / Payments | Deduplicates order and payment commands using Redis SET NX before the handler executes |
CachingBehaviour |
Customer Purchasing / Vendor Management | Serves ICacheableQuery results (bouquet search, vendor dashboards) from Redis; invalidated by version-key increment |
CQRS — File Conventions
FlowerShop.Application/
Commands/
<Domain>/
Create<Entity>Command.cs ← record + IRequest<Result<Guid>>
Create<Entity>CommandHandler.cs ← IRequestHandler<>
Update<Entity>Command.cs
Update<Entity>CommandHandler.cs
Queries/
<Domain>/
Get<Entity>Query.cs ← record + IRequest<Result<EntityDto>>
Get<Entity>QueryHandler.cs
Validators/
<Domain>/
Create<Entity>CommandValidator.cs ← AbstractValidator<>
DTOs/
<EntityDto>.cs
Events/
<DomainEvent>Handler.cs ← INotificationHandler<>
MediatR Pipeline Behaviours (execution order)
flowchart LR
REQ["IRequest sent\nvia ISender.Send()"]
--> LB["LoggingBehaviour\n(log request + response time)"]
--> VB["ValidationBehaviour\n(FluentValidation; returns\nValidationError on failure)"]
--> TVB["TenantValidationBehavior\n(enforces tenant context\nfor ITenantScopedRequest)"]
--> IKB["IdempotencyKeyBehaviour\n(checks Redis before handler)"]
--> CB["CachingBehaviour\n(for ICacheableQuery — read from Redis)"]
--> HAND["Command / Query Handler"]
Domain Layer Conventions
| Convention | Rule |
|---|---|
| Aggregate root | Inherits AggregateRoot<TId> — only root is reachable from outside |
| Value objects | Inherit ValueObject; immutable; equality by value |
| Strongly-typed IDs | VendorId, BouquetId, CustomerId, OrderId, VaseId, UserId — wrap Guid; prevent cross-aggregate ID confusion |
| **Result |
Domain methods return Result<T> — never throw for business-rule violations |
| Domain events | Raised via AggregateRoot.AddDomainEvent(); dispatched through outbox |
| No infrastructure | Domain project has zero NuGet dependencies on EF Core, Redis, etc. |
Infrastructure — EF Core Conventions
Each aggregate has a dedicated IEntityTypeConfiguration<T> class under:
FlowerShop.Infrastructure/Persistence/Configurations/<Entity>Configuration.cs
- Value objects are mapped with
OwnsOne/OwnsMany - Strongly-typed IDs are converted via
HasConversion<Guid> - No navigation properties on aggregates (cross-aggregate references use IDs only)
Key Configuration Files
| File | Purpose |
|---|---|
src/backend/FlowerShop.API/Program.cs |
DI root, middleware pipeline registration |
src/backend/FlowerShop.Infrastructure/Extensions/ServiceCollectionExtensions.cs |
All infrastructure DI registrations incl. stubs |
src/backend/FlowerShop.Infrastructure/Persistence/FlowerShopDbContext.cs |
EF Core DbContext, outbox_messages table |
src/backend/FlowerShop.API/appsettings.json |
App configuration (override via env vars in Docker) |
docker/docker-compose.dev.yml |
Local dev infrastructure |
Test Strategy
| Project | What it tests | Approach |
|---|---|---|
FlowerShop.Domain.Tests |
Aggregate invariants, value-object equality, domain state machines | xUnit, no infrastructure |
FlowerShop.Tests.Unit |
Command/query handlers, MediatR behaviours, validators | xUnit, mocked repositories |
FlowerShop.Tests.Integration |
EF Core repositories, full MediatR pipeline, controller endpoints | xUnit, real PostgreSQL via TestContainers |
Current status: 126/126 tests passing.
Migration Workflow
# From Infrastructure project
dotnet ef migrations add <Name> --startup-project ../FlowerShop.API
# From API project
dotnet ef database update
Known gap:
Cart,CartItem,OrderLine(multi-line schema),Subscription,Favourite,Notification,DeviceRegistration,SavedAddresstables requireAddCartAndOrderLinesmigration — not yet run.