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);
}
}