using Microsoft.EntityFrameworkCore; using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate; using ColaFlow.Modules.ProjectManagement.Domain.Repositories; using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence; namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories; /// /// Project repository implementation using EF Core /// public class ProjectRepository(PMDbContext context) : IProjectRepository { private readonly PMDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); // ========== Basic CRUD Operations ========== public async Task GetByIdAsync(ProjectId id, CancellationToken cancellationToken = default) { return await _context.Projects .Include(p => p.Epics) .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); } public async Task GetByKeyAsync(string key, CancellationToken cancellationToken = default) { return await _context.Projects .FirstOrDefaultAsync(p => p.Key.Value == key, cancellationToken); } public async Task> GetAllAsync(CancellationToken cancellationToken = default) { return await _context.Projects .OrderByDescending(p => p.CreatedAt) .ToListAsync(cancellationToken); } public async Task AddAsync(Project project, CancellationToken cancellationToken = default) { await _context.Projects.AddAsync(project, cancellationToken); } public void Update(Project project) { _context.Projects.Update(project); } public void Delete(Project project) { _context.Projects.Remove(project); } // ========== Aggregate Root Loading (for Command Handlers - with tracking) ========== public async Task GetProjectWithEpicAsync(EpicId epicId, CancellationToken cancellationToken = default) { // Load only the specific Epic with its Stories (filtered Include) return await _context.Projects .Include(p => p.Epics.Where(e => e.Id == epicId)) .ThenInclude(e => e.Stories) .Where(p => p.Epics.Any(e => e.Id == epicId)) .FirstOrDefaultAsync(cancellationToken); } public async Task GetProjectWithStoryAsync(StoryId storyId, CancellationToken cancellationToken = default) { // Load the Epic containing the Story, with that Story and its Tasks return await _context.Projects .Include(p => p.Epics) .ThenInclude(e => e.Stories.Where(s => s.Id == storyId)) .ThenInclude(s => s.Tasks) .Where(p => p.Epics.Any(e => e.Stories.Any(s => s.Id == storyId))) .FirstOrDefaultAsync(cancellationToken); } public async Task GetProjectWithTaskAsync(TaskId taskId, CancellationToken cancellationToken = default) { // Load the Epic and Story containing the Task, with that Task return await _context.Projects .Include(p => p.Epics) .ThenInclude(e => e.Stories) .ThenInclude(s => s.Tasks.Where(t => t.Id == taskId)) .Where(p => p.Epics.Any(e => e.Stories.Any(s => s.Tasks.Any(t => t.Id == taskId)))) .FirstOrDefaultAsync(cancellationToken); } public async Task GetProjectWithEpicsAsync(ProjectId projectId, CancellationToken cancellationToken = default) { // Load Project with all Epics, but without Stories and Tasks return await _context.Projects .Include(p => p.Epics) .FirstOrDefaultAsync(p => p.Id == projectId, cancellationToken); } // ========== Read-Only Queries (for Query Handlers - AsNoTracking) ========== public async Task GetEpicByIdReadOnlyAsync(EpicId epicId, CancellationToken cancellationToken = default) { return await _context.Set() .AsNoTracking() .Include(e => e.Stories) .ThenInclude(s => s.Tasks) .FirstOrDefaultAsync(e => e.Id == epicId, cancellationToken); } public async Task> GetEpicsByProjectIdAsync(ProjectId projectId, CancellationToken cancellationToken = default) { return await _context.Set() .AsNoTracking() .Where(e => e.ProjectId == projectId) .OrderBy(e => e.CreatedAt) .ToListAsync(cancellationToken); } public async Task GetStoryByIdReadOnlyAsync(StoryId storyId, CancellationToken cancellationToken = default) { return await _context.Set() .AsNoTracking() .Include(s => s.Tasks) .FirstOrDefaultAsync(s => s.Id == storyId, cancellationToken); } public async Task> GetStoriesByEpicIdAsync(EpicId epicId, CancellationToken cancellationToken = default) { return await _context.Set() .AsNoTracking() .Include(s => s.Tasks) .Where(s => s.EpicId == epicId) .OrderBy(s => s.CreatedAt) .ToListAsync(cancellationToken); } public async Task GetTaskByIdReadOnlyAsync(TaskId taskId, CancellationToken cancellationToken = default) { return await _context.Set() .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == taskId, cancellationToken); } public async Task> GetTasksByStoryIdAsync(StoryId storyId, CancellationToken cancellationToken = default) { return await _context.Set() .AsNoTracking() .Where(t => t.StoryId == storyId) .OrderBy(t => t.CreatedAt) .ToListAsync(cancellationToken); } public async Task GetProjectByIdReadOnlyAsync(ProjectId projectId, CancellationToken cancellationToken = default) { return await _context.Projects .AsNoTracking() .Include(p => p.Epics) .FirstOrDefaultAsync(p => p.Id == projectId, cancellationToken); } public async Task> GetAllProjectsReadOnlyAsync(CancellationToken cancellationToken = default) { return await _context.Projects .AsNoTracking() .OrderByDescending(p => p.CreatedAt) .ToListAsync(cancellationToken); } public async Task GetProjectWithFullHierarchyReadOnlyAsync(ProjectId projectId, CancellationToken cancellationToken = default) { return await _context.Projects .AsNoTracking() .Include(p => p.Epics) .ThenInclude(e => e.Stories) .ThenInclude(s => s.Tasks) .FirstOrDefaultAsync(p => p.Id == projectId, cancellationToken); } }