From 798637c6c10fb58adeecd1d8232c6668650db29a Mon Sep 17 00:00:00 2001 From: namdh861 Date: Mon, 17 Aug 2026 20:58:08 +0700 Subject: [PATCH] Convert ProjectMembers module to MediatR commands/queries Replaces IProjectMemberService/ProjectMemberService, completing the Projects module conversion. Co-Authored-By: Claude Sonnet 5 --- .../Controllers/ProjectMembersController.cs | 13 +- .../Projects/Commands/AddProjectMember.cs | 61 +++++++ .../Projects/Commands/RemoveProjectMember.cs | 31 ++++ .../UpdateMemberDocumentPermissions.cs | 54 +++++++ mws.application/Projects/IProjectService.cs | 9 -- .../Projects/ProjectMemberService.cs | 151 ------------------ .../Projects/Queries/GetProjectMembers.cs | 38 +++++ mws.infrastructure/DependencyInjection.cs | 1 - 8 files changed, 191 insertions(+), 167 deletions(-) create mode 100644 mws.application/Projects/Commands/AddProjectMember.cs create mode 100644 mws.application/Projects/Commands/RemoveProjectMember.cs create mode 100644 mws.application/Projects/Commands/UpdateMemberDocumentPermissions.cs delete mode 100644 mws.application/Projects/IProjectService.cs delete mode 100644 mws.application/Projects/ProjectMemberService.cs create mode 100644 mws.application/Projects/Queries/GetProjectMembers.cs diff --git a/mws.api/Controllers/ProjectMembersController.cs b/mws.api/Controllers/ProjectMembersController.cs index afa80c5..02c4acb 100644 --- a/mws.api/Controllers/ProjectMembersController.cs +++ b/mws.api/Controllers/ProjectMembersController.cs @@ -1,3 +1,4 @@ +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Mws.Application.Projects; @@ -7,31 +8,31 @@ namespace Mws.Api.Controllers; [ApiController] [Route("api/projects/{projectId:guid}/members")] [Authorize] -public class ProjectMembersController(IProjectMemberService memberService) : ControllerBase +public class ProjectMembersController(ISender sender) : ControllerBase { [HttpGet] public async Task>> GetAll(Guid projectId, CancellationToken ct) { - return Ok(await memberService.GetMembersAsync(User.GetUserId(), projectId, ct)); + return Ok(await sender.Send(new GetProjectMembersQuery(User.GetUserId(), projectId), ct)); } [HttpPost] public async Task> Add(Guid projectId, [FromBody] AddMemberRequest request, CancellationToken ct) { - return Ok(await memberService.AddMemberAsync(User.GetUserId(), projectId, request, ct)); + return Ok(await sender.Send(new AddProjectMemberCommand(User.GetUserId(), projectId, request), ct)); } [HttpPut("{userId:guid}/document-permissions")] public async Task> UpdateDocumentPermissions( Guid projectId, Guid userId, [FromBody] UpdateMemberDocumentPermissionsRequest request, CancellationToken ct) { - return Ok(await memberService.UpdateMemberDocumentPermissionsAsync(User.GetUserId(), projectId, userId, request, ct)); + return Ok(await sender.Send(new UpdateMemberDocumentPermissionsCommand(User.GetUserId(), projectId, userId, request), ct)); } [HttpDelete("{userId:guid}")] public async Task Remove(Guid projectId, Guid userId, CancellationToken ct) { - await memberService.RemoveMemberAsync(User.GetUserId(), projectId, userId, ct); + await sender.Send(new RemoveProjectMemberCommand(User.GetUserId(), projectId, userId), ct); return NoContent(); } -} \ No newline at end of file +} diff --git a/mws.application/Projects/Commands/AddProjectMember.cs b/mws.application/Projects/Commands/AddProjectMember.cs new file mode 100644 index 0000000..7694242 --- /dev/null +++ b/mws.application/Projects/Commands/AddProjectMember.cs @@ -0,0 +1,61 @@ +using MediatR; +using Mws.Application.Common; +using Mws.Domain.Projects; + +namespace Mws.Application.Projects; + +public record AddProjectMemberCommand(Guid UserId, Guid ProjectId, AddMemberRequest Request) : IRequest; + +public class AddProjectMemberHandler(IUnitOfWork uow) : IRequestHandler +{ + public async Task Handle(AddProjectMemberCommand command, CancellationToken ct) + { + if (await uow.Projects.GetMemberRoleAsync(command.ProjectId, command.UserId, ct) != MemberRole.Owner) + { + throw new ForbiddenException("Only the project owner can add members"); + } + + var request = command.Request; + var user = await uow.Users.GetByIdAsync(request.UserId, ct) + ?? throw new NotFoundException("User not found"); + + var already = await uow.Projects.IsMemberAsync(command.ProjectId, request.UserId, ct); + if (already) + { + throw new BadRequestException("User is already a member of this project"); + } + + var role = request.Role is MemberRole.Owner or MemberRole.Member ? request.Role : MemberRole.Member; + var member = new ProjectMember + { + ProjectId = command.ProjectId, + UserId = request.UserId, + Role = role, + }; + + uow.Projects.AddMember(member); + uow.Projects.AddMemberPermission(new ProjectMemberPermission + { + ProjectId = command.ProjectId, + UserId = request.UserId, + Screen = ProjectPermissionScreens.Documents, + CanView = true, + CanCreate = role == MemberRole.Owner, + CanEdit = role == MemberRole.Owner, + CanDelete = role == MemberRole.Owner, + }); + await uow.SaveChangesAsync(ct); + + return new ProjectMemberDto + { + UserId = user.Id, + Username = user.Username, + DisplayName = user.DisplayName, + Role = member.Role, + CanViewDocuments = true, + CanCreateDocuments = role == MemberRole.Owner, + CanEditDocuments = role == MemberRole.Owner, + CanDeleteDocuments = role == MemberRole.Owner, + }; + } +} diff --git a/mws.application/Projects/Commands/RemoveProjectMember.cs b/mws.application/Projects/Commands/RemoveProjectMember.cs new file mode 100644 index 0000000..fafd754 --- /dev/null +++ b/mws.application/Projects/Commands/RemoveProjectMember.cs @@ -0,0 +1,31 @@ +using MediatR; +using Mws.Application.Common; +using Mws.Domain.Projects; + +namespace Mws.Application.Projects; + +public record RemoveProjectMemberCommand(Guid UserId, Guid ProjectId, Guid MemberUserId) : IRequest; + +public class RemoveProjectMemberHandler(IUnitOfWork uow) : IRequestHandler +{ + public async Task Handle(RemoveProjectMemberCommand command, CancellationToken ct) + { + if (await uow.Projects.GetMemberRoleAsync(command.ProjectId, command.UserId, ct) != MemberRole.Owner) + { + throw new ForbiddenException("Only the project owner can remove members"); + } + + var member = await uow.Projects.GetMemberAsync(command.ProjectId, command.MemberUserId, ct) + ?? throw new NotFoundException("Member not found in project"); + + var owners = await uow.Projects.CountOwnersAsync(command.ProjectId, ct); + + if (member.Role == MemberRole.Owner && owners <= 1) + { + throw new BadRequestException("Cannot remove the last owner of the project"); + } + + uow.Projects.RemoveMember(member); + await uow.SaveChangesAsync(ct); + } +} diff --git a/mws.application/Projects/Commands/UpdateMemberDocumentPermissions.cs b/mws.application/Projects/Commands/UpdateMemberDocumentPermissions.cs new file mode 100644 index 0000000..08d1fb0 --- /dev/null +++ b/mws.application/Projects/Commands/UpdateMemberDocumentPermissions.cs @@ -0,0 +1,54 @@ +using MediatR; +using Mws.Application.Common; +using Mws.Domain.Projects; + +namespace Mws.Application.Projects; + +public record UpdateMemberDocumentPermissionsCommand( + Guid UserId, Guid ProjectId, Guid MemberUserId, UpdateMemberDocumentPermissionsRequest Request) : IRequest; + +public class UpdateMemberDocumentPermissionsHandler(IUnitOfWork uow) + : IRequestHandler +{ + public async Task Handle(UpdateMemberDocumentPermissionsCommand command, CancellationToken ct) + { + if (await uow.Projects.GetMemberRoleAsync(command.ProjectId, command.UserId, ct) != MemberRole.Owner) + { + throw new ForbiddenException("Only the project owner can change member permissions"); + } + + var member = await uow.Projects.GetMemberWithUserAsync(command.ProjectId, command.MemberUserId, ct) + ?? throw new NotFoundException("Member not found in project"); + + if (member.Role == MemberRole.Owner) + { + throw new BadRequestException("Owner permissions cannot be changed"); + } + + var permission = await uow.Projects.GetMemberPermissionAsync(command.ProjectId, command.MemberUserId, ProjectPermissionScreens.Documents, ct); + if (permission is null) + { + permission = new ProjectMemberPermission { ProjectId = command.ProjectId, UserId = command.MemberUserId, Screen = ProjectPermissionScreens.Documents }; + uow.Projects.AddMemberPermission(permission); + } + + var request = command.Request; + permission.CanView = request.CanViewDocuments; + permission.CanCreate = request.CanCreateDocuments; + permission.CanEdit = request.CanEditDocuments; + permission.CanDelete = request.CanDeleteDocuments; + await uow.SaveChangesAsync(ct); + + return new ProjectMemberDto + { + UserId = member.UserId, + Username = member.User.Username, + DisplayName = member.User.DisplayName, + Role = member.Role, + CanViewDocuments = permission.CanView, + CanCreateDocuments = permission.CanCreate, + CanEditDocuments = permission.CanEdit, + CanDeleteDocuments = permission.CanDelete, + }; + } +} diff --git a/mws.application/Projects/IProjectService.cs b/mws.application/Projects/IProjectService.cs deleted file mode 100644 index b0290e4..0000000 --- a/mws.application/Projects/IProjectService.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Mws.Application.Projects; - -public interface IProjectMemberService -{ - Task> GetMembersAsync(Guid userId, Guid projectId, CancellationToken ct = default); - Task AddMemberAsync(Guid userId, Guid projectId, AddMemberRequest request, CancellationToken ct = default); - Task UpdateMemberDocumentPermissionsAsync(Guid userId, Guid projectId, Guid memberUserId, UpdateMemberDocumentPermissionsRequest request, CancellationToken ct = default); - Task RemoveMemberAsync(Guid userId, Guid projectId, Guid memberUserId, CancellationToken ct = default); -} diff --git a/mws.application/Projects/ProjectMemberService.cs b/mws.application/Projects/ProjectMemberService.cs deleted file mode 100644 index 566afd6..0000000 --- a/mws.application/Projects/ProjectMemberService.cs +++ /dev/null @@ -1,151 +0,0 @@ -using Mws.Application.Common; -using Mws.Domain.Projects; - -namespace Mws.Application.Projects; - -public class ProjectMemberService(IUnitOfWork uow) : IProjectMemberService -{ - public async Task> GetMembersAsync(Guid userId, Guid projectId, CancellationToken ct = default) - { - var isMember = await uow.Projects.IsMemberAsync(projectId, userId, ct); - if (!isMember) - { - throw new NotFoundException("Project not found"); - } - - var members = await uow.Projects.GetMembersWithUserAsync(projectId, ct); - var permissions = await uow.Projects.GetMemberPermissionsAsync(projectId, ProjectPermissionScreens.Documents, ct); - - return members.Select(m => - { - permissions.TryGetValue(m.UserId, out var p); - return new ProjectMemberDto - { - UserId = m.UserId, - Username = m.User.Username, - DisplayName = m.User.DisplayName, - Role = m.Role, - CanViewDocuments = m.Role == MemberRole.Owner || (p?.CanView ?? false), - CanCreateDocuments = m.Role == MemberRole.Owner || (p?.CanCreate ?? false), - CanEditDocuments = m.Role == MemberRole.Owner || (p?.CanEdit ?? false), - CanDeleteDocuments = m.Role == MemberRole.Owner || (p?.CanDelete ?? false), - }; - }).ToList(); - } - - public async Task AddMemberAsync(Guid userId, Guid projectId, AddMemberRequest request, CancellationToken ct = default) - { - if (await GetMemberRoleAsync(userId, projectId, ct) != MemberRole.Owner) - { - throw new ForbiddenException("Only the project owner can add members"); - } - - var user = await uow.Users.GetByIdAsync(request.UserId, ct) - ?? throw new NotFoundException("User not found"); - - var already = await uow.Projects.IsMemberAsync(projectId, request.UserId, ct); - if (already) - { - throw new BadRequestException("User is already a member of this project"); - } - - var role = request.Role is MemberRole.Owner or MemberRole.Member ? request.Role : MemberRole.Member; - var member = new ProjectMember - { - ProjectId = projectId, - UserId = request.UserId, - Role = role, - }; - - uow.Projects.AddMember(member); - uow.Projects.AddMemberPermission(new ProjectMemberPermission - { - ProjectId = projectId, - UserId = request.UserId, - Screen = ProjectPermissionScreens.Documents, - CanView = true, - CanCreate = role == MemberRole.Owner, - CanEdit = role == MemberRole.Owner, - CanDelete = role == MemberRole.Owner, - }); - await uow.SaveChangesAsync(ct); - - return new ProjectMemberDto - { - UserId = user.Id, - Username = user.Username, - DisplayName = user.DisplayName, - Role = member.Role, - CanViewDocuments = true, - CanCreateDocuments = role == MemberRole.Owner, - CanEditDocuments = role == MemberRole.Owner, - CanDeleteDocuments = role == MemberRole.Owner, - }; - } - - public async Task UpdateMemberDocumentPermissionsAsync( - Guid userId, Guid projectId, Guid memberUserId, UpdateMemberDocumentPermissionsRequest request, CancellationToken ct = default) - { - if (await GetMemberRoleAsync(userId, projectId, ct) != MemberRole.Owner) - { - throw new ForbiddenException("Only the project owner can change member permissions"); - } - - var member = await uow.Projects.GetMemberWithUserAsync(projectId, memberUserId, ct) - ?? throw new NotFoundException("Member not found in project"); - - if (member.Role == MemberRole.Owner) - { - throw new BadRequestException("Owner permissions cannot be changed"); - } - - var permission = await uow.Projects.GetMemberPermissionAsync(projectId, memberUserId, ProjectPermissionScreens.Documents, ct); - if (permission is null) - { - permission = new ProjectMemberPermission { ProjectId = projectId, UserId = memberUserId, Screen = ProjectPermissionScreens.Documents }; - uow.Projects.AddMemberPermission(permission); - } - - permission.CanView = request.CanViewDocuments; - permission.CanCreate = request.CanCreateDocuments; - permission.CanEdit = request.CanEditDocuments; - permission.CanDelete = request.CanDeleteDocuments; - await uow.SaveChangesAsync(ct); - - return new ProjectMemberDto - { - UserId = member.UserId, - Username = member.User.Username, - DisplayName = member.User.DisplayName, - Role = member.Role, - CanViewDocuments = permission.CanView, - CanCreateDocuments = permission.CanCreate, - CanEditDocuments = permission.CanEdit, - CanDeleteDocuments = permission.CanDelete, - }; - } - - public async Task RemoveMemberAsync(Guid userId, Guid projectId, Guid memberUserId, CancellationToken ct = default) - { - if (await GetMemberRoleAsync(userId, projectId, ct) != MemberRole.Owner) - { - throw new ForbiddenException("Only the project owner can remove members"); - } - - var member = await uow.Projects.GetMemberAsync(projectId, memberUserId, ct) - ?? throw new NotFoundException("Member not found in project"); - - var owners = await uow.Projects.CountOwnersAsync(projectId, ct); - - if (member.Role == MemberRole.Owner && owners <= 1) - { - throw new BadRequestException("Cannot remove the last owner of the project"); - } - - uow.Projects.RemoveMember(member); - await uow.SaveChangesAsync(ct); - } - - private Task GetMemberRoleAsync(Guid userId, Guid projectId, CancellationToken ct = default) => - uow.Projects.GetMemberRoleAsync(projectId, userId, ct); -} diff --git a/mws.application/Projects/Queries/GetProjectMembers.cs b/mws.application/Projects/Queries/GetProjectMembers.cs new file mode 100644 index 0000000..5612da4 --- /dev/null +++ b/mws.application/Projects/Queries/GetProjectMembers.cs @@ -0,0 +1,38 @@ +using MediatR; +using Mws.Application.Common; +using Mws.Domain.Projects; + +namespace Mws.Application.Projects; + +public record GetProjectMembersQuery(Guid UserId, Guid ProjectId) : IRequest>; + +public class GetProjectMembersHandler(IUnitOfWork uow) : IRequestHandler> +{ + public async Task> Handle(GetProjectMembersQuery query, CancellationToken ct) + { + var isMember = await uow.Projects.IsMemberAsync(query.ProjectId, query.UserId, ct); + if (!isMember) + { + throw new NotFoundException("Project not found"); + } + + var members = await uow.Projects.GetMembersWithUserAsync(query.ProjectId, ct); + var permissions = await uow.Projects.GetMemberPermissionsAsync(query.ProjectId, ProjectPermissionScreens.Documents, ct); + + return members.Select(m => + { + permissions.TryGetValue(m.UserId, out var p); + return new ProjectMemberDto + { + UserId = m.UserId, + Username = m.User.Username, + DisplayName = m.User.DisplayName, + Role = m.Role, + CanViewDocuments = m.Role == MemberRole.Owner || (p?.CanView ?? false), + CanCreateDocuments = m.Role == MemberRole.Owner || (p?.CanCreate ?? false), + CanEditDocuments = m.Role == MemberRole.Owner || (p?.CanEdit ?? false), + CanDeleteDocuments = m.Role == MemberRole.Owner || (p?.CanDelete ?? false), + }; + }).ToList(); + } +} diff --git a/mws.infrastructure/DependencyInjection.cs b/mws.infrastructure/DependencyInjection.cs index 7246c18..ea4e72c 100644 --- a/mws.infrastructure/DependencyInjection.cs +++ b/mws.infrastructure/DependencyInjection.cs @@ -39,7 +39,6 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped();