fix(backend): compute track listens count via SQL instead of an unloaded navigation

TrackApplicationResponse.FromEntity counted track.ListeningHistories,
but none of the queries that built a response ever Included it, so
the collection was always empty and listensCount was always 0. Each
query now projects the count as a correlated subquery before mapping
to the response.
This commit is contained in:
2026-07-15 16:09:54 +02:00
parent b4b08aa3fd
commit e98798894e
7 changed files with 33 additions and 21 deletions
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using X.PagedList;
using X.PagedList.EF; using X.PagedList.EF;
namespace Musify.Application.PlayLists namespace Musify.Application.PlayLists
@@ -32,11 +33,15 @@ namespace Musify.Application.PlayLists
var totalCount = await tracksQuery.CountAsync(cancellationToken); var totalCount = await tracksQuery.CountAsync(cancellationToken);
var pagedTracks = await tracksQuery var pagedEntities = await tracksQuery
.OrderBy(plt => plt.Position) .OrderBy(plt => plt.Position)
.Select(plt => TrackApplicationResponse.FromEntity(plt.Track)) .Select(plt => new { plt.Track, ListensCount = plt.Track.ListeningHistories.Count })
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
pagedEntities.Select(x => TrackApplicationResponse.FromEntity(x.Track, x.ListensCount)),
pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount);
return PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks); return PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks);
} }
} }
@@ -123,7 +123,7 @@ namespace Musify.Application.Tracks
return Error.Failure(description: $"Failed to create track for {request.Title}"); return Error.Failure(description: $"Failed to create track for {request.Title}");
} }
return TrackApplicationResponse.FromEntity(trackEntity); return TrackApplicationResponse.FromEntity(trackEntity, listensCount: 0);
} }
} }
} }
@@ -17,10 +17,11 @@ namespace Musify.Application.Tracks
var listeningHistory = database.ListeningHistories var listeningHistory = database.ListeningHistories
.Where(l => l.UserId == query.UserId) .Where(l => l.UserId == query.UserId)
.OrderByDescending(l => l.ListenedAt) .OrderByDescending(l => l.ListenedAt)
.Select(t => TrackApplicationResponse.FromEntity(t.Track)) .Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
.AsEnumerable()
.DistinctBy(t => t.Track.Id)
.Take(ListSize) .Take(ListSize)
.DistinctBy(t => t.Id) .Select(t => TrackApplicationResponse.FromEntity(t.Track, t.ListensCount));
.AsEnumerable();
return ValueTask.FromResult(listeningHistory); return ValueTask.FromResult(listeningHistory);
} }
@@ -14,15 +14,15 @@ namespace Musify.Application.Tracks
{ {
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken)
{ {
var track = await database.Tracks.AsNoTracking() var entity = await database.Tracks.AsNoTracking()
.Where(t => t.Id == request.TrackId) .Where(t => t.Id == request.TrackId)
.Select(t => TrackApplicationResponse.FromEntity(t)) .Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count })
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (track is null) if (entity is null)
return Error.NotFound(); return Error.NotFound();
return track; return TrackApplicationResponse.FromEntity(entity.Track, entity.ListensCount);
} }
} }
} }
@@ -3,6 +3,7 @@ using Mediator;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using X.PagedList;
using X.PagedList.EF; using X.PagedList.EF;
using Musify.Application.Contracts; using Musify.Application.Contracts;
@@ -29,11 +30,15 @@ namespace Musify.Application.Tracks
var totalCount = await tracksQuery.CountAsync(cancellationToken); var totalCount = await tracksQuery.CountAsync(cancellationToken);
var pagedTracks = await tracksQuery var pagedEntities = await tracksQuery
.OrderBy(t => t.Id) .OrderBy(t => t.Id)
.Select(t => TrackApplicationResponse.FromEntity(t.Track)) .Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
pagedEntities.Select(x => TrackApplicationResponse.FromEntity(x.Track, x.ListensCount)),
pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount);
return PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks); return PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks);
} }
} }
@@ -3,6 +3,7 @@ using Mediator;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using X.PagedList;
using X.PagedList.EF; using X.PagedList.EF;
using Musify.Application.Contracts; using Musify.Application.Contracts;
@@ -36,11 +37,15 @@ namespace Musify.Application.Tracks
var totalCount = await tracksQuery.CountAsync(cancellationToken); var totalCount = await tracksQuery.CountAsync(cancellationToken);
var pagedTracks = await tracksQuery var pagedEntities = await tracksQuery
.OrderBy(t => t.Track.CreatedAt) .OrderBy(t => t.Track.CreatedAt)
.Select(t => TrackApplicationResponse.FromEntity(t.Track)) .Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
pagedEntities.Select(x => TrackApplicationResponse.FromEntity(x.Track, x.ListensCount)),
pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount);
return PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks); return PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks);
} }
} }
@@ -10,17 +10,13 @@ namespace Musify.Application.Tracks.Responses
DateTime CreatedAt, DateTime CreatedAt,
DateTime UpdatedAt) DateTime UpdatedAt)
{ {
public static TrackApplicationResponse FromEntity(Track track) public static TrackApplicationResponse FromEntity(Track track, int listensCount) =>
{ new(
var count = track.ListeningHistories.Count(x => x.TrackId == track.Id);
return new TrackApplicationResponse(
track.Id, track.Id,
track.Title, track.Title,
track.Duration, track.Duration,
count, listensCount,
track.CreatedAt, track.CreatedAt,
track.UpdatedAt); track.UpdatedAt);
}
} }
} }