A modern, full-featured e-commerce platform built with .NET 10 as a Modular Monolith with Clean Architecture, CQRS, and multi-tenant isolation.
Manage tenants, users, products, categories, orders, customers, and shopping carts β all from a single, secure, multi-tenant API.
- β¨ Features
- π Tech Stack
- π Project Structure
- π Getting Started
- π Architecture & Design Patterns
- π API Endpoints
- π Database Design
- π Security & Middleware
- π License
- Tenant isolation via shared-database with
TenantIdcolumn on every tenant-scoped table. - Automatic tenant resolution from
X-Tenant-IdHTTP header. - Tenant validation middleware β verifies JWT
tenant_idclaim matches the header.
- Registration with email uniqueness enforced per tenant.
- Login with JWT token generation (BCrypt password hashing).
- Profile retrieval (
GET /api/users/me) with Redis caching (15-min TTL). - User deactivation for account lifecycle management.
- Products β Create, read, search (paginated), update, delete. SKU uniqueness per tenant.
- Categories β Create, read by ID/slug, list all, update, delete. Self-referencing hierarchy.
- Price value object with amount + currency, DB CHECK constraint (
price > 0). - Stock tracking with DB CHECK constraint (
stock >= 0).
- Order lifecycle β Create, add items, submit, cancel.
- Order status tracking via
OrderStatusenum. - Paginated order listing with tenant isolation.
- Domain events β
OrderSubmittedDomainEventraised on submission.
- Customer profiles linked to user accounts.
- Address management via
CustomerAddressvalue object (street, city, state, country, postal code). - Self-service β customers manage their own profile via
/api/customers/me.
- Full cart CRUD β Add items, update quantities, remove items, clear cart.
- Checkout flow β Converts cart to an order and clears the cart.
- Real-time capacity checks β Duplicate prevention and stock validation.
- CQRS β Every action is a Command (mutates) or Query (reads).
- Result Pattern β All operations return
Result<T>orResult. No exceptions for control flow. - Custom Mediator β Built on native DI (no MediatR dependency).
- Soft Deletes β Entities are marked
IsDeleted, never physically removed. - Audit Trail β
CreatedAtUtc,CreatedBy,ModifiedAtUtc,ModifiedByauto-set on every entity. - Hybrid Caching β Redis-backed
HybridCachefor high-performance lookups. - Structured Logging β Serilog with console (dev) and MongoDB (prod) sinks.
- JWT Authentication β Bearer token with
sub,email,tenant_id,tenant_slugclaims.
| Layer | Technology |
|---|---|
| Runtime | .NET 10 |
| API Host | Minimal APIs (no controllers) |
| ORM | Entity Framework Core 10 |
| Database | PostgreSQL 18 (single shared DB) |
| Caching | HybridCache (with Redis 8 backing store) |
| Logging | Serilog with MongoDB 8 sink |
| Mapping | Mapster 10 (planned) |
| Mediator | Custom mediator via native DI |
| Auth | JWT Bearer (Microsoft.AspNetCore.Authentication.JwtBearer) |
| Password Hashing | BCrypt.Net-Next 4.2.0 |
| Proxy/LB | Nginx 1.29 (planned) |
| Containerization | Docker Compose (planned) |
| Testing | xUnit + FluentAssertions + Testcontainers (planned) |
StackCart/
βββ Directory.Packages.props # Central package version management
βββ StackCart.slnx # Solution file (new .slnx format)
βββ .env.example # Environment variable template
βββ project-architecture-map.md # Living architecture documentation
β
βββ src/
β βββ BuildingBlocks/ # Shared infrastructure & domain primitives
β β βββ StackCart.BuildingBlocks.Domain/
β β β βββ Abstractions/ # Entity, AggregateRoot, ValueObject, IDomainEvent
β β β βββ Shared/ # Error, Result (Result Pattern)
β β β βββ MultiTenancy/ # ITenantScoped, ITenantContext
β β β
β β βββ StackCart.BuildingBlocks.Application/
β β β βββ Messaging/ # IRequest, IRequestHandler, IMediator, Mediator
β β β βββ Caching/ # ICacheService
β β β βββ Security/ # ICurrentUser, IJwtService, IPasswordHasher
β β β βββ Abstractions/ # IApplicationDbContext
β β β βββ Extensions/ # ResultExtensions (centralized HTTP mapping)
β β β
β β βββ StackCart.BuildingBlocks.Infrastructure/
β β βββ Persistence/ # ApplicationDbContext, Interceptors
β β β βββ Interceptors/ # AuditInterceptor, SoftDeleteInterceptor, TenantSaveChangesInterceptor
β β βββ Caching/ # HybridCacheService
β β βββ Security/ # CurrentUserService, JwtService
β β βββ Tenancy/ # TenantContext
β β βββ Messaging/ # DomainEventDispatcher (stub)
β β βββ Logging/ # SerilogConfiguration
β β
β βββ Modules/ # β
Each module is a self-contained vertical slice β
β β
β βββ Tenancy/ # Multi-tenant management
β β βββ StackCart.Tenancy.Domain/ # Tenant entity, TenantSlug VO
β β βββ StackCart.Tenancy.Application/ # CreateTenant, GetTenantBySlug
β β βββ StackCart.Tenancy.Infrastructure/ # TenancyDbContext, TenantConfiguration
β β βββ StackCart.Tenancy.Presentation/ # TenancyEndpoints
β β βββ StackCart.Tenancy.Contracts/ # TenantSummaryDto, TenantCreatedIntegrationEvent
β β
β βββ Users/ # User authentication & profile
β β βββ StackCart.Users.Domain/ # User entity, UserErrors, UserRegisteredDomainEvent
β β βββ StackCart.Users.Application/ # RegisterUser, Login, GetCurrentUser, DeactivateUser
β β βββ StackCart.Users.Infrastructure/ # UsersDbContext, BCryptPasswordHasher
β β βββ StackCart.Users.Presentation/ # UserEndpoints
β β βββ StackCart.Users.Contracts/ # UserSummaryDto, LoginResponse
β β
β βββ Catalog/ # Product & category management
β β βββ StackCart.Catalog.Domain/ # Product, Category, Price VO, Sku VO
β β βββ StackCart.Catalog.Application/ # CRUD + SearchProducts (paginated)
β β βββ StackCart.Catalog.Infrastructure/ # CatalogDbContext, ProductConfiguration, CategoryConfiguration
β β βββ StackCart.Catalog.Presentation/ # CatalogEndpoints
β β βββ StackCart.Catalog.Contracts/ # ProductSummaryDto, CategorySummaryDto, PagedResult<T>
β β
β βββ Ordering/ # Order lifecycle management
β β βββ StackCart.Ordering.Domain/ # Order, OrderItem, OrderStatus enum
β β βββ StackCart.Ordering.Application/ # CreateOrder, AddOrderItem, SubmitOrder, CancelOrder, GetOrders
β β βββ StackCart.Ordering.Infrastructure/# OrderingDbContext, OrderConfiguration, OrderItemConfiguration
β β βββ StackCart.Ordering.Presentation/ # OrderingEndpoints
β β βββ StackCart.Ordering.Contracts/ # OrderSummaryDto, OrderSubmittedIntegrationEvent
β β
β βββ Customer/ # Customer profiles & shopping cart
β βββ StackCart.Customer.Domain/ # Customer, Cart, CartItem, CustomerAddress VO
β βββ StackCart.Customer.Application/ # CreateCustomer, UpdateCustomer, GetCustomer, Cart CRUD, Checkout
β βββ StackCart.Customer.Infrastructure/# CustomerDbContext, CustomerConfiguration, CartConfiguration
β βββ StackCart.Customer.Presentation/ # CustomerEndpoints
β βββ StackCart.Customer.Contracts/ # CustomerSummaryDto, CartDto
β
βββ src/StackCart.Api/ # API Host β composition root
βββ Program.cs # App startup, middleware pipeline, module registration
βββ Middleware/ # TenantResolutionMiddleware
βββ appsettings.json # Configuration (JWT, connection strings)
βββ appsettings.Development.json # Dev overrides (Serilog console sink)
- .NET 10 SDK
- PostgreSQL 18 (or Docker)
- Redis 8 (or Docker)
- MongoDB 8 (for Serilog sink β optional in dev)
git clone https://github.com/realAhmedAnwer/stack-cart.git
cd stack-cartcp .env.example .envOpen .env and adjust values as needed. The defaults are configured for local development:
CONNECTIONSTRINGS__DEFAULT=Host=localhost;Port=5432;Database=stackcart;Username=postgres;Password=your_password_here
CONNECTIONSTRINGS__MONGOLOGCONNECTION=mongodb://localhost:27017/stackcart_logs
REDIS_CONNECTION=localhost:6379
JWT__SECRET=your-super-secret-key-at-least-32-characters-long
JWT__ISSUER=StackCart
JWT__AUDIENCE=StackCart
JWT__EXPIRYMINUTES=60docker run -d --name stackcart-postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=your_password_here -e POSTGRES_DB=stackcart -p 5432:5432 postgres:18
docker run -d --name stackcart-redis -p 6379:6379 redis:8-alpine
docker run -d --name stackcart-mongo -p 27017:27017 mongo:8# From the solution root, run migrations for each module:
dotnet ef database update --project src/Modules/Tenancy/StackCart.Tenancy.Infrastructure --startup-project src/StackCart.Api
dotnet ef database update --project src/Modules/Users/StackCart.Users.Infrastructure --startup-project src/StackCart.Api
dotnet ef database update --project src/Modules/Catalog/StackCart.Catalog.Infrastructure --startup-project src/StackCart.Api
dotnet ef database update --project src/Modules/Ordering/StackCart.Ordering.Infrastructure --startup-project src/StackCart.Api
dotnet ef database update --project src/Modules/Customer/StackCart.Customer.Infrastructure --startup-project src/StackCart.Apidotnet run --project src/StackCart.ApiThe API will be available at https://localhost:5001 (or http://localhost:5000).
curl -k https://localhost:5001/healthOpen your browser and navigate to https://localhost:5001/openapi/v1.json (dev mode) to view the OpenAPI specification.
This project combines Clean Architecture (horizontal layer separation) with Vertical Slice Architecture (feature-based organization within the Application layer), creating a hybrid approach that maximizes both structural clarity and feature cohesion.
The solution enforces a strict inward dependency flow:
BuildingBlocks.Domain (no dependencies)
β
BuildingBlocks.Application
β
BuildingBlocks.Infrastructure
β
{Module}.Domain β BuildingBlocks.Domain
β
{Module}.Application β {Module}.Domain, {Module}.Contracts, BuildingBlocks.Application
β
{Module}.Infrastructure β {Module}.Application, {Module}.Domain, BuildingBlocks.Infrastructure
β
{Module}.Presentation β {Module}.Application, {Module}.Contracts, BuildingBlocks.Application
β
StackCart.Api β All module Infrastructure + Presentation, BuildingBlocks.Infrastructure
- Domain has zero external dependencies β pure entities, value objects, and enums.
- Application defines abstractions (
ICustomerDbContext,ICacheService,IJwtService) and orchestrates use cases via CQRS handlers. - Infrastructure implements abstractions and handles persistence, caching, security, and external services.
- Presentation owns Minimal API endpoints β each module registers its own routes.
- API Host composes everything at startup.
Within each module's Application layer, every business feature is a self-contained vertical slice:
StackCart.Customer.Application/Features/
βββ Customers/
β βββ CreateCustomer/
β β βββ CreateCustomerCommand.cs # Input record
β β βββ CreateCustomerHandler.cs # Handler logic
β βββ GetCustomer/
β β βββ GetCustomerQuery.cs # Query + handler
β βββ UpdateCustomer/
β βββ UpdateCustomerCommand.cs # Command + handler
βββ Cart/
βββ AddItemToCart/
βββ RemoveItemFromCart/
βββ UpdateCartItemQuantity/
βββ GetCart/
βββ ClearCart/
βββ Checkout/
Key benefits:
- Feature isolation β Adding a new feature means adding a new folder with minimal changes elsewhere.
- Layered enforceability β Clean Architecture dependency rules prevent logic leakage.
- Scalability β Slices can be developed, tested, and maintained independently.
| Concern | Convention |
|---|---|
| Commands | Mutate state, call SaveChangesAsync, wrapped in implicit transactions |
| Queries | Read-only projections with AsNoTracking(), return DTOs |
| Result Pattern | All handlers return Result<T> or Result β never throw for control flow |
Instead of MediatR, the project uses a custom mediator built on native DI:
IRequest<TResponse>β marker interface for all requests (covariant).IRequestHandler<TRequest, TResponse>β handler contract.IMediator/Mediatorβ resolves handlers dynamically viaIServiceProviderand reflection.AddMediator(params Assembly[])β scans assemblies and registers handlers as scoped services.
| Pattern | Usage |
|---|---|
| Result Pattern | All operations return Result<T> or Result. Centralized HTTP mapping via ResultExtensions. |
| Soft Delete | All entities implement ISoftDeletableEntity. Global query filters exclude deleted rows. |
| Audit Trail | CreatedAtUtc, CreatedBy, ModifiedAtUtc, ModifiedBy auto-set via AuditInterceptor. |
| Multi-Tenancy | TenantId on all tenant-scoped tables. Global query filters + TenantSaveChangesInterceptor. |
| Repository via DbContext | No explicit repository interfaces. Data access through module-specific DbContexts. |
| Hybrid Caching | Redis-backed HybridCache with tenant-isolated cache keys. |
| Global Query Filters | EF Core filters for IsDeleted == false and TenantId == current. |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /api/tenants |
Create a new tenant | No |
| GET | /api/tenants/by-slug/{slug} |
Get tenant by slug | No |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /api/users/register |
Register a new user | No |
| POST | /api/users/login |
Login and receive JWT token | No |
| GET | /api/users/me |
Get current user profile | Yes |
| POST | /api/users/{id}/deactivate |
Deactivate a user | Yes |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /api/catalog/products |
Create a product | Yes |
| GET | /api/catalog/products/{id} |
Get product by ID | Yes |
| GET | /api/catalog/products/by-sku/{sku} |
Get product by SKU | Yes |
| GET | /api/catalog/products?searchTerm&categoryId&page&pageSize |
Search products (paginated) | Yes |
| PUT | /api/catalog/products/{id} |
Update a product | Yes |
| DELETE | /api/catalog/products/{id} |
Delete a product | Yes |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /api/catalog/categories |
Create a category | Yes |
| GET | /api/catalog/categories/{id} |
Get category by ID | Yes |
| GET | /api/catalog/categories/by-slug/{slug} |
Get category by slug | Yes |
| GET | /api/catalog/categories |
List all categories | Yes |
| PUT | /api/catalog/categories/{id} |
Update a category | Yes |
| DELETE | /api/catalog/categories/{id} |
Delete a category | Yes |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /api/ordering/orders |
Create an order | Yes |
| POST | /api/ordering/orders/{id}/items |
Add item to order | Yes |
| POST | /api/ordering/orders/{id}/submit |
Submit an order | Yes |
| GET | /api/ordering/orders?page&pageSize |
List orders (paginated) | Yes |
| GET | /api/ordering/orders/{id} |
Get order by ID | Yes |
| DELETE | /api/ordering/orders/{id} |
Cancel an order | Yes |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /api/customers |
Create a customer profile | Yes |
| GET | /api/customers/me |
Get current customer profile | Yes |
| PUT | /api/customers/me |
Update current customer profile | Yes |
| Method | Route | Description | Auth |
|---|---|---|---|
| GET | /api/cart |
Get current customer's cart | Yes |
| POST | /api/cart/items |
Add item to cart | Yes |
| PUT | /api/cart/items/{productId}?quantity |
Update cart item quantity | Yes |
| DELETE | /api/cart/items/{productId} |
Remove item from cart | Yes |
| POST | /api/cart/checkout |
Checkout (convert cart to order) | Yes |
| DELETE | /api/cart |
Clear entire cart | Yes |
- Single PostgreSQL database shared across all modules.
- Every tenant-owned table has
TenantIdcolumn (Guid, FK totenants.id). - EF Core global query filters enforce tenant isolation at query level.
TenantSaveChangesInterceptorauto-populatesTenantIdon new records.- Every unique business key includes
TenantId(composite unique constraint).
| Table | Tenant-Scoped | Key Unique Constraint | Notes |
|---|---|---|---|
tenants |
No | slug UNIQUE |
No tenant filter applied |
users |
Yes | (tenant_id, email) |
BCrypt password hash stored |
categories |
Yes | (tenant_id, slug) |
Self-referencing parent_category_id |
products |
Yes | (tenant_id, sku) |
category_id FK nullable, CHECK price > 0, qty >= 0 |
orders |
Yes | β | References user_id, status as int |
order_items |
Yes | β | FK to orders, products. CHECK qty > 0 |
customers |
Yes | (tenant_id, user_id) |
Links to users table |
carts |
Yes | (tenant_id, customer_id) |
One cart per customer |
cart_items |
Yes | β | FK to carts, products |
All tenant-scoped indexes start with tenant_id:
ix_users_tenant_emailix_products_tenant_skuix_categories_tenant_slugix_orders_tenant_userix_customers_tenant_userix_carts_tenant_customer
app.UseSerilogRequestLogging();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMiddleware<TenantResolutionMiddleware>();
app.UseAuthorization();- Secret Key β Symmetric signing key (
Jwt:SecretKey) - Issuer β Token issuer (
Jwt:Issuer) - Audience β Token audience (
Jwt:Audience) - Expiration β Configurable lifetime (
Jwt:ExpirationInMinutes) - Claims β
sub(user ID),email,tenant_id,tenant_slug
TenantContextreadsX-Tenant-IdHTTP header fromIHttpContextAccessor.TenantResolutionMiddlewarevalidates that if JWTtenant_idclaim is present, it matches theX-Tenant-Idheader. Returns 403 on mismatch.
- Abstraction:
IPasswordHasherinStackCart.BuildingBlocks.Application.Security - Implementation:
BCryptPasswordHasherinStackCart.Users.Infrastructure.SecurityusingBCrypt.Net-Next
Applied in ApplicationDbContext.OnModelCreating:
ISoftDeletableEntityβe.IsDeleted == falseITenantScopedβe.TenantId == _tenantContext.TenantId
This project is licensed under the MIT License.
Built with β€οΈ by Ahmed Anwer