Performance: push ORDER BY/TAKE into SQL for hot-path reads
- IInAppNotificationRepository: typed repo with GetPagedAsync, GetRecentAsync, GetUnreadAsync — bell dropdown no longer loads all notifications then slices in C# - Add compound indexes on InAppNotifications(CompanyId, IsDeleted, CreatedAt) and (CompanyId, IsDeleted, IsRead); ContactSubmissions(CompanyId, IsDeleted, CreatedAt) - PlainRepository.GetAllAsync/FindAsync: add AsNoTracking (Announcements, Tips, ReleaseNotes) - AiUsageReportController: replace GetAllAsync + C# Where with FindAsync (SQL-level filter) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces.Repositories;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Typed repository for <see cref="InAppNotification"/>. Overrides the three hot-path
|
||||
/// read operations so ORDER BY / SKIP / TAKE run in SQL rather than in C# after a full
|
||||
/// table load — critical for the bell dropdown (Recent/Unread) which fires on every page.
|
||||
/// </summary>
|
||||
public class InAppNotificationRepository
|
||||
: Repository<InAppNotification>, IInAppNotificationRepository
|
||||
{
|
||||
public InAppNotificationRepository(ApplicationDbContext context) : base(context) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<(List<InAppNotification> Items, int TotalCount)> GetPagedAsync(
|
||||
bool isPlatformAdmin, int pageNumber, int pageSize)
|
||||
{
|
||||
// SuperAdmin path: bypass global filters, scope to CompanyId 0 (platform-only).
|
||||
// Regular path: global filter already scopes to the current tenant.
|
||||
var query = isPlatformAdmin
|
||||
? _context.Set<InAppNotification>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => !n.IsDeleted && n.CompanyId == 0)
|
||||
: _context.Set<InAppNotification>()
|
||||
.Where(n => true);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(n => n.CreatedAt)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<InAppNotification>> GetRecentAsync(
|
||||
bool isPlatformAdmin, int take = 20)
|
||||
{
|
||||
var query = isPlatformAdmin
|
||||
? _context.Set<InAppNotification>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => !n.IsDeleted && n.CompanyId == 0)
|
||||
: _context.Set<InAppNotification>()
|
||||
.Where(n => true);
|
||||
|
||||
return await query
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(n => n.CreatedAt)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<(List<InAppNotification> Items, int UnreadCount)> GetUnreadAsync(
|
||||
bool isPlatformAdmin, int take = 20)
|
||||
{
|
||||
// Count ALL unread for the badge, then cap the items list for the dropdown.
|
||||
var baseQuery = isPlatformAdmin
|
||||
? _context.Set<InAppNotification>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => !n.IsDeleted && n.CompanyId == 0 && !n.IsRead)
|
||||
: _context.Set<InAppNotification>()
|
||||
.Where(n => !n.IsRead);
|
||||
|
||||
var unreadCount = await baseQuery.CountAsync();
|
||||
|
||||
var items = await baseQuery
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(n => n.CreatedAt)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, unreadCount);
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ public class PlainRepository<T> : IPlainRepository<T> where T : class
|
||||
=> await _dbSet.FindAsync(id);
|
||||
|
||||
public virtual async Task<IEnumerable<T>> GetAllAsync()
|
||||
=> await _dbSet.ToListAsync();
|
||||
=> await _dbSet.AsNoTracking().ToListAsync();
|
||||
|
||||
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
|
||||
=> await _dbSet.Where(predicate).ToListAsync();
|
||||
=> await _dbSet.AsNoTracking().Where(predicate).ToListAsync();
|
||||
|
||||
public virtual async Task<T?> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
|
||||
=> await _dbSet.FirstOrDefaultAsync(predicate);
|
||||
|
||||
@@ -109,7 +109,7 @@ public class UnitOfWork : IUnitOfWork
|
||||
private IPlainRepository<Announcement>? _announcements;
|
||||
private IPlainRepository<BannedIp>? _bannedIps;
|
||||
private IPlainRepository<DashboardTip>? _dashboardTips;
|
||||
private IRepository<InAppNotification>? _inAppNotifications;
|
||||
private IInAppNotificationRepository? _inAppNotifications;
|
||||
private IPlainRepository<ReleaseNote>? _releaseNotes;
|
||||
|
||||
// Bug Reports
|
||||
@@ -439,8 +439,8 @@ public class UnitOfWork : IUnitOfWork
|
||||
_dashboardTips ??= new PlainRepository<DashboardTip>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="InAppNotification"/> bell-notification records; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<InAppNotification> InAppNotifications =>
|
||||
_inAppNotifications ??= new Repository<InAppNotification>(_context);
|
||||
public IInAppNotificationRepository InAppNotifications =>
|
||||
_inAppNotifications ??= new InAppNotificationRepository(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="ReleaseNote"/> platform changelog entries; no tenant filter, no soft delete.</summary>
|
||||
public IPlainRepository<ReleaseNote> ReleaseNotes =>
|
||||
|
||||
Reference in New Issue
Block a user