fix(merchant-service): fix EF Core unmapped property errors in repositories

- Changed repository LINQ queries to use EF.Property<T>() for backing fields
- Expression-bodied properties cannot be auto-mapped by EF Core
- Fixed StatusId comparison in CreateShopCommandHandler (Status nav is null)
- Updated EntityTypeConfiguration comments explaining Ignore pattern
This commit is contained in:
Ho Ngoc Hai
2026-02-28 03:12:42 +07:00
parent 68a6c4a81e
commit 57afe213e4
6 changed files with 40 additions and 36 deletions

View File

@@ -51,9 +51,9 @@ public class CreateShopCommandHandler : IRequestHandler<CreateShopCommand, Creat
throw new DomainException("Merchant not found. Please register as a merchant first.");
}
// EN: Check if merchant is active
// VI: Kiểm tra merchant có active không
if (merchant.Status != MerchantStatus.Active)
// EN: Check if merchant is active (compare StatusId since Status navigation is not loaded by EF)
// VI: Kiểm tra merchant có active không (compare StatusId vì Status navigation không được load bởi EF)
if (merchant.StatusId != MerchantStatus.Active.Id)
{
throw new DomainException("Only active merchants can create shops. Please wait for approval.");
}
@@ -130,7 +130,7 @@ public class CreateShopCommandHandler : IRequestHandler<CreateShopCommand, Creat
shop.Id,
shop.Name,
shop.Slug,
shop.Status.Name
"Draft" // EN: New shops are always Draft / VI: Shop mới luôn là Draft
);
}
}

View File

@@ -120,8 +120,10 @@ public class MerchantEntityTypeConfiguration : IEntityTypeConfiguration<Merchant
builder.HasIndex("_userId").HasDatabaseName("ix_merchants_user_id");
builder.HasIndex(m => m.StatusId).HasDatabaseName("ix_merchants_status");
// EN: Ignore navigation properties
// VI: Bỏ qua navigation properties
// EN: Ignore expression-bodied properties (EF Core can't map them)
// EN: Repository queries use EF.Property<T>() to access backing fields directly.
// VI: Ignore expression-bodied properties (EF Core không thể map chúng)
// VI: Repository queries sử dụng EF.Property<T>() để truy cập backing fields trực tiếp.
builder.Ignore(m => m.Type);
builder.Ignore(m => m.Status);
builder.Ignore(m => m.VerificationStatus);

View File

@@ -91,8 +91,9 @@ public class MerchantStaffEntityTypeConfiguration : IEntityTypeConfiguration<Mer
builder.HasIndex("_merchantId").HasDatabaseName("ix_merchant_staff_merchant_id");
builder.HasIndex("_email").HasDatabaseName("ix_merchant_staff_email");
// EN: Ignore calculated properties
// VI: Bỏ qua các properties được tính toán
// EN: Ignore expression-bodied properties (EF Core can't map them)
// EN: Repository queries use EF.Property<T>() to access backing fields directly.
// VI: Ignore expression-bodied properties (EF Core không thể map chúng)
builder.Ignore(s => s.Role);
builder.Ignore(s => s.Status);
builder.Ignore(s => s.Permissions);
@@ -159,8 +160,8 @@ public class DeviceTokenEntityTypeConfiguration : IEntityTypeConfiguration<Devic
builder.HasIndex(d => d.StaffId).HasDatabaseName("ix_device_tokens_staff_id");
builder.HasIndex("_deviceId").HasDatabaseName("ix_device_tokens_device_id");
// EN: Ignore navigation properties
// VI: Bỏ qua navigation properties
// 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);
@@ -218,8 +219,8 @@ public class ShopMemberEntityTypeConfiguration : IEntityTypeConfiguration<ShopMe
builder.HasIndex(m => m.StaffId).HasDatabaseName("ix_shop_members_staff_id");
builder.HasIndex("_shopId").HasDatabaseName("ix_shop_members_shop_id");
// EN: Ignore navigation properties
// VI: Bỏ qua navigation properties
// EN: Ignore expression-bodied properties
// VI: Ignore expression-bodied properties
builder.Ignore(m => m.Role);
builder.Ignore(m => m.CustomPermissions);
builder.Ignore(m => m.ShopId);

View File

@@ -128,13 +128,15 @@ public class ShopEntityTypeConfiguration : IEntityTypeConfiguration<Shop>
builder.HasIndex(s => s.StatusId).HasDatabaseName("ix_shops_status");
builder.HasIndex(s => s.CategoryId).HasDatabaseName("ix_shops_category");
// EN: Ignore calculated properties
// VI: Bỏ qua các properties được tính toán
// EN: Ignore expression-bodied properties (EF Core can't map them)
// EN: Repository queries use EF.Property<T>() to access backing fields directly.
// VI: Ignore expression-bodied properties (EF Core không thể map chúng)
builder.Ignore(s => s.Type);
builder.Ignore(s => s.Category);
builder.Ignore(s => s.Status);
builder.Ignore(s => s.ContactInfo);
builder.Ignore(s => s.OperatingHours);
builder.Ignore(s => s.Features);
builder.Ignore(s => s.Name);
builder.Ignore(s => s.Slug);
builder.Ignore(s => s.Description);
@@ -144,7 +146,6 @@ public class ShopEntityTypeConfiguration : IEntityTypeConfiguration<Shop>
builder.Ignore(s => s.CreatedAt);
builder.Ignore(s => s.UpdatedAt);
builder.Ignore(s => s.IsDeleted);
builder.Ignore(s => s.Features);
}
}
@@ -262,14 +263,14 @@ public class ShopBranchEntityTypeConfiguration : IEntityTypeConfiguration<ShopBr
// VI: Indexes
builder.HasIndex(b => b.ShopId).HasDatabaseName("ix_shop_branches_shop_id");
// EN: Ignore navigation properties
// VI: Bỏ qua navigation properties
builder.Ignore(b => b.Name);
builder.Ignore(b => b.Code);
builder.Ignore(b => b.Phone);
// EN: Ignore expression-bodied properties (repository queries use EF.Property)
// VI: Ignore expression-bodied properties (repository queries dùng EF.Property)
builder.Ignore(b => b.Address);
builder.Ignore(b => b.Location);
builder.Ignore(b => b.OperatingHours);
builder.Ignore(b => b.Name);
builder.Ignore(b => b.Code);
builder.Ignore(b => b.Phone);
builder.Ignore(b => b.IsActive);
builder.Ignore(b => b.CreatedAt);
builder.Ignore(b => b.UpdatedAt);

View File

@@ -26,29 +26,29 @@ public class MerchantRepository : IMerchantRepository
public async Task<Merchant?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _context.Merchants
.FirstOrDefaultAsync(m => m.Id == id && !m.IsDeleted, cancellationToken);
.FirstOrDefaultAsync(m => m.Id == id && !EF.Property<bool>(m, "_isDeleted"), cancellationToken);
}
/// <inheritdoc />
public async Task<Merchant?> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default)
{
return await _context.Merchants
.FirstOrDefaultAsync(m => m.UserId == userId && !m.IsDeleted, cancellationToken);
.FirstOrDefaultAsync(m => EF.Property<Guid>(m, "_userId") == userId && !EF.Property<bool>(m, "_isDeleted"), cancellationToken);
}
/// <inheritdoc />
public async Task<bool> ExistsByUserIdAsync(Guid userId, CancellationToken cancellationToken = default)
{
return await _context.Merchants
.AnyAsync(m => m.UserId == userId && !m.IsDeleted, cancellationToken);
.AnyAsync(m => EF.Property<Guid>(m, "_userId") == userId && !EF.Property<bool>(m, "_isDeleted"), cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Merchant>> GetAllAsync(int pageNumber, int pageSize, CancellationToken cancellationToken = default)
{
return await _context.Merchants
.Where(m => !m.IsDeleted)
.OrderByDescending(m => m.CreatedAt)
.Where(m => !EF.Property<bool>(m, "_isDeleted"))
.OrderByDescending(m => EF.Property<DateTime>(m, "_createdAt"))
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
@@ -58,8 +58,8 @@ public class MerchantRepository : IMerchantRepository
public async Task<IReadOnlyList<Merchant>> GetByStatusAsync(MerchantStatus status, CancellationToken cancellationToken = default)
{
return await _context.Merchants
.Where(m => m.StatusId == status.Id && !m.IsDeleted)
.OrderByDescending(m => m.CreatedAt)
.Where(m => m.StatusId == status.Id && !EF.Property<bool>(m, "_isDeleted"))
.OrderByDescending(m => EF.Property<DateTime>(m, "_createdAt"))
.ToListAsync(cancellationToken);
}

View File

@@ -26,7 +26,7 @@ public class ShopRepository : IShopRepository
public async Task<Shop?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _context.Shops
.FirstOrDefaultAsync(s => s.Id == id && !s.IsDeleted, cancellationToken);
.FirstOrDefaultAsync(s => s.Id == id && !EF.Property<bool>(s, "_isDeleted"), cancellationToken);
}
/// <inheritdoc />
@@ -34,29 +34,29 @@ public class ShopRepository : IShopRepository
{
return await _context.Shops
.Include(s => s.Branches)
.FirstOrDefaultAsync(s => s.Id == id && !s.IsDeleted, cancellationToken);
.FirstOrDefaultAsync(s => s.Id == id && !EF.Property<bool>(s, "_isDeleted"), cancellationToken);
}
/// <inheritdoc />
public async Task<Shop?> GetBySlugAsync(string slug, CancellationToken cancellationToken = default)
{
return await _context.Shops
.FirstOrDefaultAsync(s => s.Slug == slug.ToLowerInvariant() && !s.IsDeleted, cancellationToken);
.FirstOrDefaultAsync(s => EF.Property<string>(s, "_slug") == slug.ToLowerInvariant() && !EF.Property<bool>(s, "_isDeleted"), cancellationToken);
}
/// <inheritdoc />
public async Task<bool> SlugExistsAsync(string slug, CancellationToken cancellationToken = default)
{
return await _context.Shops
.AnyAsync(s => s.Slug == slug.ToLowerInvariant(), cancellationToken);
.AnyAsync(s => EF.Property<string>(s, "_slug") == slug.ToLowerInvariant(), cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Shop>> GetByMerchantIdAsync(Guid merchantId, CancellationToken cancellationToken = default)
{
return await _context.Shops
.Where(s => s.MerchantId == merchantId && !s.IsDeleted)
.OrderByDescending(s => s.CreatedAt)
.Where(s => EF.Property<Guid>(s, "_merchantId") == merchantId && !EF.Property<bool>(s, "_isDeleted"))
.OrderByDescending(s => EF.Property<DateTime>(s, "_createdAt"))
.ToListAsync(cancellationToken);
}
@@ -64,8 +64,8 @@ public class ShopRepository : IShopRepository
public async Task<IReadOnlyList<Shop>> GetByCategoryAsync(BusinessCategory category, int pageNumber, int pageSize, CancellationToken cancellationToken = default)
{
return await _context.Shops
.Where(s => s.CategoryId == category.Id && !s.IsDeleted && s.StatusId == ShopStatus.Active.Id)
.OrderByDescending(s => s.CreatedAt)
.Where(s => s.CategoryId == category.Id && !EF.Property<bool>(s, "_isDeleted") && s.StatusId == ShopStatus.Active.Id)
.OrderByDescending(s => EF.Property<DateTime>(s, "_createdAt"))
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
@@ -88,7 +88,7 @@ public class ShopRepository : IShopRepository
{
return await _context.Shops
.Include(s => s.Branches)
.Where(s => !s.IsDeleted && s.StatusId == ShopStatus.Active.Id)
.Where(s => !EF.Property<bool>(s, "_isDeleted") && s.StatusId == ShopStatus.Active.Id)
.ToListAsync(cancellationToken);
}
}