Перепиливание под чистый DDD

This commit is contained in:
Халимов Рустам
2026-03-22 23:59:33 +03:00
parent 5da1a2f45d
commit 6e532b021d
302 changed files with 3595 additions and 3679 deletions
@@ -0,0 +1,14 @@
using Knot.Modules.Relations.Domain;
using Microsoft.EntityFrameworkCore;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Abstractions;
public interface IRelationsDbContext
{
DbSet<Friendship> Friendships { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Abstractions;
public interface IRelationsUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,12 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Domain;
namespace Knot.Modules.Relations.Application.Abstractions;
public interface IUserRepository
{
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
Task<UserReplica?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,37 @@
using Knot.Modules.Relations.Domain;
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record AcceptFriendRequestCommand(Guid UserId, Guid FriendshipId) : ICommand<Guid>;
internal sealed class AcceptFriendRequestCommandHandler : ICommandHandler<AcceptFriendRequestCommand, Guid>
{
private readonly IRelationsDbContext _context;
private readonly IRelationsUnitOfWork _unitOfWork;
public AcceptFriendRequestCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork)
{
_context = context;
_unitOfWork = unitOfWork;
}
public async Task<Result<Guid>> Handle(AcceptFriendRequestCommand request, CancellationToken cancellationToken)
{
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || friendship.FriendId != request.UserId)
{
return Result.Failure<Guid>(IdentityErrors.FriendsNotFound);
}
friendship.Accept();
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success(friendship.Id);
}
}
@@ -0,0 +1,14 @@
using System;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends.DTOs;
public record FriendDto(
Guid Id,
string Username,
string DisplayName,
string? Avatar,
bool IsOnline,
DateTime LastSeen,
Guid FriendshipId
);
@@ -0,0 +1,17 @@
using System;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends.DTOs;
public record FriendRequestDto(
Guid Id,
FriendUserDto User,
DateTime CreatedAt
);
public record FriendUserDto(
Guid Id,
string Username,
string DisplayName,
string? Avatar
);
@@ -0,0 +1,6 @@
using System;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends.DTOs;
public record SendFriendRequest(Guid FriendId);
@@ -0,0 +1,37 @@
using Knot.Modules.Relations.Domain;
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record DeclineFriendRequestCommand(Guid UserId, Guid FriendshipId) : ICommand;
internal sealed class DeclineFriendRequestCommandHandler : ICommandHandler<DeclineFriendRequestCommand>
{
private readonly IRelationsDbContext _context;
private readonly IRelationsUnitOfWork _unitOfWork;
public DeclineFriendRequestCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork)
{
_context = context;
_unitOfWork = unitOfWork;
}
public async Task<Result> Handle(DeclineFriendRequestCommand request, CancellationToken cancellationToken)
{
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || friendship.FriendId != request.UserId)
{
return Result.Failure(IdentityErrors.FriendsNotFound);
}
friendship.Decline();
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success();
}
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record FriendDto(
Guid Id,
string Username,
string DisplayName,
string? Avatar,
bool IsOnline,
DateTime LastSeenAt,
Guid FriendshipId
);
@@ -0,0 +1,12 @@
using System;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record FriendUserDto(Guid Id, string Username, string DisplayName, string? Avatar);
public record FriendRequestDto(
Guid Id,
FriendUserDto User,
DateTime CreatedAt
);
@@ -0,0 +1,10 @@
using System;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record FriendshipStatusResponse(
string Status,
Guid? FriendshipId = null,
string? Direction = null
);
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Domain;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record GetFriendsQuery(Guid UserId) : IQuery<List<FriendDto>>;
internal sealed class GetFriendsQueryHandler : IQueryHandler<GetFriendsQuery, List<FriendDto>>
{
private readonly IRelationsDbContext _context;
private readonly IUserRepository _userRepository;
public GetFriendsQueryHandler(IRelationsDbContext context, IUserRepository userRepository)
{
_context = context;
_userRepository = userRepository;
}
public async Task<Result<List<FriendDto>>> Handle(GetFriendsQuery request, CancellationToken cancellationToken)
{
var friendships = await _context.Friendships
.Where(f => (f.UserId == request.UserId || f.FriendId == request.UserId) && f.Status == FriendshipStatus.Accepted)
.ToListAsync(cancellationToken);
var friendIds = friendships.Select(f => f.UserId == request.UserId ? f.FriendId : f.UserId).ToList();
var friends = new List<FriendDto>();
foreach (var id in friendIds)
{
var user = await _userRepository.GetByIdAsync(id, cancellationToken);
if (user == null)
{
continue;
}
var fs = friendships.First(f => f.UserId == id || f.FriendId == id);
friends.Add(new FriendDto(
user.Id,
user.Username,
user.DisplayName,
user.Avatar,
false, // IsOnline logic shouldn't be here, but we'll leave default for now
DateTime.UtcNow,
fs.Id
));
}
return Result.Success(friends);
}
}
@@ -0,0 +1,44 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record GetFriendshipStatusQuery(Guid CurrentUserId, Guid TargetUserId) : IQuery<FriendshipStatusResponse>;
internal sealed class GetFriendshipStatusQueryHandler : IQueryHandler<GetFriendshipStatusQuery, FriendshipStatusResponse>
{
private readonly IRelationsDbContext _context;
public GetFriendshipStatusQueryHandler(IRelationsDbContext context)
{
_context = context;
}
public async Task<Result<FriendshipStatusResponse>> Handle(GetFriendshipStatusQuery request, CancellationToken cancellationToken)
{
if (request.CurrentUserId == request.TargetUserId)
{
return Result.Success(new FriendshipStatusResponse("self"));
}
var fs = await _context.Friendships
.FirstOrDefaultAsync(f => (f.UserId == request.CurrentUserId && f.FriendId == request.TargetUserId) ||
(f.UserId == request.TargetUserId && f.FriendId == request.CurrentUserId), cancellationToken);
if (fs == null)
{
return Result.Success(new FriendshipStatusResponse("none"));
}
return Result.Success(new FriendshipStatusResponse(
fs.Status.ToString().ToLowerInvariant(),
fs.Id,
fs.UserId == request.CurrentUserId ? "outgoing" : "incoming"
));
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Domain;
using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record GetIncomingRequestsQuery(Guid UserId) : IQuery<List<FriendRequestDto>>;
internal sealed class GetIncomingRequestsQueryHandler : IQueryHandler<GetIncomingRequestsQuery, List<FriendRequestDto>>
{
private readonly IRelationsDbContext _context;
private readonly IUserRepository _userRepository;
public GetIncomingRequestsQueryHandler(IRelationsDbContext context, IUserRepository userRepository)
{
_context = context;
_userRepository = userRepository;
}
public async Task<Result<List<FriendRequestDto>>> Handle(GetIncomingRequestsQuery request, CancellationToken cancellationToken)
{
var friendships = await _context.Friendships
.Where(f => f.FriendId == request.UserId && f.Status == FriendshipStatus.Pending)
.ToListAsync(cancellationToken);
var requestsList = new List<FriendRequestDto>();
foreach (var fs in friendships)
{
var user = await _userRepository.GetByIdAsync(fs.UserId, cancellationToken);
if (user == null)
{
continue;
}
requestsList.Add(new FriendRequestDto(
fs.Id,
new FriendUserDto(user.Id, user.Username, user.DisplayName, user.Avatar),
fs.CreatedAt
));
}
return Result.Success(requestsList);
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Domain;
using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record GetOutgoingRequestsQuery(Guid UserId) : IQuery<List<FriendRequestDto>>;
internal sealed class GetOutgoingRequestsQueryHandler : IQueryHandler<GetOutgoingRequestsQuery, List<FriendRequestDto>>
{
private readonly IRelationsDbContext _context;
private readonly IUserRepository _userRepository;
public GetOutgoingRequestsQueryHandler(IRelationsDbContext context, IUserRepository userRepository)
{
_context = context;
_userRepository = userRepository;
}
public async Task<Result<List<FriendRequestDto>>> Handle(GetOutgoingRequestsQuery request, CancellationToken cancellationToken)
{
var friendships = await _context.Friendships
.Where(f => f.UserId == request.UserId && f.Status == FriendshipStatus.Pending)
.ToListAsync(cancellationToken);
var requestsList = new List<FriendRequestDto>();
foreach (var fs in friendships)
{
var user = await _userRepository.GetByIdAsync(fs.FriendId, cancellationToken);
if (user == null)
{
continue;
}
requestsList.Add(new FriendRequestDto(
fs.Id,
new FriendUserDto(user.Id, user.Username, user.DisplayName, user.Avatar),
fs.CreatedAt
));
}
return Result.Success(requestsList);
}
}
@@ -0,0 +1,37 @@
using Knot.Modules.Relations.Domain;
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record RemoveFriendCommand(Guid UserId, Guid FriendshipId) : ICommand;
internal sealed class RemoveFriendCommandHandler : ICommandHandler<RemoveFriendCommand>
{
private readonly IRelationsDbContext _context;
private readonly IRelationsUnitOfWork _unitOfWork;
public RemoveFriendCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork)
{
_context = context;
_unitOfWork = unitOfWork;
}
public async Task<Result> Handle(RemoveFriendCommand request, CancellationToken cancellationToken)
{
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || (friendship.UserId != request.UserId && friendship.FriendId != request.UserId))
{
return Result.Failure(IdentityErrors.FriendsNotFound);
}
_context.Friendships.Remove(friendship);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success();
}
}
@@ -0,0 +1,47 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Domain;
using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Relations.Application.Abstractions;
namespace Knot.Modules.Relations.Application.Friends;
public record SendFriendRequestCommand(Guid UserId, Guid FriendId) : ICommand;
internal sealed class SendFriendRequestCommandHandler : ICommandHandler<SendFriendRequestCommand>
{
private readonly IRelationsDbContext _context;
private readonly IRelationsUnitOfWork _unitOfWork;
public SendFriendRequestCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork)
{
_context = context;
_unitOfWork = unitOfWork;
}
public async Task<Result> Handle(SendFriendRequestCommand request, CancellationToken cancellationToken)
{
if (request.UserId == request.FriendId)
{
return Result.Failure(IdentityErrors.FriendsSelf);
}
var existing = await _context.Friendships
.FirstOrDefaultAsync(f => (f.UserId == request.UserId && f.FriendId == request.FriendId) ||
(f.UserId == request.FriendId && f.FriendId == request.UserId), cancellationToken);
if (existing != null)
{
return Result.Failure(IdentityErrors.FriendsExists);
}
var friendship = Friendship.Create(request.UserId, request.FriendId);
_context.Friendships.Add(friendship);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success();
}
}