Full-Stack ASP.NET Core Project: AI-Powered E-Commerce Platform
Complete full-stack ASP.NET Core project tutorial. Build AI-powered e-commerce with microservices, cloud deployment, real-time features & production best practices.
Tags: ASPNETCoreFullStack,AIECommerce,Microservices,Blazor,CloudDeployment,Azure,AWS,ProductionReady,RealWorldProject,CleanArchitecture
Table of Contents
1. Project Overview & Architecture
1.1 Project Vision: SmartCommerce AI
Business Problem: Traditional e-commerce platforms struggle with personalization, real-time inventory, and intelligent customer engagement. Our solution addresses these challenges with AI-powered features.
Key Features:
AI-powered product recommendations
Real-time inventory management
Intelligent search with semantic understanding
Personalized pricing and promotions
Multi-vendor marketplace support
Advanced analytics and insights
1.2 System Architecture
1.3 Technology Stack
Backend: ASP.NET Core 8, Entity Framework Core, Clean Architecture
Frontend: Blazor WebAssembly, Blazor Server, MudBlazor
AI/ML: Azure Cognitive Services, ML.NET, OpenAI
Cloud: Azure/AWS, Docker, Kubernetes
Database: Azure SQL, Cosmos DB, Redis
Messaging: Azure Service Bus, SignalR
Monitoring: Application Insights, Serilog
2. Solution Structure & Setup
2.1 Solution Architecture
SmartCommerce/
├── src/
│   ├── SmartCommerce.Web/                 # Blazor WebAssembly Frontend
│   ├── SmartCommerce.Admin/               # Blazor Server Admin Panel
│   ├── SmartCommerce.Gateway/             # API Gateway
│   ├── SmartCommerce.Services.Product/    # Product Microservice
│   ├── SmartCommerce.Services.Order/      # Order Microservice
│   ├── SmartCommerce.Services.User/       # User Microservice
│   ├── SmartCommerce.Services.Recommendation/ # AI Recommendation Service
│   ├── SmartCommerce.Services.Search/     # AI Search Service
│   ├── SmartCommerce.Shared/              # Shared Models & Contracts
│   └── SmartCommerce.Infrastructure/      # Cross-cutting Infrastructure
├── tests/
│   ├── SmartCommerce.UnitTests/
│   ├── SmartCommerce.IntegrationTests/
│   └── SmartCommerce.LoadTests/
└── deployments/
    ├── docker-compose.yml
    ├── kubernetes/
    └── azure-pipelines.yml2.2 Project Setup & Configuration
<!-- Directory.Build.props --> <Project> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <AnalysisLevel>latest</AnalysisLevel> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <DebugType>embedded</DebugType> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)' == 'Release'"> <DebugType>none</DebugType> <PublishReadyToRun>true</PublishReadyToRun> <PublishTrimmed>true</PublishTrimmed> </PropertyGroup> </Project>
// Program.cs - Main Web Application using SmartCommerce.Infrastructure; using SmartCommerce.Web.Middleware; var builder = WebApplication.CreateBuilder(args); // Add configuration sources builder.Configuration .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) .AddEnvironmentVariables() .AddUserSecrets<Program>() .AddAzureKeyVault(ConfigurationHelpers.GetKeyVaultEndpoint(builder.Configuration)); // Configure infrastructure builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddApplicationServices(); builder.Services.AddWebServices(builder.Configuration); // Configure authentication and authorization builder.Services.AddAuthenticationServices(builder.Configuration); builder.Services.AddAuthorizationPolicies(); // Add health checks builder.Services.AddCustomHealthChecks(builder.Configuration); // Configure HTTP client factory with resilience builder.Services.AddHttpClientWithResilience(builder.Configuration); var app = builder.Build(); // Configure middleware pipeline if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseWebAssemblyDebugging(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); // Custom middleware app.UseRequestLogging(); app.UseSecurityHeaders(); app.UsePerformanceMonitoring(); // Health check endpoint app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); // API endpoints app.MapControllers(); app.MapRazorPages(); app.MapFallbackToFile("index.html"); // Application startup tasks await app.RunStartupTasksAsync(); app.Run();
2.3 Infrastructure Configuration
// Infrastructure/ServiceCollectionExtensions.cs using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; using SmartCommerce.Infrastructure.Caching; using SmartCommerce.Infrastructure.Data; using SmartCommerce.Infrastructure.Logging; namespace SmartCommerce.Infrastructure { public static class ServiceCollectionExtensions { public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) { // Database Contexts services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( configuration.GetConnectionString("DefaultConnection"), sqlOptions => { sqlOptions.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName); sqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); })); // Caching services.AddStackExchangeRedisCache(options => { options.Configuration = configuration.GetConnectionString("Redis"); options.InstanceName = "SmartCommerce_"; }); services.AddSingleton<IDistributedCache, RedisCache>(); services.AddSingleton<ICacheService, DistributedCacheService>(); // Message Bus services.AddAzureServiceBus(configuration); // File Storage services.AddAzureBlobStorage(configuration); // Email Services services.AddEmailServices(configuration); // Background Services services.AddHostedService<OrderProcessingService>(); services.AddHostedService<InventorySyncService>(); services.AddHostedService<AIModelTrainingService>(); // External Services services.AddPaymentServices(configuration); services.AddShippingServices(configuration); services.AddAIServices(configuration); return services; } public static IServiceCollection AddAIServices(this IServiceCollection services, IConfiguration configuration) { // Azure Cognitive Services services.AddAzureCognitiveServices(configuration); // ML.NET Models services.AddMLServices(configuration); // Custom AI Services services.AddScoped<IProductRecommender, AIProductRecommender>(); services.AddScoped<ISearchEnhancer, CognitiveSearchEnhancer>(); services.AddScoped<IPriceOptimizer, MLPriceOptimizer>(); services.AddScoped<ISentimentAnalyzer, AzureSentimentAnalyzer>(); return services; } public static IServiceCollection AddApplicationServices(this IServiceCollection services) { // MediatR for CQRS services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ApplicationLayerEntryPoint).Assembly)); // AutoMapper services.AddAutoMapper(typeof(ApplicationLayerEntryPoint).Assembly); // FluentValidation services.AddValidatorsFromAssembly(typeof(ApplicationLayerEntryPoint).Assembly); // Domain Services services.AddScoped<IOrderService, OrderService>(); services.AddScoped<IProductService, ProductService>(); services.AddScoped<IUserService, UserService>(); services.AddScoped<IInventoryService, InventoryService>(); return services; } } }
3. Domain Layer & Core Models
3.1 Domain-Driven Design Implementation
// Domain/Common/ValueObjects.cs using System.Diagnostics; namespace SmartCommerce.Domain.Common { public abstract class ValueObject : IEquatable<ValueObject> { protected abstract IEnumerable<object> GetEqualityComponents(); public override bool Equals(object? obj) { if (obj is null || obj.GetType() != GetType()) return false; var valueObject = (ValueObject)obj; return GetEqualityComponents().SequenceEqual(valueObject.GetEqualityComponents()); } public override int GetHashCode() { return GetEqualityComponents() .Select(x => x?.GetHashCode() ?? 0) .Aggregate((x, y) => x ^ y); } public bool Equals(ValueObject? other) => Equals((object?)other); public static bool operator ==(ValueObject left, ValueObject right) => Equals(left, right); public static bool operator !=(ValueObject left, ValueObject right) => !Equals(left, right); } public class Money : ValueObject { public decimal Amount { get; } public string Currency { get; } public Money(decimal amount, string currency = "USD") { if (amount < 0) throw new ArgumentException("Money amount cannot be negative"); if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Currency cannot be empty"); Amount = amount; Currency = currency.ToUpperInvariant(); } public static Money operator +(Money left, Money right) { ValidateSameCurrency(left, right); return new Money(left.Amount + right.Amount, left.Currency); } public static Money operator -(Money left, Money right) { ValidateSameCurrency(left, right); return new Money(left.Amount - right.Amount, left.Currency); } private static void ValidateSameCurrency(Money left, Money right) { if (left.Currency != right.Currency) throw new InvalidOperationException("Cannot perform operations on different currencies"); } protected override IEnumerable<object> GetEqualityComponents() { yield return Amount; yield return Currency; } public override string ToString() => $"{Amount:C} ({Currency})"; } public class Address : ValueObject { public string Street { get; } public string City { get; } public string State { get; } public string Country { get; } public string ZipCode { get; } public Address(string street, string city, string state, string country, string zipCode) { Street = street ?? throw new ArgumentNullException(nameof(street)); City = city ?? throw new ArgumentNullException(nameof(city)); State = state ?? throw new ArgumentNullException(nameof(state)); Country = country ?? throw new ArgumentNullException(nameof(country)); ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode)); } protected override IEnumerable<object> GetEqualityComponents() { yield return Street; yield return City; yield return State; yield return Country; yield return ZipCode; } } }
3.2 Core Domain Entities
// Domain/Entities/Product.cs using SmartCommerce.Domain.Common; using SmartCommerce.Domain.Events; namespace SmartCommerce.Domain.Entities { public class Product : AuditableEntity, IAggregateRoot { public Guid Id { get; private set; } public string Name { get; private set; } public string Description { get; private set; } public Money Price { get; private set; } public string Sku { get; private set; } public int StockQuantity { get; private set; } public int ReorderLevel { get; private set; } public bool IsActive { get; private set; } public bool IsDeleted { get; private set; } public Guid CategoryId { get; private set; } public Category Category { get; private set; } public Guid? VendorId { get; private set; } public Vendor? Vendor { get; private set; } // AI-Enhanced Properties public float AIScore { get; private set; } public string? AIKeywords { get; private set; } public DateTime? LastAIAnalysis { get; private set; } // Navigation Properties private readonly List<ProductImage> _images = new(); public IReadOnlyCollection<ProductImage> Images => _images.AsReadOnly(); private readonly List<ProductReview> _reviews = new(); public IReadOnlyCollection<ProductReview> Reviews => _reviews.AsReadOnly(); private readonly List<ProductTag> _tags = new(); public IReadOnlyCollection<ProductTag> Tags => _tags.AsReadOnly(); // Private constructor for EF Core private Product() { } public Product(string name, string description, Money price, string sku, Guid categoryId, Guid? vendorId = null, int stockQuantity = 0) { Id = Guid.NewGuid(); Name = name ?? throw new ArgumentNullException(nameof(name)); Description = description ?? throw new ArgumentNullException(nameof(description)); Price = price ?? throw new ArgumentNullException(nameof(price)); Sku = sku ?? throw new ArgumentNullException(nameof(sku)); CategoryId = categoryId; VendorId = vendorId; StockQuantity = stockQuantity; IsActive = true; IsDeleted = false; ReorderLevel = 10; // Default reorder level AddDomainEvent(new ProductCreatedEvent(Id, name, sku)); } public void UpdateDetails(string name, string description, Money price) { Name = name ?? throw new ArgumentNullException(nameof(name)); Description = description ?? throw new ArgumentNullException(nameof(description)); Price = price ?? throw new ArgumentNullException(nameof(price)); AddDomainEvent(new ProductUpdatedEvent(Id)); } public void UpdateStock(int quantity) { if (quantity < 0) throw new ArgumentException("Stock quantity cannot be negative"); var oldStock = StockQuantity; StockQuantity = quantity; AddDomainEvent(new ProductStockUpdatedEvent(Id, oldStock, quantity)); // Check if we need to reorder if (quantity <= ReorderLevel) { AddDomainEvent(new LowStockEvent(Id, Name, quantity, ReorderLevel)); } } public void AddStock(int quantity) { if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); UpdateStock(StockQuantity + quantity); } public void RemoveStock(int quantity) { if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); if (quantity > StockQuantity) throw new InvalidOperationException("Insufficient stock"); UpdateStock(StockQuantity - quantity); } public void Deactivate() { if (!IsActive) return; IsActive = false; AddDomainEvent(new ProductDeactivatedEvent(Id)); } public void Activate() { if (IsActive) return; IsActive = true; AddDomainEvent(new ProductActivatedEvent(Id)); } public void MarkAsDeleted() { if (IsDeleted) return; IsDeleted = true; IsActive = false; AddDomainEvent(new ProductDeletedEvent(Id)); } public void UpdateAIScore(float score, string keywords) { AIScore = Math.Clamp(score, 0, 1); AIKeywords = keywords; LastAIAnalysis = DateTime.UtcNow; AddDomainEvent(new ProductAIAnalyzedEvent(Id, score, keywords)); } public void AddImage(string imageUrl, string altText, bool isPrimary = false) { var image = new ProductImage(Id, imageUrl, altText, isPrimary); _images.Add(image); // If this is set as primary, ensure no other images are primary if (isPrimary) { foreach (var img in _images.Where(i => i.Id != image.Id && i.IsPrimary)) { img.SetAsSecondary(); } } } public void AddReview(Guid userId, int rating, string comment, string? title = null) { var review = new ProductReview(Id, userId, rating, comment, title); _reviews.Add(review); AddDomainEvent(new ProductReviewAddedEvent(Id, userId, rating)); } public void AddTag(string tagName) { if (_tags.Any(t => t.Name.Equals(tagName, StringComparison.OrdinalIgnoreCase))) return; var tag = new ProductTag(Id, tagName); _tags.Add(tag); } public decimal CalculateDiscountedPrice(decimal discountPercentage) { if (discountPercentage < 0 || discountPercentage > 100) throw new ArgumentException("Discount percentage must be between 0 and 100"); return Price.Amount * (1 - discountPercentage / 100); } public bool IsInStock() => StockQuantity > 0; public bool NeedsReorder() => StockQuantity <= ReorderLevel; public int AvailableStock() => Math.Max(0, StockQuantity); } public class ProductImage : Entity { public Guid Id { get; private set; } public Guid ProductId { get; private set; } public string ImageUrl { get; private set; } public string AltText { get; private set; } public bool IsPrimary { get; private set; } public int DisplayOrder { get; private set; } public DateTime CreatedAt { get; private set; } public Product Product { get; private set; } private ProductImage() { } public ProductImage(Guid productId, string imageUrl, string altText, bool isPrimary = false, int displayOrder = 0) { Id = Guid.NewGuid(); ProductId = productId; ImageUrl = imageUrl ?? throw new ArgumentNullException(nameof(imageUrl)); AltText = altText ?? throw new ArgumentNullException(nameof(altText)); IsPrimary = isPrimary; DisplayOrder = displayOrder; CreatedAt = DateTime.UtcNow; } public void SetAsPrimary() { IsPrimary = true; } public void SetAsSecondary() { IsPrimary = false; } public void UpdateDisplayOrder(int order) { DisplayOrder = order; } } }
3.3 Order Management Domain
// Domain/Entities/Order.cs using SmartCommerce.Domain.Enums; using SmartCommerce.Domain.Events; namespace SmartCommerce.Domain.Entities { public class Order : AuditableEntity, IAggregateRoot { public Guid Id { get; private set; } public string OrderNumber { get; private set; } public Guid CustomerId { get; private set; } public OrderStatus Status { get; private set; } public Money TotalAmount { get; private set; } public Money DiscountAmount { get; private set; } public Money TaxAmount { get; private set; } public Money ShippingAmount { get; private set; } public Money FinalAmount { get; private set; } public string Currency { get; private set; } // Shipping Information public Address ShippingAddress { get; private set; } public Address? BillingAddress { get; private set; } // Payment Information public string? PaymentMethod { get; private set; } public string? PaymentTransactionId { get; private set; } public DateTime? PaymentDate { get; private set; } // Shipping Information public string? ShippingMethod { get; private set; } public string? TrackingNumber { get; private set; } public DateTime? ShippedDate { get; private set; } public DateTime? DeliveredDate { get; private set; } // Navigation Properties private readonly List<OrderItem> _orderItems = new(); public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly(); private readonly List<OrderStatusHistory> _statusHistory = new(); public IReadOnlyCollection<OrderStatusHistory> StatusHistory => _statusHistory.AsReadOnly(); // Private constructor for EF Core private Order() { } public Order(Guid customerId, Address shippingAddress, Address? billingAddress = null, string currency = "USD") { Id = Guid.NewGuid(); OrderNumber = GenerateOrderNumber(); CustomerId = customerId; Status = OrderStatus.Pending; Currency = currency; ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress)); BillingAddress = billingAddress; // Initialize amounts TotalAmount = new Money(0, currency); DiscountAmount = new Money(0, currency); TaxAmount = new Money(0, currency); ShippingAmount = new Money(0, currency); FinalAmount = new Money(0, currency); AddStatusHistory(OrderStatus.Pending, "Order created"); AddDomainEvent(new OrderCreatedEvent(Id, OrderNumber, customerId)); } public void AddItem(Product product, int quantity, Money unitPrice) { if (product == null) throw new ArgumentNullException(nameof(product)); if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); if (unitPrice.Amount <= 0) throw new ArgumentException("Unit price must be positive"); // Check if item already exists var existingItem = _orderItems.FirstOrDefault(item => item.ProductId == product.Id); if (existingItem != null) { existingItem.UpdateQuantity(existingItem.Quantity + quantity); } else { var orderItem = new OrderItem(Id, product.Id, product.Name, quantity, unitPrice); _orderItems.Add(orderItem); } RecalculateTotals(); AddDomainEvent(new OrderItemAddedEvent(Id, product.Id, quantity)); } public void RemoveItem(Guid productId) { var item = _orderItems.FirstOrDefault(i => i.ProductId == productId); if (item != null) { _orderItems.Remove(item); RecalculateTotals(); AddDomainEvent(new OrderItemRemovedEvent(Id, productId)); } } public void UpdateItemQuantity(Guid productId, int quantity) { if (quantity <= 0) { RemoveItem(productId); return; } var item = _orderItems.FirstOrDefault(i => i.ProductId == productId); if (item != null) { item.UpdateQuantity(quantity); RecalculateTotals(); AddDomainEvent(new OrderItemQuantityUpdatedEvent(Id, productId, quantity)); } } public void ApplyDiscount(Money discount) { if (discount.Amount < 0) throw new ArgumentException("Discount cannot be negative"); if (discount.Amount > TotalAmount.Amount) throw new ArgumentException("Discount cannot exceed order total"); DiscountAmount = discount; RecalculateTotals(); AddDomainEvent(new OrderDiscountAppliedEvent(Id, discount.Amount)); } public void SetShippingAddress(Address address) { ShippingAddress = address ?? throw new ArgumentNullException(nameof(address)); AddDomainEvent(new OrderShippingAddressUpdatedEvent(Id)); } public void SetBillingAddress(Address address) { BillingAddress = address ?? throw new ArgumentNullException(nameof(address)); AddDomainEvent(new OrderBillingAddressUpdatedEvent(Id)); } public void ProcessPayment(string paymentMethod, string transactionId) { if (Status != OrderStatus.Pending) throw new InvalidOperationException("Order is not in pending status"); PaymentMethod = paymentMethod ?? throw new ArgumentNullException(nameof(paymentMethod)); PaymentTransactionId = transactionId ?? throw new ArgumentNullException(nameof(transactionId)); PaymentDate = DateTime.UtcNow; UpdateStatus(OrderStatus.Processing, "Payment processed successfully"); AddDomainEvent(new OrderPaymentProcessedEvent(Id, paymentMethod, transactionId)); } public void MarkAsShipped(string shippingMethod, string trackingNumber) { if (Status != OrderStatus.Processing) throw new InvalidOperationException("Order must be in processing status to ship"); ShippingMethod = shippingMethod ?? throw new ArgumentNullException(nameof(shippingMethod)); TrackingNumber = trackingNumber ?? throw new ArgumentNullException(nameof(trackingNumber)); ShippedDate = DateTime.UtcNow; UpdateStatus(OrderStatus.Shipped, $"Order shipped via {shippingMethod}"); AddDomainEvent(new OrderShippedEvent(Id, shippingMethod, trackingNumber)); } public void MarkAsDelivered() { if (Status != OrderStatus.Shipped) throw new InvalidOperationException("Order must be shipped before delivery"); DeliveredDate = DateTime.UtcNow; UpdateStatus(OrderStatus.Delivered, "Order delivered successfully"); AddDomainEvent(new OrderDeliveredEvent(Id)); } public void Cancel(string reason) { if (Status == OrderStatus.Cancelled) return; if (Status == OrderStatus.Delivered) throw new InvalidOperationException("Cannot cancel a delivered order"); UpdateStatus(OrderStatus.Cancelled, reason ?? "Order cancelled"); AddDomainEvent(new OrderCancelledEvent(Id, reason)); } private void UpdateStatus(OrderStatus newStatus, string note) { var oldStatus = Status; Status = newStatus; AddStatusHistory(newStatus, note); AddDomainEvent(new OrderStatusChangedEvent(Id, oldStatus, newStatus, note)); } private void AddStatusHistory(OrderStatus status, string note) { var history = new OrderStatusHistory(Id, status, note); _statusHistory.Add(history); } private void RecalculateTotals() { var total = _orderItems.Sum(item => item.LineTotal.Amount); TotalAmount = new Money(total, Currency); // Calculate tax (simplified - in real app, use tax service) TaxAmount = new Money(total * 0.1m, Currency); // 10% tax // Calculate final amount var final = TotalAmount.Amount + TaxAmount.Amount + ShippingAmount.Amount - DiscountAmount.Amount; FinalAmount = new Money(Math.Max(0, final), Currency); } private static string GenerateOrderNumber() { return $"ORD-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpper()}"; } public bool CanBeCancelled() => Status == OrderStatus.Pending || Status == OrderStatus.Processing; public decimal CalculateTax() => TaxAmount.Amount; public decimal CalculateTotalWithoutTax() => TotalAmount.Amount - TaxAmount.Amount; } public class OrderItem : Entity { public Guid Id { get; private set; } public Guid OrderId { get; private set; } public Guid ProductId { get; private set; } public string ProductName { get; private set; } public int Quantity { get; private set; } public Money UnitPrice { get; private set; } public Money LineTotal { get; private set; } public Order Order { get; private set; } public Product Product { get; private set; } private OrderItem() { } public OrderItem(Guid orderId, Guid productId, string productName, int quantity, Money unitPrice) { Id = Guid.NewGuid(); OrderId = orderId; ProductId = productId; ProductName = productName ?? throw new ArgumentNullException(nameof(productName)); Quantity = quantity; UnitPrice = unitPrice; LineTotal = new Money(unitPrice.Amount * quantity, unitPrice.Currency); } public void UpdateQuantity(int quantity) { if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); Quantity = quantity; LineTotal = new Money(UnitPrice.Amount * quantity, UnitPrice.Currency); } public void UpdateUnitPrice(Money unitPrice) { UnitPrice = unitPrice ?? throw new ArgumentNullException(nameof(unitPrice)); LineTotal = new Money(UnitPrice.Amount * Quantity, UnitPrice.Currency); } } }
4. Infrastructure & Data Layer
4.1 Entity Framework Configuration
// Infrastructure/Data/Configurations/ProductConfiguration.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using SmartCommerce.Domain.Entities; namespace SmartCommerce.Infrastructure.Data.Configurations { public class ProductConfiguration : IEntityTypeConfiguration<Product> { public void Configure(EntityTypeBuilder<Product> builder) { builder.ToTable("Products"); builder.HasKey(p => p.Id); builder.Property(p => p.Id) .ValueGeneratedNever() .IsRequired(); builder.Property(p => p.Name) .HasMaxLength(200) .IsRequired(); builder.Property(p => p.Description) .HasMaxLength(2000) .IsRequired(); builder.Property(p => p.Sku) .HasMaxLength(100) .IsRequired(); builder.Property(p => p.StockQuantity) .IsRequired(); builder.Property(p => p.ReorderLevel) .IsRequired(); builder.Property(p => p.IsActive) .IsRequired(); builder.Property(p => p.IsDeleted) .IsRequired(); builder.Property(p => p.AIScore) .HasColumnType("decimal(3,2)"); builder.Property(p => p.AIKeywords) .HasMaxLength(500); // Value Objects as owned types builder.OwnsOne(p => p.Price, money => { money.Property(m => m.Amount) .HasColumnType("decimal(18,2)") .HasColumnName("PriceAmount"); money.Property(m => m.Currency) .HasMaxLength(3) .HasColumnName("PriceCurrency"); }); // Indexes builder.HasIndex(p => p.Sku) .IsUnique() .HasDatabaseName("IX_Products_Sku"); builder.HasIndex(p => p.Name) .HasDatabaseName("IX_Products_Name"); builder.HasIndex(p => p.CategoryId) .HasDatabaseName("IX_Products_CategoryId"); builder.HasIndex(p => p.IsActive) .HasDatabaseName("IX_Products_IsActive"); builder.HasIndex(p => new { p.IsActive, p.IsDeleted }) .HasDatabaseName("IX_Products_ActiveNotDeleted"); // Query filter for soft delete builder.HasQueryFilter(p => !p.IsDeleted); // Relationships builder.HasOne(p => p.Category) .WithMany(c => c.Products) .HasForeignKey(p => p.CategoryId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne(p => p.Vendor) .WithMany(v => v.Products) .HasForeignKey(p => p.VendorId) .OnDelete(DeleteBehavior.SetNull); builder.HasMany(p => p.Images) .WithOne(pi => pi.Product) .HasForeignKey(pi => pi.ProductId) .OnDelete(DeleteBehavior.Cascade); builder.HasMany(p => p.Reviews) .WithOne(pr => pr.Product) .HasForeignKey(pr => pr.ProductId) .OnDelete(DeleteBehavior.Cascade); builder.HasMany(p => p.Tags) .WithOne(pt => pt.Product) .HasForeignKey(pt => pt.ProductId) .OnDelete(DeleteBehavior.Cascade); } } public class OrderConfiguration : IEntityTypeConfiguration<Order> { public void Configure(EntityTypeBuilder<Order> builder) { builder.ToTable("Orders"); builder.HasKey(o => o.Id); builder.Property(o => o.Id) .ValueGeneratedNever() .IsRequired(); builder.Property(o => o.OrderNumber) .HasMaxLength(50) .IsRequired(); builder.Property(o => o.CustomerId) .IsRequired(); builder.Property(o => o.Status) .HasConversion<string>() .HasMaxLength(20) .IsRequired(); builder.Property(o => o.Currency) .HasMaxLength(3) .IsRequired(); builder.Property(o => o.PaymentMethod) .HasMaxLength(50); builder.Property(o => o.PaymentTransactionId) .HasMaxLength(100); builder.Property(o => o.ShippingMethod) .HasMaxLength(50); builder.Property(o => o.TrackingNumber) .HasMaxLength(100); // Owned types for Value Objects builder.OwnsOne(o => o.ShippingAddress, address => { address.Property(a => a.Street).HasMaxLength(200).HasColumnName("ShippingStreet"); address.Property(a => a.City).HasMaxLength(100).HasColumnName("ShippingCity"); address.Property(a => a.State).HasMaxLength(100).HasColumnName("ShippingState"); address.Property(a => a.Country).HasMaxLength(100).HasColumnName("ShippingCountry"); address.Property(a => a.ZipCode).HasMaxLength(20).HasColumnName("ShippingZipCode"); }); builder.OwnsOne(o => o.BillingAddress, address => { address.Property(a => a.Street).HasMaxLength(200).HasColumnName("BillingStreet"); address.Property(a => a.City).HasMaxLength(100).HasColumnName("BillingCity"); address.Property(a => a.State).HasMaxLength(100).HasColumnName("BillingState"); address.Property(a => a.Country).HasMaxLength(100).HasColumnName("BillingCountry"); address.Property(a => a.ZipCode).HasMaxLength(20).HasColumnName("BillingZipCode"); }); // Owned types for Money Value Objects builder.OwnsOne(o => o.TotalAmount, money => { money.Property(m => m.Amount).HasColumnType("decimal(18,2)").HasColumnName("TotalAmount"); money.Property(m => m.Currency).HasMaxLength(3).HasColumnName("TotalCurrency"); }); builder.OwnsOne(o => o.DiscountAmount, money => { money.Property(m => m.Amount).HasColumnType("decimal(18,2)").HasColumnName("DiscountAmount"); money.Property(m => m.Currency).HasMaxLength(3).HasColumnName("DiscountCurrency"); }); builder.OwnsOne(o => o.TaxAmount, money => { money.Property(m => m.Amount).HasColumnType("decimal(18,2)").HasColumnName("TaxAmount"); money.Property(m => m.Currency).HasMaxLength(3).HasColumnName("TaxCurrency"); }); builder.OwnsOne(o => o.ShippingAmount, money => { money.Property(m => m.Amount).HasColumnType("decimal(18,2)").HasColumnName("ShippingAmount"); money.Property(m => m.Currency).HasMaxLength(3).HasColumnName("ShippingCurrency"); }); builder.OwnsOne(o => o.FinalAmount, money => { money.Property(m => m.Amount).HasColumnType("decimal(18,2)").HasColumnName("FinalAmount"); money.Property(m => m.Currency).HasMaxLength(3).HasColumnName("FinalCurrency"); }); // Indexes builder.HasIndex(o => o.OrderNumber) .IsUnique() .HasDatabaseName("IX_Orders_OrderNumber"); builder.HasIndex(o => o.CustomerId) .HasDatabaseName("IX_Orders_CustomerId"); builder.HasIndex(o => o.Status) .HasDatabaseName("IX_Orders_Status"); builder.HasIndex(o => o.Created) .HasDatabaseName("IX_Orders_Created"); // Relationships builder.HasMany(o => o.OrderItems) .WithOne(oi => oi.Order) .HasForeignKey(oi => oi.OrderId) .OnDelete(DeleteBehavior.Cascade); builder.HasMany(o => o.StatusHistory) .WithOne(osh => osh.Order) .HasForeignKey(osh => osh.OrderId) .OnDelete(DeleteBehavior.Cascade); } } }
4.2 Repository Pattern Implementation
// Infrastructure/Data/Repositories/ProductRepository.cs using Microsoft.EntityFrameworkCore; using SmartCommerce.Application.Common.Interfaces; using SmartCommerce.Domain.Entities; using SmartCommerce.Domain.Specifications; namespace SmartCommerce.Infrastructure.Data.Repositories { public class ProductRepository : IProductRepository { private readonly ApplicationDbContext _context; public ProductRepository(ApplicationDbContext context) { _context = context; } public async Task<Product?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { return await _context.Products .Include(p => p.Category) .Include(p => p.Vendor) .Include(p => p.Images) .Include(p => p.Tags) .Include(p => p.Reviews) .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); } public async Task<IReadOnlyList<Product>> GetAllAsync(CancellationToken cancellationToken = default) { return await _context.Products .Include(p => p.Category) .Include(p => p.Images.Where(pi => pi.IsPrimary)) .Where(p => p.IsActive && !p.IsDeleted) .OrderBy(p => p.Name) .ToListAsync(cancellationToken); } public async Task<IReadOnlyList<Product>> GetBySpecificationAsync(ISpecification<Product> specification, CancellationToken cancellationToken = default) { return await ApplySpecification(specification).ToListAsync(cancellationToken); } public async Task<Product?> GetBySpecificationAsync(ISingleResultSpecification<Product> specification, CancellationToken cancellationToken = default) { return await ApplySpecification(specification).FirstOrDefaultAsync(cancellationToken); } public async Task<int> CountAsync(ISpecification<Product> specification, CancellationToken cancellationToken = default) { return await ApplySpecification(specification, true).CountAsync(cancellationToken); } public async Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default) { return await _context.Products.AnyAsync(p => p.Id == id && !p.IsDeleted, cancellationToken); } public async Task<Product> AddAsync(Product entity, CancellationToken cancellationToken = default) { await _context.Products.AddAsync(entity, cancellationToken); return entity; } public void Update(Product entity) { _context.Products.Update(entity); } public void Delete(Product entity) { entity.MarkAsDeleted(); _context.Products.Update(entity); } public async Task<IReadOnlyList<Product>> GetFeaturedProductsAsync(int count, CancellationToken cancellationToken = default) { return await _context.Products .Include(p => p.Category) .Include(p => p.Images.Where(pi => pi.IsPrimary)) .Where(p => p.IsActive && !p.IsDeleted && p.AIScore > 0.7f) .OrderByDescending(p => p.AIScore) .ThenByDescending(p => p.Created) .Take(count) .ToListAsync(cancellationToken); } public async Task<IReadOnlyList<Product>> SearchProductsAsync(string searchTerm, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default) { var query = _context.Products .Include(p => p.Category) .Include(p => p.Images.Where(pi => pi.IsPrimary)) .Where(p => p.IsActive && !p.IsDeleted); if (!string.IsNullOrWhiteSpace(searchTerm)) { searchTerm = searchTerm.Trim().ToLower(); query = query.Where(p => p.Name.ToLower().Contains(searchTerm) || p.Description.ToLower().Contains(searchTerm) || p.Sku.ToLower().Contains(searchTerm) || p.AIKeywords != null && p.AIKeywords.ToLower().Contains(searchTerm) || p.Tags.Any(t => t.Name.ToLower().Contains(searchTerm))); } return await query .OrderByDescending(p => p.AIScore) .ThenBy(p => p.Name) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); } public async Task<IReadOnlyList<Product>> GetProductsByCategoryAsync(Guid categoryId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default) { return await _context.Products .Include(p => p.Category) .Include(p => p.Images.Where(pi => pi.IsPrimary)) .Where(p => p.IsActive && !p.IsDeleted && p.CategoryId == categoryId) .OrderByDescending(p => p.AIScore) .ThenBy(p => p.Name) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); } public async Task UpdateAIScoresAsync(Dictionary<Guid, float> productScores, CancellationToken cancellationToken = default) { var productIds = productScores.Keys.ToList(); var products = await _context.Products .Where(p => productIds.Contains(p.Id)) .ToListAsync(cancellationToken); foreach (var product in products) { if (productScores.TryGetValue(product.Id, out var score)) { product.UpdateAIScore(score, string.Empty); // Keywords would be set separately } } _context.Products.UpdateRange(products); } private IQueryable<Product> ApplySpecification(ISpecification<Product> specification, bool forCount = false) { var query = _context.Products.AsQueryable(); // Include related entities if not counting if (!forCount) { query = query .Include(p => p.Category) .Include(p => p.Images.Where(pi => pi.IsPrimary)) .Include(p => p.Tags); } // Apply specification criteria if (specification.Criteria != null) { query = query.Where(specification.Criteria); } // Apply ordering if not counting if (!forCount && specification.OrderBy != null) { query = specification.OrderBy(query); } else if (!forCount && specification.OrderByDescending != null) { query = specification.OrderByDescending(query); } // Apply paging if not counting if (!forCount && specification.IsPagingEnabled) { query = query.Skip(specification.Skip) .Take(specification.Take); } return query; } } }
5. Application Layer & CQRS
5.1 CQRS Implementation with MediatR
// Application/Features/Products/Queries/GetProductDetail/GetProductDetailQuery.cs using AutoMapper; using AutoMapper.QueryableExtensions; using MediatR; using Microsoft.EntityFrameworkCore; using SmartCommerce.Application.Common.Interfaces; using SmartCommerce.Application.Common.Models; namespace SmartCommerce.Application.Features.Products.Queries.GetProductDetail { public record GetProductDetailQuery : IRequest<Result<ProductDetailDto>> { public Guid Id { get; init; } } public class GetProductDetailQueryHandler : IRequestHandler<GetProductDetailQuery, Result<ProductDetailDto>> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly ICurrentUserService _currentUserService; public GetProductDetailQueryHandler(IApplicationDbContext context, IMapper mapper, ICurrentUserService currentUserService) { _context = context; _mapper = mapper; _currentUserService = currentUserService; } public async Task<Result<ProductDetailDto>> Handle(GetProductDetailQuery request, CancellationToken cancellationToken) { var product = await _context.Products .Include(p => p.Category) .Include(p => p.Vendor) .Include(p => p.Images) .Include(p => p.Tags) .Include(p => p.Reviews) .ThenInclude(r => r.User) .Where(p => p.IsActive && !p.IsDeleted) .ProjectTo<ProductDetailDto>(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken); if (product == null) { return Result<ProductDetailDto>.Failure($"Product with ID {request.Id} not found."); } // Track product view for recommendations if (_currentUserService.UserId.HasValue) { // Fire and forget - don't await to avoid blocking the response _ = Task.Run(async () => { try { await TrackProductViewAsync(request.Id, _currentUserService.UserId.Value); } catch (Exception ex) { // Log but don't throw - this shouldn't affect the main request // In production, use proper logging Console.WriteLine($"Failed to track product view: {ex.Message}"); } }); } return Result<ProductDetailDto>.Success(product); } private async Task TrackProductViewAsync(Guid productId, Guid userId) { // Implementation would track product views for recommendation engine // This could be done via a message bus or direct database call var viewEvent = new ProductViewedEvent { ProductId = productId, UserId = userId, ViewedAt = DateTime.UtcNow }; // Publish event or save to database await Task.CompletedTask; } } public class ProductDetailDto { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public decimal Price { get; set; } public string Currency { get; set; } = "USD"; public string Sku { get; set; } = string.Empty; public int StockQuantity { get; set; } public bool IsInStock => StockQuantity > 0; public float AIScore { get; set; } public string? AIKeywords { get; set; } public Guid CategoryId { get; set; } public string CategoryName { get; set; } = string.Empty; public Guid? VendorId { get; set; } public string? VendorName { get; set; } public List<ProductImageDto> Images { get; set; } = new(); public List<ProductTagDto> Tags { get; set; } = new(); public List<ProductReviewDto> Reviews { get; set; } = new(); public decimal AverageRating => Reviews.Any() ? Reviews.Average(r => r.Rating) : 0; public int ReviewCount => Reviews.Count; public DateTime Created { get; set; } public DateTime? LastModified { get; set; } } public class ProductImageDto { public Guid Id { get; set; } public string ImageUrl { get; set; } = string.Empty; public string AltText { get; set; } = string.Empty; public bool IsPrimary { get; set; } public int DisplayOrder { get; set; } } public class ProductReviewDto { public Guid Id { get; set; } public Guid UserId { get; set; } public string UserName { get; set; } = string.Empty; public int Rating { get; set; } public string? Title { get; set; } public string Comment { get; set; } = string.Empty; public DateTime Created { get; set; } } }
5.2 Command Implementation
// Application/Features/Products/Commands/CreateProduct/CreateProductCommand.cs using AutoMapper; using FluentValidation; using MediatR; using SmartCommerce.Application.Common.Interfaces; using SmartCommerce.Application.Common.Models; using SmartCommerce.Domain.Entities; namespace SmartCommerce.Application.Features.Products.Commands.CreateProduct { public record CreateProductCommand : IRequest<Result<Guid>> { public string Name { get; init; } = string.Empty; public string Description { get; init; } = string.Empty; public decimal Price { get; init; } public string Currency { get; init; } = "USD"; public string Sku { get; init; } = string.Empty; public int StockQuantity { get; init; } public Guid CategoryId { get; init; } public Guid? VendorId { get; init; } public List<ProductImageCommand> Images { get; init; } = new(); public List<string> Tags { get; init; } = new(); } public class ProductImageCommand { public string ImageUrl { get; init; } = string.Empty; public string AltText { get; init; } = string.Empty; public bool IsPrimary { get; init; } } public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand> { private readonly IApplicationDbContext _context; public CreateProductCommandValidator(IApplicationDbContext context) { _context = context; RuleFor(v => v.Name) .NotEmpty().WithMessage("Name is required.") .MaximumLength(200).WithMessage("Name must not exceed 200 characters."); RuleFor(v => v.Description) .NotEmpty().WithMessage("Description is required.") .MaximumLength(2000).WithMessage("Description must not exceed 2000 characters."); RuleFor(v => v.Price) .GreaterThan(0).WithMessage("Price must be greater than 0."); RuleFor(v => v.Sku) .NotEmpty().WithMessage("SKU is required.") .MaximumLength(100).WithMessage("SKU must not exceed 100 characters.") .MustAsync(BeUniqueSku).WithMessage("The specified SKU already exists."); RuleFor(v => v.StockQuantity) .GreaterThanOrEqualTo(0).WithMessage("Stock quantity cannot be negative."); RuleFor(v => v.CategoryId) .NotEmpty().WithMessage("Category is required.") .MustAsync(CategoryExists).WithMessage("The specified category does not exist."); RuleFor(v => v.Images) .Must(HaveAtLeastOnePrimaryImage) .When(v => v.Images.Any()) .WithMessage("At least one image must be marked as primary."); RuleForEach(v => v.Tags) .NotEmpty().WithMessage("Tag cannot be empty.") .MaximumLength(50).WithMessage("Tag must not exceed 50 characters."); } private async Task<bool> BeUniqueSku(string sku, CancellationToken cancellationToken) { return await _context.Products .AllAsync(p => p.Sku != sku, cancellationToken); } private async Task<bool> CategoryExists(Guid categoryId, CancellationToken cancellationToken) { return await _context.Categories .AnyAsync(c => c.Id == categoryId, cancellationToken); } private bool HaveAtLeastOnePrimaryImage(List<ProductImageCommand> images) { return images.Any(i => i.IsPrimary); } } public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Result<Guid>> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IAIService _aiService; public CreateProductCommandHandler(IApplicationDbContext context, IMapper mapper, IAIService aiService) { _context = context; _mapper = mapper; _aiService = aiService; } public async Task<Result<Guid>> Handle(CreateProductCommand request, CancellationToken cancellationToken) { try { // Create product var price = new Money(request.Price, request.Currency); var product = new Product( request.Name, request.Description, price, request.Sku, request.CategoryId, request.VendorId, request.StockQuantity); // Add images foreach (var imageCommand in request.Images) { product.AddImage(imageCommand.ImageUrl, imageCommand.AltText, imageCommand.IsPrimary); } // Add tags foreach (var tagName in request.Tags) { product.AddTag(tagName); } // AI Analysis (fire and forget) _ = Task.Run(async () => { try { await AnalyzeProductWithAIAsync(product); } catch (Exception ex) { // Log AI analysis failure but don't fail the product creation // In production, use proper logging Console.WriteLine($"AI analysis failed for product {product.Id}: {ex.Message}"); } }, cancellationToken); // Save product await _context.Products.AddAsync(product, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return Result<Guid>.Success(product.Id); } catch (Exception ex) { return Result<Guid>.Failure($"Failed to create product: {ex.Message}"); } } private async Task AnalyzeProductWithAIAsync(Product product) { // Analyze product with AI services var analysisResult = await _aiService.AnalyzeProductAsync( product.Name, product.Description, product.Tags.Select(t => t.Name).ToList()); product.UpdateAIScore(analysisResult.Score, analysisResult.Keywords); _context.Products.Update(product); await _context.SaveChangesAsync(); } } }
6. API Layer & Controllers
6.1 API Controllers with Best Practices
// Web/Controllers/ProductsController.cs using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SmartCommerce.Application.Features.Products.Commands.CreateProduct; using SmartCommerce.Application.Features.Products.Commands.DeleteProduct; using SmartCommerce.Application.Features.Products.Commands.UpdateProduct; using SmartCommerce.Application.Features.Products.Queries.GetProductDetail; using SmartCommerce.Application.Features.Products.Queries.GetProducts; using SmartCommerce.Web.Filters; namespace SmartCommerce.Web.Controllers { [ApiController] [Route("api/[controller]")] [Produces("application/json")] [ServiceFilter(typeof(ApiExceptionFilter))] public class ProductsController : ControllerBase { private readonly IMediator _mediator; private readonly ILogger<ProductsController> _logger; public ProductsController(IMediator mediator, ILogger<ProductsController> logger) { _mediator = mediator; _logger = logger; } /// <summary> /// Get paginated list of products /// </summary> /// <param name="query">Query parameters for filtering and pagination</param> /// <returns>Paginated list of products</returns> [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<ActionResult<PaginatedList<ProductDto>>> GetProducts([FromQuery] GetProductsQuery query) { _logger.LogInformation("Getting products with query: {@Query}", query); var result = await _mediator.Send(query); if (result.Succeeded) { // Add pagination headers Response.Headers.Append("X-Pagination", result.Data.ToJson()); return Ok(result.Data.Items); } return BadRequest(result.Errors); } /// <summary> /// Get product by ID /// </summary> /// <param name="id">Product ID</param> /// <returns>Product details</returns> [HttpGet("{id:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult<ProductDetailDto>> GetProduct(Guid id) { _logger.LogInformation("Getting product with ID: {ProductId}", id); var query = new GetProductDetailQuery { Id = id }; var result = await _mediator.Send(query); if (result.Succeeded) { return Ok(result.Data); } return NotFound(result.Errors); } /// <summary> /// Create a new product /// </summary> /// <param name="command">Product creation data</param> /// <returns>Created product ID</returns> [HttpPost] [Authorize(Roles = "Admin,ProductManager")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult<Guid>> CreateProduct(CreateProductCommand command) { _logger.LogInformation("Creating new product: {@Product}", command); var result = await _mediator.Send(command); if (result.Succeeded) { return CreatedAtAction(nameof(GetProduct), new { id = result.Data }, result.Data); } return BadRequest(result.Errors); } /// <summary> /// Update an existing product /// </summary> /// <param name="id">Product ID</param> /// <param name="command">Product update data</param> /// <returns>No content</returns> [HttpPut("{id:guid}")] [Authorize(Roles = "Admin,ProductManager")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<IActionResult> UpdateProduct(Guid id, UpdateProductCommand command) { if (id != command.Id) { return BadRequest("ID in route does not match ID in body"); } _logger.LogInformation("Updating product {ProductId} with data: {@Product}", id, command); var result = await _mediator.Send(command); if (result.Succeeded) { return NoContent(); } if (result.Errors.Any(e => e.Contains("not found", StringComparison.OrdinalIgnoreCase))) { return NotFound(result.Errors); } return BadRequest(result.Errors); } /// <summary> /// Delete a product /// </summary> /// <param name="id">Product ID</param> /// <returns>No content</returns> [HttpDelete("{id:guid}")] [Authorize(Roles = "Admin,ProductManager")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<IActionResult> DeleteProduct(Guid id) { _logger.LogInformation("Deleting product with ID: {ProductId}", id); var command = new DeleteProductCommand { Id = id }; var result = await _mediator.Send(command); if (result.Succeeded) { return NoContent(); } return NotFound(result.Errors); } /// <summary> /// Search products /// </summary> /// <param name="searchTerm">Search term</param> /// <param name="page">Page number</param> /// <param name="pageSize">Page size</param> /// <returns>Search results</returns> [HttpGet("search")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<ActionResult<List<ProductDto>>> SearchProducts( [FromQuery] string searchTerm, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) { if (string.IsNullOrWhiteSpace(searchTerm) || searchTerm.Length < 2) { return BadRequest("Search term must be at least 2 characters long"); } _logger.LogInformation("Searching products with term: {SearchTerm}", searchTerm); var query = new SearchProductsQuery { SearchTerm = searchTerm, Page = page, PageSize = pageSize }; var result = await _mediator.Send(query); if (result.Succeeded) { return Ok(result.Data); } return BadRequest(result.Errors); } /// <summary> /// Get featured products /// </summary> /// <param name="count">Number of featured products to return</param> /// <returns>List of featured products</returns> [HttpGet("featured")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<List<ProductDto>>> GetFeaturedProducts([FromQuery] int count = 10) { _logger.LogInformation("Getting {Count} featured products", count); var query = new GetFeaturedProductsQuery { Count = count }; var result = await _mediator.Send(query); if (result.Succeeded) { return Ok(result.Data); } return BadRequest(result.Errors); } } }
6.2 API Versioning and Documentation
// Web/Configuration/SwaggerConfiguration.cs using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.Filters; namespace SmartCommerce.Web.Configuration { public static class SwaggerConfiguration { public static IServiceCollection AddSwaggerConfiguration(this IServiceCollection services, IConfiguration configuration) { services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "SmartCommerce API", Version = "v1", Description = "AI-Powered E-Commerce Platform API", Contact = new OpenApiContact { Name = "SmartCommerce Team", Email = "api@smartcommerce.com", Url = new Uri("https://smartcommerce.com") }, License = new OpenApiLicense { Name = "MIT License", Url = new Uri("https://opensource.org/licenses/MIT") } }); // Add JWT Authentication options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, Scheme = "oauth2", Name = "Bearer", In = ParameterLocation.Header }, new List<string>() } }); // Add XML comments var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); options.IncludeXmlComments(xmlPath); // Operation filters options.OperationFilter<AppendAuthorizeToSummaryOperationFilter>(); options.OperationFilter<SecurityRequirementsOperationFilter>(); // Schema filters options.SchemaFilter<EnumSchemaFilter>(); // Support for polymorphic types options.UseAllOfForInheritance(); options.UseOneOfForPolymorphism(); // Custom filters options.OperationFilter<CorrelationIdOperationFilter>(); }); services.AddSwaggerGenNewtonsoftSupport(); return services; } public static IApplicationBuilder UseSwaggerConfiguration(this IApplicationBuilder app) { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "SmartCommerce API V1"); options.RoutePrefix = "api-docs"; options.DocumentTitle = "SmartCommerce API Documentation"; options.EnablePersistAuthorization(); options.EnableDeepLinking(); options.DisplayOperationId(); options.DisplayRequestDuration(); options.DefaultModelsExpandDepth(-1); // Hide schemas by default // Custom CSS options.InjectStylesheet("/swagger-ui/custom.css"); }); return app; } } }
7. AI Integration Services
7.1 AI-Powered Recommendation Engine
// Infrastructure/AI/RecommendationService.cs using Microsoft.ML; using Microsoft.ML.Data; using Microsoft.ML.Trainers; using SmartCommerce.Application.Common.Interfaces; using SmartCommerce.Domain.Entities; namespace SmartCommerce.Infrastructure.AI { public interface IRecommendationService { Task<TrainingResult> TrainRecommendationModelAsync(); Task<List<Recommendation>> GetPersonalizedRecommendationsAsync(Guid userId, int count = 10); Task<List<Recommendation>> GetSimilarProductsAsync(Guid productId, int count = 5); Task RecordUserInteractionAsync(UserInteraction interaction); } public class AIRecommendationService : IRecommendationService { private readonly MLContext _mlContext; private readonly IApplicationDbContext _context; private readonly IProductRepository _productRepository; private ITransformer _model; private PredictionEngine<ProductInteraction, ProductPrediction> _predictionEngine; public AIRecommendationService(IApplicationDbContext context, IProductRepository productRepository) { _mlContext = new MLContext(seed: 0); _context = context; _productRepository = productRepository; } public async Task<TrainingResult> TrainRecommendationModelAsync() { try { // Load training data var interactions = await LoadTrainingDataAsync(); if (interactions.Count < 100) // Minimum data required { return TrainingResult.Failure("Insufficient training data"); } // Prepare data var dataView = _mlContext.Data.LoadFromEnumerable(interactions); // Data preprocessing var dataProcessPipeline = _mlContext.Transforms.Conversion.MapValueToKey( outputColumnName: "UserIdEncoded", inputColumnName: nameof(ProductInteraction.UserId)) .Append(_mlContext.Transforms.Conversion.MapValueToKey( outputColumnName: "ProductIdEncoded", inputColumnName: nameof(ProductInteraction.ProductId))); // Training configuration var options = new MatrixFactorizationTrainer.Options { MatrixColumnIndexColumnName = "UserIdEncoded", MatrixRowIndexColumnName = "ProductIdEncoded", LabelColumnName = nameof(ProductInteraction.Rating), NumberOfIterations = 20, ApproximationRank = 100, LearningRate = 0.01 }; var trainingPipeline = dataProcessPipeline.Append(_mlContext.Recommendation().Trainers.MatrixFactorization(options)); // Train model _model = trainingPipeline.Fit(dataView); // Create prediction engine _predictionEngine = _mlContext.Model.CreatePredictionEngine<ProductInteraction, ProductPrediction>(_model); // Evaluate model var testData = _mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2); var predictions = _model.Transform(testData.TestSet); var metrics = _mlContext.Regression.Evaluate(predictions, labelColumnName: nameof(ProductInteraction.Rating)); return TrainingResult.Success(metrics.RSquared, metrics.RootMeanSquaredError); } catch (Exception ex) { return TrainingResult.Failure($"Training failed: {ex.Message}"); } } public async Task<List<Recommendation>> GetPersonalizedRecommendationsAsync(Guid userId, int count = 10) { if (_model == null) { // Fallback to popular products if model not trained return await GetPopularProductsAsync(count); } var allProducts = await _productRepository.GetAllAsync(); var recommendations = new List<Recommendation>(); foreach (var product in allProducts) { var prediction = _predictionEngine.Predict(new ProductInteraction { UserId = userId.ToString(), ProductId = product.Id.ToString(), Rating = 0 // This will be predicted }); recommendations.Add(new Recommendation { ProductId = product.Id, ProductName = product.Name, Score = prediction.Score, Confidence = prediction.Confidence, Reason = "AI Personalized Recommendation" }); } return recommendations .OrderByDescending(r => r.Score) .Take(count) .ToList(); } public async Task<List<Recommendation>> GetSimilarProductsAsync(Guid productId, int count = 5) { var targetProduct = await _productRepository.GetByIdAsync(productId); if (targetProduct == null) return new List<Recommendation>(); var allProducts = await _productRepository.GetAllAsync(); var similarities = new List<Recommendation>(); foreach (var product in allProducts.Where(p => p.Id != productId)) { var similarity = CalculateProductSimilarity(targetProduct, product); similarities.Add(new Recommendation { ProductId = product.Id, ProductName = product.Name, Score = similarity, Confidence = 0.8f, // Placeholder Reason = "Similar Product" }); } return similarities .OrderByDescending(r => r.Score) .Take(count) .ToList(); } public async Task RecordUserInteractionAsync(UserInteraction interaction) { await _context.UserInteractions.AddAsync(interaction); await _context.SaveChangesAsync(); // Trigger model retraining if enough new data await CheckAndRetrainModelAsync(); } private async Task<List<ProductInteraction>> LoadTrainingDataAsync() { var interactions = await _context.UserInteractions .Where(ui => ui.InteractionType == InteractionType.Purchase || ui.InteractionType == InteractionType.View) .Select(ui => new ProductInteraction { UserId = ui.UserId.ToString(), ProductId = ui.ProductId.ToString(), Rating = CalculateRatingFromInteraction(ui.InteractionType) }) .ToListAsync(); return interactions; } private float CalculateRatingFromInteraction(InteractionType interactionType) { return interactionType switch { InteractionType.Purchase => 5.0f, InteractionType.View => 1.0f, InteractionType.AddToCart => 3.0f, InteractionType.Review => 4.0f, _ => 0.5f }; } private float CalculateProductSimilarity(Product product1, Product product2) { // Simple similarity calculation based on category and price var categorySimilarity = product1.CategoryId == product2.CategoryId ? 1.0f : 0.0f; var priceDifference = Math.Abs(product1.Price.Amount - product2.Price.Amount); var maxPrice = Math.Max(product1.Price.Amount, product2.Price.Amount); var priceSimilarity = maxPrice > 0 ? 1.0f - (priceDifference / maxPrice) : 1.0f; // Tag similarity var commonTags = product1.Tags.Select(t => t.Name) .Intersect(product2.Tags.Select(t => t.Name)) .Count(); var tagSimilarity = commonTags / (float)Math.Max(product1.Tags.Count, product2.Tags.Count); return (categorySimilarity * 0.4f) + (priceSimilarity * 0.3f) + (tagSimilarity * 0.3f); } private async Task<List<Recommendation>> GetPopularProductsAsync(int count) { var popularProducts = await _context.Products .Where(p => p.IsActive && !p.IsDeleted) .OrderByDescending(p => p.AIScore) .ThenByDescending(p => p.Reviews.Count) .Take(count) .Select(p => new Recommendation { ProductId = p.Id, ProductName = p.Name, Score = p.AIScore, Confidence = 0.7f, Reason = "Popular Product" }) .ToListAsync(); return popularProducts; } private async Task CheckAndRetrainModelAsync() { var recentInteractions = await _context.UserInteractions .Where(ui => ui.Created > DateTime.UtcNow.AddDays(-1)) .CountAsync(); if (recentInteractions >= 1000) // Retrain if 1000 new interactions { _ = Task.Run(async () => { await TrainRecommendationModelAsync(); }); } } } public class ProductInteraction { public string UserId { get; set; } = string.Empty; public string ProductId { get; set; } = string.Empty; public float Rating { get; set; } } public class ProductPrediction { public float Score { get; set; } public float Confidence { get; set; } } public record Recommendation { public Guid ProductId { get; init; } public string ProductName { get; init; } = string.Empty; public float Score { get; init; } public float Confidence { get; init; } public string Reason { get; init; } = string.Empty; } public record TrainingResult(bool Success, string? ErrorMessage = null, double? RSquared = null, double? RMSE = null) { public static TrainingResult Success(double rSquared, double rmse) => new(true, null, rSquared, rmse); public static TrainingResult Failure(string errorMessage) => new(false, errorMessage); } }
8. Blazor Frontend
8.1 Blazor WebAssembly Main Application
<!-- Web/Shared/MainLayout.razor -->
@inherits LayoutView
@using SmartCommerce.Web.Components
@using MudBlazor
<MudTheme Provider="ThemeProvider" />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
    <MudAppBar Elevation="1">
        <MudIconButton Icon="Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
        <MudSpacer />
        <MudText Typo="Typo.h6" Class="ml-3">SmartCommerce</MudText>
        <MudSpacer />
        
        <MudIconButton Icon="Icons.Material.Filled.Search" Color="Color.Inherit" />
        
        @if (IsAuthenticated)
        {
            <MudIconButton Icon="Icons.Material.Filled.ShoppingCart" Color="Color.Inherit" />
            <MudMenu Icon="@("Icons.Material.Filled.AccountCircle")" IconColor="Color.Inherit" Label="Account">
                <MudMenuItem Icon="@("Icons.Material.Filled.Person")" Href="/profile">Profile</MudMenuItem>
                <MudMenuItem Icon="@("Icons.Material.Filled.ShoppingBag")" Href="/orders">Orders</MudMenuItem>
                <MudMenuItem Icon="@("Icons.Material.Filled.ExitToApp")" OnClick="Logout">Logout</MudMenuItem>
            </MudMenu>
        }
        else
        {
            <MudButton Variant="Variant.Text" Color="Color.Inherit" Href="/login">Login</MudButton>
            <MudButton Variant="Variant.Text" Color="Color.Inherit" Href="/register">Register</MudButton>
        }
    </MudAppBar>
    
    <MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always">
        <NavMenu />
    </MudDrawer>
    
    <MudMainContent>
        <MudContainer MaxWidth="MaxWidth.Large" Class="my-4">
            @Body
        </MudContainer>
    </MudMainContent>
</MudLayout>
@code {
    private bool _drawerOpen = true;
    
    [CascadingParameter]
    private Task<AuthenticationState>? AuthenticationStateTask { get; set; }
    
    private bool IsAuthenticated { get; set; }
    protected override async Task OnInitializedAsync()
    {
        if (AuthenticationStateTask != null)
        {
            var authState = await AuthenticationStateTask;
            IsAuthenticated = authState.User.Identity?.IsAuthenticated ?? false;
        }
    }
    private void ToggleDrawer()
    {
        _drawerOpen = !_drawerOpen;
    }
    private async void Logout()
    {
        // Implement logout logic
        await InvokeAsync(StateHasChanged);
    }
}8.2 Product Listing Component
<!-- Web/Components/ProductGrid.razor -->
@using SmartCommerce.Application.Features.Products.Queries.GetProducts
@using SmartCommerce.Web.Services
@inject IMediator Mediator
@inject ISnackbar Snackbar
@inject IRecommendationService RecommendationService
@inject NavigationManager Navigation
<MudGrid Spacing="2" Justify="Justify.FlexStart">
    @if (Products == null)
    {
        @for (int i = 0; i < 8; i++)
        {
            <MudItem xs="12" sm="6" md="4" lg="3">
                <ProductCardSkeleton />
            </MudItem>
        }
    }
    else if (Products.Any())
    {
        @foreach (var product in Products)
        {
            <MudItem xs="12" sm="6" md="4" lg="3">
                <ProductCard Product="product" 
                            OnAddToCart="AddToCart"
                            OnQuickView="ShowQuickView" />
            </MudItem>
        }
        
        @if (HasMore)
        {
            <MudItem xs="12" Class="text-center my-4">
                <MudButton Variant="Variant.Outlined" 
                          Color="Color.Primary" 
                          OnClick="LoadMore"
                          Disabled="Loading"
                          EndIcon="@(Loading ? Icons.Material.Filled.Refresh : Icons.Material.Filled.Add)">
                    @(Loading ? "Loading..." : "Load More")
                </MudButton>
            </MudItem>
        }
    }
    else
    {
        <MudItem xs="12">
            <MudText Align="Align.Center" Typo="Typo.h6" Color="Color.Secondary">
                No products found
            </MudText>
        </MudItem>
    }
</MudGrid>
<MudDialog @bind-IsVisible="ShowQuickViewDialog" MaxWidth="MaxWidth.Medium">
    <DialogContent>
        @if (SelectedProduct != null)
        {
            <QuickViewDialog Product="SelectedProduct" 
                           OnAddToCart="AddToCartFromDialog"
                           OnClose="CloseQuickView" />
        }
    </DialogContent>
</MudDialog>
@code {
    [Parameter]
    public ProductSearchParameters? SearchParameters { get; set; }
    
    [Parameter]
    public EventCallback<ProductDto> OnProductSelected { get; set; }
    private List<ProductDto> Products { get; set; } = new();
    private ProductDto? SelectedProduct { get; set; }
    private bool Loading { get; set; }
    private bool HasMore { get; set; }
    private int CurrentPage { get; set; } = 1;
    private bool ShowQuickViewDialog { get; set; }
    protected override async Task OnParametersSetAsync()
    {
        if (SearchParameters != null)
        {
            await ResetAndLoadProducts();
        }
    }
    protected override async Task OnInitializedAsync()
    {
        await LoadProducts();
    }
    private async Task LoadProducts(bool loadMore = false)
    {
        if (Loading) return;
        Loading = true;
        StateHasChanged();
        try
        {
            var query = new GetProductsQuery
            {
                Page = loadMore ? CurrentPage + 1 : 1,
                PageSize = 12,
                SearchTerm = SearchParameters?.SearchTerm,
                CategoryId = SearchParameters?.CategoryId,
                MinPrice = SearchParameters?.MinPrice,
                MaxPrice = SearchParameters?.MaxPrice,
                SortBy = SearchParameters?.SortBy ?? "name",
                SortDirection = SearchParameters?.SortDirection ?? "asc"
            };
            var result = await Mediator.Send(query);
            if (result.Succeeded && result.Data != null)
            {
                if (loadMore)
                {
                    Products.AddRange(result.Data.Items);
                    CurrentPage++;
                }
                else
                {
                    Products = result.Data.Items.ToList();
                    CurrentPage = 1;
                }
                HasMore = result.Data.HasNextPage;
                
                // Record view for AI recommendations
                foreach (var product in Products)
                {
                    await RecommendationService.RecordProductViewAsync(product.Id);
                }
            }
            else
            {
                Snackbar.Add("Failed to load products", Severity.Error);
            }
        }
        catch (Exception ex)
        {
            Snackbar.Add($"Error loading products: {ex.Message}", Severity.Error);
        }
        finally
        {
            Loading = false;
            StateHasChanged();
        }
    }
    private async Task LoadMore()
    {
        await LoadProducts(true);
    }
    private async Task ResetAndLoadProducts()
    {
        Products.Clear();
        CurrentPage = 1;
        await LoadProducts();
    }
    private async Task AddToCart(ProductDto product)
    {
        try
        {
            // Implementation would add product to cart
            Snackbar.Add($"Added {product.Name} to cart", Severity.Success);
            
            // Record interaction for AI
            await RecommendationService.RecordAddToCartAsync(product.Id);
        }
        catch (Exception ex)
        {
            Snackbar.Add($"Failed to add to cart: {ex.Message}", Severity.Error);
        }
    }
    private void ShowQuickView(ProductDto product)
    {
        SelectedProduct = product;
        ShowQuickViewDialog = true;
        StateHasChanged();
    }
    private async Task AddToCartFromDialog(ProductDto product)
    {
        await AddToCart(product);
        ShowQuickViewDialog = false;
    }
    private void CloseQuickView()
    {
        ShowQuickViewDialog = false;
        SelectedProduct = null;
    }
    private async Task OnProductClick(ProductDto product)
    {
        if (OnProductSelected.HasDelegate)
        {
            await OnProductSelected.InvokeAsync(product);
        }
        else
        {
            Navigation.NavigateTo($"/products/{product.Id}");
        }
    }
}
public class ProductSearchParameters
{
    public string? SearchTerm { get; set; }
    public Guid? CategoryId { get; set; }
    public decimal? MinPrice { get; set; }
    public decimal? MaxPrice { get; set; }
    public string SortBy { get; set; } = "name";
    public string SortDirection { get; set; } = "asc";
}8.3 AI-Powered Product Recommendations Component
<!-- Web/Components/ProductRecommendations.razor -->
@using SmartCommerce.Application.Features.Products.Queries.GetProducts
@inject IMediator Mediator
@inject IRecommendationService RecommendationService
@inject IAuthService AuthService
@if (Recommendations.Any())
{
    <MudPaper Class="pa-4 mb-4" Elevation="1">
        <MudText Typo="Typo.h6" GutterBottom="true">
            @Title
            @if (!string.IsNullOrEmpty(Explanation))
            {
                <MudTooltip Text="@Explanation">
                    <MudIcon Icon="Icons.Material.Filled.Info" Size="Size.Small" Class="ml-2" />
                </MudTooltip>
            }
        </MudText>
        
        <MudGrid Spacing="2">
            @foreach (var recommendation in Recommendations)
            {
                <MudItem xs="6" sm="4" md="3" lg="2">
                    <MudCard Class="recommendation-card" Elevation="2">
                        <MudCardContent>
                            <MudLink Href="@($"/products/{recommendation.ProductId}")" Typo="Typo.body2" Class="product-link">
                                <MudImage Src="@GetProductImage(recommendation)" Height="120px" Width="100%" />
                                <MudText Typo="Typo.body2" Class="mt-2 product-name">@recommendation.ProductName</MudText>
                                <MudChip Color="Color.Secondary" Size="Size.Small" Label="@recommendation.Reason" />
                            </MudLink>
                        </MudCardContent>
                    </MudCard>
                </MudItem>
            }
        </MudGrid>
    </MudPaper>
}
@code {
    [Parameter]
    public string Title { get; set; } = "Recommended For You";
    
    [Parameter]
    public string? Explanation { get; set; }
    
    [Parameter]
    public int Count { get; set; } = 6;
    
    [Parameter]
    public Guid? ProductId { get; set; }
    private List<RecommendationDto> Recommendations { get; set; } = new();
    private Dictionary<Guid, ProductDto> ProductCache { get; set; } = new();
    protected override async Task OnInitializedAsync()
    {
        await LoadRecommendations();
    }
    protected override async Task OnParametersSetAsync()
    {
        if (ProductId.HasValue)
        {
            await LoadSimilarProducts();
        }
    }
    private async Task LoadRecommendations()
    {
        var userId = await AuthService.GetCurrentUserIdAsync();
        if (userId.HasValue)
        {
            var recommendations = await RecommendationService.GetPersonalizedRecommendationsAsync(userId.Value, Count);
            Recommendations = recommendations.Select(r => new RecommendationDto
            {
                ProductId = r.ProductId,
                ProductName = r.ProductName,
                Score = r.Score,
                Reason = r.Reason
            }).ToList();
            
            await LoadProductDetails();
        }
    }
    private async Task LoadSimilarProducts()
    {
        if (ProductId.HasValue)
        {
            var recommendations = await RecommendationService.GetSimilarProductsAsync(ProductId.Value, Count);
            Recommendations = recommendations.Select(r => new RecommendationDto
            {
                ProductId = r.ProductId,
                ProductName = r.ProductName,
                Score = r.Score,
                Reason = r.Reason
            }).ToList();
            
            await LoadProductDetails();
        }
    }
    private async Task LoadProductDetails()
    {
        var productIds = Recommendations.Select(r => r.ProductId).ToList();
        var query = new GetProductsByIdsQuery { ProductIds = productIds };
        var result = await Mediator.Send(query);
        
        if (result.Succeeded && result.Data != null)
        {
            ProductCache = result.Data.ToDictionary(p => p.Id, p => p);
        }
    }
    private string GetProductImage(RecommendationDto recommendation)
    {
        if (ProductCache.TryGetValue(recommendation.ProductId, out var product) && 
            product.Images.Any())
        {
            return product.Images.First().ImageUrl;
        }
        
        return "/images/placeholder-product.jpg";
    }
}
public class RecommendationDto
{
    public Guid ProductId { get; set; }
    public string ProductName { get; set; } = string.Empty;
    public float Score { get; set; }
    public string Reason { get; set; } = string.Empty;
}9. Real-time Features
9.1 SignalR Real-time Notifications
// Web/Hubs/NotificationHub.cs using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using SmartCommerce.Application.Common.Interfaces; namespace SmartCommerce.Web.Hubs { [Authorize] public class NotificationHub : Hub { private readonly IConnectionManager _connectionManager; private readonly ILogger<NotificationHub> _logger; public NotificationHub(IConnectionManager connectionManager, ILogger<NotificationHub> logger) { _connectionManager = connectionManager; _logger = logger; } public override async Task OnConnectedAsync() { var userId = Context.User?.FindFirst("sub")?.Value; if (userId != null && Guid.TryParse(userId, out var userGuid)) { await _connectionManager.AddConnectionAsync(userGuid, Context.ConnectionId); await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}"); _logger.LogInformation("User {UserId} connected with connection {ConnectionId}", userId, Context.ConnectionId); } await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var userId = Context.User?.FindFirst("sub")?.Value; if (userId != null && Guid.TryParse(userId, out var userGuid)) { await _connectionManager.RemoveConnectionAsync(userGuid, Context.ConnectionId); await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); _logger.LogInformation("User {UserId} disconnected from connection {ConnectionId}", userId, Context.ConnectionId); } await base.OnDisconnectedAsync(exception); } public async Task SubscribeToProduct(Guid productId) { await Groups.AddToGroupAsync(Context.ConnectionId, $"product_{productId}"); _logger.LogInformation("User subscribed to product {ProductId}", productId); } public async Task UnsubscribeFromProduct(Guid productId) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"product_{productId}"); _logger.LogInformation("User unsubscribed from product {ProductId}", productId); } public async Task JoinAdminGroup() { if (Context.User?.IsInRole("Admin") == true) { await Groups.AddToGroupAsync(Context.ConnectionId, "admins"); _logger.LogInformation("Admin user joined admin group"); } } } public interface INotificationClient { Task ReceiveNotification(NotificationDto notification); Task ProductStockUpdated(ProductStockUpdateDto update); Task OrderStatusChanged(OrderStatusUpdateDto update); Task PriceChanged(PriceChangeDto change); Task NewReviewAdded(ProductReviewDto review); } public class NotificationService : INotificationService { private readonly IHubContext<NotificationHub, INotificationClient> _hubContext; private readonly IConnectionManager _connectionManager; private readonly ILogger<NotificationService> _logger; public NotificationService( IHubContext<NotificationHub, INotificationClient> hubContext, IConnectionManager connectionManager, ILogger<NotificationService> logger) { _hubContext = hubContext; _connectionManager = connectionManager; _logger = logger; } public async Task NotifyUserAsync(Guid userId, NotificationDto notification) { try { var connections = await _connectionManager.GetConnectionsAsync(userId); if (connections.Any()) { await _hubContext.Clients.Clients(connections).ReceiveNotification(notification); _logger.LogInformation("Sent notification to user {UserId}", userId); } } catch (Exception ex) { _logger.LogError(ex, "Failed to send notification to user {UserId}", userId); } } public async Task NotifyProductSubscribersAsync(Guid productId, ProductStockUpdateDto update) { try { await _hubContext.Clients.Group($"product_{productId}").ProductStockUpdated(update); _logger.LogInformation("Notified subscribers of product {ProductId} stock update", productId); } catch (Exception ex) { _logger.LogError(ex, "Failed to notify product subscribers for product {ProductId}", productId); } } public async Task NotifyOrderUpdateAsync(Guid orderId, Guid userId, OrderStatusUpdateDto update) { try { await _hubContext.Clients.Group($"user_{userId}").OrderStatusChanged(update); _logger.LogInformation("Notified user {UserId} of order {OrderId} status update", userId, orderId); } catch (Exception ex) { _logger.LogError(ex, "Failed to notify user {UserId} of order {OrderId} update", userId, orderId); } } public async Task NotifyAdminsAsync(NotificationDto notification) { try { await _hubContext.Clients.Group("admins").ReceiveNotification(notification); _logger.LogInformation("Sent admin notification"); } catch (Exception ex) { _logger.LogError(ex, "Failed to send admin notification"); } } public async Task BroadcastPriceChangeAsync(PriceChangeDto change) { try { await _hubContext.Clients.All.PriceChanged(change); _logger.LogInformation("Broadcasted price change for product {ProductId}", change.ProductId); } catch (Exception ex) { _logger.LogError(ex, "Failed to broadcast price change for product {ProductId}", change.ProductId); } } } }
10. Testing Strategy
10.1 Comprehensive Test Suite
// Tests/Application.UnitTests/Features/Products/GetProductDetailQueryTests.cs using AutoMapper; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Moq; using SmartCommerce.Application.Common.Interfaces; using SmartCommerce.Application.Features.Products.Queries.GetProductDetail; using SmartCommerce.Domain.Entities; namespace SmartCommerce.Application.UnitTests.Features.Products.Queries { public class GetProductDetailQueryTests { private readonly Mock<IApplicationDbContext> _mockContext; private readonly Mock<IMapper> _mockMapper; private readonly Mock<ICurrentUserService> _mockCurrentUserService; private readonly GetProductDetailQueryHandler _handler; public GetProductDetailQueryTests() { _mockContext = new Mock<IApplicationDbContext>(); _mockMapper = new Mock<IMapper>(); _mockCurrentUserService = new Mock<ICurrentUserService>(); _handler = new GetProductDetailQueryHandler( _mockContext.Object, _mockMapper.Object, _mockCurrentUserService.Object); } [Fact] public async Task Handle_WithValidId_ReturnsProductDetail() { // Arrange var productId = Guid.NewGuid(); var product = new Product("Test Product", "Test Description", new Money(99.99m, "USD"), "TEST-SKU", Guid.NewGuid()); var productsMock = CreateDbSetMock(new List<Product> { product }); _mockContext.Setup(c => c.Products).Returns(productsMock.Object); var productDetailDto = new ProductDetailDto { Id = productId, Name = "Test Product" }; _mockMapper.Setup(m => m.ConfigurationProvider).Returns(new MapperConfiguration(cfg => cfg.CreateMap<Product, ProductDetailDto>())); _mockMapper.Setup(m => m.ProjectTo<ProductDetailDto>(It.IsAny<IQueryable>(), It.IsAny<object>())) .Returns(new List<ProductDetailDto> { productDetailDto }.AsQueryable()); var query = new GetProductDetailQuery { Id = productId }; // Act var result = await _handler.Handle(query, CancellationToken.None); // Assert result.Succeeded.Should().BeTrue(); result.Data.Should().NotBeNull(); result.Data.Name.Should().Be("Test Product"); } [Fact] public async Task Handle_WithNonExistentId_ReturnsFailure() { // Arrange var productId = Guid.NewGuid(); var productsMock = CreateDbSetMock(new List<Product>()); _mockContext.Setup(c => c.Products).Returns(productsMock.Object); var query = new GetProductDetailQuery { Id = productId }; // Act var result = await _handler.Handle(query, CancellationToken.None); // Assert result.Succeeded.Should().BeFalse(); result.Errors.Should().Contain($"Product with ID {productId} not found."); } [Fact] public async Task Handle_WithInactiveProduct_ReturnsFailure() { // Arrange var productId = Guid.NewGuid(); var product = new Product("Test Product", "Test Description", new Money(99.99m, "USD"), "TEST-SKU", Guid.NewGuid()); product.Deactivate(); var productsMock = CreateDbSetMock(new List<Product> { product }); _mockContext.Setup(c => c.Products).Returns(productsMock.Object); var query = new GetProductDetailQuery { Id = productId }; // Act var result = await _handler.Handle(query, CancellationToken.None); // Assert result.Succeeded.Should().BeFalse(); } private static Mock<DbSet<T>> CreateDbSetMock<T>(List<T> elements) where T : class { var queryable = elements.AsQueryable(); var dbSetMock = new Mock<DbSet<T>>(); dbSetMock.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider); dbSetMock.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression); dbSetMock.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType); dbSetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator()); return dbSetMock; } } }
11. Deployment & DevOps
11.1 Docker Configuration
# Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy project files
COPY ["src/SmartCommerce.Web/SmartCommerce.Web.csproj", "src/SmartCommerce.Web/"]
COPY ["src/SmartCommerce.Application/SmartCommerce.Application.csproj", "src/SmartCommerce.Application/"]
COPY ["src/SmartCommerce.Domain/SmartCommerce.Domain.csproj", "src/SmartCommerce.Domain/"]
COPY ["src/SmartCommerce.Infrastructure/SmartCommerce.Infrastructure.csproj", "src/SmartCommerce.Infrastructure/"]
COPY ["src/SmartCommerce.Shared/SmartCommerce.Shared.csproj", "src/SmartCommerce.Shared/"]
# Restore dependencies
RUN dotnet restore "src/SmartCommerce.Web/SmartCommerce.Web.csproj"
# Copy everything else
COPY . .
# Build and publish
WORKDIR "/src/src/SmartCommerce.Web"
RUN dotnet build "SmartCommerce.Web.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "SmartCommerce.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser
COPY --from=publish /app/publish .
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost/health || exit 1
ENTRYPOINT ["dotnet", "SmartCommerce.Web.dll"]11.2 Kubernetes Deployment
# kubernetes/web-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: smartcommerce-web labels: app: smartcommerce-web spec: replicas: 3 selector: matchLabels: app: smartcommerce-web template: metadata: labels: app: smartcommerce-web annotations: prometheus.io/scrape: "true" prometheus.io/port: "80" prometheus.io/path: "/metrics" spec: containers: - name: web image: smartcommerce.azurecr.io/web:latest ports: - containerPort: 80 - containerPort: 443 env: - name: ASPNETCORE_ENVIRONMENT value: "Production" - name: ConnectionStrings__DefaultConnection valueFrom: secretKeyRef: name: smartcommerce-secrets key: database-connection-string - name: Azure__KeyVault__Endpoint valueFrom: secretKeyRef: name: smartcommerce-secrets key: keyvault-endpoint resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 80 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 80 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 startupProbe: httpGet: path: /health/startup port: 80 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 10 --- apiVersion: v1 kind: Service metadata: name: smartcommerce-web-service spec: selector: app: smartcommerce-web ports: - name: http port: 80 targetPort: 80 - name: https port: 443 targetPort: 443 type: LoadBalancer
12. Production Readiness
12.1 Monitoring and Observability
// Infrastructure/Logging/SerilogConfiguration.cs using Serilog; using Serilog.Events; using Serilog.Sinks.ApplicationInsights.Sinks.ApplicationInsights.TelemetryConverters; using SmartCommerce.Web.Middleware; namespace SmartCommerce.Infrastructure.Logging { public static class SerilogConfiguration { public static IHostBuilder UseSerilogConfiguration(this IHostBuilder builder, IConfiguration configuration) { return builder.UseSerilog((context, services, loggerConfiguration) => { var applicationInsightsConnectionString = configuration["ApplicationInsights:ConnectionString"]; loggerConfiguration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() .Enrich.WithProperty("Application", "SmartCommerce") .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName) .Enrich.With<ActivityEnricher>() .Enrich.With<CorrelationIdEnricher>() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}") .WriteTo.Debug() .WriteTo.File( "logs/smartcommerce-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, shared: true); if (!string.IsNullOrEmpty(applicationInsightsConnectionString)) { loggerConfiguration.WriteTo.ApplicationInsights( applicationInsightsConnectionString, new TraceTelemetryConverter()); } if (context.HostingEnvironment.IsDevelopment()) { loggerConfiguration.MinimumLevel.Override("Microsoft", LogEventLevel.Information); loggerConfiguration.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning); } else { loggerConfiguration.MinimumLevel.Override("Microsoft", LogEventLevel.Warning); loggerConfiguration.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning); } }); } } public class CorrelationIdEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var correlationId = CorrelationIdMiddleware.GetCorrelationId(); if (!string.IsNullOrEmpty(correlationId)) { var correlationIdProperty = propertyFactory.CreateProperty("CorrelationId", correlationId); logEvent.AddPropertyIfAbsent(correlationIdProperty); } } } public class ActivityEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var activity = Activity.Current; if (activity != null) { logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TraceId", activity.TraceId)); logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("SpanId", activity.SpanId)); } } } }
This comprehensive full-stack ASP.NET Core project demonstrates enterprise-grade development practices with AI integration, cloud-native architecture, and production-ready features. The project showcases real-world e-commerce functionality with intelligent recommendations, real-time updates, and scalable microservices architecture.
The implementation follows Clean Architecture principles, incorporates domain-driven design, and demonstrates advanced patterns like CQRS, Event Sourcing, and AI-powered features. The project is production-ready with proper testing, monitoring, logging, and deployment configurations.
This serves as an excellent foundation for building modern, scalable, intelligent web applications using ASP.NET Core and related technologies.
.png)
0 Comments
thanks for your comments!