diff --git a/Lactose/Repositories/AlbumRepository.cs b/Lactose/Repositories/AlbumRepository.cs index b5aea31..6afa2ef 100644 --- a/Lactose/Repositories/AlbumRepository.cs +++ b/Lactose/Repositories/AlbumRepository.cs @@ -28,7 +28,7 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { } /// - public IEnumerable SearchQuery(string query, int page = 0, int pageSize = PagedParametersDto.MaxPageSize, string? sortBy = null, bool sortAsc = false, bool unassigned = false, Guid userId = default, EAccessLevel accessLevel = EAccessLevel.User, Guid? uploadedBy = null, Guid? personOwnerId = null) { + public async Task> SearchQueryAsync(string query, int page, int pageSize, string? sortBy, bool sortAsc, bool unassigned, Guid userId, EAccessLevel accessLevel, Guid? uploadedBy, Guid? personOwnerId, CancellationToken cancellationToken) { IQueryable albumsQuery = context.Albums.AsNoTracking(); if (!string.IsNullOrEmpty(query)) @@ -63,31 +63,31 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { }; var pageOffset = page * pageSize; - var albumIds = ordered + var albumIds = await ordered .Skip(pageOffset) .Take(pageSize) .Select(a => a.Id) - .ToList(); + .ToListAsync(cancellationToken); if (albumIds.Count == 0) return []; - var pagedAlbums = context.Albums + var pagedAlbums = await context.Albums .AsNoTracking() .Include(a => a.PersonOwner) .Include(a => a.CoverAsset) .Where(a => albumIds.Contains(a.Id)) - .ToList(); + .ToListAsync(cancellationToken); // Compute visible asset count per album at the DB level Dictionary assetCounts; if (accessLevel >= EAccessLevel.Curator) { - assetCounts = context.Albums + assetCounts = await context.Albums .Where(a => albumIds.Contains(a.Id)) .Select(a => new { a.Id, Count = a.Assets!.Count() }) - .ToDictionary(x => x.Id, x => x.Count); + .ToDictionaryAsync(x => x.Id, x => x.Count, cancellationToken); } else { - assetCounts = context.Albums + assetCounts = await context.Albums .Where(a => albumIds.Contains(a.Id)) .Select(a => new { a.Id, @@ -97,7 +97,7 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { (asset.Visibility == EVisibility.Private && asset.UploadedBy == userId) )) }) - .ToDictionary(x => x.Id, x => x.Count); + .ToDictionaryAsync(x => x.Id, x => x.Count, cancellationToken); } var dtos = pagedAlbums.Select(a => { @@ -109,9 +109,9 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { var orderMap = albumIds.Select((id, i) => (id, i)).ToDictionary(x => x.id, x => x.i); return [.. dtos.OrderBy(d => orderMap.GetValueOrDefault(d.Id))]; } - + /// - public void Save() => context.SaveChanges(); + public Task SaveAsync(CancellationToken cancellationToken) => context.SaveChangesAsync(cancellationToken); /// public async Task ExecuteInTransactionAsync(Func> operation) { @@ -122,20 +122,21 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { } /// - public Album? Find(Guid id) => context.Albums.FirstOrDefault(a => a.Id == id); + public Task FindAsync(Guid id, CancellationToken cancellationToken) => + context.Albums.FirstOrDefaultAsync(a => a.Id == id, cancellationToken); /// - public Album? FindWithAssets(Guid id) => context.Albums + public Task FindWithAssetsAsync(Guid id, CancellationToken cancellationToken) => context.Albums .AsSplitQuery() .Include(a => a.Assets) - .FirstOrDefault(a => a.Id == id); + .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); /// - public Album? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel) { - var album = context.Albums + public async Task FindVisibleAsync(Guid id, Guid? userId, EAccessLevel accessLevel, CancellationToken cancellationToken) { + var album = await context.Albums .AsNoTracking() .Include(a => a.PersonOwner) - .FirstOrDefault(a => a.Id == id); + .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); if (album == null) return null; @@ -143,8 +144,8 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { bool canSeeAlbum = accessLevel switch { < EAccessLevel.Maintainer => album.Visibility <= EVisibility.Protected, EAccessLevel.Maintainer when userId.HasValue => album.Visibility <= EVisibility.Protected - || (album.PersonOwnerId.HasValue && context.PersonMaintainers.Any(pm => - pm.UserId == userId.Value && pm.PersonId == album.PersonOwnerId.Value)), + || (album.PersonOwnerId.HasValue && await context.PersonMaintainers.AnyAsync(pm => + pm.UserId == userId.Value && pm.PersonId == album.PersonOwnerId.Value, cancellationToken)), EAccessLevel.Maintainer => album.Visibility <= EVisibility.Protected, _ => true }; @@ -160,7 +161,7 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { EAccessLevel.Admin => assetsQuery, EAccessLevel.Curator => assetsQuery.Where(a => a.DeletedAt == null || a.UploadedBy == userId), EAccessLevel.Maintainer when userId.HasValue && album.PersonOwnerId.HasValue - && context.PersonMaintainers.Any(pm => pm.UserId == userId.Value && pm.PersonId == album.PersonOwnerId.Value) + && await context.PersonMaintainers.AnyAsync(pm => pm.UserId == userId.Value && pm.PersonId == album.PersonOwnerId.Value, cancellationToken) => assetsQuery.Where(a => a.DeletedAt == null), _ => assetsQuery.Where(a => a.DeletedAt == null && ( a.Visibility == EVisibility.Public || @@ -169,7 +170,7 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { )) }; - album.VisibleAssetPreviews = assetsQuery + album.VisibleAssetPreviews = await assetsQuery .OrderBy(a => a.OriginalFilename) .Select(a => new AlbumAssetPreviewDto { Id = a.Id, @@ -182,46 +183,48 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { Visibility = accessLevel >= EAccessLevel.Maintainer ? a.Visibility : null, DeletedAt = accessLevel >= EAccessLevel.Admin || a.UploadedBy == userId ? a.DeletedAt : null }) - .ToList(); + .ToListAsync(cancellationToken); return album; } - - /// - public IEnumerable FindByPerson(Guid personId) => context.Albums.Where(a => a.PersonOwnerId == personId); /// - public IEnumerable FindByPersonIds(IEnumerable personIds) => - context.Albums.Where(a => a.PersonOwnerId != null && personIds.Contains(a.PersonOwnerId.Value)); + public Task> FindByPersonAsync(Guid personId, CancellationToken cancellationToken) => + context.Albums.Where(a => a.PersonOwnerId == personId).ToListAsync(cancellationToken); /// - public int BulkSetVisibilityByPersonIds(IEnumerable personIds, EVisibility visibility) => + public Task> FindByPersonIdsAsync(IEnumerable personIds, CancellationToken cancellationToken) => + context.Albums.Where(a => a.PersonOwnerId != null && personIds.Contains(a.PersonOwnerId.Value)).ToListAsync(cancellationToken); + + /// + public Task BulkSetVisibilityByPersonIdsAsync(IEnumerable personIds, EVisibility visibility, CancellationToken cancellationToken) => context.Albums .Where(a => a.PersonOwnerId != null && personIds.Contains(a.PersonOwnerId.Value)) - .ExecuteUpdate(s => s + .ExecuteUpdateAsync(s => s .SetProperty(a => a.Visibility, visibility) - .SetProperty(a => a.UpdatedAt, DateTime.UtcNow)); + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); /// - public IEnumerable FindBulk(IEnumerable ids) => context.Albums.Where(a => ids.Contains(a.Id)); + public Task> FindBulkAsync(IEnumerable ids, CancellationToken cancellationToken) => + context.Albums.Where(a => ids.Contains(a.Id)).ToListAsync(cancellationToken); /// - public IEnumerable FindByDateRange(DateTime from, DateTime to) - => context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to); + public Task> FindByDateRangeAsync(DateTime from, DateTime to, CancellationToken cancellationToken) + => context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to).ToListAsync(cancellationToken); /// - public void MergeAlbums(Guid destId, List sourceIds) { - var dest = context.Albums + public async Task MergeAlbumsAsync(Guid destId, List sourceIds, CancellationToken cancellationToken) { + var dest = await context.Albums .AsSplitQuery() .Include(a => a.Assets) - .FirstOrDefault(a => a.Id == destId); + .FirstOrDefaultAsync(a => a.Id == destId, cancellationToken); if (dest == null) return; - var sources = context.Albums + var sources = await context.Albums .AsSplitQuery() .Include(a => a.Assets) .Where(a => sourceIds.Contains(a.Id)) - .ToList(); + .ToListAsync(cancellationToken); var existingAssetIds = dest.Assets?.Select(a => a.Id).ToHashSet() ?? []; @@ -238,7 +241,7 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository { /// public void Remove(Album album) => context.Albums.Remove(album); - + /// public void Dispose() => context.Dispose(); } diff --git a/Lactose/Repositories/IAlbumRepository.cs b/Lactose/Repositories/IAlbumRepository.cs index 769f0da..d538fdb 100644 --- a/Lactose/Repositories/IAlbumRepository.cs +++ b/Lactose/Repositories/IAlbumRepository.cs @@ -10,41 +10,44 @@ namespace Lactose.Repositories; /// public interface IAlbumRepository : IDisposable { /// - /// Inserts a new album into the database + /// Inserts a new album into the database. /// - /// - public void Insert(Album album); + /// The album to insert. + void Insert(Album album); /// - /// Updates an album in the database + /// Updates an album in the database. /// - /// - public void Update(Album album); + /// The album to update. + void Update(Album album); /// /// Updates multiple albums in the database. /// /// The albums to update. - public void UpdateBulk(IEnumerable albums); + void UpdateBulk(IEnumerable albums); /// /// Saves changes to the database. /// - public void Save(); + /// Token to cancel the operation. + Task SaveAsync(CancellationToken cancellationToken); /// /// Finds an album by its ID. /// /// The ID of the album. + /// Token to cancel the operation. /// The album if found; otherwise, null. - public Album? Find(Guid id); + Task FindAsync(Guid id, CancellationToken cancellationToken); /// /// Finds an album by its ID with its assets loaded for collection replacement. /// /// The ID of the album. + /// Token to cancel the operation. /// The album with its asset collection loaded if found; otherwise, null. - public Album? FindWithAssets(Guid id); + Task FindWithAssetsAsync(Guid id, CancellationToken cancellationToken); /// /// Finds an album by its ID with assets filtered by the requesting user's access level. @@ -52,45 +55,51 @@ public interface IAlbumRepository : IDisposable { /// The ID of the album. /// The requesting user's ID for visibility-scoped asset filtering. /// The requesting user's access level. + /// Token to cancel the operation. /// The album if found; otherwise, null. - public Album? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel); + Task FindVisibleAsync(Guid id, Guid? userId, EAccessLevel accessLevel, CancellationToken cancellationToken); /// /// Finds albums by the person's ID. /// /// The ID of the person. + /// Token to cancel the operation. /// A collection of albums associated with the specified person. - public IEnumerable FindByPerson(Guid personId); + Task> FindByPersonAsync(Guid personId, CancellationToken cancellationToken); /// /// Finds albums owned by any of the specified people. /// /// The IDs of the people. + /// Token to cancel the operation. /// A collection of albums associated with the specified people. - public IEnumerable FindByPersonIds(IEnumerable personIds); + Task> FindByPersonIdsAsync(IEnumerable personIds, CancellationToken cancellationToken); /// /// Bulk-updates the visibility of all albums owned by the specified people via a single SQL UPDATE. /// /// The IDs of the people. /// The target visibility level. + /// Token to cancel the operation. /// The number of albums updated. - public int BulkSetVisibilityByPersonIds(IEnumerable personIds, EVisibility visibility); + Task BulkSetVisibilityByPersonIdsAsync(IEnumerable personIds, EVisibility visibility, CancellationToken cancellationToken); /// /// Finds multiple albums by their IDs. /// /// The IDs of the albums. + /// Token to cancel the operation. /// A collection of albums with the specified IDs. - public IEnumerable FindBulk(IEnumerable ids); + Task> FindBulkAsync(IEnumerable ids, CancellationToken cancellationToken); /// /// Finds albums within a specified date range. /// /// The start date of the range. /// The end date of the range. + /// Token to cancel the operation. /// A collection of albums created or updated within the specified date range. - public IEnumerable FindByDateRange(DateTime from, DateTime to); + Task> FindByDateRangeAsync(DateTime from, DateTime to, CancellationToken cancellationToken); /// /// Merges multiple source albums into a destination album. @@ -98,13 +107,14 @@ public interface IAlbumRepository : IDisposable { /// /// The ID of the destination album. /// The IDs of the source albums to merge. - public void MergeAlbums(Guid destId, List sourceIds); + /// Token to cancel the operation. + Task MergeAlbumsAsync(Guid destId, List sourceIds, CancellationToken cancellationToken); /// /// Removes an album from the database. /// /// The album to remove. - public void Remove(Album album); + void Remove(Album album); /// /// Searches for albums based on a query string with optional sorting, filtering, and access-level scoping. @@ -119,8 +129,9 @@ public interface IAlbumRepository : IDisposable { /// The current user's access level. Regular users only see albums with visible assets. /// Optional filter for albums containing assets uploaded by a specific user. /// Optional filter for albums owned by a specific person. + /// Token to cancel the operation. /// A list of album previews matching the search query. - public IEnumerable SearchQuery(string query, int page = 0, int pageSize = PagedParametersDto.MaxPageSize, string? sortBy = null, bool sortAsc = false, bool unassigned = false, Guid userId = default, EAccessLevel accessLevel = EAccessLevel.User, Guid? uploadedBy = null, Guid? personOwnerId = null); + Task> SearchQueryAsync(string query, int page, int pageSize, string? sortBy, bool sortAsc, bool unassigned, Guid userId, EAccessLevel accessLevel, Guid? uploadedBy, Guid? personOwnerId, CancellationToken cancellationToken); /// /// Executes the given operation inside an explicit transaction on the shared database context, @@ -129,5 +140,5 @@ public interface IAlbumRepository : IDisposable { /// The type returned by the operation. /// The work to run inside the transaction. /// The operation's result, or null if the operation returned null. - public Task ExecuteInTransactionAsync(Func> operation); -} \ No newline at end of file + Task ExecuteInTransactionAsync(Func> operation); +}