Day 12 implementation - Complete CRUD operations with tenant isolation and SignalR integration.
**Domain Layer**:
- Added TenantId value object for strong typing
- Updated Project entity to include TenantId field
- Modified Project.Create factory method to require tenantId parameter
- Updated ProjectCreatedEvent to include TenantId
**Application Layer**:
- Created UpdateProjectCommand, Handler, and Validator for project updates
- Created ArchiveProjectCommand, Handler, and Validator for archiving projects
- Updated CreateProjectCommand to include TenantId
- Modified CreateProjectCommandValidator to remove OwnerId validation (set from JWT)
- Created IProjectNotificationService interface for SignalR abstraction
- Implemented ProjectCreatedEventHandler with SignalR notifications
- Implemented ProjectUpdatedEventHandler with SignalR notifications
- Implemented ProjectArchivedEventHandler with SignalR notifications
**Infrastructure Layer**:
- Updated PMDbContext to inject IHttpContextAccessor
- Configured Global Query Filter for automatic tenant isolation
- Added TenantId property mapping in ProjectConfiguration
- Created TenantId index for query performance
**API Layer**:
- Updated ProjectsController with [Authorize] attribute
- Implemented PUT /api/v1/projects/{id} for updates
- Implemented DELETE /api/v1/projects/{id} for archiving
- Added helper methods to extract TenantId and UserId from JWT claims
- Extended IRealtimeNotificationService with Project-specific methods
- Implemented RealtimeNotificationService with tenant-aware SignalR groups
- Created ProjectNotificationServiceAdapter to bridge layers
- Registered IProjectNotificationService in Program.cs
**Features Implemented**:
- Complete CRUD operations (Create, Read, Update, Archive)
- Multi-tenant isolation via EF Core Global Query Filter
- JWT-based authorization on all endpoints
- SignalR real-time notifications for all Project events
- Clean Architecture with proper layer separation
- Domain Event pattern with MediatR
**Database Migration**:
- Migration created (not applied yet): AddTenantIdToProject
**Test Scripts**:
- Created comprehensive test scripts (test-project-simple.ps1)
- Tests cover full CRUD lifecycle and tenant isolation
**Note**: API hot reload required to apply CreateProjectCommandValidator fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
93 lines
2.9 KiB
C#
93 lines
2.9 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 Project aggregate root
|
|
/// </summary>
|
|
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
|
{
|
|
public void Configure(EntityTypeBuilder<Project> builder)
|
|
{
|
|
builder.ToTable("Projects");
|
|
|
|
// Primary key
|
|
builder.HasKey(p => p.Id);
|
|
|
|
// Id conversion (StronglyTypedId to Guid)
|
|
builder.Property(p => p.Id)
|
|
.HasConversion(
|
|
id => id.Value,
|
|
value => ProjectId.From(value))
|
|
.IsRequired()
|
|
.ValueGeneratedNever();
|
|
|
|
// TenantId conversion
|
|
builder.Property(p => p.TenantId)
|
|
.HasConversion(
|
|
id => id.Value,
|
|
value => TenantId.From(value))
|
|
.IsRequired();
|
|
|
|
// Basic properties
|
|
builder.Property(p => p.Name)
|
|
.HasMaxLength(200)
|
|
.IsRequired();
|
|
|
|
builder.Property(p => p.Description)
|
|
.HasMaxLength(2000);
|
|
|
|
// ProjectKey as owned value object
|
|
builder.OwnsOne(p => p.Key, kb =>
|
|
{
|
|
kb.Property(k => k.Value)
|
|
.HasColumnName("Key")
|
|
.HasMaxLength(20)
|
|
.IsRequired();
|
|
|
|
kb.HasIndex(k => k.Value).IsUnique();
|
|
});
|
|
|
|
// Status enumeration (stored as string)
|
|
builder.Property(p => p.Status)
|
|
.HasConversion(
|
|
s => s.Name,
|
|
name => Enumeration.FromDisplayName<ProjectStatus>(name))
|
|
.HasMaxLength(50)
|
|
.IsRequired();
|
|
|
|
// OwnerId conversion
|
|
builder.Property(p => p.OwnerId)
|
|
.HasConversion(
|
|
id => id.Value,
|
|
value => UserId.From(value))
|
|
.IsRequired();
|
|
|
|
// Timestamps
|
|
builder.Property(p => p.CreatedAt)
|
|
.IsRequired();
|
|
|
|
builder.Property(p => p.UpdatedAt);
|
|
|
|
// Relationships - Epics collection (owned by aggregate)
|
|
// Configure the one-to-many relationship with Epic
|
|
// Use string-based FK name because ProjectId is a value object with conversion configured in EpicConfiguration
|
|
builder.HasMany<Epic>("Epics")
|
|
.WithOne()
|
|
.HasForeignKey("ProjectId")
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
// Indexes for performance
|
|
builder.HasIndex(p => p.CreatedAt);
|
|
builder.HasIndex(p => p.OwnerId);
|
|
builder.HasIndex(p => p.TenantId);
|
|
|
|
// Ignore DomainEvents (handled separately)
|
|
builder.Ignore(p => p.DomainEvents);
|
|
}
|
|
}
|