fix(superadmin): resolve merchant admin query EF Core translation errors
- Fix IsDeleted/CreatedAt/BusinessName private field access in LINQ queries
using EF.Property<T>() instead of public computed properties (which are
Ignored in EF config due to DDD pattern)
- Fix Shop.MerchantId private field in GroupBy shop count query
- Split merchant list query into 2 steps (fetch + batch shop counts)
to avoid untranslatable subquery in Join+Select
- Fix BFF SuperAdminController: remove duplicate auth header setting
(AuthForwardingHandler already reads bff_session cookie)
- Fix frontend DTO field names to match API response (ShopsCount, Type)
- Fix frontend response format handling for direct {items} without {data} wrapper
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MerchantService.Domain.AggregatesModel.MerchantAggregate;
|
||||
using MerchantService.Infrastructure;
|
||||
|
||||
namespace MerchantService.API.Application.Queries.Admin;
|
||||
@@ -19,8 +20,8 @@ public record GetAllMerchantsQuery(
|
||||
string? Search = null) : IRequest<AdminMerchantListResultDto>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetAllMerchantsQuery.
|
||||
/// VI: Handler cho GetAllMerchantsQuery.
|
||||
/// EN: Handler — uses EF.Property for private backing fields (Merchant uses DDD pattern).
|
||||
/// VI: Handler — dùng EF.Property cho private backing fields (Merchant dùng DDD pattern).
|
||||
/// </summary>
|
||||
public class GetAllMerchantsQueryHandler : IRequestHandler<GetAllMerchantsQuery, AdminMerchantListResultDto>
|
||||
{
|
||||
@@ -35,46 +36,76 @@ public class GetAllMerchantsQueryHandler : IRequestHandler<GetAllMerchantsQuery,
|
||||
GetAllMerchantsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// EN: All Merchant public properties are Ignored in EF config (DDD pattern with private fields).
|
||||
// Must use EF.Property<T>(entity, "_fieldName") or join on Id columns.
|
||||
// VI: Tất cả public properties của Merchant bị Ignore trong EF config (DDD pattern với private fields).
|
||||
// Phải dùng EF.Property<T>(entity, "_fieldName") hoặc join qua Id columns.
|
||||
var query = _context.Merchants
|
||||
.AsNoTracking()
|
||||
.Where(m => !m.IsDeleted);
|
||||
.Where(m => !EF.Property<bool>(m, "_isDeleted"));
|
||||
|
||||
// EN: Apply filters / VI: Áp dụng bộ lọc
|
||||
// EN: Filter by status — join with enumeration table to match by name
|
||||
// VI: Filter theo status — join với bảng enumeration để match theo tên
|
||||
if (!string.IsNullOrEmpty(request.Status))
|
||||
{
|
||||
query = query.Where(m => m.Status.Name == request.Status);
|
||||
var statusIds = _context.Set<MerchantStatus>()
|
||||
.Where(s => s.Name == request.Status).Select(s => s.Id);
|
||||
query = query.Where(m => statusIds.Contains(m.StatusId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.VerificationStatus))
|
||||
{
|
||||
query = query.Where(m => m.VerificationStatus.Name == request.VerificationStatus);
|
||||
var verifIds = _context.Set<VerificationStatus>()
|
||||
.Where(v => v.Name == request.VerificationStatus).Select(v => v.Id);
|
||||
query = query.Where(m => verifIds.Contains(m.VerificationStatusId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Search))
|
||||
{
|
||||
var searchLower = request.Search.ToLower();
|
||||
query = query.Where(m => m.BusinessName.ToLower().Contains(searchLower));
|
||||
query = query.Where(m => EF.Property<string>(m, "_businessName").ToLower().Contains(searchLower));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
var totalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize);
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
// EN: Step 1 — fetch merchant rows joined with enum tables (no subqueries)
|
||||
// VI: Bước 1 — lấy merchant rows join với bảng enum (không subquery)
|
||||
var rawItems = await query
|
||||
.OrderByDescending(m => EF.Property<DateTime>(m, "_createdAt"))
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(m => new AdminMerchantListItemDto(
|
||||
m.Id,
|
||||
m.UserId,
|
||||
m.BusinessName,
|
||||
m.Type.Name,
|
||||
m.Status.Name,
|
||||
m.VerificationStatus.Name,
|
||||
_context.Shops.Count(s => s.MerchantId == m.Id && !s.IsDeleted),
|
||||
m.CreatedAt,
|
||||
m.VerifiedAt))
|
||||
.Join(_context.Set<MerchantType>(), m => m.TypeId, t => t.Id, (m, t) => new { m, TypeName = t.Name })
|
||||
.Join(_context.Set<MerchantStatus>(), x => x.m.StatusId, s => s.Id, (x, s) => new { x.m, x.TypeName, StatusName = s.Name })
|
||||
.Join(_context.Set<VerificationStatus>(), x => x.m.VerificationStatusId, v => v.Id, (x, v) => new { x.m, x.TypeName, x.StatusName, VerifName = v.Name })
|
||||
.Select(x => new
|
||||
{
|
||||
x.m.Id,
|
||||
UserId = EF.Property<Guid>(x.m, "_userId"),
|
||||
BusinessName = EF.Property<string>(x.m, "_businessName"),
|
||||
x.TypeName,
|
||||
x.StatusName,
|
||||
x.VerifName,
|
||||
CreatedAt = EF.Property<DateTime>(x.m, "_createdAt"),
|
||||
x.m.VerifiedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// EN: Step 2 — batch-fetch shop counts (single query)
|
||||
// VI: Bước 2 — lấy shop counts theo batch (1 query duy nhất)
|
||||
var merchantIds = rawItems.Select(r => r.Id).ToList();
|
||||
var shopCounts = await _context.Shops
|
||||
.Where(s => merchantIds.Contains(EF.Property<Guid>(s, "_merchantId")) && !EF.Property<bool>(s, "_isDeleted"))
|
||||
.GroupBy(s => EF.Property<Guid>(s, "_merchantId"))
|
||||
.Select(g => new { MerchantId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.MerchantId, x => x.Count, cancellationToken);
|
||||
|
||||
var items = rawItems.Select(r => new AdminMerchantListItemDto(
|
||||
r.Id, r.UserId, r.BusinessName, r.TypeName, r.StatusName, r.VerifName,
|
||||
shopCounts.GetValueOrDefault(r.Id, 0),
|
||||
r.CreatedAt, r.VerifiedAt
|
||||
)).ToList();
|
||||
|
||||
return new AdminMerchantListResultDto(
|
||||
items,
|
||||
totalCount,
|
||||
|
||||
@@ -37,7 +37,7 @@ public class GetAllShopsQueryHandler : IRequestHandler<GetAllShopsQuery, AdminSh
|
||||
{
|
||||
var query = _context.Shops
|
||||
.AsNoTracking()
|
||||
.Where(s => !s.IsDeleted);
|
||||
.Where(s => !EF.Property<bool>(s, "_isDeleted"));
|
||||
|
||||
// EN: Apply filters / VI: Áp dụng bộ lọc
|
||||
if (!string.IsNullOrEmpty(request.Status))
|
||||
|
||||
@@ -33,12 +33,12 @@ public class GetMerchantDetailQueryHandler : IRequestHandler<GetMerchantDetailQu
|
||||
{
|
||||
var merchant = await _context.Merchants
|
||||
.AsNoTracking()
|
||||
.Where(m => m.Id == request.MerchantId && !m.IsDeleted)
|
||||
.Where(m => m.Id == request.MerchantId && !EF.Property<bool>(m, "_isDeleted"))
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? throw new DomainException($"Merchant {request.MerchantId} not found");
|
||||
|
||||
var shopsCount = await _context.Shops
|
||||
.CountAsync(s => s.MerchantId == request.MerchantId && !s.IsDeleted, cancellationToken);
|
||||
.CountAsync(s => s.MerchantId == request.MerchantId && !EF.Property<bool>(s, "_isDeleted"), cancellationToken);
|
||||
|
||||
var staffCount = await _context.MerchantStaff
|
||||
.CountAsync(s => s.MerchantId == request.MerchantId, cancellationToken);
|
||||
|
||||
@@ -32,7 +32,7 @@ public class GetMerchantStatisticsQueryHandler : IRequestHandler<GetMerchantStat
|
||||
GetMerchantStatisticsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var merchantsQuery = _context.Merchants.Where(m => !m.IsDeleted);
|
||||
var merchantsQuery = _context.Merchants.Where(m => !EF.Property<bool>(m, "_isDeleted"));
|
||||
|
||||
var totalMerchants = await merchantsQuery.CountAsync(cancellationToken);
|
||||
|
||||
@@ -48,7 +48,7 @@ public class GetMerchantStatisticsQueryHandler : IRequestHandler<GetMerchantStat
|
||||
var banned = await merchantsQuery
|
||||
.CountAsync(m => m.StatusId == MerchantStatus.Banned.Id, cancellationToken);
|
||||
|
||||
var shopsQuery = _context.Shops.Where(s => !s.IsDeleted);
|
||||
var shopsQuery = _context.Shops.Where(s => !EF.Property<bool>(s, "_isDeleted"));
|
||||
|
||||
var totalShops = await shopsQuery.CountAsync(cancellationToken);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user