fix(data): route all TrackRepository queries through soft-delete-filtered Query

This commit is contained in:
daniel-c-harvey
2026-06-10 14:32:31 -04:00
parent 691d904273
commit f0185587f7
+10 -13
View File
@@ -11,42 +11,39 @@ namespace DeepDrftData.Repositories;
public class TrackRepository : Repository<DeepDrftContext, TrackEntity> public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
{ {
private readonly DeepDrftContext _context;
public TrackRepository( public TrackRepository(
DeepDrftContext context, DeepDrftContext context,
ILogger<Repository<DeepDrftContext, TrackEntity>> logger, ILogger<Repository<DeepDrftContext, TrackEntity>> logger,
IDbExceptionClassifier? classifier = null) IDbExceptionClassifier? classifier = null)
: base(context, logger, classifier: classifier) : base(context, logger, classifier: classifier)
{ {
_context = context;
} }
// Lookup by vault entry key. The base Repository<> only exposes id-based queries, so this // Lookup by vault entry key. The base Repository<> only exposes id-based queries, so this
// queries the DbSet directly. Returns null on miss (service wraps in ResultContainer). // uses Query (soft-delete filtered) rather than the raw DbSet.
public async Task<TrackEntity?> GetByEntryKeyAsync(string entryKey) public async Task<TrackEntity?> GetByEntryKeyAsync(string entryKey)
=> await _context.Tracks.FirstOrDefaultAsync(t => t.EntryKey == entryKey); => await Query.FirstOrDefaultAsync(t => t.EntryKey == entryKey);
// Picks one track uniformly at random. Two round-trips (count, then a single offset row) // Picks one track uniformly at random. Two round-trips (count, then a single offset row)
// rather than ORDER BY random() so the database never sorts the whole table — the catalogue // rather than ORDER BY random() so the database never sorts the whole table — the catalogue
// is small today but this keeps the cost flat as it grows. Returns null when empty so the // is small today but this keeps the cost flat as it grows. Returns null when empty so the
// service surfaces a valid empty-library state, not an error. Queries the DbSet directly, // service surfaces a valid empty-library state, not an error. Uses Query (soft-delete
// mirroring GetByEntryKeyAsync, since the base Repository<> exposes only id-based reads. // filtered) so deleted tracks are never candidates.
public async Task<TrackEntity?> GetRandomAsync(CancellationToken cancellationToken = default) public async Task<TrackEntity?> GetRandomAsync(CancellationToken cancellationToken = default)
{ {
var count = await _context.Tracks.CountAsync(cancellationToken); var count = await Query.CountAsync(cancellationToken);
if (count == 0) if (count == 0)
return null; return null;
var index = Random.Shared.Next(count); var index = Random.Shared.Next(count);
return await _context.Tracks return await Query
.OrderBy(t => t.Id) .OrderBy(t => t.Id)
.Skip(index) .Skip(index)
.Take(1) .Take(1)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
} }
// Paged query with optional filter predicates. Built directly off the DbSet rather than the // Paged query with optional filter predicates. Built off Query (soft-delete filtered) rather than the
// base GetPagedAsync(paging) overload, which takes no where-clause. The OrderBy expression and // base GetPagedAsync(paging) overload, which takes no where-clause. The OrderBy expression and
// direction ride in on the PagingParameters the manager already built, so sort + filter + // direction ride in on the PagingParameters the manager already built, so sort + filter +
// pagination compose. Filter predicates apply before sort and Skip/Take so TotalCount reflects // pagination compose. Filter predicates apply before sort and Skip/Take so TotalCount reflects
@@ -56,7 +53,7 @@ public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
TrackFilter? filter, TrackFilter? filter,
CancellationToken ct = default) CancellationToken ct = default)
{ {
IQueryable<TrackEntity> query = _context.Tracks; IQueryable<TrackEntity> query = Query;
if (filter is not null) if (filter is not null)
{ {
@@ -105,7 +102,7 @@ public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
// Distinct albums (non-null) with track counts and a representative cover key. The cover is the // Distinct albums (non-null) with track counts and a representative cover key. The cover is the
// first non-null ImagePath in the group; GroupBy + projection keeps it a single round-trip. // first non-null ImagePath in the group; GroupBy + projection keeps it a single round-trip.
public async Task<List<AlbumSummaryDto>> GetDistinctAlbumsAsync(CancellationToken ct = default) public async Task<List<AlbumSummaryDto>> GetDistinctAlbumsAsync(CancellationToken ct = default)
=> await _context.Tracks => await Query
.Where(t => t.Album != null) .Where(t => t.Album != null)
.GroupBy(t => t.Album!) .GroupBy(t => t.Album!)
.Select(g => new AlbumSummaryDto .Select(g => new AlbumSummaryDto
@@ -123,7 +120,7 @@ public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
// Distinct genres (non-null) with track counts. // Distinct genres (non-null) with track counts.
public async Task<List<GenreSummaryDto>> GetDistinctGenresAsync(CancellationToken ct = default) public async Task<List<GenreSummaryDto>> GetDistinctGenresAsync(CancellationToken ct = default)
=> await _context.Tracks => await Query
.Where(t => t.Genre != null) .Where(t => t.Genre != null)
.GroupBy(t => t.Genre!) .GroupBy(t => t.Genre!)
.Select(g => new GenreSummaryDto .Select(g => new GenreSummaryDto