--- task_id: sprint_2_story_3_task_2 story: sprint_2_story_3 status: not_started estimated_hours: 4 created_date: 2025-11-05 assignee: Backend Team --- # Task 2: Implement Sprint Repository and EF Core Configuration **Story**: Story 3 - Sprint Management Module **Estimated**: 4 hours ## Description Create the Sprint repository pattern implementation with EF Core configuration, including multi-tenant isolation, database migration, and indexes for query optimization. ## Acceptance Criteria - [ ] ISprintRepository interface defined - [ ] SprintRepository implementation with multi-tenant filtering - [ ] EF Core entity configuration created - [ ] Database migration generated and applied - [ ] Indexes created for performance - [ ] Unit tests for repository methods ## Implementation Details **Files to Create**: 1. **Repository Interface**: `colaflow-api/src/ColaFlow.Domain/Repositories/ISprintRepository.cs` ```csharp public interface ISprintRepository { Task GetByIdAsync(Guid sprintId, CancellationToken cancellationToken = default); Task> GetByProjectIdAsync(Guid projectId, CancellationToken cancellationToken = default); Task> GetActiveSprintsAsync(CancellationToken cancellationToken = default); Task GetActiveSprintForProjectAsync(Guid projectId, CancellationToken cancellationToken = default); Task AddAsync(Sprint sprint, CancellationToken cancellationToken = default); void Update(Sprint sprint); void Delete(Sprint sprint); Task SaveChangesAsync(CancellationToken cancellationToken = default); } ``` 2. **Repository Implementation**: `colaflow-api/src/ColaFlow.Infrastructure/Repositories/SprintRepository.cs` ```csharp public class SprintRepository : ISprintRepository { private readonly ColaFlowDbContext _context; private readonly ITenantContext _tenantContext; public SprintRepository(ColaFlowDbContext context, ITenantContext tenantContext) { _context = context; _tenantContext = tenantContext; } public async Task GetByIdAsync(Guid sprintId, CancellationToken cancellationToken = default) { var tenantId = _tenantContext.TenantId; return await _context.Sprints .Include(s => s.Project) .Include(s => s.Tasks) .FirstOrDefaultAsync(s => s.TenantId == tenantId && s.Id == sprintId, cancellationToken); } public async Task> GetByProjectIdAsync(Guid projectId, CancellationToken cancellationToken = default) { var tenantId = _tenantContext.TenantId; return await _context.Sprints .Include(s => s.Tasks) .Where(s => s.TenantId == tenantId && s.ProjectId == projectId) .OrderByDescending(s => s.StartDate) .AsNoTracking() .ToListAsync(cancellationToken); } public async Task> GetActiveSprintsAsync(CancellationToken cancellationToken = default) { var tenantId = _tenantContext.TenantId; return await _context.Sprints .Include(s => s.Project) .Include(s => s.Tasks) .Where(s => s.TenantId == tenantId && s.Status == SprintStatus.Active) .AsNoTracking() .ToListAsync(cancellationToken); } public async Task GetActiveSprintForProjectAsync(Guid projectId, CancellationToken cancellationToken = default) { var tenantId = _tenantContext.TenantId; return await _context.Sprints .Include(s => s.Tasks) .FirstOrDefaultAsync(s => s.TenantId == tenantId && s.ProjectId == projectId && s.Status == SprintStatus.Active, cancellationToken); } public async Task AddAsync(Sprint sprint, CancellationToken cancellationToken = default) { await _context.Sprints.AddAsync(sprint, cancellationToken); } public void Update(Sprint sprint) { _context.Sprints.Update(sprint); } public void Delete(Sprint sprint) { _context.Sprints.Remove(sprint); } public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { return await _context.SaveChangesAsync(cancellationToken); } } ``` 3. **EF Core Configuration**: `colaflow-api/src/ColaFlow.Infrastructure/Data/Configurations/SprintConfiguration.cs` ```csharp public class SprintConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Sprints"); builder.HasKey(s => s.Id); // Properties builder.Property(s => s.Name) .IsRequired() .HasMaxLength(100); builder.Property(s => s.Goal) .HasMaxLength(500); builder.Property(s => s.StartDate) .IsRequired(); builder.Property(s => s.EndDate) .IsRequired(); builder.Property(s => s.Status) .IsRequired() .HasConversion(); // Store as string for readability builder.Property(s => s.TenantId) .IsRequired(); builder.Property(s => s.CreatedAt) .IsRequired(); builder.Property(s => s.UpdatedAt); // Relationships builder.HasOne(s => s.Project) .WithMany() .HasForeignKey(s => s.ProjectId) .OnDelete(DeleteBehavior.Cascade); builder.HasMany(s => s.Tasks) .WithOne() .HasForeignKey("SprintId") .OnDelete(DeleteBehavior.SetNull); // Indexes builder.HasIndex(s => new { s.TenantId, s.ProjectId }); builder.HasIndex(s => new { s.TenantId, s.Status }); builder.HasIndex(s => s.StartDate); // Multi-tenant global query filter builder.HasQueryFilter(s => s.TenantId == Guid.Empty); // Replaced at runtime by TenantContext // Ignore domain events (not persisted) builder.Ignore(s => s.DomainEvents); } } ``` 4. **Database Migration**: Create migration ```bash cd colaflow-api/src/ColaFlow.Infrastructure dotnet ef migrations add AddSprintEntity --project ../ColaFlow.Infrastructure --startup-project ../../ColaFlow.API ``` **Migration will create**: ```sql CREATE TABLE Sprints ( Id UUID PRIMARY KEY, TenantId UUID NOT NULL, ProjectId UUID NOT NULL, Name VARCHAR(100) NOT NULL, Goal VARCHAR(500), StartDate TIMESTAMP NOT NULL, EndDate TIMESTAMP NOT NULL, Status VARCHAR(20) NOT NULL, CreatedAt TIMESTAMP NOT NULL, UpdatedAt TIMESTAMP, CONSTRAINT FK_Sprints_Projects FOREIGN KEY (ProjectId) REFERENCES Projects(Id) ON DELETE CASCADE, CONSTRAINT FK_Sprints_Tenants FOREIGN KEY (TenantId) REFERENCES Tenants(Id) ON DELETE CASCADE ); CREATE INDEX IX_Sprints_TenantId_ProjectId ON Sprints(TenantId, ProjectId); CREATE INDEX IX_Sprints_TenantId_Status ON Sprints(TenantId, Status); CREATE INDEX IX_Sprints_StartDate ON Sprints(StartDate); ``` 5. **Update WorkTask**: Add SprintId foreign key ```csharp // In WorkTask entity public Guid? SprintId { get; private set; } public void AssignToSprint(Guid sprintId) { SprintId = sprintId; } public void RemoveFromSprint() { SprintId = null; } ``` 6. **DI Registration**: Update `colaflow-api/src/ColaFlow.Infrastructure/DependencyInjection.cs` ```csharp services.AddScoped(); ``` ## Technical Notes - Use composite indexes for multi-tenant queries - Store Status as string for readability in database - Use `AsNoTracking()` for read-only queries (performance) - Global query filter enforces multi-tenant isolation - Tasks relationship uses SetNull (tasks remain when sprint deleted) ## Testing **Unit Tests**: `colaflow-api/tests/ColaFlow.Infrastructure.Tests/Repositories/SprintRepositoryTests.cs` ```csharp public class SprintRepositoryTests { [Fact] public async Task GetByIdAsync_ShouldReturnSprint_WhenExists() { // Arrange var (context, tenantContext, repository) = CreateTestContext(); var sprint = CreateTestSprint(tenantContext.TenantId); await context.Sprints.AddAsync(sprint); await context.SaveChangesAsync(); // Act var result = await repository.GetByIdAsync(sprint.Id); // Assert Assert.NotNull(result); Assert.Equal(sprint.Id, result.Id); } [Fact] public async Task GetByProjectIdAsync_ShouldReturnOnlyCurrentTenantSprints() { // Test multi-tenant isolation // ... } } ``` --- **Created**: 2025-11-05 by Backend Agent