zaruyashar/ekomart-ecommerce

CSS

0

78 commits

updated Jul 12, 2026

See the code

README

πŸ›’ Ekomart β€” Final Project (SoftITO Backend Program)

ASP.NET Core MVC (.NET 10) e-commerce platform β€” N-tier architecture, dual-ORM data access (EF Core + Dapper), Identity + Google OAuth + JWT, RBAC, persistent cart, PDF/Excel/QR generation, AI-powered review moderation, and a production deployment behind Cloudflare.

πŸ”— Live demo: nej.software πŸŽ“ Program archive (all 11 projects): softITO-backend-2026

A note on this README: Our final presentation day was replaced at the last minute with a final exam + class boat trip, so there was no in-person walkthrough β€” no slide deck, no live demo, no Cloudflare/architecture talk-through in front of the class. Everything that would have been said out loud is written down here instead: the planning, the architectural decisions (and why, not just what), the folder structure, and screenshot evidence for every major feature, organized by area and collapsed by default so this stays readable.


Table of Contents


Tech Stack

Core Framework

  • ASP.NET Core MVC, .NET 10
  • Razor Views + partials, hand-ported from a static HTML/React template (no JSX reused β€” the template was visual reference only, confirmed to have zero real backend logic)

Architecture

  • N-tier: Entities β†’ Data β†’ Business β†’ Api / Web, one-directional, compiler-enforced
  • Repository + Unit of Work pattern, ORM-agnostic interfaces (IRepository<T>, IUnitOfWork)
  • CQRS-lite: EF Core for transactional writes, Dapper for read-heavy/paginated queries β€” chosen per-operation by the service layer, not split into separate deployed APIs

Data Access

  • Microsoft.EntityFrameworkCore.SqlServer, EF Core Tools/Design (migrations)
  • Dapper (raw SQL reads: dashboard stats, paginated admin lists, filtered searches)
  • SQL Server (Azure SQL Database in production)
  • Explicit per-relationship FK cascade design (Restrict on convergent paths, Cascade on single paths)

Authentication & Authorization

  • ASP.NET Core Identity (cookie-based), two roles: Customer, Admin
  • Google OAuth (external login, customer-facing only)
  • JWT Bearer authentication for the separate Ekomart.Api project
  • Role-based access control enforced server-side at controller/Area level
  • Least-privilege admin provisioning: no public admin signup β€” seed account + in-app role promotion only
  • Role-aware sliding-expiration inactivity timeout (15 min Admin, 30 min Customer)
  • Customer soft-deletion (IsActive flag), login-blocked across all three auth surfaces (Identity, Google, JWT)

Caching

  • IMemoryCache, selectively applied to read-heavy catalog data with explicit write-path invalidation

Logging

  • Serilog (console + SQL Server sink), structured request logging + explicit business-event logging as separate concerns
  • Automatic 30-day log retention via a lightweight IHostedService
  • Reverse-proxy-aware client IP capture (Cloudflare X-Forwarded-For/X-Forwarded-Proto)

Documents / Exports

  • QuestPDF β€” order confirmation PDF receipts with embedded QR codes (Community license tier)
  • QRCoder β€” QR generation, deep-linking to the order tracking tab
  • ClosedXML β€” admin Excel exports (Orders, Transactions)

AI / Content Moderation

  • Hugging Face Inference API integration for product review moderation
  • Translation step (Helsinki-NLP/opus-mt-tr-en) β†’ toxicity classification (unitary/toxic-bert)
  • Automated Approved / Hidden / Pending gating with manual admin override
  • Resilient IHttpClientFactory-based HTTP client with categorized failure logging (auth, rate-limit, cold-model, network, timeout)

Mapping / Validation

  • AutoMapper (entity ↔ DTO mapping β€” DTOs only ever returned to controllers, never raw entities)
  • FluentValidation (registration, address, checkout, and other user-submitted input)

API Documentation

  • Scalar.AspNetCore + Microsoft.AspNetCore.OpenApi (Swashbuckle explicitly not used β€” incompatible with .NET 10's newer minimal-hosting OpenAPI pipeline)

Frontend / UX

  • SweetAlert2 (custom-themed, project-wide) replacing all native browser alert/confirm dialogs
  • Fully responsive (desktop, tablet, mobile), including a redesigned mobile navigation pattern
  • Server-side partial/substring product search

Commerce

  • Persistent, database-backed cart (no session/localStorage cart), authenticated-Customer-only
  • Real-time stock re-validation at checkout completion (prevents overselling)
  • Atomic multi-table transaction on order placement (Order + OrderItems + stock decrement + Payment + Transaction + cart-clear β€” all-or-nothing)
  • Simulated/mock payment processing (real linked Payment/Transaction rows, no live gateway)
  • Threshold-based shipping calculation ($70 free-shipping cutoff)

DevOps / Deployment

  • Hosted on Azure App Service (Web + separate Api), Azure SQL Database
  • GitHub Actions CI/CD, per-project build/publish targeting
  • Cloudflare-proxied custom domain (nej.software) with forwarded-headers trust configuration
  • Environment-variable-based secrets management in production (zero hardcoded credentials)

Architecture

Ekomart.Entities  β†’  Ekomart.Data  β†’  Ekomart.Business  β†’  Ekomart.Api / Ekomart (Web)
   (POCOs,             (EF Core +        (services,           (thin API           (MVC
   zero deps)          Dapper,           caching,             controllers,        presentation,
                       Repo/UoW)         validation)          JWT, Scalar)        Areas, Identity)

One-directional, compiler-enforced dependencies. Entities has zero framework dependencies. Only Data touches EF Core/Dapper directly β€” Business never sees AppDbContext, only IRepository<T>/IUnitOfWork. Both Api and Web depend on Business; Web additionally references Data directly since it owns DI registration (Program.cs) for the EF DbContext and Identity stores β€” a hosting concern, not a layering violation.

Data pipeline in short: reads flow through IMemoryCache β†’ Dapper read repositories β†’ flat DTOs (no change tracking). Writes flow through the EF Core repository path, wrapped in a Unit of Work transaction, with cache invalidation on success and a separate Serilog business-event log entry.


Folder Structure

EKOMART.sln
β”‚
β”œβ”€β”€ πŸ“¦ EKOMART.Entities/                  # POCOs only β€” zero project dependencies
β”‚   β”œβ”€β”€ Concrete/
β”‚   β”‚   └── Enums/
β”‚   └── Identity/                         # ApplicationUser, ApplicationRole
β”‚
β”œβ”€β”€ πŸ—„οΈ  EKOMART.Data/                      # EF configs + Dapper repos, Repo/UoW impl
β”‚   β”œβ”€β”€ Abstract/                         # IRepository<T>, IUnitOfWork interfaces
β”‚   β”œβ”€β”€ Concrete/
β”‚   β”‚   β”œβ”€β”€ EfCore/
β”‚   β”‚   β”‚   └── Configurations/           # EF entity type configs
β”‚   β”‚   └── Dapper/                       # connection factory, SQL read repos
β”‚   β”œβ”€β”€ Identity/
β”‚   β”œβ”€β”€ Migrations/
β”‚   β”œβ”€β”€ Seed/
β”‚   └── UnitOfWork/
β”‚
β”œβ”€β”€ βš™οΈ  EKOMART.Business/                  # service layer, caching, validation
β”‚   β”œβ”€β”€ Abstract/
β”‚   β”œβ”€β”€ Concrete/
β”‚   β”œβ”€β”€ Caching/
β”‚   β”œβ”€β”€ Dtos/                             # ⭐ added mid-project, one folder per entity
β”‚   β”‚   β”œβ”€β”€ Address/  Brand/  Cart/  Category/  Dashboard/
β”‚   β”‚   β”œβ”€β”€ Identity/  Order/  OrderItem/  Payment/
β”‚   β”‚   └── Product/  Review/  Transaction/  Vendor/
β”‚   β”œβ”€β”€ Mapping/                          # AutoMapper profile
β”‚   └── Validation/                       # FluentValidation rules
β”‚
β”œβ”€β”€ 🌐 EKOMART.Api/                        # thin API controllers, JWT-secured
β”‚   β”œβ”€β”€ Controllers/
β”‚   β”œβ”€β”€ Middleware/
β”‚   β”œβ”€β”€ Models/
β”‚   β”œβ”€β”€ OpenApi/                          # Scalar/OpenAPI config
β”‚   β”œβ”€β”€ Properties/
β”‚   └── Services/
β”‚
└── πŸ–₯️  EKOMART/                           # MVC presentation layer (Web)
    β”œβ”€β”€ Areas/
    β”‚   β”œβ”€β”€ Admin/
    β”‚   β”‚   β”œβ”€β”€ Controllers/
    β”‚   β”‚   β”œβ”€β”€ Models/
    β”‚   β”‚   └── Views/
    β”‚   β”‚       β”œβ”€β”€ Brands/  Categories/  Dashboard/  Orders/
    β”‚   β”‚       └── Products/  Reviews/  Transactions/  Users/  Vendors/
    β”‚   └── Customer/                     # ⚠️ empty β€” kept for symmetry, unused
    β”‚       β”œβ”€β”€ Controllers/
    β”‚       β”œβ”€β”€ Models/
    β”‚       └── Views/
    β”‚
    β”œβ”€β”€ Controllers/                      # storefront (public, no area)
    β”œβ”€β”€ Views/
    β”‚   β”œβ”€β”€ About/  Account/  Cart/  Checkout/
    β”‚   β”œβ”€β”€ Favorites/  Home/  Orders/  Shop/
    β”‚   └── Shared/                       # _StoreLayout, _AdminLayout
    β”‚
    β”œβ”€β”€ Models/                           # view models, not domain entities
    β”œβ”€β”€ Services/                         # PDF/Excel/QR generation
    β”œβ”€β”€ Logs/                             # Serilog file-sink fallback
    β”œβ”€β”€ Properties/
    └── wwwroot/
        β”œβ”€β”€ assets/                       # storefront CSS/JS/images/fonts
        β”œβ”€β”€ assets-admin/                 # admin dashboard CSS/JS/images
        β”œβ”€β”€ css/  js/
        └── lib/                          # SweetAlert2, Bootstrap, jQuery

Customer & Admin Journeys

Customer: anonymous storefront browsing (Dapper reads, [AllowAnonymous]) β†’ register/login (Identity or "Sign in with Google", always assigned the Customer role) β†’ My Account (Dashboard, Orders, Track Order, My Address, Account Details) β†’ cart β†’ checkout, wrapped in a single Unit of Work transaction β†’ PDF receipt with embedded QR code linking back to Track Order.

Admin: dedicated /Admin/Login (Identity-only, no OAuth) β†’ /Admin/Dashboard (cached Dapper aggregate stats) β†’ Products/Vendors/Orders/Reviews CRUD (EF Core writes) β†’ Manage Users with server-side search + pagination (handles 100k+ users without loading them all into memory) β†’ Excel export via ClosedXML. Admin role is flat, not row-scoped β€” a second admin account has identical access to the first, by design.


Screenshots

🏬 Storefront & Shopping β€” landing page, shop grid, quick view, favorites, Google sign-in
Storefront 1Storefront 2
Storefront 3Storefront 4
Shop grid 1Shop grid 2
Shop grid 3Quick view 1
Quick view 2Quick view zoom
Add to favoritesSign in with Google
πŸ›’ Cart, Checkout & Account β€” checkout flow, addresses, orders, invoices, tracking
Checkout pageCheckout address notification
Address added at checkoutOrder complete
Order complete invoiceOrder tracking
Orders listUser account details 1
User account details 2
πŸ› οΈ Admin Dashboard & CRUD β€” dashboard, products, orders, reviews, transactions, Excel export
Admin dashboard 1Admin dashboard 2
Admin categoriesAdmin products pagination
Admin order detailsAdmin order status change (SweetAlert2)
Admin orders filteringAdmin orders view
Admin review 1Admin review visibility status change
Admin transactionsExcel export of selected view results
πŸ” RBAC & User Management β€” role promotion, manage users, enforced admin/customer separation
Admin manage usersAdmin account settings
RBAC admin promotionAdmin can view storefront but can't act as customer 1
Admin can view storefront but can't act as customer 2
πŸ€– AI Review Moderation (Hugging Face) β€” translation + toxicity scoring pipeline
Turkish translation supportToxic comment analysis in progress
Toxic comment analysis completeToxic comment analysis complete, comment posted
Toxic comment blocked, pending admin approval
πŸ“± Responsive Design β€” mobile & tablet layouts
Mobile 1Mobile 2
Mobile 3Tablet 1
Tablet 2Tablet 3
☁️ Deployment β€” Cloudflare-proxied production deployment
Cloudflare config 1Cloudflare config 2
Live deployment overview

Live at nej.software, proxied through Cloudflare with forwarded-headers trust configured so Serilog logs the real client IP rather than Cloudflare's edge IP.


Architectural Decisions Log

Click to expand β€” full decisions list with tradeoffs

Foundational / Layering

  • N-tier, one-directional: Entities β†’ Data β†’ Business β†’ Api/Web. Entities has zero framework deps. Only Data touches EF Core/Dapper. Tradeoff: compiler-enforced boundaries over convention-based discipline.
  • Web references Data directly too (not just Business) β€” needed for Program.cs to register AppDbContext/Identity stores. Tradeoff: a hosting concern, not a layering violation β€” accepted as pragmatic.
  • CQRS-lite: EF Core for transactional writes, Dapper for read-heavy/paginated queries, behind the same hand-written IRepository<T>/IUnitOfWork interfaces. Tradeoff: real-world dual-ORM pattern over a single-ORM "simpler" build, chosen because it's what the spec was actually testing.
  • GetQueryable() added to IRepository<T> after Business-layer services were found bypassing the repository (injecting AppDbContext directly) to support paging. Tradeoff: extended the abstraction rather than accept the leak β€” verified via full-codebase grep, not just claimed.
  • /Shop's public listing stayed EF-based, not migrated to Dapper, despite being read-heavy. Tradeoff: known, accepted inconsistency β€” flagged as a backlog item, not silently ignored.
  • FK cascade behavior set per-relationship: Restrict on convergent paths (Product, Brand, cross-user Review edges), Cascade on single paths (Orderβ†’User, Cartβ†’User). Tradeoff: SQL Server hard-rejects multi-cascade-path migrations β€” this was deliberate design, not discovered by accident.
  • IDesignTimeDbContextFactory kept separate from runtime DI, since a class library has no Program.cs of its own for dotnet ef to resolve against.

Auth / Security

  • ASP.NET Core Identity, two roles (Customer/Admin) β€” not custom session auth. Tradeoff: required by spec; also composes with JWT for the Api project in a way session auth wouldn't.
  • Google OAuth for Customer login only β€” Admin is Identity-only, no external redirect surface at all. Tradeoff: deliberate security separation over auth-method uniformity.
  • Admin accounts only via seed or in-app promotion β€” public registration always assigns Customer, never user-selectable. Tradeoff: closes a privilege-escalation path entirely, at the cost of no self-service admin signup (by design).
  • Promotion screen blocks promoting Google-only accounts (no local password) to Admin, since Admin login has no OAuth path β€” would create an unreachable admin.
  • RBAC enforced server-side via [Authorize(Roles=...)] at controller/Area level, not just hidden nav links. Real bug this caught: an Admin could originally add items to Cart via direct URL navigation, since CartController only had a plain [Authorize] β€” fixed to Roles = "Customer".
  • Manage Users is server-side searched + paginated (Dapper LIKE + OFFSET/FETCH), explicitly rejecting a .Users.ToList() pattern seen in a reference project, which breaks at real scale.
  • Customer soft-delete (IsActive flag), not hard delete β€” no cascading removal of Orders/Reviews/Addresses/Cart/Favorites. Tradeoff: preserves historical/financial data integrity over a "clean" full account wipe; email stays permanently claimed (not freed for re-registration) as the simpler of two workable options.
  • Login blocked for soft-deleted accounts across every auth surface β€” standard Identity, Google OAuth, and the separate JWT-based Api β€” via a CustomSignInManager.CanSignInAsync override plus an explicit IsActive check added to the Api's direct password-check path once that gap was found.
  • Role-aware inactivity session timeout: 15 min Admin, 30 min Customer, sliding, via OnValidatePrincipal (not a second cookie scheme). Tradeoff: more complex than splitting schemes, but avoids duplicating Identity's cookie infrastructure for a difference of degree, not kind.
  • "Remember Me" and inactivity timeout are orthogonal β€” a persisted login still respects the idle timeout; persistence β‰  exemption.
  • No hardcoded secrets, anywhere, ever β€” User Secrets locally, environment variables in production. Motivated by two prior reference projects found with live API keys committed to source.

Commerce Domain

  • No real payment gateway β€” Payment/Transaction rows are real and correctly linked, but the payment itself is simulated (SweetAlert2 confirmation + cosmetic method dropdown). Tradeoff: full schema honesty without production payment-processing complexity/risk, appropriate for a demo.
  • Persistent, DB-backed cart (Cart/CartItem tables) tied to UserId β€” not session, not client-only storage. Tradeoff: two extra tables for a cart that survives logout/device-switch, the industry-standard pattern for logged-in commerce.
  • Cart requires authenticated Customer β€” no guest cart, no cart-merge-on-login. Tradeoff: sidesteps the hardest part of real cart systems (merge-conflict resolution) since anonymous browsing was already fine without it.
  • Anonymous Add-to-Cart intent preserved through the login redirect (both standard and Google paths) β€” a small UX fix, not full guest-cart persistence.
  • Checkout requires a pre-existing saved Address β€” no inline address form at checkout. Forced "My Address" to become fully functional earlier than planned, closing a dependency gap proactively.
  • Stock re-validated at checkout completion, not just at add-to-cart β€” prevents overselling from stale cart state.
  • Order + OrderItems + stock decrement + Payment + Transaction + cart-clear as one Unit-of-Work transaction β€” all-or-nothing. This is the concrete proof-of-understanding for why UoW matters, not just that it exists.
  • New orders default to Processing, never auto-advance to Completed β€” a real bug (found and fixed) where checkout was hardcoding the terminal status; status changes are admin-only, manual, by design (no automation in this phase).
  • Real shipping-fee logic: free at β‰₯$70 subtotal, flat $1.99 fee below it, $0 for a genuinely empty cart β€” replaced an earlier hardcoded fake "Free Shipping" label with no underlying calculation.
  • Add-to-Cart race condition fixed at two levels: a client-side request-in-flight guard (prevents duplicate rapid-click requests) and an atomic SQL-level quantity increment server-side (fixes the underlying read-then-write lost-update bug, which would have persisted even with perfect client-side debouncing alone).

Cross-Cutting

  • IMemoryCache applied selectively (Product/Category reads), with explicit invalidation on writes β€” not blanket-cached everywhere. Tradeoff: demonstrates the requirement meaningfully rather than risking missed-invalidation bugs from over-applying it.
  • Serilog, SQL Server sink, generic request logging + explicit business-event logging as separate concerns, over a hand-rolled logger. Chosen for structured, well-known tooling over reinventing one.
  • Static-asset requests demoted to Debug level, kept out of the DB sink entirely β€” a real bug where every CSS/JS/image request was logged at Information, exploding the log table (500+ pages) for zero signal.
  • 30-day automatic log retention via a lightweight IHostedService, not a new job-scheduler dependency.
  • Dashboard's log panel converted to AJAX partial refresh, once usage volume made a full-page reload on every pagination click a real cost, not just a cosmetic nitpick.
  • QuestPDF + ClosedXML + QRCoder, with explicit Community-license-tier awareness (QuestPDF.Settings.License = LicenseType.Community).
  • QR code deep-links to the existing Track Order tab β€” deliberately not a new cargo-tracking UI, to keep the payment/fulfillment simulation honest about being a demo rather than implying real logistics.
  • AutoMapper as one flat MappingProfile, not one class per entity β€” accepted deviation from an original per-entity instruction, since splitting further would be over-engineering at this project's actual size.
  • Scalar + Microsoft.AspNetCore.OpenApi for API docs, Swashbuckle deliberately dropped β€” proved incompatible with .NET 10's newer minimal-hosting OpenAPI pipeline.
  • Hugging Face content moderation on reviews: translation (Helsinki-NLP/opus-mt-tr-en, always applied regardless of input language, no detection step) β†’ toxicity scoring (unitary/toxic-bert) β†’ auto Approved/Hidden/Pending, with a manual admin override β€” no separate moderation queue. Uses IHttpClientFactory, not raw HttpClient, avoiding socket exhaustion. A real endpoint migration bug was found and fixed (the old api-inference.huggingface.co domain stopped resolving; moved to router.huggingface.co), and a real EF Core default-value bug was found and fixed (silently downgrading every new Approved review to Pending).
  • AI/chatbot integration explicitly declined, despite being spec-allowed as optional β€” deliberate scope control given the project's already-large required surface area.
  • Cloudflare-aware forwarded-headers middleware, added at deployment time, with a fix to ensure Serilog logs the real client IP (not Cloudflare's edge IP) once headers are trusted.

Frontend / Process

  • Template used as visual reference only β€” hand-ported into Razor, none of its React/JSX reused directly; confirmed the template itself had zero real backend logic.
  • Product catalog built asset-driven β€” inventory the actual images first, name products to match what they depict, not the reverse (which produced a mismatch once and was corrected).
  • Phased, narrowly-scoped build order with explicit approval gates between phases.
  • Visual claims require actual screenshot evidence, not just a text report β€” established after several real bugs (a debug banner, mismatched images, a cart quantity bug) were reported "fixed" in text while still visibly broken.
  • Root-cause diagnosis required before patching, not guess-and-check β€” this pattern directly found and fixed several real, non-obvious bugs across the project (the HF endpoint failure, the ReviewStatus default-value collision, a CSS min-width:0 grid-sizing bug recurring in two separate places, the dual-cause Add-to-Cart race condition).
  • SMTP/email (confirmation, forgot-password, email-change) explicitly deferred, contingent on remaining time post-deployment β€” a deliberate scope decision, not an oversight.

Known Limitations / Backlog

  • SMTP/email flows (confirmation, forgot-password, email-change) were deferred, contingent on remaining time β€” deliberately scoped out rather than half-built. Will be coming as new feat.

Part of the SoftITO Backend Program β€” 320-hour backend engineering journey, Istanbul Chamber of Commerce.

And this, ladies and gentlemen, concludes our intense backend journey for the time being. Time to catch some sleep and enjoy a well-earned break β€” but I've already got a running list of things I can't wait to build next!

A peaceful night sky with a shooting star

zaruyashar/ekomart-ecommerce

CSS

0

78 commits

updated Jul 12, 2026

See the code

README

πŸ›’ Ekomart β€” Final Project (SoftITO Backend Program)

ASP.NET Core MVC (.NET 10) e-commerce platform β€” N-tier architecture, dual-ORM data access (EF Core + Dapper), Identity + Google OAuth + JWT, RBAC, persistent cart, PDF/Excel/QR generation, AI-powered review moderation, and a production deployment behind Cloudflare.

πŸ”— Live demo: nej.software πŸŽ“ Program archive (all 11 projects): softITO-backend-2026

A note on this README: Our final presentation day was replaced at the last minute with a final exam + class boat trip, so there was no in-person walkthrough β€” no slide deck, no live demo, no Cloudflare/architecture talk-through in front of the class. Everything that would have been said out loud is written down here instead: the planning, the architectural decisions (and why, not just what), the folder structure, and screenshot evidence for every major feature, organized by area and collapsed by default so this stays readable.


Table of Contents


Tech Stack

Core Framework

  • ASP.NET Core MVC, .NET 10
  • Razor Views + partials, hand-ported from a static HTML/React template (no JSX reused β€” the template was visual reference only, confirmed to have zero real backend logic)

Architecture

  • N-tier: Entities β†’ Data β†’ Business β†’ Api / Web, one-directional, compiler-enforced
  • Repository + Unit of Work pattern, ORM-agnostic interfaces (IRepository<T>, IUnitOfWork)
  • CQRS-lite: EF Core for transactional writes, Dapper for read-heavy/paginated queries β€” chosen per-operation by the service layer, not split into separate deployed APIs

Data Access

  • Microsoft.EntityFrameworkCore.SqlServer, EF Core Tools/Design (migrations)
  • Dapper (raw SQL reads: dashboard stats, paginated admin lists, filtered searches)
  • SQL Server (Azure SQL Database in production)
  • Explicit per-relationship FK cascade design (Restrict on convergent paths, Cascade on single paths)

Authentication & Authorization

  • ASP.NET Core Identity (cookie-based), two roles: Customer, Admin
  • Google OAuth (external login, customer-facing only)
  • JWT Bearer authentication for the separate Ekomart.Api project
  • Role-based access control enforced server-side at controller/Area level
  • Least-privilege admin provisioning: no public admin signup β€” seed account + in-app role promotion only
  • Role-aware sliding-expiration inactivity timeout (15 min Admin, 30 min Customer)
  • Customer soft-deletion (IsActive flag), login-blocked across all three auth surfaces (Identity, Google, JWT)

Caching

  • IMemoryCache, selectively applied to read-heavy catalog data with explicit write-path invalidation

Logging

  • Serilog (console + SQL Server sink), structured request logging + explicit business-event logging as separate concerns
  • Automatic 30-day log retention via a lightweight IHostedService
  • Reverse-proxy-aware client IP capture (Cloudflare X-Forwarded-For/X-Forwarded-Proto)

Documents / Exports

  • QuestPDF β€” order confirmation PDF receipts with embedded QR codes (Community license tier)
  • QRCoder β€” QR generation, deep-linking to the order tracking tab
  • ClosedXML β€” admin Excel exports (Orders, Transactions)

AI / Content Moderation

  • Hugging Face Inference API integration for product review moderation
  • Translation step (Helsinki-NLP/opus-mt-tr-en) β†’ toxicity classification (unitary/toxic-bert)
  • Automated Approved / Hidden / Pending gating with manual admin override
  • Resilient IHttpClientFactory-based HTTP client with categorized failure logging (auth, rate-limit, cold-model, network, timeout)

Mapping / Validation

  • AutoMapper (entity ↔ DTO mapping β€” DTOs only ever returned to controllers, never raw entities)
  • FluentValidation (registration, address, checkout, and other user-submitted input)

API Documentation

  • Scalar.AspNetCore + Microsoft.AspNetCore.OpenApi (Swashbuckle explicitly not used β€” incompatible with .NET 10's newer minimal-hosting OpenAPI pipeline)

Frontend / UX

  • SweetAlert2 (custom-themed, project-wide) replacing all native browser alert/confirm dialogs
  • Fully responsive (desktop, tablet, mobile), including a redesigned mobile navigation pattern
  • Server-side partial/substring product search

Commerce

  • Persistent, database-backed cart (no session/localStorage cart), authenticated-Customer-only
  • Real-time stock re-validation at checkout completion (prevents overselling)
  • Atomic multi-table transaction on order placement (Order + OrderItems + stock decrement + Payment + Transaction + cart-clear β€” all-or-nothing)
  • Simulated/mock payment processing (real linked Payment/Transaction rows, no live gateway)
  • Threshold-based shipping calculation ($70 free-shipping cutoff)

DevOps / Deployment

  • Hosted on Azure App Service (Web + separate Api), Azure SQL Database
  • GitHub Actions CI/CD, per-project build/publish targeting
  • Cloudflare-proxied custom domain (nej.software) with forwarded-headers trust configuration
  • Environment-variable-based secrets management in production (zero hardcoded credentials)

Architecture

Ekomart.Entities  β†’  Ekomart.Data  β†’  Ekomart.Business  β†’  Ekomart.Api / Ekomart (Web)
   (POCOs,             (EF Core +        (services,           (thin API           (MVC
   zero deps)          Dapper,           caching,             controllers,        presentation,
                       Repo/UoW)         validation)          JWT, Scalar)        Areas, Identity)

One-directional, compiler-enforced dependencies. Entities has zero framework dependencies. Only Data touches EF Core/Dapper directly β€” Business never sees AppDbContext, only IRepository<T>/IUnitOfWork. Both Api and Web depend on Business; Web additionally references Data directly since it owns DI registration (Program.cs) for the EF DbContext and Identity stores β€” a hosting concern, not a layering violation.

Data pipeline in short: reads flow through IMemoryCache β†’ Dapper read repositories β†’ flat DTOs (no change tracking). Writes flow through the EF Core repository path, wrapped in a Unit of Work transaction, with cache invalidation on success and a separate Serilog business-event log entry.


Folder Structure

EKOMART.sln
β”‚
β”œβ”€β”€ πŸ“¦ EKOMART.Entities/                  # POCOs only β€” zero project dependencies
β”‚   β”œβ”€β”€ Concrete/
β”‚   β”‚   └── Enums/
β”‚   └── Identity/                         # ApplicationUser, ApplicationRole
β”‚
β”œβ”€β”€ πŸ—„οΈ  EKOMART.Data/                      # EF configs + Dapper repos, Repo/UoW impl
β”‚   β”œβ”€β”€ Abstract/                         # IRepository<T>, IUnitOfWork interfaces
β”‚   β”œβ”€β”€ Concrete/
β”‚   β”‚   β”œβ”€β”€ EfCore/
β”‚   β”‚   β”‚   └── Configurations/           # EF entity type configs
β”‚   β”‚   └── Dapper/                       # connection factory, SQL read repos
β”‚   β”œβ”€β”€ Identity/
β”‚   β”œβ”€β”€ Migrations/
β”‚   β”œβ”€β”€ Seed/
β”‚   └── UnitOfWork/
β”‚
β”œβ”€β”€ βš™οΈ  EKOMART.Business/                  # service layer, caching, validation
β”‚   β”œβ”€β”€ Abstract/
β”‚   β”œβ”€β”€ Concrete/
β”‚   β”œβ”€β”€ Caching/
β”‚   β”œβ”€β”€ Dtos/                             # ⭐ added mid-project, one folder per entity
β”‚   β”‚   β”œβ”€β”€ Address/  Brand/  Cart/  Category/  Dashboard/
β”‚   β”‚   β”œβ”€β”€ Identity/  Order/  OrderItem/  Payment/
β”‚   β”‚   └── Product/  Review/  Transaction/  Vendor/
β”‚   β”œβ”€β”€ Mapping/                          # AutoMapper profile
β”‚   └── Validation/                       # FluentValidation rules
β”‚
β”œβ”€β”€ 🌐 EKOMART.Api/                        # thin API controllers, JWT-secured
β”‚   β”œβ”€β”€ Controllers/
β”‚   β”œβ”€β”€ Middleware/
β”‚   β”œβ”€β”€ Models/
β”‚   β”œβ”€β”€ OpenApi/                          # Scalar/OpenAPI config
β”‚   β”œβ”€β”€ Properties/
β”‚   └── Services/
β”‚
└── πŸ–₯️  EKOMART/                           # MVC presentation layer (Web)
    β”œβ”€β”€ Areas/
    β”‚   β”œβ”€β”€ Admin/
    β”‚   β”‚   β”œβ”€β”€ Controllers/
    β”‚   β”‚   β”œβ”€β”€ Models/
    β”‚   β”‚   └── Views/
    β”‚   β”‚       β”œβ”€β”€ Brands/  Categories/  Dashboard/  Orders/
    β”‚   β”‚       └── Products/  Reviews/  Transactions/  Users/  Vendors/
    β”‚   └── Customer/                     # ⚠️ empty β€” kept for symmetry, unused
    β”‚       β”œβ”€β”€ Controllers/
    β”‚       β”œβ”€β”€ Models/
    β”‚       └── Views/
    β”‚
    β”œβ”€β”€ Controllers/                      # storefront (public, no area)
    β”œβ”€β”€ Views/
    β”‚   β”œβ”€β”€ About/  Account/  Cart/  Checkout/
    β”‚   β”œβ”€β”€ Favorites/  Home/  Orders/  Shop/
    β”‚   └── Shared/                       # _StoreLayout, _AdminLayout
    β”‚
    β”œβ”€β”€ Models/                           # view models, not domain entities
    β”œβ”€β”€ Services/                         # PDF/Excel/QR generation
    β”œβ”€β”€ Logs/                             # Serilog file-sink fallback
    β”œβ”€β”€ Properties/
    └── wwwroot/
        β”œβ”€β”€ assets/                       # storefront CSS/JS/images/fonts
        β”œβ”€β”€ assets-admin/                 # admin dashboard CSS/JS/images
        β”œβ”€β”€ css/  js/
        └── lib/                          # SweetAlert2, Bootstrap, jQuery

Customer & Admin Journeys

Customer: anonymous storefront browsing (Dapper reads, [AllowAnonymous]) β†’ register/login (Identity or "Sign in with Google", always assigned the Customer role) β†’ My Account (Dashboard, Orders, Track Order, My Address, Account Details) β†’ cart β†’ checkout, wrapped in a single Unit of Work transaction β†’ PDF receipt with embedded QR code linking back to Track Order.

Admin: dedicated /Admin/Login (Identity-only, no OAuth) β†’ /Admin/Dashboard (cached Dapper aggregate stats) β†’ Products/Vendors/Orders/Reviews CRUD (EF Core writes) β†’ Manage Users with server-side search + pagination (handles 100k+ users without loading them all into memory) β†’ Excel export via ClosedXML. Admin role is flat, not row-scoped β€” a second admin account has identical access to the first, by design.


Screenshots

🏬 Storefront & Shopping β€” landing page, shop grid, quick view, favorites, Google sign-in
Storefront 1Storefront 2
Storefront 3Storefront 4
Shop grid 1Shop grid 2
Shop grid 3Quick view 1
Quick view 2Quick view zoom
Add to favoritesSign in with Google
πŸ›’ Cart, Checkout & Account β€” checkout flow, addresses, orders, invoices, tracking
Checkout pageCheckout address notification
Address added at checkoutOrder complete
Order complete invoiceOrder tracking
Orders listUser account details 1
User account details 2
πŸ› οΈ Admin Dashboard & CRUD β€” dashboard, products, orders, reviews, transactions, Excel export
Admin dashboard 1Admin dashboard 2
Admin categoriesAdmin products pagination
Admin order detailsAdmin order status change (SweetAlert2)
Admin orders filteringAdmin orders view
Admin review 1Admin review visibility status change
Admin transactionsExcel export of selected view results
πŸ” RBAC & User Management β€” role promotion, manage users, enforced admin/customer separation
Admin manage usersAdmin account settings
RBAC admin promotionAdmin can view storefront but can't act as customer 1
Admin can view storefront but can't act as customer 2
πŸ€– AI Review Moderation (Hugging Face) β€” translation + toxicity scoring pipeline
Turkish translation supportToxic comment analysis in progress
Toxic comment analysis completeToxic comment analysis complete, comment posted
Toxic comment blocked, pending admin approval
πŸ“± Responsive Design β€” mobile & tablet layouts
Mobile 1Mobile 2
Mobile 3Tablet 1
Tablet 2Tablet 3
☁️ Deployment β€” Cloudflare-proxied production deployment
Cloudflare config 1Cloudflare config 2
Live deployment overview

Live at nej.software, proxied through Cloudflare with forwarded-headers trust configured so Serilog logs the real client IP rather than Cloudflare's edge IP.


Architectural Decisions Log

Click to expand β€” full decisions list with tradeoffs

Foundational / Layering

  • N-tier, one-directional: Entities β†’ Data β†’ Business β†’ Api/Web. Entities has zero framework deps. Only Data touches EF Core/Dapper. Tradeoff: compiler-enforced boundaries over convention-based discipline.
  • Web references Data directly too (not just Business) β€” needed for Program.cs to register AppDbContext/Identity stores. Tradeoff: a hosting concern, not a layering violation β€” accepted as pragmatic.
  • CQRS-lite: EF Core for transactional writes, Dapper for read-heavy/paginated queries, behind the same hand-written IRepository<T>/IUnitOfWork interfaces. Tradeoff: real-world dual-ORM pattern over a single-ORM "simpler" build, chosen because it's what the spec was actually testing.
  • GetQueryable() added to IRepository<T> after Business-layer services were found bypassing the repository (injecting AppDbContext directly) to support paging. Tradeoff: extended the abstraction rather than accept the leak β€” verified via full-codebase grep, not just claimed.
  • /Shop's public listing stayed EF-based, not migrated to Dapper, despite being read-heavy. Tradeoff: known, accepted inconsistency β€” flagged as a backlog item, not silently ignored.
  • FK cascade behavior set per-relationship: Restrict on convergent paths (Product, Brand, cross-user Review edges), Cascade on single paths (Orderβ†’User, Cartβ†’User). Tradeoff: SQL Server hard-rejects multi-cascade-path migrations β€” this was deliberate design, not discovered by accident.
  • IDesignTimeDbContextFactory kept separate from runtime DI, since a class library has no Program.cs of its own for dotnet ef to resolve against.

Auth / Security

  • ASP.NET Core Identity, two roles (Customer/Admin) β€” not custom session auth. Tradeoff: required by spec; also composes with JWT for the Api project in a way session auth wouldn't.
  • Google OAuth for Customer login only β€” Admin is Identity-only, no external redirect surface at all. Tradeoff: deliberate security separation over auth-method uniformity.
  • Admin accounts only via seed or in-app promotion β€” public registration always assigns Customer, never user-selectable. Tradeoff: closes a privilege-escalation path entirely, at the cost of no self-service admin signup (by design).
  • Promotion screen blocks promoting Google-only accounts (no local password) to Admin, since Admin login has no OAuth path β€” would create an unreachable admin.
  • RBAC enforced server-side via [Authorize(Roles=...)] at controller/Area level, not just hidden nav links. Real bug this caught: an Admin could originally add items to Cart via direct URL navigation, since CartController only had a plain [Authorize] β€” fixed to Roles = "Customer".
  • Manage Users is server-side searched + paginated (Dapper LIKE + OFFSET/FETCH), explicitly rejecting a .Users.ToList() pattern seen in a reference project, which breaks at real scale.
  • Customer soft-delete (IsActive flag), not hard delete β€” no cascading removal of Orders/Reviews/Addresses/Cart/Favorites. Tradeoff: preserves historical/financial data integrity over a "clean" full account wipe; email stays permanently claimed (not freed for re-registration) as the simpler of two workable options.
  • Login blocked for soft-deleted accounts across every auth surface β€” standard Identity, Google OAuth, and the separate JWT-based Api β€” via a CustomSignInManager.CanSignInAsync override plus an explicit IsActive check added to the Api's direct password-check path once that gap was found.
  • Role-aware inactivity session timeout: 15 min Admin, 30 min Customer, sliding, via OnValidatePrincipal (not a second cookie scheme). Tradeoff: more complex than splitting schemes, but avoids duplicating Identity's cookie infrastructure for a difference of degree, not kind.
  • "Remember Me" and inactivity timeout are orthogonal β€” a persisted login still respects the idle timeout; persistence β‰  exemption.
  • No hardcoded secrets, anywhere, ever β€” User Secrets locally, environment variables in production. Motivated by two prior reference projects found with live API keys committed to source.

Commerce Domain

  • No real payment gateway β€” Payment/Transaction rows are real and correctly linked, but the payment itself is simulated (SweetAlert2 confirmation + cosmetic method dropdown). Tradeoff: full schema honesty without production payment-processing complexity/risk, appropriate for a demo.
  • Persistent, DB-backed cart (Cart/CartItem tables) tied to UserId β€” not session, not client-only storage. Tradeoff: two extra tables for a cart that survives logout/device-switch, the industry-standard pattern for logged-in commerce.
  • Cart requires authenticated Customer β€” no guest cart, no cart-merge-on-login. Tradeoff: sidesteps the hardest part of real cart systems (merge-conflict resolution) since anonymous browsing was already fine without it.
  • Anonymous Add-to-Cart intent preserved through the login redirect (both standard and Google paths) β€” a small UX fix, not full guest-cart persistence.
  • Checkout requires a pre-existing saved Address β€” no inline address form at checkout. Forced "My Address" to become fully functional earlier than planned, closing a dependency gap proactively.
  • Stock re-validated at checkout completion, not just at add-to-cart β€” prevents overselling from stale cart state.
  • Order + OrderItems + stock decrement + Payment + Transaction + cart-clear as one Unit-of-Work transaction β€” all-or-nothing. This is the concrete proof-of-understanding for why UoW matters, not just that it exists.
  • New orders default to Processing, never auto-advance to Completed β€” a real bug (found and fixed) where checkout was hardcoding the terminal status; status changes are admin-only, manual, by design (no automation in this phase).
  • Real shipping-fee logic: free at β‰₯$70 subtotal, flat $1.99 fee below it, $0 for a genuinely empty cart β€” replaced an earlier hardcoded fake "Free Shipping" label with no underlying calculation.
  • Add-to-Cart race condition fixed at two levels: a client-side request-in-flight guard (prevents duplicate rapid-click requests) and an atomic SQL-level quantity increment server-side (fixes the underlying read-then-write lost-update bug, which would have persisted even with perfect client-side debouncing alone).

Cross-Cutting

  • IMemoryCache applied selectively (Product/Category reads), with explicit invalidation on writes β€” not blanket-cached everywhere. Tradeoff: demonstrates the requirement meaningfully rather than risking missed-invalidation bugs from over-applying it.
  • Serilog, SQL Server sink, generic request logging + explicit business-event logging as separate concerns, over a hand-rolled logger. Chosen for structured, well-known tooling over reinventing one.
  • Static-asset requests demoted to Debug level, kept out of the DB sink entirely β€” a real bug where every CSS/JS/image request was logged at Information, exploding the log table (500+ pages) for zero signal.
  • 30-day automatic log retention via a lightweight IHostedService, not a new job-scheduler dependency.
  • Dashboard's log panel converted to AJAX partial refresh, once usage volume made a full-page reload on every pagination click a real cost, not just a cosmetic nitpick.
  • QuestPDF + ClosedXML + QRCoder, with explicit Community-license-tier awareness (QuestPDF.Settings.License = LicenseType.Community).
  • QR code deep-links to the existing Track Order tab β€” deliberately not a new cargo-tracking UI, to keep the payment/fulfillment simulation honest about being a demo rather than implying real logistics.
  • AutoMapper as one flat MappingProfile, not one class per entity β€” accepted deviation from an original per-entity instruction, since splitting further would be over-engineering at this project's actual size.
  • Scalar + Microsoft.AspNetCore.OpenApi for API docs, Swashbuckle deliberately dropped β€” proved incompatible with .NET 10's newer minimal-hosting OpenAPI pipeline.
  • Hugging Face content moderation on reviews: translation (Helsinki-NLP/opus-mt-tr-en, always applied regardless of input language, no detection step) β†’ toxicity scoring (unitary/toxic-bert) β†’ auto Approved/Hidden/Pending, with a manual admin override β€” no separate moderation queue. Uses IHttpClientFactory, not raw HttpClient, avoiding socket exhaustion. A real endpoint migration bug was found and fixed (the old api-inference.huggingface.co domain stopped resolving; moved to router.huggingface.co), and a real EF Core default-value bug was found and fixed (silently downgrading every new Approved review to Pending).
  • AI/chatbot integration explicitly declined, despite being spec-allowed as optional β€” deliberate scope control given the project's already-large required surface area.
  • Cloudflare-aware forwarded-headers middleware, added at deployment time, with a fix to ensure Serilog logs the real client IP (not Cloudflare's edge IP) once headers are trusted.

Frontend / Process

  • Template used as visual reference only β€” hand-ported into Razor, none of its React/JSX reused directly; confirmed the template itself had zero real backend logic.
  • Product catalog built asset-driven β€” inventory the actual images first, name products to match what they depict, not the reverse (which produced a mismatch once and was corrected).
  • Phased, narrowly-scoped build order with explicit approval gates between phases.
  • Visual claims require actual screenshot evidence, not just a text report β€” established after several real bugs (a debug banner, mismatched images, a cart quantity bug) were reported "fixed" in text while still visibly broken.
  • Root-cause diagnosis required before patching, not guess-and-check β€” this pattern directly found and fixed several real, non-obvious bugs across the project (the HF endpoint failure, the ReviewStatus default-value collision, a CSS min-width:0 grid-sizing bug recurring in two separate places, the dual-cause Add-to-Cart race condition).
  • SMTP/email (confirmation, forgot-password, email-change) explicitly deferred, contingent on remaining time post-deployment β€” a deliberate scope decision, not an oversight.

Known Limitations / Backlog

  • SMTP/email flows (confirmation, forgot-password, email-change) were deferred, contingent on remaining time β€” deliberately scoped out rather than half-built. Will be coming as new feat.

Part of the SoftITO Backend Program β€” 320-hour backend engineering journey, Istanbul Chamber of Commerce.

And this, ladies and gentlemen, concludes our intense backend journey for the time being. Time to catch some sleep and enjoy a well-earned break β€” but I've already got a running list of things I can't wait to build next!

A peaceful night sky with a shooting star