Files
Yaojia Wang 12a4248430 feat(backend): Day 15 Task 1&2 - Add TenantId to Epic/Story/WorkTask and implement TenantContext
This commit completes Day 15's primary objectives:
1. Database Migration - Add TenantId columns to Epic, Story, and WorkTask entities
2. TenantContext Service - Implement tenant context retrieval from JWT claims

Changes:
- Added TenantId property to Epic, Story, and WorkTask domain entities
- Updated entity factory methods to require TenantId parameter
- Modified Project.CreateEpic to pass TenantId from parent aggregate
- Modified Epic.CreateStory and Story.CreateTask to propagate TenantId
- Added EF Core configurations for TenantId mapping with proper indexes
- Created EF Core migration: AddTenantIdToEpicStoryTask
  * Adds tenant_id columns to Epics, Stories, and Tasks tables
  * Creates indexes: ix_epics_tenant_id, ix_stories_tenant_id, ix_tasks_tenant_id
  * Uses default Guid.Empty for existing data (backward compatible)
- Implemented ITenantContext interface in Application layer
- Implemented TenantContext service in Infrastructure layer
  * Retrieves tenant ID from JWT claims (tenant_id or tenantId)
  * Throws UnauthorizedAccessException if tenant context unavailable
- Registered TenantContext as scoped service in DI container
- Added Global Query Filters for Epic, Story, and WorkTask entities
  * Ensures automatic tenant isolation at database query level
  * Prevents cross-tenant data access

Architecture:
- Follows the same pattern as Issue Management Module (Day 14)
- Maintains consistency with Project entity multi-tenancy implementation
- Ensures data isolation through both domain logic and database filters

Note: Unit tests require updates to pass TenantId parameter - will be addressed in follow-up commits

Reference: Day 15 roadmap (DAY15-22-PROJECTMANAGEMENT-ROADMAP.md)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 16:44:09 +01:00

99 lines
3.0 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Shared.Kernel.Common;
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Configurations;
/// <summary>
/// Entity configuration for WorkTask entity
/// </summary>
public class WorkTaskConfiguration : IEntityTypeConfiguration<WorkTask>
{
public void Configure(EntityTypeBuilder<WorkTask> builder)
{
builder.ToTable("Tasks");
// Primary key
builder.HasKey("Id");
// Id conversion
builder.Property(t => t.Id)
.HasConversion(
id => id.Value,
value => TaskId.From(value))
.IsRequired()
.ValueGeneratedNever();
// TenantId (required for multi-tenancy)
builder.Property(t => t.TenantId)
.HasConversion(
id => id.Value,
value => TenantId.From(value))
.IsRequired()
.HasColumnName("tenant_id");
// StoryId (foreign key)
builder.Property(t => t.StoryId)
.HasConversion(
id => id.Value,
value => StoryId.From(value))
.IsRequired();
// Basic properties
builder.Property(t => t.Title)
.HasMaxLength(200)
.IsRequired();
builder.Property(t => t.Description)
.HasMaxLength(4000);
// Status enumeration
builder.Property(t => t.Status)
.HasConversion(
s => s.Name,
name => Enumeration.FromDisplayName<WorkItemStatus>(name))
.HasMaxLength(50)
.IsRequired();
// Priority enumeration
builder.Property(t => t.Priority)
.HasConversion(
p => p.Name,
name => Enumeration.FromDisplayName<TaskPriority>(name))
.HasMaxLength(50)
.IsRequired();
// CreatedBy conversion
builder.Property(t => t.CreatedBy)
.HasConversion(
id => id.Value,
value => UserId.From(value))
.IsRequired();
// AssigneeId (optional)
builder.Property(t => t.AssigneeId)
.HasConversion(
id => id != null ? id.Value : (Guid?)null,
value => value.HasValue ? UserId.From(value.Value) : null);
// Effort tracking
builder.Property(t => t.EstimatedHours);
builder.Property(t => t.ActualHours);
// Timestamps
builder.Property(t => t.CreatedAt)
.IsRequired();
builder.Property(t => t.UpdatedAt);
// Indexes
builder.HasIndex(t => t.TenantId)
.HasDatabaseName("ix_tasks_tenant_id");
builder.HasIndex(t => t.StoryId);
builder.HasIndex(t => t.AssigneeId);
builder.HasIndex(t => t.CreatedAt);
}
}