3 Commits

Author SHA1 Message Date
REDCODE
41c262d157 fix: restore Include(p => p.Albums) in PersonRepository queries
The visibility subquery doesn't load the Albums navigation, so TotalAlbums in the mapper was always 0. Added back a lightweight Include(p => p.Albums) (without Assets/SharedWith chain) to both SearchQuery and GetAllVisible.
2026-07-11 20:39:14 +02:00
REDCODE
837491fa4a fix: guard ILIKE filter in AlbumRepository.SearchQuery against null/empty query 2026-07-11 20:36:33 +02:00
REDCODE
5a2f61d261 perf: push visibility filter into SQL for PersonRepository queries
Replace client-side visibility filtering with EF Core Any() subqueries in SearchQuery and GetAllVisible. This eliminates the cartesian product from the Include chain (Person -> Albums -> Assets -> SharedWith) and lets the database short-circuit the auth check with indexes. Adds null-forgiving operators (!) on navigation access in the query predicates.
2026-07-11 20:35:56 +02:00
2 changed files with 27 additions and 35 deletions

View File

@@ -28,8 +28,10 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
IQueryable<Album> albumsQuery = context.Albums
.Include(a => a.CoverAsset)
.Include(a => a.PersonOwner)
.Include(a => a.Assets)!.ThenInclude(a => a.SharedWith)
.Where(x => EF.Functions.ILike(x.Title, $"%{query}%"));
.Include(a => a.Assets)!.ThenInclude(a => a.SharedWith);
if (!string.IsNullOrEmpty(query))
albumsQuery = albumsQuery.Where(x => EF.Functions.ILike(x.Title, $"%{query}%"));
if (unassigned)
albumsQuery = albumsQuery.Where(a => a.PersonOwnerId == null);

View File

@@ -30,18 +30,16 @@ public class PersonRepository(LactoseDbContext context) : IPersonRepository {
if (accessLevel != EAccessLevel.User)
return GetAll();
var all = context.People
.Include(p => p.Albums)!
.ThenInclude(a => a.Assets)!
.ThenInclude(a => a.SharedWith)
return context.People
.Include(p => p.Albums)
.Where(p => p.Albums!.Any(a =>
a.Assets!.Any(asset => asset.DeletedAt == null && (
asset.IsPubliclyShared ||
asset.OwnerId == userId ||
asset.SharedWith!.Any(u => u.Id == userId)
))
))
.ToList();
return all.Where(p => p.Albums != null && p.Albums.Any(a =>
a.Assets != null && a.Assets.Any(asset => asset.DeletedAt == null && (
asset.IsPubliclyShared ||
asset.OwnerId == userId ||
(asset.SharedWith != null && asset.SharedWith.Any(u => u.Id == userId))
))));
}
/// <inheritdoc />
@@ -61,38 +59,30 @@ public class PersonRepository(LactoseDbContext context) : IPersonRepository {
/// <inheritdoc />
public IEnumerable<Person> SearchQuery(string query, int page = 0, int pageSize = 30, string? sortBy = null, bool sortAsc = true, Guid userId = default, EAccessLevel accessLevel = EAccessLevel.User) {
IQueryable<Person> peopleQuery = context.People
.Include(p => p.Albums)!
.ThenInclude(a => a.Assets)!
.ThenInclude(a => a.SharedWith);
IQueryable<Person> peopleQuery = context.People.Include(p => p.Albums);
// Apply search filter
if (!string.IsNullOrEmpty(query))
peopleQuery = peopleQuery.Where(p => EF.Functions.ILike(p.Name, $"%{query}%"));
// Apply sorting
if (accessLevel == EAccessLevel.User)
peopleQuery = peopleQuery.Where(p => p.Albums!.Any(a =>
a.Assets!.Any(asset => asset.DeletedAt == null && (
asset.IsPubliclyShared ||
asset.OwnerId == userId ||
asset.SharedWith!.Any(u => u.Id == userId)
))
));
IOrderedQueryable<Person> ordered = sortBy?.ToLower() switch
{
"created" => sortAsc ? peopleQuery.OrderBy(p => p.CreatedAt) : peopleQuery.OrderByDescending(p => p.CreatedAt),
"albums" => sortAsc ? peopleQuery.OrderBy(p => p.Albums.Count) : peopleQuery.OrderByDescending(p => p.Albums.Count),
_ => sortAsc ? peopleQuery.OrderBy(p => p.Name) : peopleQuery.OrderByDescending(p => p.Name),
"albums" => sortAsc ? peopleQuery.OrderBy(p => p.Albums!.Count) : peopleQuery.OrderByDescending(p => p.Albums!.Count),
_ => sortAsc ? peopleQuery.OrderBy(p => p.Name) : peopleQuery.OrderByDescending(p => p.Name),
};
var result = ordered
return ordered
.Skip(page * pageSize)
.Take(pageSize)
.ToList();
// Filter by visibility client-side (the nested SharedWith.Any() cannot be translated to SQL)
if (accessLevel == EAccessLevel.User)
result = result.Where(p => p.Albums != null && p.Albums.Any(a =>
a.Assets != null && a.Assets.Any(asset => asset.DeletedAt == null && (
asset.IsPubliclyShared ||
asset.OwnerId == userId ||
(asset.SharedWith != null && asset.SharedWith.Any(u => u.Id == userId))
)))).ToList();
return result;
.Take(pageSize);
}
/// <inheritdoc />