From 64e7b4e00d9755e3b1aca4d80a3002fae1bced31 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 4 Mar 2026 12:36:19 +0700 Subject: [PATCH] refactor: update EF Core backing field mapping and ignore DDD enumeration types --- .../Controllers/OrderController.cs | 2 +- .../Queries/GetProductByIdQueryHandler.cs | 46 ++++---- .../Queries/GetProductsQueryHandler.cs | 22 ++-- .../ProductAggregate/Product.cs | 8 -- .../CatalogContext.cs | 9 +- .../ProductEntityTypeConfiguration.cs | 10 +- .../InventoryItemEntityTypeConfiguration.cs | 26 ++--- .../InventoryContext.cs | 6 +- .../Repositories/InventoryRepository.cs | 4 +- .../MerchantStaffEntityTypeConfiguration.cs | 110 +++++++++--------- .../MerchantServiceContext.cs | 7 ++ 11 files changed, 132 insertions(+), 118 deletions(-) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs index 80ac4db9..5cf8b1d8 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs @@ -57,7 +57,7 @@ public class OrderController : ControllerBase /// [HttpPost("pos/orders")] public Task CreatePosOrder([FromBody] JsonElement body) => - _order.PostAsJsonAsync("/api/v1/pos/orders", body).ProxyAsync(); + _order.PostAsJsonAsync("/api/v1/orders", body).ProxyAsync(); /// /// EN: Get POS dashboard data — daily revenue, order count, popular items. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductByIdQueryHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductByIdQueryHandler.cs index 770a2a88..192fa07f 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductByIdQueryHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductByIdQueryHandler.cs @@ -5,6 +5,8 @@ using System.Text.Json; using MediatR; using Microsoft.EntityFrameworkCore; using CatalogService.API.Application.DTOs; +using CatalogService.Domain.AggregatesModel.ProductAggregate; +using CatalogService.Domain.SeedWork; using CatalogService.Infrastructure; namespace CatalogService.API.Application.Queries; @@ -24,27 +26,29 @@ public class GetProductByIdQueryHandler : IRequestHandler Handle(GetProductByIdQuery request, CancellationToken cancellationToken) { - var product = await _context.Products - .Where(p => p.Id == request.ProductId) - .Select(p => new ProductDto - { - Id = p.Id, - ShopId = p.ShopId, - Name = p.Name, - Description = p.Description, - Price = p.Price, - Type = p.Type.Name, - Attributes = p.Attributes != null - ? JsonSerializer.Deserialize>(p.Attributes.RootElement.GetRawText()) - : null, - ImageUrl = p.ImageUrl, - Sku = p.Sku, - IsActive = p.IsActive, - CreatedAt = p.CreatedAt, - UpdatedAt = p.UpdatedAt - }) - .FirstOrDefaultAsync(cancellationToken); + var typeMap = Enumeration.GetAll().ToDictionary(t => t.Id, t => t.Name); - return product; + var entity = await _context.Products + .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); + + if (entity == null) return null; + + return new ProductDto + { + Id = entity.Id, + ShopId = entity.ShopId, + Name = entity.Name, + Description = entity.Description, + Price = entity.Price, + Type = typeMap.GetValueOrDefault(entity.TypeId, "Unknown"), + Attributes = entity.Attributes != null + ? JsonSerializer.Deserialize>(entity.Attributes.RootElement.GetRawText()) + : null, + ImageUrl = entity.ImageUrl, + Sku = entity.Sku, + IsActive = entity.IsActive, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + }; } } diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductsQueryHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductsQueryHandler.cs index 0a6210b7..2b0d06dd 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductsQueryHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetProductsQueryHandler.cs @@ -5,6 +5,8 @@ using System.Text.Json; using MediatR; using Microsoft.EntityFrameworkCore; using CatalogService.API.Application.DTOs; +using CatalogService.Domain.AggregatesModel.ProductAggregate; +using CatalogService.Domain.SeedWork; using CatalogService.Infrastructure; namespace CatalogService.API.Application.Queries; @@ -36,24 +38,30 @@ public class GetProductsQueryHandler : IRequestHandler p.Type.Name == request.Type); + // EN: Resolve TypeId from enumeration name for server-side filtering. + // VI: Resolve TypeId từ tên enumeration để lọc phía server. + var typeEnum = Enumeration.FromDisplayName(request.Type); + query = query.Where(p => p.TypeId == typeEnum.Id); } // EN: Get total count // VI: Lấy tổng số var totalCount = await query.CountAsync(cancellationToken); - // EN: Apply pagination and load entities, then project in memory - // (JsonDocument cannot be projected server-side by EF Core). - // VI: Phân trang và load entities, rồi project trong bộ nhớ - // (JsonDocument không thể project server-side bởi EF Core). + // EN: Apply pagination and load entities, then project in memory. + // Type navigation is ignored by EF Core, so resolve name from Enumeration. + // VI: Phân trang và load entities, rồi project trong bộ nhớ. + // Type navigation bị ignore bởi EF Core, nên resolve tên từ Enumeration. var entities = await query - .Include(p => p.Type) .OrderBy(p => p.Name) .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken); + // EN: Build TypeId → Name lookup from Enumeration. + // VI: Build lookup TypeId → Name từ Enumeration. + var typeMap = Enumeration.GetAll().ToDictionary(t => t.Id, t => t.Name); + var products = entities.Select(p => new ProductDto { Id = p.Id, @@ -61,7 +69,7 @@ public class GetProductsQueryHandler : IRequestHandler>(p.Attributes.RootElement.GetRawText()) : null, diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Product.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Product.cs index b39b26d4..62726f8e 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Product.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Product.cs @@ -18,7 +18,6 @@ public class Product : Entity, IAggregateRoot private string _name = null!; private string? _description; private decimal _price; - private ProductType _type = null!; private JsonDocument? _attributes; // Type-specific attributes in JSONB private string? _imageUrl; private string? _sku; @@ -50,12 +49,6 @@ public class Product : Entity, IAggregateRoot /// public decimal Price => _price; - /// - /// EN: Product type. - /// VI: Loại sản phẩm. - /// - public ProductType Type => _type; - /// /// EN: Type ID for EF Core mapping. /// VI: Type ID cho EF Core mapping. @@ -132,7 +125,6 @@ public class Product : Entity, IAggregateRoot _name = name.Trim(); _description = description?.Trim(); _price = price; - _type = type; TypeId = type.Id; _attributes = attributes; _sku = sku?.Trim(); diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogContext.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogContext.cs index 04ee6251..450a1322 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogContext.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogContext.cs @@ -53,7 +53,14 @@ public class CatalogContext : DbContext, IUnitOfWork // VI: Áp dụng các cấu hình entity modelBuilder.ApplyConfiguration(new ProductEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new CategoryEntityTypeConfiguration()); - modelBuilder.ApplyConfiguration(new ProductTypeEntityTypeConfiguration()); + + // EN: Ignore ProductType so EF Core does NOT auto-discover Product.TypeId as a FK. + // ProductType is a DDD Enumeration resolved in-memory; the product_types table + // is seeded by the initial migration and not managed at runtime. + // VI: Ignore ProductType để EF Core KHÔNG tự phát hiện Product.TypeId là FK. + // ProductType là DDD Enumeration được resolve trong bộ nhớ; bảng product_types + // được seed bởi migration khởi tạo và không được quản lý lúc runtime. + modelBuilder.Ignore(); } /// diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/ProductEntityTypeConfiguration.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/ProductEntityTypeConfiguration.cs index 63c98c9a..1c497819 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/ProductEntityTypeConfiguration.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/ProductEntityTypeConfiguration.cs @@ -98,11 +98,9 @@ public class ProductEntityTypeConfiguration : IEntityTypeConfiguration builder.HasIndex(p => p.Sku).HasDatabaseName("ix_products_sku"); builder.HasIndex(p => p.IsActive).HasDatabaseName("ix_products_is_active"); - // EN: Configure navigation to ProductType via TypeId foreign key. - // VI: Cấu hình navigation đến ProductType qua TypeId foreign key. - builder.HasOne(p => p.Type) - .WithMany() - .HasForeignKey(p => p.TypeId) - .OnDelete(DeleteBehavior.Restrict); + // EN: TypeId is a plain FK column — no navigation property to ProductType. + // Type name is resolved from Enumeration in query handlers. + // VI: TypeId là cột FK thuần — không có navigation property đến ProductType. + // Tên Type được resolve từ Enumeration trong query handlers. } } diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs index d14fe365..b6112703 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs @@ -16,12 +16,14 @@ public class InventoryItemEntityTypeConfiguration : IEntityTypeConfiguration i.Id); builder.Property(i => i.Id).HasColumnName("id").ValueGeneratedNever(); - builder.Property("_productId").HasColumnName("product_id").IsRequired(); - builder.Property("_shopId").HasColumnName("shop_id").IsRequired(); - builder.Property("_quantity").HasColumnName("quantity").IsRequired(); - builder.Property("_reservedQuantity").HasColumnName("reserved_quantity").IsRequired(); - builder.Property("_reorderLevel").HasColumnName("reorder_level").HasDefaultValue(10); - builder.Property("_updatedAt").HasColumnName("updated_at"); + builder.Property(i => i.ProductId).HasField("_productId").HasColumnName("product_id").IsRequired(); + builder.Property(i => i.ShopId).HasField("_shopId").HasColumnName("shop_id").IsRequired(); + builder.Property(i => i.Quantity).HasField("_quantity").HasColumnName("quantity").IsRequired(); + builder.Property(i => i.ReservedQuantity).HasField("_reservedQuantity").HasColumnName("reserved_quantity").IsRequired(); + builder.Property(i => i.ReorderLevel).HasField("_reorderLevel").HasColumnName("reorder_level").HasDefaultValue(10); + builder.Property(i => i.UpdatedAt).HasField("_updatedAt").HasColumnName("updated_at"); + + builder.Ignore(i => i.CreatedAt); // Owned InventoryTransaction collection builder.OwnsMany(i => i.Transactions, txn => @@ -44,15 +46,11 @@ public class InventoryItemEntityTypeConfiguration : IEntityTypeConfiguration t.CreatedAt); }); - builder.HasIndex("_productId").HasDatabaseName("ix_inventory_product_id"); - builder.HasIndex("_shopId").HasDatabaseName("ix_inventory_shop_id"); + builder.HasIndex(i => i.ProductId).HasDatabaseName("ix_inventory_product_id"); + builder.HasIndex(i => i.ShopId).HasDatabaseName("ix_inventory_shop_id"); - builder.Ignore(i => i.ProductId); - builder.Ignore(i => i.ShopId); - builder.Ignore(i => i.Quantity); - builder.Ignore(i => i.ReservedQuantity); + // EN: AvailableQuantity is computed (Quantity - ReservedQuantity), not stored. + // VI: AvailableQuantity được tính toán (Quantity - ReservedQuantity), không lưu. builder.Ignore(i => i.AvailableQuantity); - builder.Ignore(i => i.ReorderLevel); - builder.Ignore(i => i.UpdatedAt); } } diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryContext.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryContext.cs index 5c3f628f..01f26ceb 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryContext.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryContext.cs @@ -43,7 +43,11 @@ public class InventoryContext : DbContext, IUnitOfWork protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new InventoryItemEntityTypeConfiguration()); - modelBuilder.ApplyConfiguration(new TransactionTypeEntityTypeConfiguration()); + + // EN: Ignore TransactionType so EF Core does NOT auto-discover TypeId as a FK. + // TransactionType is a DDD Enumeration resolved in-memory. + // VI: Ignore TransactionType để EF Core KHÔNG tự phát hiện TypeId là FK. + modelBuilder.Ignore(); } public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/InventoryRepository.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/InventoryRepository.cs index 1b7a37a8..b31c1c9b 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/InventoryRepository.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/InventoryRepository.cs @@ -95,14 +95,14 @@ public class InventoryRepository : IInventoryRepository CancellationToken cancellationToken = default) { var query = _context.InventoryItems - .Where(i => i.AvailableQuantity <= i.ReorderLevel); + .Where(i => (i.Quantity - i.ReservedQuantity) <= i.ReorderLevel); if (shopId.HasValue) query = query.Where(i => i.ShopId == shopId.Value); var total = await query.CountAsync(cancellationToken); var items = await query - .OrderBy(i => i.AvailableQuantity) + .OrderBy(i => i.Quantity - i.ReservedQuantity) .Skip(skip) .Take(take) .ToListAsync(cancellationToken); diff --git a/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs b/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs index ceff6379..8e3160d0 100644 --- a/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs +++ b/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs @@ -23,14 +23,17 @@ public class MerchantStaffEntityTypeConfiguration : IEntityTypeConfiguration("_userId") + builder.Property(s => s.UserId) + .HasField("_userId") .HasColumnName("user_id"); - builder.Property("_merchantId") + builder.Property(s => s.MerchantId) + .HasField("_merchantId") .HasColumnName("merchant_id") .IsRequired(); - builder.Property("_employeeCode") + builder.Property(s => s.EmployeeCode) + .HasField("_employeeCode") .HasColumnName("employee_code") .HasMaxLength(20); @@ -42,33 +45,41 @@ public class MerchantStaffEntityTypeConfiguration : IEntityTypeConfiguration("_permissions") + builder.Property(s => s.Permissions) + .HasField("_permissions") .HasColumnName("permissions") .HasConversion(); - builder.Property("_phone") + builder.Property(s => s.Phone) + .HasField("_phone") .HasColumnName("phone") .HasMaxLength(20); - builder.Property("_email") + builder.Property(s => s.Email) + .HasField("_email") .HasColumnName("email") .HasMaxLength(100); - builder.Property("_pinCodeHash") + builder.Property(s => s.PinCodeHash) + .HasField("_pinCodeHash") .HasColumnName("pin_code_hash") .HasMaxLength(100); - builder.Property("_joinedAt") + builder.Property(s => s.JoinedAt) + .HasField("_joinedAt") .HasColumnName("joined_at"); - builder.Property("_terminatedAt") + builder.Property(s => s.TerminatedAt) + .HasField("_terminatedAt") .HasColumnName("terminated_at"); - builder.Property("_createdAt") + builder.Property(s => s.CreatedAt) + .HasField("_createdAt") .HasColumnName("created_at") .IsRequired(); - builder.Property("_updatedAt") + builder.Property(s => s.UpdatedAt) + .HasField("_updatedAt") .HasColumnName("updated_at"); // EN: Configure navigation to device tokens @@ -87,26 +98,14 @@ public class MerchantStaffEntityTypeConfiguration : IEntityTypeConfiguration s.UserId).HasDatabaseName("ix_merchant_staff_user_id"); + builder.HasIndex(s => s.MerchantId).HasDatabaseName("ix_merchant_staff_merchant_id"); + builder.HasIndex(s => s.Email).HasDatabaseName("ix_merchant_staff_email"); - // EN: Ignore expression-bodied properties (EF Core can't map them) - // EN: Repository queries use EF.Property() to access backing fields directly. - // VI: Ignore expression-bodied properties (EF Core không thể map chúng) + // EN: Ignore Enumeration navigation properties (resolved in-memory). + // VI: Ignore Enumeration navigation properties (resolve trong bộ nhớ). builder.Ignore(s => s.Role); builder.Ignore(s => s.Status); - builder.Ignore(s => s.Permissions); - builder.Ignore(s => s.UserId); - builder.Ignore(s => s.MerchantId); - builder.Ignore(s => s.EmployeeCode); - builder.Ignore(s => s.Phone); - builder.Ignore(s => s.Email); - builder.Ignore(s => s.PinCodeHash); - builder.Ignore(s => s.JoinedAt); - builder.Ignore(s => s.TerminatedAt); - builder.Ignore(s => s.CreatedAt); - builder.Ignore(s => s.UpdatedAt); } } @@ -130,44 +129,41 @@ public class DeviceTokenEntityTypeConfiguration : IEntityTypeConfiguration("_deviceId") + builder.Property(d => d.DeviceId) + .HasField("_deviceId") .HasColumnName("device_id") .HasMaxLength(100) .IsRequired(); - builder.Property("_deviceName") + builder.Property(d => d.DeviceName) + .HasField("_deviceName") .HasColumnName("device_name") .HasMaxLength(100); - builder.Property("_fcmToken") + builder.Property(d => d.FcmToken) + .HasField("_fcmToken") .HasColumnName("fcm_token") .HasMaxLength(500); - builder.Property("_platform") + builder.Property(d => d.Platform) + .HasField("_platform") .HasColumnName("platform") .HasMaxLength(20) .IsRequired(); - builder.Property("_lastUsedAt") + builder.Property(d => d.LastUsedAt) + .HasField("_lastUsedAt") .HasColumnName("last_used_at"); - builder.Property("_createdAt") + builder.Property(d => d.CreatedAt) + .HasField("_createdAt") .HasColumnName("created_at") .IsRequired(); // EN: Indexes // VI: Indexes builder.HasIndex(d => d.StaffId).HasDatabaseName("ix_device_tokens_staff_id"); - builder.HasIndex("_deviceId").HasDatabaseName("ix_device_tokens_device_id"); - - // EN: Ignore expression-bodied properties - // VI: Ignore expression-bodied properties - builder.Ignore(d => d.DeviceId); - builder.Ignore(d => d.DeviceName); - builder.Ignore(d => d.FcmToken); - builder.Ignore(d => d.Platform); - builder.Ignore(d => d.LastUsedAt); - builder.Ignore(d => d.CreatedAt); + builder.HasIndex(d => d.DeviceId).HasDatabaseName("ix_device_tokens_device_id"); } } @@ -191,42 +187,42 @@ public class ShopMemberEntityTypeConfiguration : IEntityTypeConfiguration("_shopId") + builder.Property(m => m.ShopId) + .HasField("_shopId") .HasColumnName("shop_id") .IsRequired(); - builder.Property("_branchId") + builder.Property(m => m.BranchId) + .HasField("_branchId") .HasColumnName("branch_id"); builder.Property(m => m.RoleId) .HasColumnName("role_id") .IsRequired(); - builder.Property("_customPermissions") + builder.Property(m => m.CustomPermissions) + .HasField("_customPermissions") .HasColumnName("custom_permissions") .HasConversion(); - builder.Property("_isPrimary") + builder.Property(m => m.IsPrimary) + .HasField("_isPrimary") .HasColumnName("is_primary") .HasDefaultValue(false); - builder.Property("_assignedAt") + builder.Property(m => m.AssignedAt) + .HasField("_assignedAt") .HasColumnName("assigned_at") .IsRequired(); // EN: Indexes // VI: Indexes builder.HasIndex(m => m.StaffId).HasDatabaseName("ix_shop_members_staff_id"); - builder.HasIndex("_shopId").HasDatabaseName("ix_shop_members_shop_id"); + builder.HasIndex(m => m.ShopId).HasDatabaseName("ix_shop_members_shop_id"); - // EN: Ignore expression-bodied properties - // VI: Ignore expression-bodied properties + // EN: Ignore Enumeration navigation property (resolved in-memory). + // VI: Ignore Enumeration navigation property (resolve trong bộ nhớ). builder.Ignore(m => m.Role); - builder.Ignore(m => m.CustomPermissions); - builder.Ignore(m => m.ShopId); - builder.Ignore(m => m.BranchId); - builder.Ignore(m => m.IsPrimary); - builder.Ignore(m => m.AssignedAt); } } diff --git a/services/merchant-service-net/src/MerchantService.Infrastructure/MerchantServiceContext.cs b/services/merchant-service-net/src/MerchantService.Infrastructure/MerchantServiceContext.cs index 9c43139c..64850771 100644 --- a/services/merchant-service-net/src/MerchantService.Infrastructure/MerchantServiceContext.cs +++ b/services/merchant-service-net/src/MerchantService.Infrastructure/MerchantServiceContext.cs @@ -86,6 +86,13 @@ public class MerchantServiceContext : DbContext, IUnitOfWork // EN: Apply entity configurations from this assembly // VI: Áp dụng các cấu hình entity từ assembly này modelBuilder.ApplyConfigurationsFromAssembly(typeof(MerchantServiceContext).Assembly); + + // EN: Ignore Enumeration types so EF Core does NOT auto-discover FKs. + // These are DDD Enumerations resolved in-memory; their tables are seeded by migrations. + // VI: Ignore Enumeration types để EF Core KHÔNG tự phát hiện FK. + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); } ///