project file service using Aggregator.Web.Models.ProjectViewModels; using NscEcms.Project.Grpc; using NscEcms.Startup; using NscEcms.Startup.ApiBaseResponses; using NscEcms.Util; namespace Aggregator.Web.Services.Implementations { internal sealed class ProjectFileService(ProjectGrpcApi.ProjectGrpcApiClient client, ITokenService tokenService) : AggregatorServiceBase(tokenService), IProjectFileService { private readonly int _deadLine = 45; public async Task>> GetProjectFileAssignees(Guid projectFileId) { var response = await client.GetProjectFileAssigneesAsync( new GetProjectFileAssigneesGrpcCommand { ProjectFileId = projectFileId.ToString() }, await GetAuthMetaData(), deadline: DateTime.UtcNow.AddSeconds(_deadLine) ); var contentRes = response.Content.Unpack(); var assignees = contentRes.Assignees.Select(a => new GetProjectFileAssigneeResponseViewModel { Id = a.Id.ToGuid(), Name = a.Name, Department = a.Department, Role = a.Role, AvatarUrl = a.AvatarUrl }).ToList(); return new ApiResponse>(assignees, response.Message); } } } === project controller using Aggregator.Web.Models.ProjectViewModels; using Aggregator.Web.Services; using Aggregator.Web.Services.Implementations; using Microsoft.AspNetCore.Mvc; using NscEcms.Startup.ApiBaseResponses; using NscEcms.Util.Aggregators; namespace Aggregator.Web.AppVersion.v1.Controllers { public class ProjectController(IProjectFileService projectService) : ApiControllerBase { /// /// Get List Of Project File Assignee /// /// The ID of the project file /// List of project file assignees [HttpGet("{projectFileId:guid}")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ApiResponse>))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ApiErrorResponse))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ApiErrorResponse))] public async Task GetAllProjectFileAssignees(Guid projectFileId) => Ok(await projectService.GetProjectFileAssignees(projectFileId)); } } === projectGrpcService [Authorize(ProjectFilePermissionConstant.ViewProjectFile)] public override async Task GetProjectFileAssignees(GetProjectFileAssigneesGrpcCommand request, ServerCallContext context) { var result = await mediator.Send(new GetProjectFileAssigneesCommand(request.ProjectFileId.ToGuid())); var responseMessage = new GetProjectFileAssigneesResultMessage(); if (result != null && result.Any()) { responseMessage.Assignees.AddRange(result.Select(assignee => new GetProjectFileAssigneeMessage { Id = assignee.Id.ToString(), Name = assignee.Name, Department = assignee.Department, Role = assignee.Role, AvatarUrl = assignee.AvatarUrl })); } return new ServiceResponseMessage { Message = result.IsDefault() ? "Project file assignees retrieved successfully." : "No assignees found for this project file.", Content = Any.Pack(responseMessage) }; } === proto file rpc GetProjectFileAssignees(GetProjectFileAssigneesGrpcCommand) returns (nsc.ecms.ServiceResponseMessage) {} message GetProjectFileAssigneesGrpcCommand { string project_file_id = 1; } message GetProjectFileAssigneeMessage { string id = 1; string name = 2; string department = 3; string role = 4; string avatar_url = 5; } message GetProjectFileAssigneesResultMessage { repeated GetProjectFileAssigneeMessage assignees = 1; } === View Model namespace Aggregator.Web.Models.ProjectViewModels { public class GetProjectFileAssigneeResponseViewModel { public Guid Id { get; set; } public string Name { get; set; } public string Department { get; set; } public string Role { get; set; } public string AvatarUrl { get; set; } } } ==== project service implementation using Aggregator.Web.Models.ProjectViewModels; using NscEcms.Project.Grpc; using NscEcms.Startup; using NscEcms.Startup.ApiBaseResponses; using NscEcms.Util; namespace Aggregator.Web.Services.Implementations { internal sealed class ProjectFileService(ProjectGrpcApi.ProjectGrpcApiClient client, ITokenService tokenService) : AggregatorServiceBase(tokenService), IProjectFileService { private readonly int _deadLine = 45; public async Task>> GetProjectFileAssignees(Guid projectFileId) { var response = await client.GetProjectFileAssigneesAsync( new GetProjectFileAssigneesGrpcCommand { ProjectFileId = projectFileId.ToString() }, await GetAuthMetaData(), deadline: DateTime.UtcNow.AddSeconds(_deadLine) ); var contentRes = response.Content.Unpack(); var assignees = contentRes.Assignees.Select(a => new GetProjectFileAssigneeResponseViewModel { Id = a.Id.ToGuid(), Name = a.Name, Department = a.Department, Role = a.Role, AvatarUrl = a.AvatarUrl }).ToList(); return new ApiResponse>(assignees, response.Message); } } } === interface using Aggregator.Web.Models.ProjectViewModels; using NscEcms.Startup.ApiBaseResponses; namespace Aggregator.Web.Services { public interface IProjectFileService { Task>> GetProjectFileAssignees(Guid projectFileId); } } === project file command using MediatR; using Project.Api.Application.Models; namespace Project.Api.Application.Commands { public record GetProjectFileAssigneesCommand(Guid ProjectFileId) : IRequest>; } === project file commandhandler using MediatR; using Project.Api.Application.Commands; using Project.Api.Application.Models; using Project.Api.Application.Queries; namespace Project.Api.Application.CommandHandlers { public class GetProjectFileAssigneesCommandHandler(IProjectFileQueries queries) : IRequestHandler> { public async Task> Handle(GetProjectFileAssigneesCommand request, CancellationToken cancellationToken) { return await Task.FromResult(queries.GetProjectFileAssignees(request.ProjectFileId)); } } } === Models using NscEcms.Util.SharedModels; namespace Project.Api.Application.Models { public class GetProjectFileAssigneeModel : IdModel { public string Name { get; set; } public string Role { get; set; } public string Department { get; set; } public string AvatarUrl { get; set; } // Store the avatar image URL } } === interface project file query using Project.Api.Application.Models; using Project.Api.Application.Models.SqlQuery; namespace Project.Api.Application.Queries { public interface IProjectFileQueries { List GetProjectFileAssignees(Guid projectFileId); } } === project file query using Dapper; using NscEcms.Util; using Project.Api.Application.Models; using Project.Api.Application.Models.SqlQuery; using Project.Api.Infrastructure; namespace Project.Api.Application.Queries { internal sealed class ProjectFileQueries(ProjectContext context) : QueryBase(context), IProjectFileQueries { public List GetProjectFileAssignees(Guid projectId) { var result = RunSqlQuery((connection) => { const string sql = @" SELECT u.""Id"", CONCAT(u.""FirstName"", ' ', u.""LastName"") AS ""Name"", r.""Name"" AS ""Role"", d.""Name"" AS ""Department"", u.""AvatarUrl"" FROM dbo.""ProjectAssignees"" pa LEFT JOIN dbo.""Users"" u ON pa.""UserId"" = u.""Id"" LEFT JOIN dbo.""Roles"" r ON u.""RoleId"" = r.""Id"" LEFT JOIN dbo.""Departments"" d ON u.""DepartmentId"" = d.""Id"" WHERE pa.""ProjectId"" = @ProjectId ORDER BY u.""FirstName"", u.""LastName""; "; var result = connection.Query(sql, new { ProjectId = projectId }); return result?.ToList() ?? []; }); return result; } } } SELECT p."OwnerUserId" AS "Id", CONCAT(u."FirstName", ' ', u."LastName") AS "Name", j."Name" AS "JobTitle", d."Name" AS "Department", '' AS "AvatarUrl" FROM dbo."ProjectTasks" AS p LEFT JOIN dbo."Users" AS u ON p."OwnerUserId" = u."Id" LEFT JOIN dbo."JobTitles" AS j ON u."JobTitleId" = j."Id" LEFT JOIN dbo."Departments" AS d ON u."DepartmentId" = d."Id" UNION SELECT p."OwnerUserId" AS "Id", CONCAT(u."FirstName", ' ', u."LastName") AS "Name", j."Name" AS "JobTitle", d."Name" AS "Department", '' AS "AvatarUrl" FROM dbo."ProjectWorkflowTasks" AS p LEFT JOIN dbo."Users" AS u ON p."OwnerUserId" = u."Id" LEFT JOIN dbo."JobTitles" AS j ON u."JobTitleId" = j."Id" LEFT JOIN dbo."Departments" AS d ON u."DepartmentId" = d."Id";