feat(Lactose): convert AlbumRepository to async

This commit is contained in:
2026-08-21 18:16:55 +02:00
parent 2d1d1e0d26
commit c0164a75a0
2 changed files with 74 additions and 60 deletions
+42 -39
View File
@@ -28,7 +28,7 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
}
/// <inheritdoc />
public IEnumerable<AlbumPreviewDto> 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<List<AlbumPreviewDto>> SearchQueryAsync(string query, int page, int pageSize, string? sortBy, bool sortAsc, bool unassigned, Guid userId, EAccessLevel accessLevel, Guid? uploadedBy, Guid? personOwnerId, CancellationToken cancellationToken) {
IQueryable<Album> 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<Guid, int> 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))];
}
/// <inheritdoc />
public void Save() => context.SaveChanges();
public Task SaveAsync(CancellationToken cancellationToken) => context.SaveChangesAsync(cancellationToken);
/// <inheritdoc />
public async Task<TResult?> ExecuteInTransactionAsync<TResult>(Func<Task<TResult?>> operation) {
@@ -122,20 +122,21 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
}
/// <inheritdoc />
public Album? Find(Guid id) => context.Albums.FirstOrDefault(a => a.Id == id);
public Task<Album?> FindAsync(Guid id, CancellationToken cancellationToken) =>
context.Albums.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
/// <inheritdoc />
public Album? FindWithAssets(Guid id) => context.Albums
public Task<Album?> FindWithAssetsAsync(Guid id, CancellationToken cancellationToken) => context.Albums
.AsSplitQuery()
.Include(a => a.Assets)
.FirstOrDefault(a => a.Id == id);
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
/// <inheritdoc />
public Album? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel) {
var album = context.Albums
public async Task<Album?> 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;
}
/// <inheritdoc />
public IEnumerable<Album> FindByPerson(Guid personId) => context.Albums.Where(a => a.PersonOwnerId == personId);
/// <inheritdoc />
public IEnumerable<Album> FindByPersonIds(IEnumerable<Guid> personIds) =>
context.Albums.Where(a => a.PersonOwnerId != null && personIds.Contains(a.PersonOwnerId.Value));
public Task<List<Album>> FindByPersonAsync(Guid personId, CancellationToken cancellationToken) =>
context.Albums.Where(a => a.PersonOwnerId == personId).ToListAsync(cancellationToken);
/// <inheritdoc />
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility) =>
public Task<List<Album>> FindByPersonIdsAsync(IEnumerable<Guid> personIds, CancellationToken cancellationToken) =>
context.Albums.Where(a => a.PersonOwnerId != null && personIds.Contains(a.PersonOwnerId.Value)).ToListAsync(cancellationToken);
/// <inheritdoc />
public Task<int> BulkSetVisibilityByPersonIdsAsync(IEnumerable<Guid> 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);
/// <inheritdoc />
public IEnumerable<Album> FindBulk(IEnumerable<Guid> ids) => context.Albums.Where(a => ids.Contains(a.Id));
public Task<List<Album>> FindBulkAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken) =>
context.Albums.Where(a => ids.Contains(a.Id)).ToListAsync(cancellationToken);
/// <inheritdoc />
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to)
=> context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to);
public Task<List<Album>> FindByDateRangeAsync(DateTime from, DateTime to, CancellationToken cancellationToken)
=> context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to).ToListAsync(cancellationToken);
/// <inheritdoc />
public void MergeAlbums(Guid destId, List<Guid> sourceIds) {
var dest = context.Albums
public async Task MergeAlbumsAsync(Guid destId, List<Guid> 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 {
/// <inheritdoc />
public void Remove(Album album) => context.Albums.Remove(album);
/// <inheritdoc />
public void Dispose() => context.Dispose();
}
+32 -21
View File
@@ -10,41 +10,44 @@ namespace Lactose.Repositories;
/// </summary>
public interface IAlbumRepository : IDisposable {
/// <summary>
/// Inserts a new album into the database
/// Inserts a new album into the database.
/// </summary>
/// <param name="album"></param>
public void Insert(Album album);
/// <param name="album">The album to insert.</param>
void Insert(Album album);
/// <summary>
/// Updates an album in the database
/// Updates an album in the database.
/// </summary>
/// <param name="album"></param>
public void Update(Album album);
/// <param name="album">The album to update.</param>
void Update(Album album);
/// <summary>
/// Updates multiple albums in the database.
/// </summary>
/// <param name="albums">The albums to update.</param>
public void UpdateBulk(IEnumerable<Album> albums);
void UpdateBulk(IEnumerable<Album> albums);
/// <summary>
/// Saves changes to the database.
/// </summary>
public void Save();
/// <param name="cancellationToken">Token to cancel the operation.</param>
Task SaveAsync(CancellationToken cancellationToken);
/// <summary>
/// Finds an album by its ID.
/// </summary>
/// <param name="id">The ID of the album.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The album if found; otherwise, null.</returns>
public Album? Find(Guid id);
Task<Album?> FindAsync(Guid id, CancellationToken cancellationToken);
/// <summary>
/// Finds an album by its ID with its assets loaded for collection replacement.
/// </summary>
/// <param name="id">The ID of the album.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The album with its asset collection loaded if found; otherwise, null.</returns>
public Album? FindWithAssets(Guid id);
Task<Album?> FindWithAssetsAsync(Guid id, CancellationToken cancellationToken);
/// <summary>
/// Finds an album by its ID with assets filtered by the requesting user's access level.
@@ -52,45 +55,51 @@ public interface IAlbumRepository : IDisposable {
/// <param name="id">The ID of the album.</param>
/// <param name="userId">The requesting user's ID for visibility-scoped asset filtering.</param>
/// <param name="accessLevel">The requesting user's access level.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The album if found; otherwise, null.</returns>
public Album? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel);
Task<Album?> FindVisibleAsync(Guid id, Guid? userId, EAccessLevel accessLevel, CancellationToken cancellationToken);
/// <summary>
/// Finds albums by the person's ID.
/// </summary>
/// <param name="personId">The ID of the person.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A collection of albums associated with the specified person.</returns>
public IEnumerable<Album> FindByPerson(Guid personId);
Task<List<Album>> FindByPersonAsync(Guid personId, CancellationToken cancellationToken);
/// <summary>
/// Finds albums owned by any of the specified people.
/// </summary>
/// <param name="personIds">The IDs of the people.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A collection of albums associated with the specified people.</returns>
public IEnumerable<Album> FindByPersonIds(IEnumerable<Guid> personIds);
Task<List<Album>> FindByPersonIdsAsync(IEnumerable<Guid> personIds, CancellationToken cancellationToken);
/// <summary>
/// Bulk-updates the visibility of all albums owned by the specified people via a single SQL UPDATE.
/// </summary>
/// <param name="personIds">The IDs of the people.</param>
/// <param name="visibility">The target visibility level.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The number of albums updated.</returns>
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility);
Task<int> BulkSetVisibilityByPersonIdsAsync(IEnumerable<Guid> personIds, EVisibility visibility, CancellationToken cancellationToken);
/// <summary>
/// Finds multiple albums by their IDs.
/// </summary>
/// <param name="ids">The IDs of the albums.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A collection of albums with the specified IDs.</returns>
public IEnumerable<Album> FindBulk(IEnumerable<Guid> ids);
Task<List<Album>> FindBulkAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken);
/// <summary>
/// Finds albums within a specified date range.
/// </summary>
/// <param name="from">The start date of the range.</param>
/// <param name="to">The end date of the range.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A collection of albums created or updated within the specified date range.</returns>
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to);
Task<List<Album>> FindByDateRangeAsync(DateTime from, DateTime to, CancellationToken cancellationToken);
/// <summary>
/// Merges multiple source albums into a destination album.
@@ -98,13 +107,14 @@ public interface IAlbumRepository : IDisposable {
/// </summary>
/// <param name="destId">The ID of the destination album.</param>
/// <param name="sourceIds">The IDs of the source albums to merge.</param>
public void MergeAlbums(Guid destId, List<Guid> sourceIds);
/// <param name="cancellationToken">Token to cancel the operation.</param>
Task MergeAlbumsAsync(Guid destId, List<Guid> sourceIds, CancellationToken cancellationToken);
/// <summary>
/// Removes an album from the database.
/// </summary>
/// <param name="album">The album to remove.</param>
public void Remove(Album album);
void Remove(Album album);
/// <summary>
/// Searches for albums based on a query string with optional sorting, filtering, and access-level scoping.
@@ -119,8 +129,9 @@ public interface IAlbumRepository : IDisposable {
/// <param name="accessLevel">The current user's access level. Regular users only see albums with visible assets.</param>
/// <param name="uploadedBy">Optional filter for albums containing assets uploaded by a specific user.</param>
/// <param name="personOwnerId">Optional filter for albums owned by a specific person.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A list of album previews matching the search query.</returns>
public IEnumerable<AlbumPreviewDto> 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<List<AlbumPreviewDto>> SearchQueryAsync(string query, int page, int pageSize, string? sortBy, bool sortAsc, bool unassigned, Guid userId, EAccessLevel accessLevel, Guid? uploadedBy, Guid? personOwnerId, CancellationToken cancellationToken);
/// <summary>
/// Executes the given operation inside an explicit transaction on the shared database context,
@@ -129,5 +140,5 @@ public interface IAlbumRepository : IDisposable {
/// <typeparam name="TResult">The type returned by the operation.</typeparam>
/// <param name="operation">The work to run inside the transaction.</param>
/// <returns>The operation's result, or null if the operation returned null.</returns>
public Task<TResult?> ExecuteInTransactionAsync<TResult>(Func<Task<TResult?>> operation);
}
Task<TResult?> ExecuteInTransactionAsync<TResult>(Func<Task<TResult?>> operation);
}