Add shared pagination, projections and ownership helpers; simplify query and command handlers
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Domain.Abstractions;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Shared;
|
||||
|
||||
public static class OwnershipExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads an active, tracked entity owned by <paramref name="userId"/>: NotFound when it is missing
|
||||
/// or not active, Forbidden when someone else owns it.
|
||||
/// </summary>
|
||||
public static async Task<ErrorOr<T>> FindOwnedAsync<T>(
|
||||
this DbSet<T> set, Guid id, long userId, CancellationToken cancellationToken)
|
||||
where T : class, IOwnedEntity, IHasLifeCycle
|
||||
{
|
||||
var entity = await set.SingleOrDefaultAsync(
|
||||
e => e.Id == id && e.LifeCycleStatus == LifeCycleStatus.Active, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
return AppErrors.NotFound(typeof(T).Name, id);
|
||||
|
||||
return entity.OwnerUserId == userId
|
||||
? entity
|
||||
: AppErrors.Forbidden(typeof(T).Name, id);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using X.PagedList;
|
||||
|
||||
namespace Musify.Application.Shared;
|
||||
|
||||
public record PaginatedResponse<T>(
|
||||
IReadOnlyCollection<T> Items,
|
||||
int PageNumber,
|
||||
int PageSize,
|
||||
int PageCount,
|
||||
int TotalItemCount,
|
||||
bool HasNextPage,
|
||||
bool HasPreviousPage)
|
||||
{
|
||||
public static PaginatedResponse<T> FromPagedList(IPagedList<T> pagedList)
|
||||
{
|
||||
return new PaginatedResponse<T>(
|
||||
pagedList.ToArray(),
|
||||
pagedList.PageNumber,
|
||||
pagedList.PageSize,
|
||||
pagedList.PageCount,
|
||||
pagedList.TotalItemCount,
|
||||
pagedList.HasNextPage,
|
||||
pagedList.HasPreviousPage);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,45 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Musify.Application.Shared;
|
||||
|
||||
public record PageRequest(int PageNumber = 1, int PageSize = 20)
|
||||
{
|
||||
public const int MaxPageSize = 100;
|
||||
}
|
||||
|
||||
public record PaginatedResponse<T>(
|
||||
IReadOnlyList<T> Items,
|
||||
int PageNumber,
|
||||
int PageSize,
|
||||
int PageCount,
|
||||
int TotalItemCount,
|
||||
bool HasNextPage,
|
||||
bool HasPreviousPage);
|
||||
|
||||
public static class PaginationExtensions
|
||||
{
|
||||
/// <summary>Counts and pages the same (already filtered, ordered and projected) query.</summary>
|
||||
public static async Task<PaginatedResponse<T>> ToPaginatedAsync<T>(
|
||||
this IQueryable<T> query, PageRequest page, CancellationToken cancellationToken)
|
||||
{
|
||||
var pageNumber = Math.Max(page.PageNumber, 1);
|
||||
var pageSize = Math.Clamp(page.PageSize, 1, PageRequest.MaxPageSize);
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var pageCount = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
|
||||
return new PaginatedResponse<T>(
|
||||
items,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
pageCount,
|
||||
totalCount,
|
||||
HasNextPage: pageNumber < pageCount,
|
||||
HasPreviousPage: pageNumber > 1 && pageCount > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Musify.Application.Shared;
|
||||
|
||||
public static class TextNormalizer
|
||||
{
|
||||
public static string Normalize(string value) => value.Trim().ToUpperInvariant();
|
||||
}
|
||||
Reference in New Issue
Block a user