From be72afeb147c5ea088f5391da4abb4f5cbc55b14 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Nov 2025 01:12:02 +0000 Subject: [PATCH 1/4] feat: Add backend foundation with clean architecture - Set up .NET 8 solution with 4 projects (Domain, Application, Infrastructure, Api) - Implement Domain layer: * Core entities: Board, Column, Card, Label, CardLabel * Domain exceptions and error codes * Base Entity class with audit fields * Result pattern for error handling - Implement Application layer: * Repository interfaces (IUnitOfWork pattern) * DTOs for all entities * Service layer with business logic (BoardService, ColumnService, CardService, LabelService) * WIP limit enforcement in CardService - Implement Infrastructure layer: * EF Core DbContext with SQLite support * Entity configurations with Fluent API * Proper relationships and cascade rules - Add comprehensive domain tests: * BoardTests, ColumnTests, CardTests, LabelTests * Test domain invariants and validation rules All layers follow clean architecture principles with proper dependency flow. --- backend/Taskdeck.sln | 61 +++++ backend/src/Taskdeck.Api/Taskdeck.Api.csproj | 22 ++ .../src/Taskdeck.Application/DTOs/BoardDto.cs | 31 +++ .../src/Taskdeck.Application/DTOs/CardDto.cs | 39 ++++ .../Taskdeck.Application/DTOs/ColumnDto.cs | 25 +++ .../src/Taskdeck.Application/DTOs/LabelDto.cs | 21 ++ .../Interfaces/IBoardRepository.cs | 9 + .../Interfaces/ICardRepository.cs | 11 + .../Interfaces/IColumnRepository.cs | 9 + .../Interfaces/ILabelRepository.cs | 8 + .../Interfaces/IRepository.cs | 10 + .../Interfaces/IUnitOfWork.cs | 14 ++ .../Services/BoardService.cs | 135 ++++++++++++ .../Services/CardService.cs | 208 ++++++++++++++++++ .../Services/ColumnService.cs | 100 +++++++++ .../Services/LabelService.cs | 86 ++++++++ .../Taskdeck.Application.csproj | 17 ++ backend/src/Taskdeck.Domain/Common/Entity.cs | 62 ++++++ backend/src/Taskdeck.Domain/Common/Result.cs | 31 +++ backend/src/Taskdeck.Domain/Entities/Board.cs | 97 ++++++++ backend/src/Taskdeck.Domain/Entities/Card.cs | 117 ++++++++++ .../src/Taskdeck.Domain/Entities/CardLabel.cs | 18 ++ .../src/Taskdeck.Domain/Entities/Column.cs | 104 +++++++++ backend/src/Taskdeck.Domain/Entities/Label.cs | 86 ++++++++ .../Exceptions/DomainException.cs | 26 +++ .../Taskdeck.Domain/Taskdeck.Domain.csproj | 9 + .../Configurations/BoardConfiguration.cs | 49 +++++ .../Configurations/CardConfiguration.cs | 48 ++++ .../Configurations/CardLabelConfiguration.cs | 25 +++ .../Configurations/ColumnConfiguration.cs | 42 ++++ .../Configurations/LabelConfiguration.cs | 37 ++++ .../Persistence/TaskdeckDbContext.cs | 24 ++ .../Taskdeck.Infrastructure.csproj | 23 ++ .../Taskdeck.Application.Tests.csproj | 30 +++ .../Entities/BoardTests.cs | 106 +++++++++ .../Entities/CardTests.cs | 155 +++++++++++++ .../Entities/ColumnTests.cs | 117 ++++++++++ .../Entities/LabelTests.cs | 116 ++++++++++ .../Taskdeck.Domain.Tests.csproj | 29 +++ 39 files changed, 2157 insertions(+) create mode 100644 backend/Taskdeck.sln create mode 100644 backend/src/Taskdeck.Api/Taskdeck.Api.csproj create mode 100644 backend/src/Taskdeck.Application/DTOs/BoardDto.cs create mode 100644 backend/src/Taskdeck.Application/DTOs/CardDto.cs create mode 100644 backend/src/Taskdeck.Application/DTOs/ColumnDto.cs create mode 100644 backend/src/Taskdeck.Application/DTOs/LabelDto.cs create mode 100644 backend/src/Taskdeck.Application/Interfaces/IBoardRepository.cs create mode 100644 backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs create mode 100644 backend/src/Taskdeck.Application/Interfaces/IColumnRepository.cs create mode 100644 backend/src/Taskdeck.Application/Interfaces/ILabelRepository.cs create mode 100644 backend/src/Taskdeck.Application/Interfaces/IRepository.cs create mode 100644 backend/src/Taskdeck.Application/Interfaces/IUnitOfWork.cs create mode 100644 backend/src/Taskdeck.Application/Services/BoardService.cs create mode 100644 backend/src/Taskdeck.Application/Services/CardService.cs create mode 100644 backend/src/Taskdeck.Application/Services/ColumnService.cs create mode 100644 backend/src/Taskdeck.Application/Services/LabelService.cs create mode 100644 backend/src/Taskdeck.Application/Taskdeck.Application.csproj create mode 100644 backend/src/Taskdeck.Domain/Common/Entity.cs create mode 100644 backend/src/Taskdeck.Domain/Common/Result.cs create mode 100644 backend/src/Taskdeck.Domain/Entities/Board.cs create mode 100644 backend/src/Taskdeck.Domain/Entities/Card.cs create mode 100644 backend/src/Taskdeck.Domain/Entities/CardLabel.cs create mode 100644 backend/src/Taskdeck.Domain/Entities/Column.cs create mode 100644 backend/src/Taskdeck.Domain/Entities/Label.cs create mode 100644 backend/src/Taskdeck.Domain/Exceptions/DomainException.cs create mode 100644 backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj create mode 100644 backend/src/Taskdeck.Infrastructure/Persistence/Configurations/BoardConfiguration.cs create mode 100644 backend/src/Taskdeck.Infrastructure/Persistence/Configurations/CardConfiguration.cs create mode 100644 backend/src/Taskdeck.Infrastructure/Persistence/Configurations/CardLabelConfiguration.cs create mode 100644 backend/src/Taskdeck.Infrastructure/Persistence/Configurations/ColumnConfiguration.cs create mode 100644 backend/src/Taskdeck.Infrastructure/Persistence/Configurations/LabelConfiguration.cs create mode 100644 backend/src/Taskdeck.Infrastructure/Persistence/TaskdeckDbContext.cs create mode 100644 backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj create mode 100644 backend/tests/Taskdeck.Application.Tests/Taskdeck.Application.Tests.csproj create mode 100644 backend/tests/Taskdeck.Domain.Tests/Entities/BoardTests.cs create mode 100644 backend/tests/Taskdeck.Domain.Tests/Entities/CardTests.cs create mode 100644 backend/tests/Taskdeck.Domain.Tests/Entities/ColumnTests.cs create mode 100644 backend/tests/Taskdeck.Domain.Tests/Entities/LabelTests.cs create mode 100644 backend/tests/Taskdeck.Domain.Tests/Taskdeck.Domain.Tests.csproj diff --git a/backend/Taskdeck.sln b/backend/Taskdeck.sln new file mode 100644 index 000000000..7f1410f1f --- /dev/null +++ b/backend/Taskdeck.sln @@ -0,0 +1,61 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9B8F9CAD-2E3F-5G4B-AC6D-7E8F9GA1C2D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Domain", "src\Taskdeck.Domain\Taskdeck.Domain.csproj", "{A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Application", "src\Taskdeck.Application\Taskdeck.Application.csproj", "{B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Infrastructure", "src\Taskdeck.Infrastructure\Taskdeck.Infrastructure.csproj", "{C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Api", "src\Taskdeck.Api\Taskdeck.Api.csproj", "{D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Domain.Tests", "tests\Taskdeck.Domain.Tests\Taskdeck.Domain.Tests.csproj", "{E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Application.Tests", "tests\Taskdeck.Application.Tests\Taskdeck.Application.Tests.csproj", "{F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Release|Any CPU.Build.0 = Release|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B} = {9B8F9CAD-2E3F-5G4B-AC6D-7E8F9GA1C2D} + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C} = {9B8F9CAD-2E3F-5G4B-AC6D-7E8F9GA1C2D} + EndGlobalSection +EndGlobal diff --git a/backend/src/Taskdeck.Api/Taskdeck.Api.csproj b/backend/src/Taskdeck.Api/Taskdeck.Api.csproj new file mode 100644 index 000000000..2418d6fbd --- /dev/null +++ b/backend/src/Taskdeck.Api/Taskdeck.Api.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/backend/src/Taskdeck.Application/DTOs/BoardDto.cs b/backend/src/Taskdeck.Application/DTOs/BoardDto.cs new file mode 100644 index 000000000..967be7f87 --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/BoardDto.cs @@ -0,0 +1,31 @@ +namespace Taskdeck.Application.DTOs; + +public record BoardDto( + Guid Id, + string Name, + string? Description, + bool IsArchived, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record BoardDetailDto( + Guid Id, + string Name, + string? Description, + bool IsArchived, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt, + List Columns +); + +public record CreateBoardDto( + string Name, + string? Description +); + +public record UpdateBoardDto( + string? Name, + string? Description, + bool? IsArchived +); diff --git a/backend/src/Taskdeck.Application/DTOs/CardDto.cs b/backend/src/Taskdeck.Application/DTOs/CardDto.cs new file mode 100644 index 000000000..ba48ed69a --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/CardDto.cs @@ -0,0 +1,39 @@ +namespace Taskdeck.Application.DTOs; + +public record CardDto( + Guid Id, + Guid BoardId, + Guid ColumnId, + string Title, + string Description, + DateTimeOffset? DueDate, + bool IsBlocked, + string? BlockReason, + int Position, + List Labels, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record CreateCardDto( + Guid BoardId, + Guid ColumnId, + string Title, + string? Description, + DateTimeOffset? DueDate, + List? LabelIds +); + +public record UpdateCardDto( + string? Title, + string? Description, + DateTimeOffset? DueDate, + bool? IsBlocked, + string? BlockReason, + List? LabelIds +); + +public record MoveCardDto( + Guid TargetColumnId, + int TargetPosition +); diff --git a/backend/src/Taskdeck.Application/DTOs/ColumnDto.cs b/backend/src/Taskdeck.Application/DTOs/ColumnDto.cs new file mode 100644 index 000000000..3627e9a82 --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/ColumnDto.cs @@ -0,0 +1,25 @@ +namespace Taskdeck.Application.DTOs; + +public record ColumnDto( + Guid Id, + Guid BoardId, + string Name, + int Position, + int? WipLimit, + int CardCount, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record CreateColumnDto( + Guid BoardId, + string Name, + int? Position, + int? WipLimit +); + +public record UpdateColumnDto( + string? Name, + int? Position, + int? WipLimit +); diff --git a/backend/src/Taskdeck.Application/DTOs/LabelDto.cs b/backend/src/Taskdeck.Application/DTOs/LabelDto.cs new file mode 100644 index 000000000..9d9ef0ddc --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/LabelDto.cs @@ -0,0 +1,21 @@ +namespace Taskdeck.Application.DTOs; + +public record LabelDto( + Guid Id, + Guid BoardId, + string Name, + string ColorHex, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record CreateLabelDto( + Guid BoardId, + string Name, + string ColorHex +); + +public record UpdateLabelDto( + string? Name, + string? ColorHex +); diff --git a/backend/src/Taskdeck.Application/Interfaces/IBoardRepository.cs b/backend/src/Taskdeck.Application/Interfaces/IBoardRepository.cs new file mode 100644 index 000000000..136e76f01 --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/IBoardRepository.cs @@ -0,0 +1,9 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface IBoardRepository : IRepository +{ + Task> SearchAsync(string? searchText, bool includeArchived, CancellationToken cancellationToken = default); + Task GetByIdWithDetailsAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs b/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs new file mode 100644 index 000000000..fdfe75a3b --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs @@ -0,0 +1,11 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface ICardRepository : IRepository +{ + Task> GetByBoardIdAsync(Guid boardId, CancellationToken cancellationToken = default); + Task> GetByColumnIdAsync(Guid columnId, CancellationToken cancellationToken = default); + Task> SearchAsync(Guid boardId, string? searchText, Guid? labelId, Guid? columnId, CancellationToken cancellationToken = default); + Task GetByIdWithLabelsAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Taskdeck.Application/Interfaces/IColumnRepository.cs b/backend/src/Taskdeck.Application/Interfaces/IColumnRepository.cs new file mode 100644 index 000000000..3da06ca67 --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/IColumnRepository.cs @@ -0,0 +1,9 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface IColumnRepository : IRepository +{ + Task> GetByBoardIdAsync(Guid boardId, CancellationToken cancellationToken = default); + Task GetByIdWithCardsAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Taskdeck.Application/Interfaces/ILabelRepository.cs b/backend/src/Taskdeck.Application/Interfaces/ILabelRepository.cs new file mode 100644 index 000000000..e8419434b --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/ILabelRepository.cs @@ -0,0 +1,8 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface ILabelRepository : IRepository