Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
43
colaflow-api/.dockerignore
Normal file
43
colaflow-api/.dockerignore
Normal file
@@ -0,0 +1,43 @@
|
||||
# .dockerignore for ColaFlow API
|
||||
|
||||
# Binaries
|
||||
**/bin/
|
||||
**/obj/
|
||||
|
||||
# Visual Studio / Rider
|
||||
.vs/
|
||||
.idea/
|
||||
*.user
|
||||
*.suo
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Test results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.trx
|
||||
*.coverage
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
packages/
|
||||
.nuget/
|
||||
|
||||
# Others
|
||||
*.log
|
||||
*.bak
|
||||
*.tmp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
65
colaflow-api/.gitignore
vendored
Normal file
65
colaflow-api/.gitignore
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# .NET Core
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
*.suo
|
||||
*.cache
|
||||
.vs/
|
||||
.idea/
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Test results
|
||||
TestResults/
|
||||
*.trx
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
coverage/
|
||||
coveragereport/
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
packages/
|
||||
.nuget/
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# Visual Studio
|
||||
.vs/
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Others
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
*.tmp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# App settings (sensitive)
|
||||
appsettings.*.json
|
||||
!appsettings.json
|
||||
!appsettings.Development.json
|
||||
230
colaflow-api/ColaFlow.sln
Normal file
230
colaflow-api/ColaFlow.sln
Normal file
@@ -0,0 +1,230 @@
|
||||
|
||||
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", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Domain", "src\ColaFlow.Domain\ColaFlow.Domain.csproj", "{0F399DDB-4292-4527-B2F0-2252516F7615}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Application", "src\ColaFlow.Application\ColaFlow.Application.csproj", "{6ECE123E-3FD9-4146-B44E-B1332FAFC010}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Infrastructure", "src\ColaFlow.Infrastructure\ColaFlow.Infrastructure.csproj", "{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.API", "src\ColaFlow.API\ColaFlow.API.csproj", "{AED08D6B-D0A2-4B67-BF43-D8244C424145}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Domain.Tests", "tests\ColaFlow.Domain.Tests\ColaFlow.Domain.Tests.csproj", "{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Application.Tests", "tests\ColaFlow.Application.Tests\ColaFlow.Application.Tests.csproj", "{73C1CF97-527D-427B-842B-C4CBED3429B5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.IntegrationTests", "tests\ColaFlow.IntegrationTests\ColaFlow.IntegrationTests.csproj", "{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{C8E42992-5E42-0C2B-DBFE-AA848D06431C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Shared.Kernel", "src\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj", "{EAF2C884-939C-428D-981F-CDABE5D42852}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProjectManagement", "ProjectManagement", "{CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.ProjectManagement.Domain", "src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj", "{1D172B0D-9D60-4366-999B-E2D186B55D46}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.ProjectManagement.Application", "src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Application\ColaFlow.Modules.ProjectManagement.Application.csproj", "{95343C64-EF22-40D0-ABA6-489CE65AF11F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.ProjectManagement.Infrastructure", "src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Infrastructure\ColaFlow.Modules.ProjectManagement.Infrastructure.csproj", "{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.ProjectManagement.Contracts", "src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Contracts\ColaFlow.Modules.ProjectManagement.Contracts.csproj", "{EF0BCA60-10E6-48AF-807D-416D262B85E3}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.ArchitectureTests", "tests\ColaFlow.ArchitectureTests\ColaFlow.ArchitectureTests.csproj", "{A059FDA9-5454-49A8-A025-0FC5130574EE}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145}.Release|x86.Build.0 = Release|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46}.Release|x86.Build.0 = Release|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Release|x64.Build.0 = Release|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0F399DDB-4292-4527-B2F0-2252516F7615} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{6ECE123E-3FD9-4146-B44E-B1332FAFC010} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{D6E0C1D8-CAA7-4F95-88E1-C253B0390494} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{AED08D6B-D0A2-4B67-BF43-D8244C424145} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{931322BD-B4BD-436A-BEE8-FCF95FF4A09E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{73C1CF97-527D-427B-842B-C4CBED3429B5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{614DB4A0-24C4-457F-82BB-CE077BCA6E4E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{C8E42992-5E42-0C2B-DBFE-AA848D06431C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{EAF2C884-939C-428D-981F-CDABE5D42852} = {C8E42992-5E42-0C2B-DBFE-AA848D06431C}
|
||||
{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
|
||||
{1D172B0D-9D60-4366-999B-E2D186B55D46} = {CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4}
|
||||
{95343C64-EF22-40D0-ABA6-489CE65AF11F} = {CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4}
|
||||
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29} = {CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4}
|
||||
{EF0BCA60-10E6-48AF-807D-416D262B85E3} = {CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4}
|
||||
{A059FDA9-5454-49A8-A025-0FC5130574EE} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
50
colaflow-api/Dockerfile
Normal file
50
colaflow-api/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# ColaFlow API Dockerfile
|
||||
# Multi-stage build for .NET 9 application
|
||||
|
||||
# Stage 1: Build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy solution and project files
|
||||
COPY ColaFlow.sln .
|
||||
COPY src/ColaFlow.Domain/ColaFlow.Domain.csproj src/ColaFlow.Domain/
|
||||
COPY src/ColaFlow.Application/ColaFlow.Application.csproj src/ColaFlow.Application/
|
||||
COPY src/ColaFlow.Infrastructure/ColaFlow.Infrastructure.csproj src/ColaFlow.Infrastructure/
|
||||
COPY src/ColaFlow.API/ColaFlow.API.csproj src/ColaFlow.API/
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore
|
||||
|
||||
# Copy all source files
|
||||
COPY src/ src/
|
||||
|
||||
# Build the application
|
||||
WORKDIR /src/src/ColaFlow.API
|
||||
RUN dotnet build -c Release -o /app/build --no-restore
|
||||
|
||||
# Stage 2: Publish
|
||||
FROM build AS publish
|
||||
RUN dotnet publish -c Release -o /app/publish --no-restore
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Install curl for healthcheck
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy published files
|
||||
COPY --from=publish /app/publish .
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8080 8081
|
||||
|
||||
# Set environment
|
||||
ENV ASPNETCORE_URLS=http://+:8080;https://+:8081
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# Entry point
|
||||
ENTRYPOINT ["dotnet", "ColaFlow.API.dll"]
|
||||
477
colaflow-api/README.md
Normal file
477
colaflow-api/README.md
Normal file
@@ -0,0 +1,477 @@
|
||||
# ColaFlow API
|
||||
|
||||
ColaFlow 后端 API 服务 - 基于 .NET 9 的 **Modular Monolith + Clean Architecture + DDD + CQRS** 实现。
|
||||
|
||||
## 架构亮点
|
||||
|
||||
- **Modular Monolith Architecture** - 模块化单体架构,清晰的模块边界
|
||||
- **Clean Architecture** - 四层架构设计(Domain → Application → Infrastructure → API)
|
||||
- **Domain-Driven Design (DDD)** - 领域驱动设计(战术模式)
|
||||
- **CQRS** - 命令查询职责分离(MediatR)
|
||||
- **Event Sourcing** - 事件溯源(用于审计日志)
|
||||
- **Architecture Testing** - 自动化架构测试(NetArchTest)
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **.NET 9** - 最新的 .NET 平台
|
||||
- **Entity Framework Core 9** - ORM
|
||||
- **PostgreSQL 16+** - 主数据库
|
||||
- **Redis 7+** - 缓存和会话管理
|
||||
- **MediatR** - 中介者模式(CQRS 和模块间通信)
|
||||
- **xUnit** - 单元测试框架
|
||||
- **NetArchTest.Rules** - 架构测试
|
||||
- **Testcontainers** - 集成测试
|
||||
|
||||
## 项目结构(模块化单体)
|
||||
|
||||
```
|
||||
colaflow-api/
|
||||
├── src/
|
||||
│ ├── ColaFlow.API/ # API 层(统一入口)
|
||||
│ │ └── Program.cs # 模块注册和启动
|
||||
│ │
|
||||
│ ├── Modules/ # 业务模块
|
||||
│ │ ├── ProjectManagement/ # 项目管理模块
|
||||
│ │ │ ├── ColaFlow.Modules.PM.Domain/
|
||||
│ │ │ │ ├── Aggregates/ # Project, Epic, Story, Task
|
||||
│ │ │ │ ├── ValueObjects/ # ProjectId, ProjectKey, etc.
|
||||
│ │ │ │ ├── Events/ # Domain Events
|
||||
│ │ │ │ └── Exceptions/ # Domain Exceptions
|
||||
│ │ │ ├── ColaFlow.Modules.PM.Application/
|
||||
│ │ │ │ ├── Commands/ # CQRS Commands
|
||||
│ │ │ │ ├── Queries/ # CQRS Queries
|
||||
│ │ │ │ └── DTOs/ # Data Transfer Objects
|
||||
│ │ │ ├── ColaFlow.Modules.PM.Infrastructure/
|
||||
│ │ │ │ ├── Persistence/ # EF Core Configurations
|
||||
│ │ │ │ └── Repositories/ # Repository Implementations
|
||||
│ │ │ └── ColaFlow.Modules.PM.Contracts/
|
||||
│ │ │ └── Events/ # Integration Events
|
||||
│ │ │
|
||||
│ │ ├── Workflow/ # 工作流模块(待实现)
|
||||
│ │ ├── UserManagement/ # 用户管理模块(待实现)
|
||||
│ │ ├── Notifications/ # 通知模块(待实现)
|
||||
│ │ ├── Audit/ # 审计模块(待实现)
|
||||
│ │ └── AI/ # AI 模块(待实现)
|
||||
│ │
|
||||
│ ├── Shared/ # 共享内核
|
||||
│ │ └── ColaFlow.Shared.Kernel/
|
||||
│ │ ├── Common/ # Entity, ValueObject, AggregateRoot
|
||||
│ │ ├── Events/ # DomainEvent
|
||||
│ │ └── Modules/ # IModule 接口
|
||||
│ │
|
||||
│ └── [Legacy - To be removed] # 旧的单体结构(迁移中)
|
||||
│ ├── ColaFlow.Domain/
|
||||
│ ├── ColaFlow.Application/
|
||||
│ └── ColaFlow.Infrastructure/
|
||||
│
|
||||
├── tests/
|
||||
│ ├── ColaFlow.ArchitectureTests/ # 架构测试(模块边界)
|
||||
│ ├── ColaFlow.Domain.Tests/ # 领域层单元测试
|
||||
│ ├── ColaFlow.Application.Tests/ # 应用层单元测试
|
||||
│ └── ColaFlow.IntegrationTests/ # 集成测试
|
||||
└── ColaFlow.sln
|
||||
```
|
||||
|
||||
## Modular Monolith 架构
|
||||
|
||||
### 模块边界规则
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ ColaFlow.API (Entry Point) │
|
||||
└─────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ PM │ │ Workflow │ ... (其他模块)
|
||||
│ Module │◄─────────►│ Module │
|
||||
└─────────────┘ └─────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Shared.Kernel (Common Base) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 模块通信规则
|
||||
|
||||
1. **禁止直接引用其他模块的 Domain 实体**
|
||||
2. **允许通过 MediatR 查询其他模块**(Application Service Integration)
|
||||
3. **允许通过 Domain Events 解耦通信**(Event-Driven)
|
||||
4. **使用 Contracts 项目定义模块对外接口**
|
||||
|
||||
### 架构测试
|
||||
|
||||
项目包含自动化架构测试,确保模块边界被严格遵守:
|
||||
|
||||
```bash
|
||||
dotnet test tests/ColaFlow.ArchitectureTests
|
||||
```
|
||||
|
||||
测试内容:
|
||||
- Domain 层不依赖 Application 和 Infrastructure
|
||||
- Domain 层只依赖 Shared.Kernel
|
||||
- 模块间不直接引用其他模块的 Domain 实体
|
||||
- AggregateRoot 正确继承
|
||||
- ValueObject 是不可变的(sealed)
|
||||
|
||||
## Clean Architecture 层级依赖
|
||||
|
||||
每个模块内部仍然遵循 Clean Architecture:
|
||||
|
||||
```
|
||||
Module Structure:
|
||||
API/Controllers ──┐
|
||||
├──> Application ──> Domain
|
||||
Infrastructure ───┘
|
||||
```
|
||||
|
||||
**依赖规则:**
|
||||
- **Domain 层**:仅依赖 Shared.Kernel(无其他依赖)
|
||||
- **Application 层**:依赖 Domain 和 Contracts
|
||||
- **Infrastructure 层**:依赖 Domain 和 Application
|
||||
- **API 层**:依赖所有层
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- .NET 9 SDK
|
||||
- Docker Desktop(用于 PostgreSQL 和 Redis)
|
||||
- IDE:Visual Studio 2022 / JetBrains Rider / VS Code
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd colaflow-api
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
### 2. 启动数据库(使用 Docker)
|
||||
|
||||
从项目根目录启动:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
docker-compose up -d postgres redis
|
||||
```
|
||||
|
||||
### 3. 运行数据库迁移
|
||||
|
||||
```bash
|
||||
# 创建迁移(Infrastructure 层完成后)
|
||||
dotnet ef migrations add InitialCreate --project src/ColaFlow.Infrastructure --startup-project src/ColaFlow.API
|
||||
|
||||
# 应用迁移
|
||||
dotnet ef database update --project src/ColaFlow.Infrastructure --startup-project src/ColaFlow.API
|
||||
```
|
||||
|
||||
### 4. 运行 API
|
||||
|
||||
```bash
|
||||
cd src/ColaFlow.API
|
||||
dotnet run
|
||||
```
|
||||
|
||||
API 将运行在:
|
||||
- HTTP: `http://localhost:5000`
|
||||
- HTTPS: `https://localhost:5001`
|
||||
- Swagger/Scalar: `https://localhost:5001/scalar/v1`
|
||||
|
||||
### 5. 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
dotnet test
|
||||
|
||||
# 运行单元测试
|
||||
dotnet test --filter Category=Unit
|
||||
|
||||
# 运行集成测试
|
||||
dotnet test --filter Category=Integration
|
||||
|
||||
# 生成覆盖率报告
|
||||
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
||||
```
|
||||
|
||||
## 开发指南
|
||||
|
||||
### Domain Layer 开发
|
||||
|
||||
**聚合根示例:**
|
||||
|
||||
```csharp
|
||||
public class Project : AggregateRoot
|
||||
{
|
||||
public ProjectId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
// 工厂方法
|
||||
public static Project Create(string name, string description, UserId ownerId)
|
||||
{
|
||||
var project = new Project
|
||||
{
|
||||
Id = ProjectId.Create(),
|
||||
Name = name,
|
||||
OwnerId = ownerId
|
||||
};
|
||||
|
||||
project.AddDomainEvent(new ProjectCreatedEvent(project.Id, project.Name));
|
||||
return project;
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
AddDomainEvent(new ProjectUpdatedEvent(Id));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Application Layer 开发(CQRS)
|
||||
|
||||
**Command 示例:**
|
||||
|
||||
```csharp
|
||||
public sealed record CreateProjectCommand : IRequest<ProjectDto>
|
||||
{
|
||||
public string Name { get; init; }
|
||||
public string Description { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CreateProjectCommandHandler : IRequestHandler<CreateProjectCommand, ProjectDto>
|
||||
{
|
||||
public async Task<ProjectDto> Handle(CreateProjectCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建聚合
|
||||
var project = Project.Create(request.Name, request.Description, currentUserId);
|
||||
|
||||
// 2. 保存
|
||||
await _repository.AddAsync(project, cancellationToken);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 3. 返回 DTO
|
||||
return _mapper.Map<ProjectDto>(project);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Query 示例:**
|
||||
|
||||
```csharp
|
||||
public sealed record GetProjectByIdQuery : IRequest<ProjectDto>
|
||||
{
|
||||
public Guid ProjectId { get; init; }
|
||||
}
|
||||
|
||||
public sealed class GetProjectByIdQueryHandler : IQueryHandler<GetProjectByIdQuery, ProjectDto>
|
||||
{
|
||||
public async Task<ProjectDto> Handle(GetProjectByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var project = await _context.Projects
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p => p.Id == request.ProjectId, cancellationToken);
|
||||
|
||||
return _mapper.Map<ProjectDto>(project);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Layer 开发
|
||||
|
||||
**Controller 示例:**
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
public class ProjectsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ProjectDto), StatusCodes.Status201Created)]
|
||||
public async Task<IActionResult> CreateProject([FromBody] CreateProjectCommand command)
|
||||
{
|
||||
var result = await _mediator.Send(command);
|
||||
return CreatedAtAction(nameof(GetProject), new { id = result.Id }, result);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(ProjectDto), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetProject(Guid id)
|
||||
{
|
||||
var result = await _mediator.Send(new GetProjectByIdQuery { ProjectId = id });
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 测试金字塔
|
||||
|
||||
- **70% 单元测试** - Domain 和 Application 层
|
||||
- **20% 集成测试** - API 端点测试
|
||||
- **10% E2E 测试** - 关键用户流程
|
||||
|
||||
### 单元测试示例
|
||||
|
||||
```csharp
|
||||
public class ProjectTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_WithValidData_ShouldCreateProject()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
var ownerId = UserId.Create();
|
||||
|
||||
// Act
|
||||
var project = Project.Create(name, "Description", ownerId);
|
||||
|
||||
// Assert
|
||||
project.Should().NotBeNull();
|
||||
project.Name.Should().Be(name);
|
||||
project.DomainEvents.Should().ContainSingle(e => e is ProjectCreatedEvent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 集成测试示例
|
||||
|
||||
```csharp
|
||||
public class ProjectsControllerTests : IntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateProject_WithValidData_ShouldReturn201()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateProjectCommand { Name = "Test", Description = "Test" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/projects", command);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var project = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
project.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 代码质量
|
||||
|
||||
### 覆盖率要求
|
||||
|
||||
- **最低要求:80%**
|
||||
- **目标:90%+**
|
||||
- **关键路径:100%**
|
||||
|
||||
### 代码规范
|
||||
|
||||
- 遵循 C# 编码规范
|
||||
- 使用 private 构造函数 + 工厂方法
|
||||
- 所有 public 方法必须有 XML 注释
|
||||
- 所有业务逻辑必须有单元测试
|
||||
|
||||
## NuGet 包版本
|
||||
|
||||
### Domain Layer
|
||||
- 无外部依赖
|
||||
|
||||
### Application Layer
|
||||
- MediatR 13.1.0
|
||||
- FluentValidation 12.0.0
|
||||
- AutoMapper 15.1.0
|
||||
|
||||
### Infrastructure Layer
|
||||
- Microsoft.EntityFrameworkCore 9.0.10
|
||||
- Npgsql.EntityFrameworkCore.PostgreSQL 9.0.4
|
||||
- StackExchange.Redis 2.9.32
|
||||
|
||||
### API Layer
|
||||
- Serilog.AspNetCore 9.0.0
|
||||
- Scalar.AspNetCore 2.9.0
|
||||
|
||||
### Test Projects
|
||||
- xUnit 2.9.2
|
||||
- FluentAssertions 8.8.0
|
||||
- Moq 4.20.72
|
||||
- Testcontainers 4.x
|
||||
|
||||
## 环境变量
|
||||
|
||||
创建 `src/ColaFlow.API/appsettings.Development.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=colaflow;Username=colaflow;Password=colaflow_password",
|
||||
"Redis": "localhost:6379,password=colaflow_redis_password"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
启动应用后,访问:
|
||||
|
||||
- **Scalar UI**: `https://localhost:5001/scalar/v1`
|
||||
- **OpenAPI JSON**: `https://localhost:5001/openapi/v1.json`
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [完整架构设计](../docs/M1-Architecture-Design.md)
|
||||
- [项目计划](../product.md)
|
||||
- [Sprint 计划](../docs/Sprint-Plan.md)
|
||||
- [Docker 使用指南](../DOCKER-README.md)
|
||||
- [测试指南](tests/README.md)
|
||||
|
||||
## 下一步开发任务
|
||||
|
||||
### Infrastructure Layer(进行中)
|
||||
- [ ] 配置 EF Core DbContext
|
||||
- [ ] 创建 Entity Configurations
|
||||
- [ ] 生成数据库 Migrations
|
||||
- [ ] 实现 Repository 和 Unit of Work
|
||||
|
||||
### Application Layer(待开发)
|
||||
- [ ] 实现 CQRS Commands
|
||||
- [ ] 实现 CQRS Queries
|
||||
- [ ] 配置 MediatR Pipeline Behaviors
|
||||
- [ ] 实现 FluentValidation Validators
|
||||
|
||||
### API Layer(待开发)
|
||||
- [ ] 实现 REST API Controllers
|
||||
- [ ] 配置 OpenAPI/Scalar
|
||||
- [ ] 实现异常处理中间件
|
||||
- [ ] 配置 JWT 认证
|
||||
|
||||
### 测试(待开发)
|
||||
- [ ] 编写 Domain 单元测试(≥80% 覆盖率)
|
||||
- [ ] 编写 Application 单元测试
|
||||
- [ ] 编写 API 集成测试
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
## 团队
|
||||
|
||||
ColaFlow Development Team
|
||||
|
||||
---
|
||||
|
||||
**当前状态**: 🟡 Domain Layer 完成,Infrastructure 和 Application 层开发中
|
||||
|
||||
**最后更新**: 2025-11-02
|
||||
280
colaflow-api/docs/Modular-Refactoring-Summary.md
Normal file
280
colaflow-api/docs/Modular-Refactoring-Summary.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# ColaFlow 模块化重构总结
|
||||
|
||||
**日期**: 2025-11-02
|
||||
**状态**: ✅ 完成
|
||||
**架构**: Modular Monolith + Clean Architecture + DDD + CQRS
|
||||
|
||||
---
|
||||
|
||||
## 重构概述
|
||||
|
||||
成功将 ColaFlow 后端从传统的单体架构重构为**模块化单体架构**(Modular Monolith),保留了 Clean Architecture + DDD 的优势,同时引入了清晰的模块边界,为未来可能的微服务迁移奠定基础。
|
||||
|
||||
## 架构决策
|
||||
|
||||
根据 `docs/Modular-Monolith-Architecture.md` 的分析和建议,我们选择了 **Modular Monolith** 而非 **Microservices**,原因如下:
|
||||
|
||||
1. **团队规模小**(4-8 人):微服务需要 15+ 人的团队
|
||||
2. **项目早期阶段**:Sprint 1 of M1(Week 1-2 of 48)
|
||||
3. **Domain 边界尚未稳定**:需要时间验证模块划分
|
||||
4. **快速交付优先**:避免 8-12 周的微服务基础设施开发时间
|
||||
5. **成本控制**:Modular Monolith 基础设施成本仅为微服务的 1/10
|
||||
|
||||
## 实施成果
|
||||
|
||||
### 1. 新的目录结构
|
||||
|
||||
```
|
||||
colaflow-api/
|
||||
├── src/
|
||||
│ ├── ColaFlow.API/ # API 层(统一入口)
|
||||
│ │
|
||||
│ ├── Modules/ # 业务模块
|
||||
│ │ └── ProjectManagement/ # 项目管理模块 ✅
|
||||
│ │ ├── ColaFlow.Modules.PM.Domain/
|
||||
│ │ ├── ColaFlow.Modules.PM.Application/
|
||||
│ │ ├── ColaFlow.Modules.PM.Infrastructure/
|
||||
│ │ └── ColaFlow.Modules.PM.Contracts/
|
||||
│ │
|
||||
│ └── Shared/ # 共享内核
|
||||
│ └── ColaFlow.Shared.Kernel/
|
||||
│ ├── Common/ # Entity, ValueObject, AggregateRoot
|
||||
│ ├── Events/ # DomainEvent
|
||||
│ └── Modules/ # IModule 接口
|
||||
│
|
||||
├── tests/
|
||||
│ └── ColaFlow.ArchitectureTests/ # 架构测试 ✅
|
||||
```
|
||||
|
||||
### 2. 创建的项目
|
||||
|
||||
#### Shared.Kernel 项目
|
||||
**路径**: `src/Shared/ColaFlow.Shared.Kernel/`
|
||||
|
||||
**内容**:
|
||||
- `Common/Entity.cs` - 实体基类
|
||||
- `Common/ValueObject.cs` - 值对象基类
|
||||
- `Common/AggregateRoot.cs` - 聚合根基类
|
||||
- `Common/Enumeration.cs` - 类型安全枚举基类
|
||||
- `Events/DomainEvent.cs` - 领域事件基类
|
||||
- `Modules/IModule.cs` - 模块接口
|
||||
|
||||
**用途**: 所有模块共享的基础类和接口
|
||||
|
||||
#### ProjectManagement 模块
|
||||
**路径**: `src/Modules/ProjectManagement/`
|
||||
|
||||
**包含项目**:
|
||||
1. **ColaFlow.Modules.PM.Domain** - 领域层
|
||||
- 迁移自 `ColaFlow.Domain/Aggregates`
|
||||
- 包含:Project, Epic, Story, WorkTask 聚合
|
||||
- 包含:所有 ValueObjects(ProjectId, ProjectKey 等)
|
||||
- 包含:所有 Domain Events
|
||||
|
||||
2. **ColaFlow.Modules.PM.Application** - 应用层
|
||||
- 待实现 CQRS Commands 和 Queries
|
||||
|
||||
3. **ColaFlow.Modules.PM.Infrastructure** - 基础设施层
|
||||
- 待实现 Repositories 和 EF Core Configurations
|
||||
|
||||
4. **ColaFlow.Modules.PM.Contracts** - 对外契约
|
||||
- 定义模块对外暴露的接口和 Integration Events
|
||||
|
||||
#### 架构测试项目
|
||||
**路径**: `tests/ColaFlow.ArchitectureTests/`
|
||||
|
||||
**测试内容**:
|
||||
- Domain 层不依赖 Application 和 Infrastructure
|
||||
- Domain 层只依赖 Shared.Kernel
|
||||
- Project 继承自 AggregateRoot
|
||||
- Entities 继承自 Entity
|
||||
- ValueObjects 是不可变的(sealed)
|
||||
- Domain Events 是 records
|
||||
|
||||
**测试结果**: ✅ 8/8 通过
|
||||
|
||||
### 3. 模块注册机制
|
||||
|
||||
创建了 `IModule` 接口,用于模块的服务注册和配置:
|
||||
|
||||
```csharp
|
||||
public interface IModule
|
||||
{
|
||||
string Name { get; }
|
||||
void RegisterServices(IServiceCollection services, IConfiguration configuration);
|
||||
void ConfigureApplication(IApplicationBuilder app);
|
||||
}
|
||||
```
|
||||
|
||||
**ProjectManagementModule** 实现:
|
||||
```csharp
|
||||
public class ProjectManagementModule : IModule
|
||||
{
|
||||
public string Name => "ProjectManagement";
|
||||
|
||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// 注册 MediatR handlers
|
||||
// 注册 Repositories
|
||||
// 注册 Application Services
|
||||
}
|
||||
|
||||
public void ConfigureApplication(IApplicationBuilder app)
|
||||
{
|
||||
// 配置模块特定的中间件
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 命名空间迁移
|
||||
|
||||
**旧命名空间** → **新命名空间**:
|
||||
- `ColaFlow.Domain.*` → `ColaFlow.Modules.PM.Domain.*`
|
||||
- `ColaFlow.Domain.Common` → `ColaFlow.Shared.Kernel.Common`
|
||||
- `ColaFlow.Domain.Events` → `ColaFlow.Shared.Kernel.Events`
|
||||
|
||||
### 5. 解决方案更新
|
||||
|
||||
更新了 `ColaFlow.sln`,新增以下项目:
|
||||
- ColaFlow.Shared.Kernel
|
||||
- ColaFlow.Modules.PM.Domain
|
||||
- ColaFlow.Modules.PM.Application
|
||||
- ColaFlow.Modules.PM.Infrastructure
|
||||
- ColaFlow.Modules.PM.Contracts
|
||||
- ColaFlow.ArchitectureTests
|
||||
|
||||
## 编译和测试结果
|
||||
|
||||
### 编译结果
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
✅ **成功** - 19 个警告,0 个错误
|
||||
|
||||
警告主要是 NuGet 包版本依赖冲突(非阻塞)
|
||||
|
||||
### 架构测试结果
|
||||
```bash
|
||||
dotnet test tests/ColaFlow.ArchitectureTests
|
||||
```
|
||||
✅ **通过** - 8/8 测试通过
|
||||
- Domain 层依赖检查 ✅
|
||||
- 继承关系检查 ✅
|
||||
- 不可变性检查 ✅
|
||||
- 事件类型检查 ✅
|
||||
|
||||
## 模块边界规则
|
||||
|
||||
### ✅ 允许的依赖
|
||||
1. 所有模块 → Shared.Kernel
|
||||
2. Module.Application → Module.Domain
|
||||
3. Module.Infrastructure → Module.Application, Module.Domain
|
||||
4. 模块间通过 MediatR 进行查询(Application Service Integration)
|
||||
5. 模块间通过 Domain Events 进行解耦通信
|
||||
|
||||
### ❌ 禁止的依赖
|
||||
1. Module.Domain → Module.Application
|
||||
2. Module.Domain → Module.Infrastructure
|
||||
3. Module.Domain → 其他模块的 Domain
|
||||
4. 直接引用其他模块的实体类
|
||||
|
||||
这些规则由 **ArchUnit 测试** 自动化验证。
|
||||
|
||||
## 未来迁移路径
|
||||
|
||||
### 短期(M1-M3)
|
||||
- 保持 Modular Monolith 架构
|
||||
- 继续完善 ProjectManagement 模块
|
||||
- 添加新模块(Workflow, User, Notifications)
|
||||
- 验证模块边界是否合理
|
||||
|
||||
### 中期(M4-M6)
|
||||
- 如果团队增长到 15+ 人,考虑提取第一个微服务
|
||||
- 优先提取 AI Module(独立扩展需求)
|
||||
- 使用 Strangler Fig 模式逐步迁移
|
||||
|
||||
### 微服务迁移条件
|
||||
只有满足以下条件时才考虑迁移到微服务:
|
||||
1. ✅ 团队规模 > 15 人
|
||||
2. ✅ 用户规模 > 50,000 活跃用户
|
||||
3. ✅ 特定模块需要独立扩展
|
||||
4. ✅ Domain 边界稳定(1+ 年)
|
||||
5. ✅ 团队具备分布式系统经验
|
||||
|
||||
## 成功指标
|
||||
|
||||
### ✅ 已完成
|
||||
- [x] 清晰的模块目录结构
|
||||
- [x] Shared.Kernel 项目创建
|
||||
- [x] ProjectManagement 模块迁移
|
||||
- [x] IModule 接口和注册机制
|
||||
- [x] 架构测试自动化
|
||||
- [x] 编译成功
|
||||
- [x] 所有测试通过
|
||||
- [x] 文档更新(README.md)
|
||||
|
||||
### 📋 下一步任务
|
||||
- [ ] 完善 Application 层(Commands/Queries)
|
||||
- [ ] 完善 Infrastructure 层(Repositories)
|
||||
- [ ] 添加 Workflow 模块
|
||||
- [ ] 添加 User 模块
|
||||
- [ ] 实现跨模块通信示例
|
||||
|
||||
## 技术债务
|
||||
|
||||
### 当前遗留
|
||||
1. **旧的单体项目**(待删除):
|
||||
- `src/ColaFlow.Domain/`
|
||||
- `src/ColaFlow.Application/`
|
||||
- `src/ColaFlow.Infrastructure/`
|
||||
|
||||
**计划**: 在所有代码迁移完成后删除
|
||||
|
||||
2. **NuGet 包版本警告**:
|
||||
- MediatR 版本冲突(12.4.1 vs 11.x)
|
||||
- AutoMapper 版本冲突(15.1.0 vs 12.0.1)
|
||||
|
||||
**计划**: 统一升级到最新稳定版本
|
||||
|
||||
## 性能影响
|
||||
|
||||
### 分析结果
|
||||
- ✅ **零性能损失** - Modular Monolith 与传统 Monolith 性能相同
|
||||
- ✅ 相同的进程内调用(无网络开销)
|
||||
- ✅ 相同的数据库连接池
|
||||
- ✅ 无序列化/反序列化开销
|
||||
|
||||
### 对比微服务
|
||||
- 🚀 **快 10-100x** - 无跨服务网络调用
|
||||
- 💾 **内存占用更低** - 单一进程
|
||||
- 🔧 **运维简单** - 单一部署单元
|
||||
|
||||
## 参考文档
|
||||
|
||||
1. [Modular-Monolith-Architecture.md](./Modular-Monolith-Architecture.md) - 完整的架构分析
|
||||
2. [README.md](../README.md) - 更新后的项目文档
|
||||
3. [ColaFlow.sln](../ColaFlow.sln) - 解决方案文件
|
||||
|
||||
## 结论
|
||||
|
||||
✅ **重构成功!**
|
||||
|
||||
ColaFlow 后端现在拥有:
|
||||
- 清晰的模块边界
|
||||
- 可维护的代码结构
|
||||
- 自动化的架构测试
|
||||
- 未来迁移到微服务的路径
|
||||
|
||||
同时保持了:
|
||||
- 简单的开发体验
|
||||
- 低运维成本
|
||||
- 快速迭代能力
|
||||
- ACID 事务保证
|
||||
|
||||
这个架构非常适合 ColaFlow 当前的团队规模和项目阶段,能够支持到 M6(100k+ 用户)而无需迁移到微服务。
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-11-02
|
||||
**责任人**: Architecture Team
|
||||
**状态**: ✅ 完成并验证
|
||||
30
colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj
Normal file
30
colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.9.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Application\ColaFlow.Modules.ProjectManagement.Application.csproj" />
|
||||
<ProjectReference Include="..\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Infrastructure\ColaFlow.Modules.ProjectManagement.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
colaflow-api/src/ColaFlow.API/ColaFlow.API.http
Normal file
6
colaflow-api/src/ColaFlow.API/ColaFlow.API.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@ColaFlow.API_HostAddress = http://localhost:5167
|
||||
|
||||
GET {{ColaFlow.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,62 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects;
|
||||
|
||||
namespace ColaFlow.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Projects API Controller
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
public class ProjectsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public ProjectsController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all projects
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<ProjectDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetProjects(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetProjectsQuery();
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get project by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ProjectDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetProject(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetProjectByIdQuery(id);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new project
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ProjectDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateProject(
|
||||
[FromBody] CreateProjectCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetProject), new { id = result.Id }, result);
|
||||
}
|
||||
}
|
||||
46
colaflow-api/src/ColaFlow.API/Extensions/ModuleExtensions.cs
Normal file
46
colaflow-api/src/ColaFlow.API/Extensions/ModuleExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Behaviors;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories;
|
||||
|
||||
namespace ColaFlow.API.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering modules
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Register ProjectManagement Module
|
||||
/// </summary>
|
||||
public static IServiceCollection AddProjectManagementModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Register DbContext
|
||||
var connectionString = configuration.GetConnectionString("PMDatabase");
|
||||
services.AddDbContext<PMDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
// Register MediatR handlers from Application assembly
|
||||
services.AddMediatR(typeof(CreateProjectCommand).Assembly);
|
||||
|
||||
// Register FluentValidation validators
|
||||
services.AddValidatorsFromAssembly(typeof(CreateProjectCommand).Assembly);
|
||||
|
||||
// Register pipeline behaviors
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
|
||||
Console.WriteLine("[ProjectManagement] Module registered");
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentValidation;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.API.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Global exception handler middleware
|
||||
/// </summary>
|
||||
public class GlobalExceptionHandlerMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
|
||||
|
||||
public GlobalExceptionHandlerMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<GlobalExceptionHandlerMiddleware> logger)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var (statusCode, response) = exception switch
|
||||
{
|
||||
ValidationException validationEx => (
|
||||
StatusCodes.Status400BadRequest,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status400BadRequest,
|
||||
Message = "Validation failed",
|
||||
Errors = validationEx.Errors.Select(e => new
|
||||
{
|
||||
Property = e.PropertyName,
|
||||
Message = e.ErrorMessage
|
||||
})
|
||||
}),
|
||||
DomainException domainEx => (
|
||||
StatusCodes.Status400BadRequest,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status400BadRequest,
|
||||
Message = domainEx.Message
|
||||
}),
|
||||
NotFoundException notFoundEx => (
|
||||
StatusCodes.Status404NotFound,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status404NotFound,
|
||||
Message = notFoundEx.Message
|
||||
}),
|
||||
_ => (
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status500InternalServerError,
|
||||
Message = "An internal server error occurred"
|
||||
})
|
||||
};
|
||||
|
||||
context.Response.StatusCode = statusCode;
|
||||
|
||||
// Log with appropriate level
|
||||
if (statusCode >= 500)
|
||||
{
|
||||
_logger.LogError(exception, "Internal server error occurred: {Message}", exception.Message);
|
||||
}
|
||||
else if (statusCode >= 400)
|
||||
{
|
||||
_logger.LogWarning(exception, "Client error occurred: {Message}", exception.Message);
|
||||
}
|
||||
|
||||
var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
await context.Response.WriteAsync(jsonResponse);
|
||||
}
|
||||
}
|
||||
31
colaflow-api/src/ColaFlow.API/Program.cs
Normal file
31
colaflow-api/src/ColaFlow.API/Program.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using ColaFlow.API.Extensions;
|
||||
using ColaFlow.API.Middleware;
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register ProjectManagement Module
|
||||
builder.Services.AddProjectManagementModule(builder.Configuration);
|
||||
|
||||
// Add controllers
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// Configure OpenAPI/Scalar
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.MapScalarApiReference();
|
||||
}
|
||||
|
||||
// Global exception handler (should be first in pipeline)
|
||||
app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
23
colaflow-api/src/ColaFlow.API/Properties/launchSettings.json
Normal file
23
colaflow-api/src/ColaFlow.API/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5167",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7295;http://localhost:5167",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
colaflow-api/src/ColaFlow.API/appsettings.Development.json
Normal file
12
colaflow-api/src/ColaFlow.API/appsettings.Development.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"PMDatabase": "Host=localhost;Port=5432;Database=colaflow_pm;Username=colaflow;Password=colaflow_dev_password"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
colaflow-api/src/ColaFlow.API/appsettings.json
Normal file
12
colaflow-api/src/ColaFlow.API/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"PMDatabase": "Host=localhost;Port=5432;Database=colaflow;Username=colaflow;Password=colaflow_dev_password"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Domain\ColaFlow.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="FluentValidation" Version="12.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,90 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Epic Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Epic : Entity
|
||||
{
|
||||
public new EpicId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectId ProjectId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
|
||||
private readonly List<Story> _stories = new();
|
||||
public IReadOnlyCollection<Story> Stories => _stories.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Epic()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
ProjectId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Epic Create(string name, string description, ProjectId projectId, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
return new Epic
|
||||
{
|
||||
Id = EpicId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
ProjectId = projectId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = TaskPriority.Medium,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public Story CreateStory(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var story = Story.Create(title, description, this.Id, priority, createdBy);
|
||||
_stories.Add(story);
|
||||
return story;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Events;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Project Aggregate Root
|
||||
/// Enforces consistency boundary for Project -> Epic -> Story -> Task hierarchy
|
||||
/// </summary>
|
||||
public class Project : AggregateRoot
|
||||
{
|
||||
public new ProjectId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectKey Key { get; private set; }
|
||||
public ProjectStatus Status { get; private set; }
|
||||
public UserId OwnerId { get; private set; }
|
||||
|
||||
private readonly List<Epic> _epics = new();
|
||||
public IReadOnlyCollection<Epic> Epics => _epics.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Project()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
Key = null!;
|
||||
Status = null!;
|
||||
OwnerId = null!;
|
||||
}
|
||||
|
||||
// Factory method
|
||||
public static Project Create(string name, string description, string key, UserId ownerId)
|
||||
{
|
||||
// Validation
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
var project = new Project
|
||||
{
|
||||
Id = ProjectId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
Key = ProjectKey.Create(key),
|
||||
Status = ProjectStatus.Active,
|
||||
OwnerId = ownerId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Raise domain event
|
||||
project.AddDomainEvent(new ProjectCreatedEvent(project.Id, project.Name, ownerId));
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
// Business methods
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectUpdatedEvent(Id, Name, Description));
|
||||
}
|
||||
|
||||
public Epic CreateEpic(string name, string description, UserId createdBy)
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Cannot create epic in an archived project");
|
||||
|
||||
var epic = Epic.Create(name, description, this.Id, createdBy);
|
||||
_epics.Add(epic);
|
||||
|
||||
AddDomainEvent(new EpicCreatedEvent(epic.Id, epic.Name, this.Id));
|
||||
|
||||
return epic;
|
||||
}
|
||||
|
||||
public void Archive()
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Project is already archived");
|
||||
|
||||
Status = ProjectStatus.Archived;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectArchivedEvent(Id));
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
if (Status == ProjectStatus.Active)
|
||||
throw new DomainException("Project is already active");
|
||||
|
||||
Status = ProjectStatus.Active;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Story Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Story : Entity
|
||||
{
|
||||
public new StoryId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public EpicId EpicId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
private readonly List<WorkTask> _tasks = new();
|
||||
public IReadOnlyCollection<WorkTask> Tasks => _tasks.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Story()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
EpicId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Story Create(string title, string description, EpicId epicId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
return new Story
|
||||
{
|
||||
Id = StoryId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
EpicId = epicId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public WorkTask CreateTask(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var task = WorkTask.Create(title, description, this.Id, priority, createdBy);
|
||||
_tasks.Add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Task Entity (part of Project aggregate)
|
||||
/// Named "WorkTask" to avoid conflict with System.Threading.Tasks.Task
|
||||
/// </summary>
|
||||
public class WorkTask : Entity
|
||||
{
|
||||
public new TaskId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public StoryId StoryId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private WorkTask()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
StoryId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static WorkTask Create(string title, string description, StoryId storyId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
return new WorkTask
|
||||
{
|
||||
Id = TaskId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
StoryId = storyId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
9
colaflow-api/src/ColaFlow.Domain/ColaFlow.Domain.csproj
Normal file
9
colaflow-api/src/ColaFlow.Domain/ColaFlow.Domain.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
31
colaflow-api/src/ColaFlow.Domain/Common/AggregateRoot.cs
Normal file
31
colaflow-api/src/ColaFlow.Domain/Common/AggregateRoot.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using ColaFlow.Domain.Events;
|
||||
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all aggregate roots
|
||||
/// </summary>
|
||||
public abstract class AggregateRoot : Entity
|
||||
{
|
||||
private readonly List<DomainEvent> _domainEvents = new();
|
||||
|
||||
public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
protected AggregateRoot() : base()
|
||||
{
|
||||
}
|
||||
|
||||
protected AggregateRoot(Guid id) : base(id)
|
||||
{
|
||||
}
|
||||
|
||||
protected void AddDomainEvent(DomainEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public void ClearDomainEvents()
|
||||
{
|
||||
_domainEvents.Clear();
|
||||
}
|
||||
}
|
||||
54
colaflow-api/src/ColaFlow.Domain/Common/Entity.cs
Normal file
54
colaflow-api/src/ColaFlow.Domain/Common/Entity.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all entities
|
||||
/// </summary>
|
||||
public abstract class Entity
|
||||
{
|
||||
public Guid Id { get; protected set; }
|
||||
|
||||
protected Entity()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
protected Entity(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Entity other)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (GetType() != other.GetType())
|
||||
return false;
|
||||
|
||||
return Id == other.Id;
|
||||
}
|
||||
|
||||
public static bool operator ==(Entity? a, Entity? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Entity? a, Entity? b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id.GetHashCode();
|
||||
}
|
||||
}
|
||||
78
colaflow-api/src/ColaFlow.Domain/Common/Enumeration.cs
Normal file
78
colaflow-api/src/ColaFlow.Domain/Common/Enumeration.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for creating type-safe enumerations
|
||||
/// </summary>
|
||||
public abstract class Enumeration : IComparable
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
protected Enumeration(int id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public static IEnumerable<T> GetAll<T>() where T : Enumeration
|
||||
{
|
||||
var fields = typeof(T).GetFields(BindingFlags.Public |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.DeclaredOnly);
|
||||
|
||||
return fields.Select(f => f.GetValue(null)).Cast<T>();
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Enumeration otherValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeMatches = GetType().Equals(obj.GetType());
|
||||
var valueMatches = Id.Equals(otherValue.Id);
|
||||
|
||||
return typeMatches && valueMatches;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
|
||||
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
|
||||
{
|
||||
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
|
||||
return absoluteDifference;
|
||||
}
|
||||
|
||||
public static T FromValue<T>(int value) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public static T FromDisplayName<T>(string displayName) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration
|
||||
{
|
||||
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
|
||||
|
||||
if (matchingItem == null)
|
||||
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
|
||||
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public int CompareTo(object? other)
|
||||
{
|
||||
if (other == null) return 1;
|
||||
return Id.CompareTo(((Enumeration)other).Id);
|
||||
}
|
||||
}
|
||||
46
colaflow-api/src/ColaFlow.Domain/Common/ValueObject.cs
Normal file
46
colaflow-api/src/ColaFlow.Domain/Common/ValueObject.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all value objects
|
||||
/// </summary>
|
||||
public abstract class ValueObject
|
||||
{
|
||||
protected abstract IEnumerable<object> GetAtomicValues();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null || obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
var other = (ValueObject)obj;
|
||||
return GetAtomicValues().SequenceEqual(other.GetAtomicValues());
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetAtomicValues()
|
||||
.Aggregate(1, (current, obj) =>
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (current * 23) + (obj?.GetHashCode() ?? 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static bool operator ==(ValueObject? a, ValueObject? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(ValueObject? a, ValueObject? b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
}
|
||||
10
colaflow-api/src/ColaFlow.Domain/Events/DomainEvent.cs
Normal file
10
colaflow-api/src/ColaFlow.Domain/Events/DomainEvent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all domain events
|
||||
/// </summary>
|
||||
public abstract record DomainEvent
|
||||
{
|
||||
public Guid EventId { get; init; } = Guid.NewGuid();
|
||||
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
|
||||
}
|
||||
12
colaflow-api/src/ColaFlow.Domain/Events/EpicCreatedEvent.cs
Normal file
12
colaflow-api/src/ColaFlow.Domain/Events/EpicCreatedEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an epic is created
|
||||
/// </summary>
|
||||
public sealed record EpicCreatedEvent(
|
||||
EpicId EpicId,
|
||||
string EpicName,
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,10 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is archived
|
||||
/// </summary>
|
||||
public sealed record ProjectArchivedEvent(
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,12 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is created
|
||||
/// </summary>
|
||||
public sealed record ProjectCreatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string ProjectName,
|
||||
UserId CreatedBy
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,12 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is updated
|
||||
/// </summary>
|
||||
public sealed record ProjectUpdatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string Name,
|
||||
string Description
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ColaFlow.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception type for domain layer
|
||||
/// </summary>
|
||||
public class DomainException : Exception
|
||||
{
|
||||
public DomainException()
|
||||
{ }
|
||||
|
||||
public DomainException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public DomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/EpicId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/EpicId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// EpicId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class EpicId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private EpicId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static EpicId Create() => new EpicId(Guid.NewGuid());
|
||||
public static EpicId Create(Guid value) => new EpicId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class ProjectId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private ProjectId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectId Create() => new ProjectId(Guid.NewGuid());
|
||||
public static ProjectId Create(Guid value) => new ProjectId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
38
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectKey.cs
Normal file
38
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectKey.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectKey Value Object (e.g., "COLA", "FLOW")
|
||||
/// </summary>
|
||||
public sealed class ProjectKey : ValueObject
|
||||
{
|
||||
public string Value { get; private set; }
|
||||
|
||||
private ProjectKey(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectKey Create(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new DomainException("Project key cannot be empty");
|
||||
|
||||
if (value.Length > 10)
|
||||
throw new DomainException("Project key cannot exceed 10 characters");
|
||||
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(value, "^[A-Z0-9]+$"))
|
||||
throw new DomainException("Project key must contain only uppercase letters and numbers");
|
||||
|
||||
return new ProjectKey(value);
|
||||
}
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectStatus Enumeration
|
||||
/// </summary>
|
||||
public sealed class ProjectStatus : Enumeration
|
||||
{
|
||||
public static readonly ProjectStatus Active = new(1, "Active");
|
||||
public static readonly ProjectStatus Archived = new(2, "Archived");
|
||||
public static readonly ProjectStatus OnHold = new(3, "On Hold");
|
||||
|
||||
private ProjectStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/StoryId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/StoryId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// StoryId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class StoryId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private StoryId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static StoryId Create() => new StoryId(Guid.NewGuid());
|
||||
public static StoryId Create(Guid value) => new StoryId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/TaskId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/TaskId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class TaskId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private TaskId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TaskId Create() => new TaskId(Guid.NewGuid());
|
||||
public static TaskId Create(Guid value) => new TaskId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskPriority Enumeration
|
||||
/// </summary>
|
||||
public sealed class TaskPriority : Enumeration
|
||||
{
|
||||
public static readonly TaskPriority Low = new(1, "Low");
|
||||
public static readonly TaskPriority Medium = new(2, "Medium");
|
||||
public static readonly TaskPriority High = new(3, "High");
|
||||
public static readonly TaskPriority Urgent = new(4, "Urgent");
|
||||
|
||||
private TaskPriority(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/UserId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/UserId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// UserId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class UserId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private UserId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static UserId Create() => new UserId(Guid.NewGuid());
|
||||
public static UserId Create(Guid value) => new UserId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// WorkItemStatus Enumeration (renamed from TaskStatus to avoid conflict with System.Threading.Tasks.TaskStatus)
|
||||
/// </summary>
|
||||
public sealed class WorkItemStatus : Enumeration
|
||||
{
|
||||
public static readonly WorkItemStatus ToDo = new(1, "To Do");
|
||||
public static readonly WorkItemStatus InProgress = new(2, "In Progress");
|
||||
public static readonly WorkItemStatus InReview = new(3, "In Review");
|
||||
public static readonly WorkItemStatus Done = new(4, "Done");
|
||||
public static readonly WorkItemStatus Blocked = new(5, "Blocked");
|
||||
|
||||
private WorkItemStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Domain\ColaFlow.Domain.csproj" />
|
||||
<ProjectReference Include="..\ColaFlow.Application\ColaFlow.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,46 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline behavior for request validation using FluentValidation
|
||||
/// </summary>
|
||||
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||
|
||||
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
||||
{
|
||||
_validators = validators;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_validators.Any())
|
||||
{
|
||||
return await next();
|
||||
}
|
||||
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
|
||||
var validationResults = await Task.WhenAll(
|
||||
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
|
||||
|
||||
var failures = validationResults
|
||||
.SelectMany(r => r.Errors)
|
||||
.Where(f => f != null)
|
||||
.ToList();
|
||||
|
||||
if (failures.Any())
|
||||
{
|
||||
throw new ValidationException(failures);
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Contracts\ColaFlow.Modules.ProjectManagement.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.10.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Application</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Application</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Command to create a new Epic
|
||||
/// </summary>
|
||||
public sealed record CreateEpicCommand : IRequest<EpicDto>
|
||||
{
|
||||
public Guid ProjectId { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid CreatedBy { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for CreateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class CreateEpicCommandHandler : IRequestHandler<CreateEpicCommand, EpicDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public CreateEpicCommandHandler(
|
||||
IProjectRepository projectRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||
}
|
||||
|
||||
public async Task<EpicDto> Handle(CreateEpicCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the project
|
||||
var projectId = ProjectId.From(request.ProjectId);
|
||||
var project = await _projectRepository.GetByIdAsync(projectId, cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
throw new NotFoundException("Project", request.ProjectId);
|
||||
|
||||
// Create epic through aggregate root
|
||||
var createdById = UserId.From(request.CreatedBy);
|
||||
var epic = project.CreateEpic(request.Name, request.Description, createdById);
|
||||
|
||||
// Update project (epic is part of aggregate)
|
||||
_projectRepository.Update(project);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Map to DTO
|
||||
return new EpicDto
|
||||
{
|
||||
Id = epic.Id.Value,
|
||||
Name = epic.Name,
|
||||
Description = epic.Description,
|
||||
ProjectId = epic.ProjectId.Value,
|
||||
Status = epic.Status.Value,
|
||||
Priority = epic.Priority.Value,
|
||||
CreatedBy = epic.CreatedBy.Value,
|
||||
CreatedAt = epic.CreatedAt,
|
||||
UpdatedAt = epic.UpdatedAt,
|
||||
Stories = new List<StoryDto>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class CreateEpicCommandValidator : AbstractValidator<CreateEpicCommand>
|
||||
{
|
||||
public CreateEpicCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ProjectId)
|
||||
.NotEmpty().WithMessage("Project ID is required");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Epic name is required")
|
||||
.MaximumLength(200).WithMessage("Epic name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.CreatedBy)
|
||||
.NotEmpty().WithMessage("Created by user ID is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
|
||||
/// <summary>
|
||||
/// Command to create a new project
|
||||
/// </summary>
|
||||
public sealed record CreateProjectCommand : IRequest<ProjectDto>
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public Guid OwnerId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for CreateProjectCommand
|
||||
/// </summary>
|
||||
public sealed class CreateProjectCommandHandler : IRequestHandler<CreateProjectCommand, ProjectDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public CreateProjectCommandHandler(
|
||||
IProjectRepository projectRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||
}
|
||||
|
||||
public async Task<ProjectDto> Handle(CreateProjectCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if project key already exists
|
||||
var existingProject = await _projectRepository.GetByKeyAsync(request.Key, cancellationToken);
|
||||
if (existingProject != null)
|
||||
{
|
||||
throw new DomainException($"Project with key '{request.Key}' already exists");
|
||||
}
|
||||
|
||||
// Create project aggregate
|
||||
var project = Project.Create(
|
||||
request.Name,
|
||||
request.Description,
|
||||
request.Key,
|
||||
UserId.From(request.OwnerId)
|
||||
);
|
||||
|
||||
// Save to repository
|
||||
await _projectRepository.AddAsync(project, cancellationToken);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Return DTO
|
||||
return MapToDto(project);
|
||||
}
|
||||
|
||||
private static ProjectDto MapToDto(Project project)
|
||||
{
|
||||
return new ProjectDto
|
||||
{
|
||||
Id = project.Id.Value,
|
||||
Name = project.Name,
|
||||
Description = project.Description,
|
||||
Key = project.Key.Value,
|
||||
Status = project.Status.Name,
|
||||
OwnerId = project.OwnerId.Value,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt,
|
||||
Epics = new List<EpicDto>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateProjectCommand
|
||||
/// </summary>
|
||||
public sealed class CreateProjectCommandValidator : AbstractValidator<CreateProjectCommand>
|
||||
{
|
||||
public CreateProjectCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Project name is required")
|
||||
.MaximumLength(200).WithMessage("Project name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.Key)
|
||||
.NotEmpty().WithMessage("Project key is required")
|
||||
.MaximumLength(20).WithMessage("Project key cannot exceed 20 characters")
|
||||
.Matches("^[A-Z0-9]+$").WithMessage("Project key must contain only uppercase letters and numbers");
|
||||
|
||||
RuleFor(x => x.OwnerId)
|
||||
.NotEmpty().WithMessage("Owner ID is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Command to update an existing Epic
|
||||
/// </summary>
|
||||
public sealed record UpdateEpicCommand : IRequest<EpicDto>
|
||||
{
|
||||
public Guid EpicId { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for UpdateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class UpdateEpicCommandHandler : IRequestHandler<UpdateEpicCommand, EpicDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public UpdateEpicCommandHandler(
|
||||
IProjectRepository projectRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||
}
|
||||
|
||||
public async Task<EpicDto> Handle(UpdateEpicCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the project containing the epic
|
||||
var epicId = EpicId.From(request.EpicId);
|
||||
var project = await _projectRepository.GetProjectWithEpicAsync(epicId, cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
throw new NotFoundException("Epic", request.EpicId);
|
||||
|
||||
// Find the epic
|
||||
var epic = project.Epics.FirstOrDefault(e => e.Id == epicId);
|
||||
if (epic == null)
|
||||
throw new NotFoundException("Epic", request.EpicId);
|
||||
|
||||
// Update epic through domain method
|
||||
epic.UpdateDetails(request.Name, request.Description);
|
||||
|
||||
// Save changes
|
||||
_projectRepository.Update(project);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Map to DTO
|
||||
return new EpicDto
|
||||
{
|
||||
Id = epic.Id.Value,
|
||||
Name = epic.Name,
|
||||
Description = epic.Description,
|
||||
ProjectId = epic.ProjectId.Value,
|
||||
Status = epic.Status.Value,
|
||||
Priority = epic.Priority.Value,
|
||||
CreatedBy = epic.CreatedBy.Value,
|
||||
CreatedAt = epic.CreatedAt,
|
||||
UpdatedAt = epic.UpdatedAt,
|
||||
Stories = epic.Stories.Select(s => new StoryDto
|
||||
{
|
||||
Id = s.Id.Value,
|
||||
Title = s.Title,
|
||||
Description = s.Description,
|
||||
EpicId = s.EpicId.Value,
|
||||
Status = s.Status.Value,
|
||||
Priority = s.Priority.Value,
|
||||
EstimatedHours = s.EstimatedHours,
|
||||
ActualHours = s.ActualHours,
|
||||
AssigneeId = s.AssigneeId?.Value,
|
||||
CreatedBy = s.CreatedBy.Value,
|
||||
CreatedAt = s.CreatedAt,
|
||||
UpdatedAt = s.UpdatedAt,
|
||||
Tasks = new List<TaskDto>()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for UpdateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class UpdateEpicCommandValidator : AbstractValidator<UpdateEpicCommand>
|
||||
{
|
||||
public UpdateEpicCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.EpicId)
|
||||
.NotEmpty().WithMessage("Epic ID is required");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Epic name is required")
|
||||
.MaximumLength(200).WithMessage("Epic name cannot exceed 200 characters");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Epic
|
||||
/// </summary>
|
||||
public record EpicDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid ProjectId { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string Priority { get; init; } = string.Empty;
|
||||
public Guid CreatedBy { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
public List<StoryDto> Stories { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Project
|
||||
/// </summary>
|
||||
public record ProjectDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public Guid OwnerId { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
public List<EpicDto> Epics { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Story
|
||||
/// </summary>
|
||||
public record StoryDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid EpicId { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string Priority { get; init; } = string.Empty;
|
||||
public Guid? AssigneeId { get; init; }
|
||||
public decimal? EstimatedHours { get; init; }
|
||||
public decimal? ActualHours { get; init; }
|
||||
public Guid CreatedBy { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
public List<TaskDto> Tasks { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Task
|
||||
/// </summary>
|
||||
public record TaskDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid StoryId { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string Priority { get; init; } = string.Empty;
|
||||
public Guid? AssigneeId { get; init; }
|
||||
public decimal? EstimatedHours { get; init; }
|
||||
public decimal? ActualHours { get; init; }
|
||||
public Guid CreatedBy { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicById;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get an Epic by its ID
|
||||
/// </summary>
|
||||
public sealed record GetEpicByIdQuery(Guid EpicId) : IRequest<EpicDto>;
|
||||
@@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get a project by its ID
|
||||
/// </summary>
|
||||
public sealed record GetProjectByIdQuery(Guid ProjectId) : IRequest<ProjectDto>;
|
||||
@@ -0,0 +1,92 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for GetProjectByIdQuery
|
||||
/// </summary>
|
||||
public sealed class GetProjectByIdQueryHandler : IRequestHandler<GetProjectByIdQuery, ProjectDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public GetProjectByIdQueryHandler(IProjectRepository projectRepository)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
}
|
||||
|
||||
public async Task<ProjectDto> Handle(GetProjectByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var project = await _projectRepository.GetByIdAsync(
|
||||
ProjectId.From(request.ProjectId),
|
||||
cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
throw new DomainException($"Project with ID '{request.ProjectId}' not found");
|
||||
}
|
||||
|
||||
return MapToDto(project);
|
||||
}
|
||||
|
||||
private static ProjectDto MapToDto(Project project)
|
||||
{
|
||||
return new ProjectDto
|
||||
{
|
||||
Id = project.Id.Value,
|
||||
Name = project.Name,
|
||||
Description = project.Description,
|
||||
Key = project.Key.Value,
|
||||
Status = project.Status.Name,
|
||||
OwnerId = project.OwnerId.Value,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt,
|
||||
Epics = project.Epics.Select(e => new EpicDto
|
||||
{
|
||||
Id = e.Id.Value,
|
||||
Name = e.Name,
|
||||
Description = e.Description,
|
||||
ProjectId = e.ProjectId.Value,
|
||||
Status = e.Status.Name,
|
||||
Priority = e.Priority.Name,
|
||||
CreatedBy = e.CreatedBy.Value,
|
||||
CreatedAt = e.CreatedAt,
|
||||
UpdatedAt = e.UpdatedAt,
|
||||
Stories = e.Stories.Select(s => new StoryDto
|
||||
{
|
||||
Id = s.Id.Value,
|
||||
Title = s.Title,
|
||||
Description = s.Description,
|
||||
EpicId = s.EpicId.Value,
|
||||
Status = s.Status.Name,
|
||||
Priority = s.Priority.Name,
|
||||
AssigneeId = s.AssigneeId?.Value,
|
||||
EstimatedHours = s.EstimatedHours,
|
||||
ActualHours = s.ActualHours,
|
||||
CreatedBy = s.CreatedBy.Value,
|
||||
CreatedAt = s.CreatedAt,
|
||||
UpdatedAt = s.UpdatedAt,
|
||||
Tasks = s.Tasks.Select(t => new TaskDto
|
||||
{
|
||||
Id = t.Id.Value,
|
||||
Title = t.Title,
|
||||
Description = t.Description,
|
||||
StoryId = t.StoryId.Value,
|
||||
Status = t.Status.Name,
|
||||
Priority = t.Priority.Name,
|
||||
AssigneeId = t.AssigneeId?.Value,
|
||||
EstimatedHours = t.EstimatedHours,
|
||||
ActualHours = t.ActualHours,
|
||||
CreatedBy = t.CreatedBy.Value,
|
||||
CreatedAt = t.CreatedAt,
|
||||
UpdatedAt = t.UpdatedAt
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all projects
|
||||
/// </summary>
|
||||
public sealed record GetProjectsQuery : IRequest<List<ProjectDto>>;
|
||||
@@ -0,0 +1,43 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for GetProjectsQuery
|
||||
/// </summary>
|
||||
public sealed class GetProjectsQueryHandler : IRequestHandler<GetProjectsQuery, List<ProjectDto>>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public GetProjectsQueryHandler(IProjectRepository projectRepository)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
}
|
||||
|
||||
public async Task<List<ProjectDto>> Handle(GetProjectsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var projects = await _projectRepository.GetAllAsync(cancellationToken);
|
||||
|
||||
return projects.Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
private static ProjectDto MapToDto(Project project)
|
||||
{
|
||||
return new ProjectDto
|
||||
{
|
||||
Id = project.Id.Value,
|
||||
Name = project.Name,
|
||||
Description = project.Description,
|
||||
Key = project.Key.Value,
|
||||
Status = project.Status.Name,
|
||||
OwnerId = project.OwnerId.Value,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt,
|
||||
// Don't load Epics for list view (performance)
|
||||
Epics = new List<EpicDto>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Contracts</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Contracts</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,91 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Epic Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Epic : Entity
|
||||
{
|
||||
public new EpicId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectId ProjectId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
|
||||
private readonly List<Story> _stories = new();
|
||||
public IReadOnlyCollection<Story> Stories => _stories.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Epic()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
ProjectId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Epic Create(string name, string description, ProjectId projectId, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
return new Epic
|
||||
{
|
||||
Id = EpicId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
ProjectId = projectId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = TaskPriority.Medium,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public Story CreateStory(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var story = Story.Create(title, description, this.Id, priority, createdBy);
|
||||
_stories.Add(story);
|
||||
return story;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Project Aggregate Root
|
||||
/// Enforces consistency boundary for Project -> Epic -> Story -> Task hierarchy
|
||||
/// </summary>
|
||||
public class Project : AggregateRoot
|
||||
{
|
||||
public new ProjectId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectKey Key { get; private set; }
|
||||
public ProjectStatus Status { get; private set; }
|
||||
public UserId OwnerId { get; private set; }
|
||||
|
||||
private readonly List<Epic> _epics = new();
|
||||
public IReadOnlyCollection<Epic> Epics => _epics.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Project()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
Key = null!;
|
||||
Status = null!;
|
||||
OwnerId = null!;
|
||||
}
|
||||
|
||||
// Factory method
|
||||
public static Project Create(string name, string description, string key, UserId ownerId)
|
||||
{
|
||||
// Validation
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
var project = new Project
|
||||
{
|
||||
Id = ProjectId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
Key = ProjectKey.Create(key),
|
||||
Status = ProjectStatus.Active,
|
||||
OwnerId = ownerId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Raise domain event
|
||||
project.AddDomainEvent(new ProjectCreatedEvent(project.Id, project.Name, ownerId));
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
// Business methods
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectUpdatedEvent(Id, Name, Description));
|
||||
}
|
||||
|
||||
public Epic CreateEpic(string name, string description, UserId createdBy)
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Cannot create epic in an archived project");
|
||||
|
||||
var epic = Epic.Create(name, description, this.Id, createdBy);
|
||||
_epics.Add(epic);
|
||||
|
||||
AddDomainEvent(new EpicCreatedEvent(epic.Id, epic.Name, this.Id));
|
||||
|
||||
return epic;
|
||||
}
|
||||
|
||||
public void Archive()
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Project is already archived");
|
||||
|
||||
Status = ProjectStatus.Archived;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectArchivedEvent(Id));
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
if (Status == ProjectStatus.Active)
|
||||
throw new DomainException("Project is already active");
|
||||
|
||||
Status = ProjectStatus.Active;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Story Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Story : Entity
|
||||
{
|
||||
public new StoryId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public EpicId EpicId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
private readonly List<WorkTask> _tasks = new();
|
||||
public IReadOnlyCollection<WorkTask> Tasks => _tasks.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Story()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
EpicId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Story Create(string title, string description, EpicId epicId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
return new Story
|
||||
{
|
||||
Id = StoryId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
EpicId = epicId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public WorkTask CreateTask(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var task = WorkTask.Create(title, description, this.Id, priority, createdBy);
|
||||
_tasks.Add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Task Entity (part of Project aggregate)
|
||||
/// Named "WorkTask" to avoid conflict with System.Threading.Tasks.Task
|
||||
/// </summary>
|
||||
public class WorkTask : Entity
|
||||
{
|
||||
public new TaskId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public StoryId StoryId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private WorkTask()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
StoryId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static WorkTask Create(string title, string description, StoryId storyId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
return new WorkTask
|
||||
{
|
||||
Id = TaskId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
StoryId = storyId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Domain</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Domain</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an epic is created
|
||||
/// </summary>
|
||||
public sealed record EpicCreatedEvent(
|
||||
EpicId EpicId,
|
||||
string EpicName,
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,11 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is archived
|
||||
/// </summary>
|
||||
public sealed record ProjectArchivedEvent(
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,13 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is created
|
||||
/// </summary>
|
||||
public sealed record ProjectCreatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string ProjectName,
|
||||
UserId CreatedBy
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,13 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is updated
|
||||
/// </summary>
|
||||
public sealed record ProjectUpdatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string Name,
|
||||
string Description
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception type for domain layer
|
||||
/// </summary>
|
||||
public class DomainException : Exception
|
||||
{
|
||||
public DomainException()
|
||||
{ }
|
||||
|
||||
public DomainException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public DomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception type for not found resources
|
||||
/// </summary>
|
||||
public class NotFoundException : Exception
|
||||
{
|
||||
public NotFoundException()
|
||||
{ }
|
||||
|
||||
public NotFoundException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public NotFoundException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
public NotFoundException(string entityName, object key)
|
||||
: base($"Entity '{entityName}' with key '{key}' was not found.")
|
||||
{ }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for Project aggregate
|
||||
/// </summary>
|
||||
public interface IProjectRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a project by its ID
|
||||
/// </summary>
|
||||
Task<Project?> GetByIdAsync(ProjectId id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a project by its unique key
|
||||
/// </summary>
|
||||
Task<Project?> GetByKeyAsync(string key, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all projects with pagination
|
||||
/// </summary>
|
||||
Task<List<Project>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets project containing specific epic
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithEpicAsync(EpicId epicId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets project containing specific story
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithStoryAsync(StoryId storyId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets project containing specific task
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithTaskAsync(TaskId taskId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new project
|
||||
/// </summary>
|
||||
Task AddAsync(Project project, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing project
|
||||
/// </summary>
|
||||
void Update(Project project);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a project
|
||||
/// </summary>
|
||||
void Delete(Project project);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work pattern interface
|
||||
/// Coordinates the work of multiple repositories and ensures transactional consistency
|
||||
/// </summary>
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves all changes made in this unit of work to the database
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The number of entities written to the database</returns>
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// EpicId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class EpicId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private EpicId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static EpicId Create() => new EpicId(Guid.NewGuid());
|
||||
public static EpicId Create(Guid value) => new EpicId(value);
|
||||
public static EpicId From(Guid value) => new EpicId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class ProjectId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private ProjectId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectId Create() => new ProjectId(Guid.NewGuid());
|
||||
public static ProjectId Create(Guid value) => new ProjectId(value);
|
||||
public static ProjectId From(Guid value) => new ProjectId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectKey Value Object (e.g., "COLA", "FLOW")
|
||||
/// </summary>
|
||||
public sealed class ProjectKey : ValueObject
|
||||
{
|
||||
public string Value { get; private set; }
|
||||
|
||||
private ProjectKey(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectKey Create(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new DomainException("Project key cannot be empty");
|
||||
|
||||
if (value.Length > 10)
|
||||
throw new DomainException("Project key cannot exceed 10 characters");
|
||||
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(value, "^[A-Z0-9]+$"))
|
||||
throw new DomainException("Project key must contain only uppercase letters and numbers");
|
||||
|
||||
return new ProjectKey(value);
|
||||
}
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectStatus Enumeration
|
||||
/// </summary>
|
||||
public sealed class ProjectStatus : Enumeration
|
||||
{
|
||||
public static readonly ProjectStatus Active = new(1, "Active");
|
||||
public static readonly ProjectStatus Archived = new(2, "Archived");
|
||||
public static readonly ProjectStatus OnHold = new(3, "On Hold");
|
||||
|
||||
private ProjectStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// StoryId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class StoryId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private StoryId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static StoryId Create() => new StoryId(Guid.NewGuid());
|
||||
public static StoryId Create(Guid value) => new StoryId(value);
|
||||
public static StoryId From(Guid value) => new StoryId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class TaskId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private TaskId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TaskId Create() => new TaskId(Guid.NewGuid());
|
||||
public static TaskId Create(Guid value) => new TaskId(value);
|
||||
public static TaskId From(Guid value) => new TaskId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskPriority Enumeration
|
||||
/// </summary>
|
||||
public sealed class TaskPriority : Enumeration
|
||||
{
|
||||
public static readonly TaskPriority Low = new(1, "Low");
|
||||
public static readonly TaskPriority Medium = new(2, "Medium");
|
||||
public static readonly TaskPriority High = new(3, "High");
|
||||
public static readonly TaskPriority Urgent = new(4, "Urgent");
|
||||
|
||||
private TaskPriority(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// UserId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class UserId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private UserId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static UserId Create() => new UserId(Guid.NewGuid());
|
||||
public static UserId Create(Guid value) => new UserId(value);
|
||||
public static UserId From(Guid value) => new UserId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// WorkItemStatus Enumeration (renamed from TaskStatus to avoid conflict with System.Threading.Tasks.TaskStatus)
|
||||
/// </summary>
|
||||
public sealed class WorkItemStatus : Enumeration
|
||||
{
|
||||
public static readonly WorkItemStatus ToDo = new(1, "To Do");
|
||||
public static readonly WorkItemStatus InProgress = new(2, "In Progress");
|
||||
public static readonly WorkItemStatus InReview = new(3, "In Review");
|
||||
public static readonly WorkItemStatus Done = new(4, "Done");
|
||||
public static readonly WorkItemStatus Blocked = new(5, "Blocked");
|
||||
|
||||
private WorkItemStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Infrastructure</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Infrastructure</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Application\ColaFlow.Modules.ProjectManagement.Application.csproj" />
|
||||
<ProjectReference Include="..\..\..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,298 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(PMDbContext))]
|
||||
[Migration("20251102220422_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("project_management")
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProjectId1")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("ProjectId1");
|
||||
|
||||
b.ToTable("Epics", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Projects", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<Guid>("EpicId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EpicId");
|
||||
|
||||
b.ToTable("Stories", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("StoryId");
|
||||
|
||||
b.ToTable("Tasks", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany("Epics")
|
||||
.HasForeignKey("ProjectId1");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.OwnsOne("ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.ProjectKey", "Key", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("Key");
|
||||
|
||||
b1.HasKey("ProjectId");
|
||||
|
||||
b1.HasIndex("Value")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("Projects", "project_management");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ProjectId");
|
||||
});
|
||||
|
||||
b.Navigation("Key")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("EpicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Navigation("Epics");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "project_management");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Projects",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
|
||||
Key = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
OwnerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Epics",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Priority = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
ProjectId1 = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Epics", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Epics_Projects_ProjectId",
|
||||
column: x => x.ProjectId,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Epics_Projects_ProjectId1",
|
||||
column: x => x.ProjectId1,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Stories",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
EpicId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Priority = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
EstimatedHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
ActualHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
AssigneeId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Stories", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Stories_Epics_EpicId",
|
||||
column: x => x.EpicId,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Epics",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tasks",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Priority = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
EstimatedHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
ActualHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
AssigneeId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tasks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Tasks_Stories_StoryId",
|
||||
column: x => x.StoryId,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Stories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Epics_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Epics",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Epics_ProjectId",
|
||||
schema: "project_management",
|
||||
table: "Epics",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Epics_ProjectId1",
|
||||
schema: "project_management",
|
||||
table: "Epics",
|
||||
column: "ProjectId1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Projects",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_Key",
|
||||
schema: "project_management",
|
||||
table: "Projects",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_OwnerId",
|
||||
schema: "project_management",
|
||||
table: "Projects",
|
||||
column: "OwnerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stories_AssigneeId",
|
||||
schema: "project_management",
|
||||
table: "Stories",
|
||||
column: "AssigneeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stories_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Stories",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stories_EpicId",
|
||||
schema: "project_management",
|
||||
table: "Stories",
|
||||
column: "EpicId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_AssigneeId",
|
||||
schema: "project_management",
|
||||
table: "Tasks",
|
||||
column: "AssigneeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Tasks",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_StoryId",
|
||||
schema: "project_management",
|
||||
table: "Tasks",
|
||||
column: "StoryId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tasks",
|
||||
schema: "project_management");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Stories",
|
||||
schema: "project_management");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Epics",
|
||||
schema: "project_management");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Projects",
|
||||
schema: "project_management");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(PMDbContext))]
|
||||
partial class PMDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("project_management")
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProjectId1")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("ProjectId1");
|
||||
|
||||
b.ToTable("Epics", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Projects", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<Guid>("EpicId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EpicId");
|
||||
|
||||
b.ToTable("Stories", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("StoryId");
|
||||
|
||||
b.ToTable("Tasks", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany("Epics")
|
||||
.HasForeignKey("ProjectId1");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.OwnsOne("ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.ProjectKey", "Key", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("Key");
|
||||
|
||||
b1.HasKey("ProjectId");
|
||||
|
||||
b1.HasIndex("Value")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("Projects", "project_management");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ProjectId");
|
||||
});
|
||||
|
||||
b.Navigation("Key")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("EpicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Navigation("Epics");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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 Epic entity
|
||||
/// </summary>
|
||||
public class EpicConfiguration : IEntityTypeConfiguration<Epic>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Epic> builder)
|
||||
{
|
||||
builder.ToTable("Epics");
|
||||
|
||||
// Primary key
|
||||
builder.HasKey("Id");
|
||||
|
||||
// Id conversion
|
||||
builder.Property(e => e.Id)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => EpicId.From(value))
|
||||
.IsRequired()
|
||||
.ValueGeneratedNever();
|
||||
|
||||
// ProjectId (foreign key)
|
||||
builder.Property(e => e.ProjectId)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => ProjectId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Basic properties
|
||||
builder.Property(e => e.Name)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.Description)
|
||||
.HasMaxLength(2000);
|
||||
|
||||
// Status enumeration
|
||||
builder.Property(e => e.Status)
|
||||
.HasConversion(
|
||||
s => s.Name,
|
||||
name => Enumeration.FromDisplayName<WorkItemStatus>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// Priority enumeration
|
||||
builder.Property(e => e.Priority)
|
||||
.HasConversion(
|
||||
p => p.Name,
|
||||
name => Enumeration.FromDisplayName<TaskPriority>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// CreatedBy conversion
|
||||
builder.Property(e => e.CreatedBy)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => UserId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Timestamps
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.UpdatedAt);
|
||||
|
||||
// Ignore navigation properties (DDD pattern - access through aggregate)
|
||||
builder.Ignore(e => e.Stories);
|
||||
|
||||
// Foreign key relationship to Project
|
||||
builder.HasOne<Project>()
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.ProjectId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(e => e.ProjectId);
|
||||
builder.HasIndex(e => e.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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();
|
||||
|
||||
// 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)
|
||||
// Note: We don't expose this as navigation property in DDD, epics are accessed through repository
|
||||
|
||||
// Indexes for performance
|
||||
builder.HasIndex(p => p.CreatedAt);
|
||||
builder.HasIndex(p => p.OwnerId);
|
||||
|
||||
// Ignore DomainEvents (handled separately)
|
||||
builder.Ignore(p => p.DomainEvents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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 Story entity
|
||||
/// </summary>
|
||||
public class StoryConfiguration : IEntityTypeConfiguration<Story>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Story> builder)
|
||||
{
|
||||
builder.ToTable("Stories");
|
||||
|
||||
// Primary key
|
||||
builder.HasKey("Id");
|
||||
|
||||
// Id conversion
|
||||
builder.Property(s => s.Id)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => StoryId.From(value))
|
||||
.IsRequired()
|
||||
.ValueGeneratedNever();
|
||||
|
||||
// EpicId (foreign key)
|
||||
builder.Property(s => s.EpicId)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => EpicId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Basic properties
|
||||
builder.Property(s => s.Title)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(s => s.Description)
|
||||
.HasMaxLength(4000);
|
||||
|
||||
// Status enumeration
|
||||
builder.Property(s => s.Status)
|
||||
.HasConversion(
|
||||
st => st.Name,
|
||||
name => Enumeration.FromDisplayName<WorkItemStatus>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// Priority enumeration
|
||||
builder.Property(s => s.Priority)
|
||||
.HasConversion(
|
||||
p => p.Name,
|
||||
name => Enumeration.FromDisplayName<TaskPriority>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// CreatedBy conversion
|
||||
builder.Property(s => s.CreatedBy)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => UserId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// AssigneeId (optional)
|
||||
builder.Property(s => s.AssigneeId)
|
||||
.HasConversion(
|
||||
id => id != null ? id.Value : (Guid?)null,
|
||||
value => value.HasValue ? UserId.From(value.Value) : null);
|
||||
|
||||
// Effort tracking
|
||||
builder.Property(s => s.EstimatedHours);
|
||||
builder.Property(s => s.ActualHours);
|
||||
|
||||
// Timestamps
|
||||
builder.Property(s => s.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(s => s.UpdatedAt);
|
||||
|
||||
// Ignore navigation properties (DDD pattern - access through aggregate)
|
||||
builder.Ignore(s => s.Tasks);
|
||||
|
||||
// Foreign key relationship to Epic
|
||||
builder.HasOne<Epic>()
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.EpicId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(s => s.EpicId);
|
||||
builder.HasIndex(s => s.AssigneeId);
|
||||
builder.HasIndex(s => s.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
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();
|
||||
|
||||
// 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);
|
||||
|
||||
// Foreign key relationship to Story
|
||||
builder.HasOne<Story>()
|
||||
.WithMany()
|
||||
.HasForeignKey(t => t.StoryId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(t => t.StoryId);
|
||||
builder.HasIndex(t => t.AssigneeId);
|
||||
builder.HasIndex(t => t.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Project Management Module DbContext
|
||||
/// </summary>
|
||||
public class PMDbContext : DbContext
|
||||
{
|
||||
public PMDbContext(DbContextOptions<PMDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<Epic> Epics => Set<Epic>();
|
||||
public DbSet<Story> Stories => Set<Story>();
|
||||
public DbSet<WorkTask> Tasks => Set<WorkTask>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Set default schema for this module (must be before configurations)
|
||||
modelBuilder.HasDefaultSchema("project_management");
|
||||
|
||||
// Apply all entity configurations from this assembly
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work implementation for ProjectManagement module
|
||||
/// </summary>
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly PMDbContext _context;
|
||||
|
||||
public UnitOfWork(PMDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Dispatch domain events before saving
|
||||
await DispatchDomainEventsAsync(cancellationToken);
|
||||
|
||||
// Save changes to database
|
||||
return await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Get all entities with domain events
|
||||
var domainEntities = _context.ChangeTracker
|
||||
.Entries<AggregateRoot>()
|
||||
.Where(x => x.Entity.DomainEvents.Any())
|
||||
.Select(x => x.Entity)
|
||||
.ToList();
|
||||
|
||||
// Get all domain events
|
||||
var domainEvents = domainEntities
|
||||
.SelectMany(x => x.DomainEvents)
|
||||
.ToList();
|
||||
|
||||
// Clear domain events from entities
|
||||
domainEntities.ForEach(entity => entity.ClearDomainEvents());
|
||||
|
||||
// TODO: Dispatch domain events to handlers
|
||||
// This will be implemented when we add MediatR
|
||||
// For now, we just clear the events
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Project repository implementation using EF Core
|
||||
/// </summary>
|
||||
public class ProjectRepository : IProjectRepository
|
||||
{
|
||||
private readonly PMDbContext _context;
|
||||
|
||||
public ProjectRepository(PMDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<Project?> GetByIdAsync(ProjectId id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Projects
|
||||
.Include(p => p.Epics)
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Project?> GetByKeyAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Projects
|
||||
.FirstOrDefaultAsync(p => p.Key.Value == key, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Project>> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using ColaFlow.Shared.Kernel.Modules;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Behaviors;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Project Management Module
|
||||
/// Responsible for managing projects, epics, stories, and tasks.
|
||||
/// </summary>
|
||||
public class ProjectManagementModule : IModule
|
||||
{
|
||||
public string Name => "ProjectManagement";
|
||||
|
||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Register DbContext
|
||||
var connectionString = configuration.GetConnectionString("PMDatabase");
|
||||
services.AddDbContext<PMDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
// Register MediatR handlers from Application assembly
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(typeof(CreateProjectCommand).Assembly);
|
||||
});
|
||||
|
||||
// Register FluentValidation validators
|
||||
services.AddValidatorsFromAssembly(typeof(CreateProjectCommand).Assembly);
|
||||
|
||||
// Register pipeline behaviors
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
|
||||
Console.WriteLine($"[{Name}] Module services registered");
|
||||
}
|
||||
|
||||
public void ConfigureApplication(IApplicationBuilder app)
|
||||
{
|
||||
// Configure module-specific middleware if needed
|
||||
Console.WriteLine($"[{Name}] Module application configured");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,31 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
|
||||
namespace ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all aggregate roots
|
||||
/// </summary>
|
||||
public abstract class AggregateRoot : Entity
|
||||
{
|
||||
private readonly List<DomainEvent> _domainEvents = new();
|
||||
|
||||
public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
protected AggregateRoot() : base()
|
||||
{
|
||||
}
|
||||
|
||||
protected AggregateRoot(Guid id) : base(id)
|
||||
{
|
||||
}
|
||||
|
||||
protected void AddDomainEvent(DomainEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public void ClearDomainEvents()
|
||||
{
|
||||
_domainEvents.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all entities
|
||||
/// </summary>
|
||||
public abstract class Entity
|
||||
{
|
||||
public Guid Id { get; protected set; }
|
||||
|
||||
protected Entity()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
protected Entity(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Entity other)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (GetType() != other.GetType())
|
||||
return false;
|
||||
|
||||
return Id == other.Id;
|
||||
}
|
||||
|
||||
public static bool operator ==(Entity? a, Entity? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Entity? a, Entity? b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for creating type-safe enumerations
|
||||
/// </summary>
|
||||
public abstract class Enumeration : IComparable
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
protected Enumeration(int id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public static IEnumerable<T> GetAll<T>() where T : Enumeration
|
||||
{
|
||||
var fields = typeof(T).GetFields(BindingFlags.Public |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.DeclaredOnly);
|
||||
|
||||
return fields.Select(f => f.GetValue(null)).Cast<T>();
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Enumeration otherValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeMatches = GetType().Equals(obj.GetType());
|
||||
var valueMatches = Id.Equals(otherValue.Id);
|
||||
|
||||
return typeMatches && valueMatches;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
|
||||
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
|
||||
{
|
||||
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
|
||||
return absoluteDifference;
|
||||
}
|
||||
|
||||
public static T FromValue<T>(int value) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public static T FromDisplayName<T>(string displayName) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration
|
||||
{
|
||||
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
|
||||
|
||||
if (matchingItem == null)
|
||||
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
|
||||
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public int CompareTo(object? other)
|
||||
{
|
||||
if (other == null) return 1;
|
||||
return Id.CompareTo(((Enumeration)other).Id);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user