Add shared pagination, projections and ownership helpers; simplify query and command handlers
This commit is contained in:
@@ -24,16 +24,13 @@ public class FollowUserCommandHandler(
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.FollowedId, cancellationToken);
|
||||
if (!followedExists)
|
||||
{
|
||||
logger.LogInformation("User {FollowedId} not found", request.FollowedId);
|
||||
return Error.NotFound(description: $"User {request.FollowedId} not found");
|
||||
}
|
||||
return AppErrors.NotFound("User", request.FollowedId);
|
||||
|
||||
var alreadyFollowing = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
|
||||
if (alreadyFollowing)
|
||||
return new Success();
|
||||
return Result.Success;
|
||||
|
||||
await database.UserFollows.AddAsync(new UserFollow
|
||||
{
|
||||
@@ -49,6 +46,6 @@ public class FollowUserCommandHandler(
|
||||
|
||||
logger.LogInformation("User {FollowerId} followed user {FollowedId}", request.FollowerId, request.FollowedId);
|
||||
|
||||
return new Success();
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetListeningStatsQuery(long UserId) : IQuery<ListeningStatsResponse>;
|
||||
|
||||
public record ListeningStatsResponse(int TracksThisWeek, int SecondsThisWeek, int StreakDays);
|
||||
|
||||
public class GetListeningStatsQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetListeningStatsQuery, ListeningStatsResponse>
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Users.Responses;
|
||||
using Musify.Application.Shared;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
@@ -20,9 +21,8 @@ public class GetUserByIdQueryHandler(IDatabase database)
|
||||
.Select(u => UserApplicationResponse.FromEntity(u))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
return Error.NotFound();
|
||||
|
||||
return user;
|
||||
return user == null
|
||||
? AppErrors.NotFound("User", request.Id)
|
||||
: user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
using X.PagedList.EF;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
@@ -17,20 +16,18 @@ public class GetUserFollowersQueryHandler(IDatabase database)
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return Error.NotFound();
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
if (!await FollowVisibility.CanViewFollowersAsync(database, request.ViewerId, request.UserId, cancellationToken))
|
||||
return Error.Forbidden();
|
||||
return AppErrors.Forbidden("User", request.UserId);
|
||||
|
||||
var followsQuery = database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowedId == request.UserId);
|
||||
|
||||
var totalCount = await followsQuery.CountAsync(cancellationToken);
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
var page = await followsQuery
|
||||
return await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowedId == request.UserId)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ThenBy(f => f.Id)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Follower.Id,
|
||||
f.Follower.Name,
|
||||
@@ -38,8 +35,6 @@ public class GetUserFollowersQueryHandler(IDatabase database)
|
||||
f.Follower.SecondName,
|
||||
f.Follower.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowerId)))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserSummaryResponse>.FromPagedList(page);
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
using X.PagedList.EF;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
@@ -17,17 +16,15 @@ public class GetUserFollowingQueryHandler(IDatabase database)
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowingQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return Error.NotFound();
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
var followsQuery = database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowerId == request.UserId);
|
||||
|
||||
var totalCount = await followsQuery.CountAsync(cancellationToken);
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
var page = await followsQuery
|
||||
return await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowerId == request.UserId)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ThenBy(f => f.Id)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Followed.Id,
|
||||
f.Followed.Name,
|
||||
@@ -35,8 +32,6 @@ public class GetUserFollowingQueryHandler(IDatabase database)
|
||||
f.Followed.SecondName,
|
||||
f.Followed.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowedId)))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserSummaryResponse>.FromPagedList(page);
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Serialization;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetUserProfileQuery(long UserId, long? ViewerId = null) : IQuery<ErrorOr<UserProfileResponse>>;
|
||||
|
||||
public record UserProfileResponse(
|
||||
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
DateTime CreatedAt,
|
||||
int FollowersCount,
|
||||
int FollowingCount,
|
||||
bool IsFollowing,
|
||||
bool CanViewFollowers);
|
||||
|
||||
public class GetUserProfileQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
|
||||
{
|
||||
@@ -41,7 +29,7 @@ public class GetUserProfileQueryHandler(IDatabase database)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
return Error.NotFound();
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
var followersCount = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
using X.PagedList.EF;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch, long? ViewerId = null)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
: IQuery<PaginatedResponse<UserSummaryResponse>>;
|
||||
|
||||
public class GetUsersQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUsersQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
: IQueryHandler<GetUsersQuery, PaginatedResponse<UserSummaryResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
public async ValueTask<PaginatedResponse<UserSummaryResponse>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var usersQuery = database.Users
|
||||
.AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrEmpty(request.UsernameSearch))
|
||||
if (!string.IsNullOrWhiteSpace(request.UsernameSearch))
|
||||
{
|
||||
var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant();
|
||||
usersQuery = usersQuery.Where(u => u.NormalizedName.Contains(normalizedUsername));
|
||||
var normalizedUsername = TextNormalizer.Normalize(request.UsernameSearch);
|
||||
usersQuery = usersQuery.Where(user => user.NormalizedName.Contains(normalizedUsername));
|
||||
}
|
||||
|
||||
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
||||
var viewerId = request.ViewerId;
|
||||
var pagedUsers = await usersQuery
|
||||
.OrderBy(u => u.Name)
|
||||
.Select(u => new UserSummaryResponse(
|
||||
u.Id,
|
||||
u.Name,
|
||||
u.FirstName,
|
||||
u.SecondName,
|
||||
u.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(f => f.FollowerId == viewerId && f.FollowedId == u.Id)))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserSummaryResponse>.FromPagedList(pagedUsers);
|
||||
return await usersQuery
|
||||
.OrderBy(user => user.Name)
|
||||
.ThenBy(user => user.Id)
|
||||
.Select(user => new UserSummaryResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.FirstName,
|
||||
user.SecondName,
|
||||
user.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(f => f.FollowerId == viewerId && f.FollowedId == user.Id)))
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Musify.Application.Users.Responses;
|
||||
|
||||
public record ListeningStatsResponse(int TracksThisWeek, int SecondsThisWeek, int StreakDays);
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Musify.Application.Serialization;
|
||||
|
||||
namespace Musify.Application.Users.Responses;
|
||||
|
||||
public record UserProfileResponse(
|
||||
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
DateTime CreatedAt,
|
||||
int FollowersCount,
|
||||
int FollowingCount,
|
||||
bool IsFollowing,
|
||||
bool CanViewFollowers);
|
||||
@@ -3,6 +3,7 @@ using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
@@ -29,7 +30,7 @@ public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandH
|
||||
{
|
||||
Id = request.Id,
|
||||
Name = request.Name,
|
||||
NormalizedName = request.Name.ToUpperInvariant(),
|
||||
NormalizedName = TextNormalizer.Normalize(request.Name),
|
||||
FirstName = request.FirstName,
|
||||
SecondName = request.SecondName,
|
||||
ProfilePictureUrl = request.ProfilePictureUrl
|
||||
@@ -38,11 +39,11 @@ public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandH
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Provisioned user {UserId}", request.Id);
|
||||
|
||||
return new Success();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
user.Name = request.Name;
|
||||
user.NormalizedName = request.Name.ToUpperInvariant();
|
||||
user.NormalizedName = TextNormalizer.Normalize(request.Name);
|
||||
user.FirstName = request.FirstName;
|
||||
user.SecondName = request.SecondName;
|
||||
user.ProfilePictureUrl = request.ProfilePictureUrl;
|
||||
@@ -51,6 +52,6 @@ public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandH
|
||||
if (written > 0)
|
||||
logger.LogInformation("Synced profile for user {UserId}", request.Id);
|
||||
|
||||
return new Success();
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ public class UnfollowUserCommandHandler(
|
||||
.SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
|
||||
|
||||
if (follow == null)
|
||||
return new Success();
|
||||
return Result.Success;
|
||||
|
||||
database.UserFollows.Remove(follow);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("User {FollowerId} unfollowed user {FollowedId}", request.FollowerId, request.FollowedId);
|
||||
|
||||
return new Success();
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user