Change editor rule

This commit is contained in:
2026-09-24 13:24:43 +02:00
parent 16478678b5
commit 73e37dbecd
401 changed files with 14735 additions and 12995 deletions
@@ -6,46 +6,47 @@ using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Domain.Entities;
namespace Musify.Application.Users;
public record FollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
public class FollowUserCommandHandler(
IDatabase database,
ILogger<FollowUserCommandHandler> logger)
: ICommandHandler<FollowUserCommand, ErrorOr<Success>>
namespace Musify.Application.Users
{
public async ValueTask<ErrorOr<Success>> Handle(FollowUserCommand request, CancellationToken cancellationToken)
public record FollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
public class FollowUserCommandHandler(
IDatabase database,
ILogger<FollowUserCommandHandler> logger)
: ICommandHandler<FollowUserCommand, ErrorOr<Success>>
{
if (request.FollowerId == request.FollowedId)
return Error.Validation(description: "A user cannot follow themselves.");
var followedExists = await database.Users
.AsNoTracking()
.AnyAsync(u => u.Id == request.FollowedId, cancellationToken);
if (!followedExists)
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 Result.Success;
await database.UserFollows.AddAsync(new UserFollow
public async ValueTask<ErrorOr<Success>> Handle(FollowUserCommand request, CancellationToken cancellationToken)
{
FollowerId = request.FollowerId,
FollowedId = request.FollowedId
}, cancellationToken);
if (request.FollowerId == request.FollowedId)
return Error.Validation(description: "A user cannot follow themselves.");
var followedExists = await database.Users
.AsNoTracking()
.AnyAsync(u => u.Id == request.FollowedId, cancellationToken);
if (!followedExists)
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 Result.Success;
await database.UserFollows.AddAsync(new UserFollow
{
FollowerId = request.FollowerId,
FollowedId = request.FollowedId
}, cancellationToken);
// A concurrent double click can hit the unique index; already following is a successful no-op.
var saved = await database.TrySaveChangesAsync(
AppErrors.Conflict("UserFollow.Duplicate", "Already following this user."), cancellationToken);
if (saved.IsError)
return Result.Success;
logger.LogInformation("User {FollowerId} followed user {FollowedId}", request.FollowerId, request.FollowedId);
// A concurrent double click can hit the unique index; already following is a successful no-op.
var saved = await database.TrySaveChangesAsync(
AppErrors.Conflict("UserFollow.Duplicate", "Already following this user."), cancellationToken);
if (saved.IsError)
return Result.Success;
logger.LogInformation("User {FollowerId} followed user {FollowedId}", request.FollowerId, request.FollowedId);
return Result.Success;
}
}
}
@@ -1,29 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts;
namespace Musify.Application.Users;
public static class FollowVisibility
namespace Musify.Application.Users
{
public static async Task<bool> CanViewFollowersAsync(
IDatabase database,
long? viewerId,
long userId,
CancellationToken cancellationToken)
public static class FollowVisibility
{
if (viewerId is not { } viewer)
return false;
public static async Task<bool> CanViewFollowersAsync(
IDatabase database,
long? viewerId,
long userId,
CancellationToken cancellationToken)
{
if (viewerId is not { } viewer)
return false;
if (viewer == userId)
return true;
if (viewer == userId)
return true;
var mutualRows = await database.UserFollows
.AsNoTracking()
.CountAsync(f =>
(f.FollowerId == viewer && f.FollowedId == userId) ||
(f.FollowerId == userId && f.FollowedId == viewer),
cancellationToken);
var mutualRows = await database.UserFollows
.AsNoTracking()
.CountAsync(f =>
(f.FollowerId == viewer && f.FollowedId == userId) ||
(f.FollowerId == userId && f.FollowedId == viewer),
cancellationToken);
return mutualRows == 2;
return mutualRows == 2;
}
}
}
@@ -3,50 +3,51 @@ using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts;
using Musify.Application.Users.Responses;
namespace Musify.Application.Users;
public record GetListeningStatsQuery(long UserId) : IQuery<ListeningStatsResponse>;
public class GetListeningStatsQueryHandler(IDatabase database)
: IQueryHandler<GetListeningStatsQuery, ListeningStatsResponse>
namespace Musify.Application.Users
{
public async ValueTask<ListeningStatsResponse> Handle(GetListeningStatsQuery request, CancellationToken cancellationToken)
public record GetListeningStatsQuery(long UserId) : IQuery<ListeningStatsResponse>;
public class GetListeningStatsQueryHandler(IDatabase database)
: IQueryHandler<GetListeningStatsQuery, ListeningStatsResponse>
{
var weekAgo = DateTime.UtcNow.AddDays(-7);
var thisWeekHistory = database.ListeningHistories
.AsNoTracking()
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
var tracksThisWeek = await thisWeekHistory
.Where(l => l.IsCounted)
.Select(l => l.TrackId)
.Distinct()
.CountAsync(cancellationToken);
var secondsThisWeek = await thisWeekHistory
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => l.PlayedSeconds ?? (l.IsCounted ? t.DurationSeconds : 0))
.SumAsync(cancellationToken);
var listenedDates = await database.ListeningHistories
.AsNoTracking()
.Where(l => l.UserId == request.UserId && l.IsCounted)
.Select(l => l.ListenedAt.Date)
.Distinct()
.ToListAsync(cancellationToken);
var listenedDateSet = listenedDates.ToHashSet();
var streakDays = 0;
var day = DateTime.UtcNow.Date;
if (!listenedDateSet.Contains(day))
day = day.AddDays(-1);
while (listenedDateSet.Contains(day))
public async ValueTask<ListeningStatsResponse> Handle(GetListeningStatsQuery request, CancellationToken cancellationToken)
{
streakDays++;
day = day.AddDays(-1);
}
var weekAgo = DateTime.UtcNow.AddDays(-7);
return new ListeningStatsResponse(tracksThisWeek, (int)Math.Round(secondsThisWeek), streakDays);
var thisWeekHistory = database.ListeningHistories
.AsNoTracking()
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
var tracksThisWeek = await thisWeekHistory
.Where(l => l.IsCounted)
.Select(l => l.TrackId)
.Distinct()
.CountAsync(cancellationToken);
var secondsThisWeek = await thisWeekHistory
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => l.PlayedSeconds ?? (l.IsCounted ? t.DurationSeconds : 0))
.SumAsync(cancellationToken);
var listenedDates = await database.ListeningHistories
.AsNoTracking()
.Where(l => l.UserId == request.UserId && l.IsCounted)
.Select(l => l.ListenedAt.Date)
.Distinct()
.ToListAsync(cancellationToken);
var listenedDateSet = listenedDates.ToHashSet();
var streakDays = 0;
var day = DateTime.UtcNow.Date;
if (!listenedDateSet.Contains(day))
day = day.AddDays(-1);
while (listenedDateSet.Contains(day))
{
streakDays++;
day = day.AddDays(-1);
}
return new ListeningStatsResponse(tracksThisWeek, (int)Math.Round(secondsThisWeek), streakDays);
}
}
}
@@ -5,24 +5,25 @@ using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Application.Users.Responses;
namespace Musify.Application.Users;
public record GetUserByIdQuery(long Id)
: IQuery<ErrorOr<UserApplicationResponse>>;
public class GetUserByIdQueryHandler(IDatabase database)
: IQueryHandler<GetUserByIdQuery, ErrorOr<UserApplicationResponse>>
namespace Musify.Application.Users
{
public async ValueTask<ErrorOr<UserApplicationResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
var user = await database.Users
.AsNoTracking()
.Where(u => u.Id == request.Id)
.Select(u => UserApplicationResponse.FromEntity(u))
.SingleOrDefaultAsync(cancellationToken);
public record GetUserByIdQuery(long Id)
: IQuery<ErrorOr<UserApplicationResponse>>;
return user == null
? AppErrors.NotFound("User", request.Id)
: user;
public class GetUserByIdQueryHandler(IDatabase database)
: IQueryHandler<GetUserByIdQuery, ErrorOr<UserApplicationResponse>>
{
public async ValueTask<ErrorOr<UserApplicationResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
var user = await database.Users
.AsNoTracking()
.Where(u => u.Id == request.Id)
.Select(u => UserApplicationResponse.FromEntity(u))
.SingleOrDefaultAsync(cancellationToken);
return user == null
? AppErrors.NotFound("User", request.Id)
: user;
}
}
}
@@ -5,36 +5,37 @@ using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Application.Users.Responses;
namespace Musify.Application.Users;
public record GetUserFollowersQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
public class GetUserFollowersQueryHandler(IDatabase database)
: IQueryHandler<GetUserFollowersQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
namespace Musify.Application.Users
{
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowersQuery request, CancellationToken cancellationToken)
public record GetUserFollowersQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
public class GetUserFollowersQueryHandler(IDatabase database)
: IQueryHandler<GetUserFollowersQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
{
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
return AppErrors.NotFound("User", request.UserId);
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowersQuery request, CancellationToken cancellationToken)
{
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
return AppErrors.NotFound("User", request.UserId);
if (!await FollowVisibility.CanViewFollowersAsync(database, request.ViewerId, request.UserId, cancellationToken))
return AppErrors.Forbidden("User", request.UserId);
if (!await FollowVisibility.CanViewFollowersAsync(database, request.ViewerId, request.UserId, cancellationToken))
return AppErrors.Forbidden("User", request.UserId);
var viewerId = request.ViewerId;
var viewerId = request.ViewerId;
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,
f.Follower.FirstName,
f.Follower.SecondName,
f.Follower.ProfilePictureUrl,
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowerId)))
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
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,
f.Follower.FirstName,
f.Follower.SecondName,
f.Follower.ProfilePictureUrl,
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowerId)))
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
}
}
}
@@ -5,33 +5,34 @@ using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Application.Users.Responses;
namespace Musify.Application.Users;
public record GetUserFollowingQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
public class GetUserFollowingQueryHandler(IDatabase database)
: IQueryHandler<GetUserFollowingQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
namespace Musify.Application.Users
{
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowingQuery request, CancellationToken cancellationToken)
public record GetUserFollowingQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
public class GetUserFollowingQueryHandler(IDatabase database)
: IQueryHandler<GetUserFollowingQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
{
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
return AppErrors.NotFound("User", request.UserId);
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowingQuery request, CancellationToken cancellationToken)
{
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
return AppErrors.NotFound("User", request.UserId);
var viewerId = request.ViewerId;
var viewerId = request.ViewerId;
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,
f.Followed.FirstName,
f.Followed.SecondName,
f.Followed.ProfilePictureUrl,
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowedId)))
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
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,
f.Followed.FirstName,
f.Followed.SecondName,
f.Followed.ProfilePictureUrl,
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowedId)))
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
}
}
}
@@ -5,58 +5,59 @@ using Musify.Application.Contracts;
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 class GetUserProfileQueryHandler(IDatabase database)
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
namespace Musify.Application.Users
{
public async ValueTask<ErrorOr<UserProfileResponse>> Handle(GetUserProfileQuery request, CancellationToken cancellationToken)
public record GetUserProfileQuery(long UserId, long? ViewerId = null) : IQuery<ErrorOr<UserProfileResponse>>;
public class GetUserProfileQueryHandler(IDatabase database)
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
{
var user = await database.Users
.AsNoTracking()
.Where(u => u.Id == request.UserId)
.Select(u => new
{
u.Id,
u.Name,
u.FirstName,
u.SecondName,
u.ProfilePictureUrl,
u.CreatedAt
})
.SingleOrDefaultAsync(cancellationToken);
if (user == null)
return AppErrors.NotFound("User", request.UserId);
var followersCount = await database.UserFollows
.AsNoTracking()
.CountAsync(f => f.FollowedId == request.UserId, cancellationToken);
var followingCount = await database.UserFollows
.AsNoTracking()
.CountAsync(f => f.FollowerId == request.UserId, cancellationToken);
var isFollowing = request.ViewerId is { } viewerId && viewerId != request.UserId &&
await database.UserFollows
public async ValueTask<ErrorOr<UserProfileResponse>> Handle(GetUserProfileQuery request, CancellationToken cancellationToken)
{
var user = await database.Users
.AsNoTracking()
.AnyAsync(f => f.FollowerId == viewerId && f.FollowedId == request.UserId, cancellationToken);
.Where(u => u.Id == request.UserId)
.Select(u => new
{
u.Id,
u.Name,
u.FirstName,
u.SecondName,
u.ProfilePictureUrl,
u.CreatedAt
})
.SingleOrDefaultAsync(cancellationToken);
var canViewFollowers = await FollowVisibility.CanViewFollowersAsync(
database, request.ViewerId, request.UserId, cancellationToken);
if (user == null)
return AppErrors.NotFound("User", request.UserId);
return new UserProfileResponse(
user.Id,
user.Name,
user.FirstName,
user.SecondName,
user.ProfilePictureUrl,
user.CreatedAt,
followersCount,
followingCount,
isFollowing,
canViewFollowers);
var followersCount = await database.UserFollows
.AsNoTracking()
.CountAsync(f => f.FollowedId == request.UserId, cancellationToken);
var followingCount = await database.UserFollows
.AsNoTracking()
.CountAsync(f => f.FollowerId == request.UserId, cancellationToken);
var isFollowing = request.ViewerId is { } viewerId && viewerId != request.UserId &&
await database.UserFollows
.AsNoTracking()
.AnyAsync(f => f.FollowerId == viewerId && f.FollowedId == request.UserId, cancellationToken);
var canViewFollowers = await FollowVisibility.CanViewFollowersAsync(
database, request.ViewerId, request.UserId, cancellationToken);
return new UserProfileResponse(
user.Id,
user.Name,
user.FirstName,
user.SecondName,
user.ProfilePictureUrl,
user.CreatedAt,
followersCount,
followingCount,
isFollowing,
canViewFollowers);
}
}
}
@@ -4,37 +4,38 @@ using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Application.Users.Responses;
namespace Musify.Application.Users;
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch, long? ViewerId = null)
: IQuery<PaginatedResponse<UserSummaryResponse>>;
public class GetUsersQueryHandler(IDatabase database)
: IQueryHandler<GetUsersQuery, PaginatedResponse<UserSummaryResponse>>
namespace Musify.Application.Users
{
public async ValueTask<PaginatedResponse<UserSummaryResponse>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch, long? ViewerId = null)
: IQuery<PaginatedResponse<UserSummaryResponse>>;
public class GetUsersQueryHandler(IDatabase database)
: IQueryHandler<GetUsersQuery, PaginatedResponse<UserSummaryResponse>>
{
var usersQuery = database.Users
.AsNoTracking();
if (!string.IsNullOrWhiteSpace(request.UsernameSearch))
public async ValueTask<PaginatedResponse<UserSummaryResponse>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
{
var normalizedUsername = TextNormalizer.Normalize(request.UsernameSearch);
usersQuery = usersQuery.Where(user => user.NormalizedName.Contains(normalizedUsername));
var usersQuery = database.Users
.AsNoTracking();
if (!string.IsNullOrWhiteSpace(request.UsernameSearch))
{
var normalizedUsername = TextNormalizer.Normalize(request.UsernameSearch);
usersQuery = usersQuery.Where(user => user.NormalizedName.Contains(normalizedUsername));
}
var viewerId = request.ViewerId;
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);
}
var viewerId = request.ViewerId;
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);
}
}
@@ -2,15 +2,16 @@ using Mediator;
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts;
namespace Musify.Application.Users;
public record IsFollowingUserQuery(long FollowerId, long FollowedId) : IQuery<bool>;
public class IsFollowingUserQueryHandler(IDatabase database)
: IQueryHandler<IsFollowingUserQuery, bool>
namespace Musify.Application.Users
{
public ValueTask<bool> Handle(IsFollowingUserQuery request, CancellationToken cancellationToken) =>
new(database.UserFollows
.AsNoTracking()
.AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken));
public record IsFollowingUserQuery(long FollowerId, long FollowedId) : IQuery<bool>;
public class IsFollowingUserQueryHandler(IDatabase database)
: IQueryHandler<IsFollowingUserQuery, bool>
{
public ValueTask<bool> Handle(IsFollowingUserQuery request, CancellationToken cancellationToken) =>
new(database.UserFollows
.AsNoTracking()
.AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken));
}
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Users.Responses;
public record ListeningStatsResponse(int TracksThisWeek, int SecondsThisWeek, int StreakDays);
namespace Musify.Application.Users.Responses
{
public record ListeningStatsResponse(int TracksThisWeek, int SecondsThisWeek, int StreakDays);
}
@@ -1,25 +1,26 @@
using Musify.Domain.Entities;
namespace Musify.Application.Users.Responses;
public record UserApplicationResponse(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl,
DateTime CreatedAt,
DateTime UpdatedAt)
namespace Musify.Application.Users.Responses
{
public static UserApplicationResponse FromEntity(User user)
public record UserApplicationResponse(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl,
DateTime CreatedAt,
DateTime UpdatedAt)
{
return new UserApplicationResponse(
user.Id,
user.Name,
user.FirstName,
user.SecondName,
user.ProfilePictureUrl,
user.CreatedAt,
user.UpdatedAt);
public static UserApplicationResponse FromEntity(User user)
{
return new UserApplicationResponse(
user.Id,
user.Name,
user.FirstName,
user.SecondName,
user.ProfilePictureUrl,
user.CreatedAt,
user.UpdatedAt);
}
}
}
@@ -1,14 +1,15 @@
namespace Musify.Application.Users.Responses;
public record UserProfileResponse(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl,
DateTime CreatedAt,
int FollowersCount,
int FollowingCount,
bool IsFollowing,
bool CanViewFollowers);
namespace Musify.Application.Users.Responses
{
public record UserProfileResponse(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl,
DateTime CreatedAt,
int FollowersCount,
int FollowingCount,
bool IsFollowing,
bool CanViewFollowers);
}
@@ -1,10 +1,11 @@
namespace Musify.Application.Users.Responses;
public record UserSummaryResponse(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl,
bool IsFollowedByViewer);
namespace Musify.Application.Users.Responses
{
public record UserSummaryResponse(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl,
bool IsFollowedByViewer);
}
@@ -6,52 +6,53 @@ using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Domain.Entities;
namespace Musify.Application.Users;
public record SyncUserCommand(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl)
: ICommand<ErrorOr<Success>>;
public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandHandler> logger)
: ICommandHandler<SyncUserCommand, ErrorOr<Success>>
namespace Musify.Application.Users
{
public async ValueTask<ErrorOr<Success>> Handle(SyncUserCommand request, CancellationToken cancellationToken)
public record SyncUserCommand(
long Id,
string Name,
string? FirstName,
string? SecondName,
string? ProfilePictureUrl)
: ICommand<ErrorOr<Success>>;
public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandHandler> logger)
: ICommandHandler<SyncUserCommand, ErrorOr<Success>>
{
var user = await database.Users
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
if (user == null)
public async ValueTask<ErrorOr<Success>> Handle(SyncUserCommand request, CancellationToken cancellationToken)
{
await database.Users.AddAsync(new User
{
Id = request.Id,
Name = request.Name,
NormalizedName = TextNormalizer.Normalize(request.Name),
FirstName = request.FirstName,
SecondName = request.SecondName,
ProfilePictureUrl = request.ProfilePictureUrl
}, cancellationToken);
var user = await database.Users
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("Provisioned user {UserId}", request.Id);
if (user == null)
{
await database.Users.AddAsync(new User
{
Id = request.Id,
Name = request.Name,
NormalizedName = TextNormalizer.Normalize(request.Name),
FirstName = request.FirstName,
SecondName = request.SecondName,
ProfilePictureUrl = request.ProfilePictureUrl
}, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("Provisioned user {UserId}", request.Id);
return Result.Success;
}
user.Name = request.Name;
user.NormalizedName = TextNormalizer.Normalize(request.Name);
user.FirstName = request.FirstName;
user.SecondName = request.SecondName;
user.ProfilePictureUrl = request.ProfilePictureUrl;
var written = await database.SaveChangesAsync(cancellationToken);
if (written > 0)
logger.LogInformation("Synced profile for user {UserId}", request.Id);
return Result.Success;
}
user.Name = request.Name;
user.NormalizedName = TextNormalizer.Normalize(request.Name);
user.FirstName = request.FirstName;
user.SecondName = request.SecondName;
user.ProfilePictureUrl = request.ProfilePictureUrl;
var written = await database.SaveChangesAsync(cancellationToken);
if (written > 0)
logger.LogInformation("Synced profile for user {UserId}", request.Id);
return Result.Success;
}
}
@@ -4,28 +4,29 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
namespace Musify.Application.Users;
public record UnfollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
public class UnfollowUserCommandHandler(
IDatabase database,
ILogger<UnfollowUserCommandHandler> logger)
: ICommandHandler<UnfollowUserCommand, ErrorOr<Success>>
namespace Musify.Application.Users
{
public async ValueTask<ErrorOr<Success>> Handle(UnfollowUserCommand request, CancellationToken cancellationToken)
public record UnfollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
public class UnfollowUserCommandHandler(
IDatabase database,
ILogger<UnfollowUserCommandHandler> logger)
: ICommandHandler<UnfollowUserCommand, ErrorOr<Success>>
{
var follow = await database.UserFollows
.SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
public async ValueTask<ErrorOr<Success>> Handle(UnfollowUserCommand request, CancellationToken cancellationToken)
{
var follow = await database.UserFollows
.SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
if (follow == null)
return Result.Success;
database.UserFollows.Remove(follow);
await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("User {FollowerId} unfollowed user {FollowedId}", request.FollowerId, request.FollowedId);
if (follow == null)
return Result.Success;
database.UserFollows.Remove(follow);
await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("User {FollowerId} unfollowed user {FollowedId}", request.FollowerId, request.FollowedId);
return Result.Success;
}
}
}