From 6408c351c4e1d1a9b75691dc8af115c01286cd51 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 3 Jun 2024 21:33:15 +1000 Subject: [PATCH 01/87] 18 migrate from ca to vsa (#19) * Create new warehouse module * Moved Warehouse.Application to Warehouse.Module * Tidied up shared kernel * Moved domain into Warehouse module * Move Warehouse Endpoints into Warehouse Module * Move Warehouse Endpoints to Warehouse Module * Remove Order Application project * Move Order Domain to Order Module * Move Orders Endpoints to Orders Module --- ModularMonolith.sln | 61 ++++--------------- .../Behaviours/LoggingBehaviour.cs | 2 +- .../Behaviours/PerformanceBehaviour.cs | 5 +- .../Behaviours/UnhandledExceptionBehaviour.cs | 5 +- .../Behaviours/ValidationBehaviour.cs | 5 +- .../Common.SharedKernel.csproj | 8 ++- .../Domain/Identifiers/ProductId.cs | 3 +- .../EndpointRouteBuilderExt.cs | 5 +- .../Exceptions/NotFoundException.cs | 2 +- .../Exceptions/ValidationException.cs | 2 +- .../Persistence}/MoneyConfiguration.cs | 6 +- .../Features}/Customers/Address.cs | 2 +- .../Features}/Customers/Customer.cs | 2 +- .../Customers/CustomerCreatedEvent.cs | 2 +- .../Features/Customers/CustomerId.cs | 3 + .../Features}/Orders/LineItem.cs | 2 +- .../Features}/Orders/LineItemCreatedEvent.cs | 2 +- .../Features/Orders/LineItemId.cs | 3 + .../Features}/Orders/Order.cs | 4 +- .../Features}/Orders/OrderByIdSpec.cs | 2 +- .../Features}/Orders/OrderCreatedEvent.cs | 4 +- .../Module.Orders/Features/Orders/OrderId.cs | 4 ++ .../Orders/OrderReadyForShippingEvent.cs | 2 +- .../Features}/Orders/OrderSpec.cs | 2 +- .../Features}/Orders/OrderStatus.cs | 2 +- .../Module.Orders.csproj} | 8 ++- .../OrdersModule.cs | 12 ++-- .../Modules.Orders.Application.csproj | 13 ---- .../Customers/CustomerId.cs | 3 - .../Orders/LineItemId.cs | 3 - .../Modules.Orders.Domain/Orders/OrderId.cs | 4 -- .../Modules.Orders.Endpoints.csproj | 18 ------ .../Categories/CategoryService.cs | 19 ------ .../Common/Interfaces/IWarehouseDbContext.cs | 14 ----- .../DependencyInjection.cs | 29 --------- .../Categories/CategoryId.cs | 3 - .../Modules.Warehouse.Domain.csproj | 17 ------ .../Modules.Warehouse.Endpoints.csproj | 19 ------ .../WarehouseModule.cs | 28 --------- .../Modules.Warehouse.Infrastructure.csproj | 29 --------- .../Persistence/DepdendencyInjection.cs} | 17 +++--- .../Common}/Persistence/WarehouseDbContext.cs | 9 ++- .../WarehouseDbContextInitializer.cs | 10 +-- .../Features/Categories/CategoryService.cs | 19 ++++++ .../Features/Categories/Domain}/Category.cs | 2 +- .../Categories/Domain}/CategoryByIdSpec.cs | 2 +- .../Domain}/CategoryCreatedEvent.cs | 2 +- .../Features/Categories/Domain/CategoryId.cs | 3 + .../Categories/Domain}/ICategoryService.cs | 2 +- .../Persistence}/CategoryConfiguration.cs | 4 +- .../CreateProduct/CreateProductCommand.cs | 13 ++-- .../Products/Domain}/IProductRepository.cs | 2 +- .../Products/Domain}/LowStockEvent.cs | 2 +- .../Features/Products/Domain}/Product.cs | 4 +- .../Products/Domain}/ProductByIdSpec.cs | 2 +- .../Products/Domain}/ProductCreatedEvent.cs | 2 +- .../Features/Products/Domain}/Sku.cs | 2 +- .../Products/Endpoints}/ProductEndpoints.cs | 11 ++-- .../Persistence}/ProductConfiguration.cs | 8 +-- .../Features}/Products/ProductRepository.cs | 10 +-- .../Queries/GetProducts/GetProductsQuery.cs | 11 ++-- .../GlobalUsings.cs | 0 .../Modules.Warehouse.csproj} | 13 +++- .../Modules.Warehouse/WarehouseModule.cs | 45 ++++++++++++++ src/WebApi/Program.cs | 23 +++++-- src/WebApi/WebApi.csproj | 4 +- 66 files changed, 234 insertions(+), 347 deletions(-) rename src/{Modules/Warehouse/Modules.Warehouse.Application/Common => Common/Common.SharedKernel}/Behaviours/LoggingBehaviour.cs (89%) rename src/{Modules/Warehouse/Modules.Warehouse.Application/Common => Common/Common.SharedKernel}/Behaviours/PerformanceBehaviour.cs (90%) rename src/{Modules/Warehouse/Modules.Warehouse.Application/Common => Common/Common.SharedKernel}/Behaviours/UnhandledExceptionBehaviour.cs (86%) rename src/{Modules/Warehouse/Modules.Warehouse.Application/Common => Common/Common.SharedKernel}/Behaviours/ValidationBehaviour.cs (91%) rename src/{Modules/Warehouse/Modules.Warehouse.Endpoints/Extensions => Common/Common.SharedKernel}/EndpointRouteBuilderExt.cs (90%) rename src/{Modules/Warehouse/Modules.Warehouse.Application/Common => Common/Common.SharedKernel}/Exceptions/NotFoundException.cs (87%) rename src/{Modules/Warehouse/Modules.Warehouse.Application/Common => Common/Common.SharedKernel}/Exceptions/ValidationException.cs (89%) rename src/{Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations => Common/Common.SharedKernel/Persistence}/MoneyConfiguration.cs (64%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Customers/Address.cs (94%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Customers/Customer.cs (96%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Customers/CustomerCreatedEvent.cs (87%) create mode 100644 src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/LineItem.cs (97%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/LineItemCreatedEvent.cs (89%) create mode 100644 src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/Order.cs (98%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/OrderByIdSpec.cs (83%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/OrderCreatedEvent.cs (73%) create mode 100644 src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/OrderReadyForShippingEvent.cs (83%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/OrderSpec.cs (79%) rename src/Modules/Orders/{Modules.Orders.Domain => Module.Orders/Features}/Orders/OrderStatus.cs (71%) rename src/Modules/Orders/{Modules.Orders.Domain/Modules.Orders.Domain.csproj => Module.Orders/Module.Orders.csproj} (72%) rename src/Modules/Orders/{Modules.Orders.Endpoints => Module.Orders}/OrdersModule.cs (59%) delete mode 100644 src/Modules/Orders/Modules.Orders.Application/Modules.Orders.Application.csproj delete mode 100644 src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerId.cs delete mode 100644 src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemId.cs delete mode 100644 src/Modules/Orders/Modules.Orders.Domain/Orders/OrderId.cs delete mode 100644 src/Modules/Orders/Modules.Orders.Endpoints/Modules.Orders.Endpoints.csproj delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Application/Categories/CategoryService.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Application/Common/Interfaces/IWarehouseDbContext.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Application/DependencyInjection.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryId.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Modules.Warehouse.Infrastructure.csproj rename src/Modules/Warehouse/{Modules.Warehouse.Infrastructure/DependencyInjection.cs => Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs} (53%) rename src/Modules/Warehouse/{Modules.Warehouse.Infrastructure => Modules.Warehouse/Common}/Persistence/WarehouseDbContext.cs (84%) rename src/Modules/Warehouse/{Modules.Warehouse.Infrastructure => Modules.Warehouse/Common}/Persistence/WarehouseDbContextInitializer.cs (93%) create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Categories => Modules.Warehouse/Features/Categories/Domain}/Category.cs (94%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Categories => Modules.Warehouse/Features/Categories/Domain}/CategoryByIdSpec.cs (80%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Categories => Modules.Warehouse/Features/Categories/Domain}/CategoryCreatedEvent.cs (68%) create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Categories => Modules.Warehouse/Features/Categories/Domain}/ICategoryService.cs (59%) rename src/Modules/Warehouse/{Modules.Warehouse.Infrastructure/Persistence/Configurations => Modules.Warehouse/Features/Categories/Persistence}/CategoryConfiguration.cs (81%) rename src/Modules/Warehouse/{Modules.Warehouse.Application => Modules.Warehouse/Features}/Products/Commands/CreateProduct/CreateProductCommand.cs (65%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Products => Modules.Warehouse/Features/Products/Domain}/IProductRepository.cs (68%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Products => Modules.Warehouse/Features/Products/Domain}/LowStockEvent.cs (74%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Products => Modules.Warehouse/Features/Products/Domain}/Product.cs (95%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Products => Modules.Warehouse/Features/Products/Domain}/ProductByIdSpec.cs (83%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Products => Modules.Warehouse/Features/Products/Domain}/ProductCreatedEvent.cs (83%) rename src/Modules/Warehouse/{Modules.Warehouse.Domain/Products => Modules.Warehouse/Features/Products/Domain}/Sku.cs (86%) rename src/Modules/Warehouse/{Modules.Warehouse.Endpoints => Modules.Warehouse/Features/Products/Endpoints}/ProductEndpoints.cs (70%) rename src/Modules/Warehouse/{Modules.Warehouse.Infrastructure/Persistence/Configurations => Modules.Warehouse/Features/Products/Persistence}/ProductConfiguration.cs (80%) rename src/Modules/Warehouse/{Modules.Warehouse.Application => Modules.Warehouse/Features}/Products/ProductRepository.cs (59%) rename src/Modules/Warehouse/{Modules.Warehouse.Application => Modules.Warehouse/Features}/Products/Queries/GetProducts/GetProductsQuery.cs (67%) rename src/Modules/Warehouse/{Modules.Warehouse.Application => Modules.Warehouse}/GlobalUsings.cs (100%) rename src/Modules/Warehouse/{Modules.Warehouse.Application/Modules.Warehouse.Application.csproj => Modules.Warehouse/Modules.Warehouse.csproj} (54%) create mode 100644 src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index a63edcf..e9d7fc4 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -9,27 +9,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{9161 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Orders", "Orders", "{92D97012-135E-4AA1-AE1C-8C0803E9F6AC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Orders.Application", "src\Modules\Orders\Modules.Orders.Application\Modules.Orders.Application.csproj", "{C477D519-7B62-49DB-9B71-307BD637DA99}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Orders.Domain", "src\Modules\Orders\Modules.Orders.Domain\Modules.Orders.Domain.csproj", "{E2A903CC-01B1-4E30-9454-B0A4FE68133D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Orders.Endpoints", "src\Modules\Orders\Modules.Orders.Endpoints\Modules.Orders.Endpoints.csproj", "{841BBC7F-E258-4315-A5D0-09833D165125}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Warehouse", "Warehouse", "{D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Application", "src\Modules\Warehouse\Modules.Warehouse.Application\Modules.Warehouse.Application.csproj", "{A8A66142-1496-46D5-916B-A166CEB880E5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Domain", "src\Modules\Warehouse\Modules.Warehouse.Domain\Modules.Warehouse.Domain.csproj", "{9B7CCD18-6F88-4A12-A2CF-5D97C17F0470}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Endpoints", "src\Modules\Warehouse\Modules.Warehouse.Endpoints\Modules.Warehouse.Endpoints.csproj", "{17A97D45-868F-4BED-9C17-C6FD39C1384E}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "src\WebApi\WebApi.csproj", "{61A6637F-3057-4658-A8F7-945D66C945B1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{3E4B904F-1D6C-437B-8208-C6D17F995528}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.SharedKernel", "src\Common\Common.SharedKernel\Common.SharedKernel.csproj", "{C626352C-44BE-412D-B4A3-05E180A39BAF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Infrastructure", "src\Modules\Warehouse\Modules.Warehouse.Infrastructure\Modules.Warehouse.Infrastructure.csproj", "{A11F8E79-FD2B-458A-967A-46BFC1C1791D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse", "src\Modules\Warehouse\Modules.Warehouse\Modules.Warehouse.csproj", "{74ED43AC-972C-465B-AF8A-30A5532C408E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Orders", "src\Modules\Orders\Module.Orders\Module.Orders.csproj", "{6DBAEEED-701C-4C56-A761-D79986094759}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -40,30 +30,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C477D519-7B62-49DB-9B71-307BD637DA99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C477D519-7B62-49DB-9B71-307BD637DA99}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C477D519-7B62-49DB-9B71-307BD637DA99}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C477D519-7B62-49DB-9B71-307BD637DA99}.Release|Any CPU.Build.0 = Release|Any CPU - {E2A903CC-01B1-4E30-9454-B0A4FE68133D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2A903CC-01B1-4E30-9454-B0A4FE68133D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2A903CC-01B1-4E30-9454-B0A4FE68133D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2A903CC-01B1-4E30-9454-B0A4FE68133D}.Release|Any CPU.Build.0 = Release|Any CPU - {841BBC7F-E258-4315-A5D0-09833D165125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {841BBC7F-E258-4315-A5D0-09833D165125}.Debug|Any CPU.Build.0 = Debug|Any CPU - {841BBC7F-E258-4315-A5D0-09833D165125}.Release|Any CPU.ActiveCfg = Release|Any CPU - {841BBC7F-E258-4315-A5D0-09833D165125}.Release|Any CPU.Build.0 = Release|Any CPU - {A8A66142-1496-46D5-916B-A166CEB880E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A8A66142-1496-46D5-916B-A166CEB880E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A8A66142-1496-46D5-916B-A166CEB880E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A8A66142-1496-46D5-916B-A166CEB880E5}.Release|Any CPU.Build.0 = Release|Any CPU - {9B7CCD18-6F88-4A12-A2CF-5D97C17F0470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9B7CCD18-6F88-4A12-A2CF-5D97C17F0470}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9B7CCD18-6F88-4A12-A2CF-5D97C17F0470}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9B7CCD18-6F88-4A12-A2CF-5D97C17F0470}.Release|Any CPU.Build.0 = Release|Any CPU - {17A97D45-868F-4BED-9C17-C6FD39C1384E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {17A97D45-868F-4BED-9C17-C6FD39C1384E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {17A97D45-868F-4BED-9C17-C6FD39C1384E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {17A97D45-868F-4BED-9C17-C6FD39C1384E}.Release|Any CPU.Build.0 = Release|Any CPU {61A6637F-3057-4658-A8F7-945D66C945B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {61A6637F-3057-4658-A8F7-945D66C945B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {61A6637F-3057-4658-A8F7-945D66C945B1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -72,24 +38,23 @@ Global {C626352C-44BE-412D-B4A3-05E180A39BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {C626352C-44BE-412D-B4A3-05E180A39BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU {C626352C-44BE-412D-B4A3-05E180A39BAF}.Release|Any CPU.Build.0 = Release|Any CPU - {A11F8E79-FD2B-458A-967A-46BFC1C1791D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A11F8E79-FD2B-458A-967A-46BFC1C1791D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A11F8E79-FD2B-458A-967A-46BFC1C1791D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A11F8E79-FD2B-458A-967A-46BFC1C1791D}.Release|Any CPU.Build.0 = Release|Any CPU + {74ED43AC-972C-465B-AF8A-30A5532C408E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74ED43AC-972C-465B-AF8A-30A5532C408E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74ED43AC-972C-465B-AF8A-30A5532C408E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74ED43AC-972C-465B-AF8A-30A5532C408E}.Release|Any CPU.Build.0 = Release|Any CPU + {6DBAEEED-701C-4C56-A761-D79986094759}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DBAEEED-701C-4C56-A761-D79986094759}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DBAEEED-701C-4C56-A761-D79986094759}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DBAEEED-701C-4C56-A761-D79986094759}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} - {C477D519-7B62-49DB-9B71-307BD637DA99} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} - {E2A903CC-01B1-4E30-9454-B0A4FE68133D} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} - {841BBC7F-E258-4315-A5D0-09833D165125} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} - {A8A66142-1496-46D5-916B-A166CEB880E5} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} - {9B7CCD18-6F88-4A12-A2CF-5D97C17F0470} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} - {17A97D45-868F-4BED-9C17-C6FD39C1384E} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {61A6637F-3057-4658-A8F7-945D66C945B1} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} {3E4B904F-1D6C-437B-8208-C6D17F995528} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} {C626352C-44BE-412D-B4A3-05E180A39BAF} = {3E4B904F-1D6C-437B-8208-C6D17F995528} - {A11F8E79-FD2B-458A-967A-46BFC1C1791D} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} + {74ED43AC-972C-465B-AF8A-30A5532C408E} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} + {6DBAEEED-701C-4C56-A761-D79986094759} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} EndGlobalSection EndGlobal diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/LoggingBehaviour.cs b/src/Common/Common.SharedKernel/Behaviours/LoggingBehaviour.cs similarity index 89% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/LoggingBehaviour.cs rename to src/Common/Common.SharedKernel/Behaviours/LoggingBehaviour.cs index 12fa6bb..5cc1e5d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/LoggingBehaviour.cs +++ b/src/Common/Common.SharedKernel/Behaviours/LoggingBehaviour.cs @@ -1,7 +1,7 @@ using MediatR.Pipeline; using Microsoft.Extensions.Logging; -namespace Modules.Warehouse.Application.Common.Behaviours; +namespace Common.SharedKernel.Behaviours; public class LoggingBehaviour(ILogger logger) : IRequestPreProcessor diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/PerformanceBehaviour.cs b/src/Common/Common.SharedKernel/Behaviours/PerformanceBehaviour.cs similarity index 90% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/PerformanceBehaviour.cs rename to src/Common/Common.SharedKernel/Behaviours/PerformanceBehaviour.cs index 06a3703..d594b2b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/PerformanceBehaviour.cs +++ b/src/Common/Common.SharedKernel/Behaviours/PerformanceBehaviour.cs @@ -1,7 +1,8 @@ -using Microsoft.Extensions.Logging; +using MediatR; +using Microsoft.Extensions.Logging; using System.Diagnostics; -namespace Modules.Warehouse.Application.Common.Behaviours; +namespace Common.SharedKernel.Behaviours; public class PerformanceBehaviour(ILogger logger) : IPipelineBehavior diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs b/src/Common/Common.SharedKernel/Behaviours/UnhandledExceptionBehaviour.cs similarity index 86% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs rename to src/Common/Common.SharedKernel/Behaviours/UnhandledExceptionBehaviour.cs index 8089729..d567595 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs +++ b/src/Common/Common.SharedKernel/Behaviours/UnhandledExceptionBehaviour.cs @@ -1,6 +1,7 @@ -using Microsoft.Extensions.Logging; +using MediatR; +using Microsoft.Extensions.Logging; -namespace Modules.Warehouse.Application.Common.Behaviours; +namespace Common.SharedKernel.Behaviours; public class UnhandledExceptionBehaviour(ILogger logger) : IPipelineBehavior diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/ValidationBehaviour.cs b/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs similarity index 91% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/ValidationBehaviour.cs rename to src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs index 625e85d..7443f40 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Behaviours/ValidationBehaviour.cs +++ b/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs @@ -1,4 +1,7 @@ -namespace Modules.Warehouse.Application.Common.Behaviours; +using FluentValidation; +using MediatR; + +namespace Common.SharedKernel.Behaviours; public class ValidationBehaviour(IEnumerable> validators) : IPipelineBehavior diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index b151896..351aa61 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -7,7 +7,13 @@ - + + + + + + + diff --git a/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs b/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs index d8dd8e1..393c55c 100644 --- a/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs +++ b/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs @@ -1,3 +1,4 @@ namespace Common.SharedKernel.Domain.Identifiers; -public record ProductId(Guid Value); \ No newline at end of file +// TODO: Consider moving into relevant modules +public record ProductId(Guid Value); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Extensions/EndpointRouteBuilderExt.cs b/src/Common/Common.SharedKernel/EndpointRouteBuilderExt.cs similarity index 90% rename from src/Modules/Warehouse/Modules.Warehouse.Endpoints/Extensions/EndpointRouteBuilderExt.cs rename to src/Common/Common.SharedKernel/EndpointRouteBuilderExt.cs index 3e34c0a..5c00e42 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Extensions/EndpointRouteBuilderExt.cs +++ b/src/Common/Common.SharedKernel/EndpointRouteBuilderExt.cs @@ -1,4 +1,7 @@ -namespace Modules.Warehouse.Endpoints.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Common.SharedKernel; public static class EndpointRouteBuilderExt { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Exceptions/NotFoundException.cs b/src/Common/Common.SharedKernel/Exceptions/NotFoundException.cs similarity index 87% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Common/Exceptions/NotFoundException.cs rename to src/Common/Common.SharedKernel/Exceptions/NotFoundException.cs index 0c5392f..c5ac114 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Exceptions/NotFoundException.cs +++ b/src/Common/Common.SharedKernel/Exceptions/NotFoundException.cs @@ -1,4 +1,4 @@ -namespace Modules.Warehouse.Application.Common.Exceptions; +namespace Common.SharedKernel.Exceptions; public class NotFoundException : Exception { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Exceptions/ValidationException.cs b/src/Common/Common.SharedKernel/Exceptions/ValidationException.cs similarity index 89% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Common/Exceptions/ValidationException.cs rename to src/Common/Common.SharedKernel/Exceptions/ValidationException.cs index 037f370..f57d739 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Exceptions/ValidationException.cs +++ b/src/Common/Common.SharedKernel/Exceptions/ValidationException.cs @@ -1,6 +1,6 @@ using FluentValidation.Results; -namespace Modules.Warehouse.Application.Common.Exceptions; +namespace Common.SharedKernel.Exceptions; public class ValidationException() : Exception("One or more validation failures have occurred.") { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/MoneyConfiguration.cs b/src/Common/Common.SharedKernel/Persistence/MoneyConfiguration.cs similarity index 64% rename from src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/MoneyConfiguration.cs rename to src/Common/Common.SharedKernel/Persistence/MoneyConfiguration.cs index ead652f..fcf61df 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/MoneyConfiguration.cs +++ b/src/Common/Common.SharedKernel/Persistence/MoneyConfiguration.cs @@ -1,11 +1,11 @@ using Common.SharedKernel.Domain.Entities; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Modules.Warehouse.Infrastructure.Persistence.Configurations; +namespace Common.SharedKernel.Persistence; -internal static class MoneyConfiguration +public static class MoneyConfiguration { - internal static void BuildAction(ComplexPropertyBuilder priceBuilder) + public static void BuildAction(ComplexPropertyBuilder priceBuilder) { priceBuilder.Property(m => m.Currency) .HasConversion(currency => currency.Symbol, value => new Currency(value)) diff --git a/src/Modules/Orders/Modules.Orders.Domain/Customers/Address.cs b/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs similarity index 94% rename from src/Modules/Orders/Modules.Orders.Domain/Customers/Address.cs rename to src/Modules/Orders/Module.Orders/Features/Customers/Address.cs index 439a04b..28e44f6 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Customers/Address.cs +++ b/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs @@ -1,6 +1,6 @@ using Ardalis.GuardClauses; -namespace Modules.Orders.Domain.Customers; +namespace Module.Orders.Features.Customers; public record Address { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Customers/Customer.cs b/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs similarity index 96% rename from src/Modules/Orders/Modules.Orders.Domain/Customers/Customer.cs rename to src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs index 1c6bf34..4b2e96f 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Customers/Customer.cs +++ b/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs @@ -1,7 +1,7 @@ using Ardalis.GuardClauses; using Common.SharedKernel.Domain.Base; -namespace Modules.Orders.Domain.Customers; +namespace Module.Orders.Features.Customers; public class Customer : AggregateRoot { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs similarity index 87% rename from src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerCreatedEvent.cs rename to src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs index d62abed..154a5d9 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Orders.Domain.Customers; +namespace Module.Orders.Features.Customers; public record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : DomainEvent { diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs new file mode 100644 index 0000000..89496f1 --- /dev/null +++ b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs @@ -0,0 +1,3 @@ +namespace Module.Orders.Features.Customers; + +public record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItem.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs similarity index 97% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/LineItem.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs index 59e4e14..3b5ec4b 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItem.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs @@ -4,7 +4,7 @@ using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public class LineItem : Entity { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs similarity index 89% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemCreatedEvent.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs index b5cb39f..71cd244 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : DomainEvent { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs new file mode 100644 index 0000000..61f9d5a --- /dev/null +++ b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs @@ -0,0 +1,3 @@ +namespace Module.Orders.Features.Orders; + +public record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/Order.cs b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs similarity index 98% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/Order.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/Order.cs index aa4c484..1e4b23f 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/Order.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs @@ -3,9 +3,9 @@ using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; -using Modules.Orders.Domain.Customers; +using Module.Orders.Features.Customers; -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public class Order : AggregateRoot { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderByIdSpec.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs similarity index 83% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/OrderByIdSpec.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs index 7288908..c9b974d 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderByIdSpec.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public class OrderByIdSpec : OrderSpec, ISingleResultSpecification { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs similarity index 73% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/OrderCreatedEvent.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs index 04734d2..f1d8f25 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs @@ -1,7 +1,7 @@ using Common.SharedKernel.Domain.Base; -using Modules.Orders.Domain.Customers; +using Module.Orders.Features.Customers; -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : DomainEvent { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs new file mode 100644 index 0000000..0cdab95 --- /dev/null +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs @@ -0,0 +1,4 @@ +namespace Module.Orders.Features.Orders; + +public record OrderId(Guid Value); + diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs similarity index 83% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/OrderReadyForShippingEvent.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs index b00dd2b..4cc8e43 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public record OrderReadyForShippingEvent(OrderId OrderId) : DomainEvent { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderSpec.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs similarity index 79% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/OrderSpec.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs index dd24a47..e5c2fe3 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderSpec.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public class OrderSpec : Specification { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderStatus.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs similarity index 71% rename from src/Modules/Orders/Modules.Orders.Domain/Orders/OrderStatus.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs index 346e7a4..dafce7a 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderStatus.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs @@ -1,4 +1,4 @@ -namespace Modules.Orders.Domain.Orders; +namespace Module.Orders.Features.Orders; public enum OrderStatus { diff --git a/src/Modules/Orders/Modules.Orders.Domain/Modules.Orders.Domain.csproj b/src/Modules/Orders/Module.Orders/Module.Orders.csproj similarity index 72% rename from src/Modules/Orders/Modules.Orders.Domain/Modules.Orders.Domain.csproj rename to src/Modules/Orders/Module.Orders/Module.Orders.csproj index 383d33d..d257f01 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Modules.Orders.Domain.csproj +++ b/src/Modules/Orders/Module.Orders/Module.Orders.csproj @@ -7,12 +7,18 @@ - + + + + + + + diff --git a/src/Modules/Orders/Modules.Orders.Endpoints/OrdersModule.cs b/src/Modules/Orders/Module.Orders/OrdersModule.cs similarity index 59% rename from src/Modules/Orders/Modules.Orders.Endpoints/OrdersModule.cs rename to src/Modules/Orders/Module.Orders/OrdersModule.cs index 12f52ae..830ae48 100644 --- a/src/Modules/Orders/Modules.Orders.Endpoints/OrdersModule.cs +++ b/src/Modules/Orders/Module.Orders/OrdersModule.cs @@ -1,12 +1,16 @@ -namespace Modules.Orders.Endpoints; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Module.Orders; public static class OrdersModule { - public static void AddOrdersServices(this IServiceCollection services) + public static void AddOrders(this IServiceCollection services) { } - public static void UseOrdersModule(this WebApplication app) + public static void UseOrders(this WebApplication app) { app.MapGet("/api/orders", () => { @@ -24,4 +28,4 @@ public static void UseOrdersModule(this WebApplication app) } } -record OrderDto(string Name, string Description); \ No newline at end of file +record OrderDto(string Name, string Description); diff --git a/src/Modules/Orders/Modules.Orders.Application/Modules.Orders.Application.csproj b/src/Modules/Orders/Modules.Orders.Application/Modules.Orders.Application.csproj deleted file mode 100644 index 54a3fe7..0000000 --- a/src/Modules/Orders/Modules.Orders.Application/Modules.Orders.Application.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - diff --git a/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerId.cs b/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerId.cs deleted file mode 100644 index c6b1778..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Orders.Domain.Customers; - -public record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemId.cs b/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemId.cs deleted file mode 100644 index 0f17f6f..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Orders.Domain.Orders; - -public record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderId.cs b/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderId.cs deleted file mode 100644 index 2525f52..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderId.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Modules.Orders.Domain.Orders; - -public record OrderId(Guid Value); - diff --git a/src/Modules/Orders/Modules.Orders.Endpoints/Modules.Orders.Endpoints.csproj b/src/Modules/Orders/Modules.Orders.Endpoints/Modules.Orders.Endpoints.csproj deleted file mode 100644 index 467e75d..0000000 --- a/src/Modules/Orders/Modules.Orders.Endpoints/Modules.Orders.Endpoints.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - library - net8.0 - enable - enable - - - - - - - - - - - diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Categories/CategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse.Application/Categories/CategoryService.cs deleted file mode 100644 index bc70f3d..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Categories/CategoryService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Modules.Warehouse.Application.Common.Interfaces; -using Modules.Warehouse.Domain.Categories; - -namespace Modules.Warehouse.Application.Categories; - -public class CategoryRepository : ICategoryRepository -{ - private readonly IWarehouseDbContext _dbContext; - - public CategoryRepository(IWarehouseDbContext dbContext) - { - _dbContext = dbContext; - } - - public bool CategoryExists(string categoryName) - { - return _dbContext.Categories.Any(c => c.Name == categoryName); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Interfaces/IWarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Interfaces/IWarehouseDbContext.cs deleted file mode 100644 index e935e25..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Interfaces/IWarehouseDbContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Modules.Warehouse.Domain.Categories; -using Modules.Warehouse.Domain.Products; - -namespace Modules.Warehouse.Application.Common.Interfaces; - -public interface IWarehouseDbContext -{ - public DbSet Categories { get; } - - public DbSet Products { get; } - - public Task SaveChangesAsync(CancellationToken cancellationToken); -} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/DependencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse.Application/DependencyInjection.cs deleted file mode 100644 index 84c1242..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/DependencyInjection.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Modules.Warehouse.Application.Common.Behaviours; -using Modules.Warehouse.Application.Products; -using Modules.Warehouse.Domain.Products; - -namespace Modules.Warehouse.Application; - -public static class DependencyInjection -{ - public static IServiceCollection AddApplication(this IServiceCollection services) - { - var applicationAssembly = typeof(DependencyInjection).Assembly; - - services.AddValidatorsFromAssembly(applicationAssembly); - - services.AddMediatR(config => - { - config.RegisterServicesFromAssembly(applicationAssembly); - config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); - config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); - config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); - }); - - services.AddTransient(); - - return services; - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryId.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryId.cs deleted file mode 100644 index 17953a5..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Warehouse.Domain.Categories; - -public record CategoryId(Guid Value); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj b/src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj deleted file mode 100644 index 422e651..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj deleted file mode 100644 index 09a8133..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - library - net8.0 - enable - enable - - - - - - - - - - - - diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs deleted file mode 100644 index fa29dd2..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Modules.Warehouse.Application; -using Modules.Warehouse.Infrastructure; -using Modules.Warehouse.Infrastructure.Persistence; - -namespace Modules.Warehouse.Endpoints; - -public static class WarehouseModule -{ - public static void AddWarehouseServices(this IServiceCollection services, IConfiguration configuration) - { - services.AddApplication(); - services.AddInfrastructure(configuration); - } - - public static async Task UseWarehouseModule(this WebApplication app) - { - if (app.Environment.IsDevelopment()) - { - // Initialise and seed database - using var scope = app.Services.CreateScope(); - var initializer = scope.ServiceProvider.GetRequiredService(); - await initializer.InitializeAsync(); - await initializer.SeedAsync(); - } - - app.MapProductEndpoints(); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Modules.Warehouse.Infrastructure.csproj b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Modules.Warehouse.Infrastructure.csproj deleted file mode 100644 index c2477f7..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Modules.Warehouse.Infrastructure.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/DependencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs similarity index 53% rename from src/Modules/Warehouse/Modules.Warehouse.Infrastructure/DependencyInjection.cs rename to src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index f7e42ad..06598de 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/DependencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -1,29 +1,26 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Modules.Warehouse.Application.Common.Interfaces; -using Modules.Warehouse.Infrastructure.Persistence; -namespace Modules.Warehouse.Infrastructure; +namespace Modules.Warehouse.Common.Persistence; -public static class DependencyInjection +internal static class DepdendencyInjection { - public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config) + internal static void AddPersistence(this IServiceCollection services, IConfiguration config) { var connectionString = config.GetConnectionString("DefaultConnection"); - services.AddDbContext(options => + services.AddDbContext(options => options.UseSqlServer(connectionString, builder => { - builder.MigrationsAssembly(typeof(DependencyInjection).Assembly.FullName); + builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); builder.EnableRetryOnFailure(); })); //services.AddSingleton(); + // TODO: Consider moving to up.ps1 services.AddScoped(); // services.AddScoped(); // services.AddScoped(); // services.AddScoped(); - - return services; } } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs similarity index 84% rename from src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContext.cs rename to src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index d0383c6..608909a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -1,11 +1,10 @@ using Microsoft.EntityFrameworkCore; -using Modules.Warehouse.Application.Common.Interfaces; -using Modules.Warehouse.Domain.Categories; -using Modules.Warehouse.Domain.Products; +using Modules.Warehouse.Features.Categories.Domain; +using Modules.Warehouse.Features.Products.Domain; -namespace Modules.Warehouse.Infrastructure.Persistence; +namespace Modules.Warehouse.Common.Persistence; -public class WarehouseDbContext : DbContext, IWarehouseDbContext +public class WarehouseDbContext : DbContext { // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; // private readonly OutboxInterceptor _outboxInterceptor; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContextInitializer.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs similarity index 93% rename from src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContextInitializer.cs rename to src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs index a3273f7..2c8fced 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContextInitializer.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs @@ -2,12 +2,12 @@ using Common.SharedKernel.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Modules.Warehouse.Application.Categories; -using Modules.Warehouse.Application.Products; -using Modules.Warehouse.Domain.Categories; -using Modules.Warehouse.Domain.Products; +using Modules.Warehouse.Features.Categories; +using Modules.Warehouse.Features.Categories.Domain; +using Modules.Warehouse.Features.Products; +using Modules.Warehouse.Features.Products.Domain; -namespace Modules.Warehouse.Infrastructure.Persistence; +namespace Modules.Warehouse.Common.Persistence; public class WarehouseDbContextInitializer { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs new file mode 100644 index 0000000..060e1fd --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs @@ -0,0 +1,19 @@ +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Features.Categories.Domain; + +namespace Modules.Warehouse.Features.Categories; + +public class CategoryRepository : ICategoryRepository +{ + private readonly WarehouseDbContext _dbContext; + + public CategoryRepository(WarehouseDbContext dbContext) + { + _dbContext = dbContext; + } + + public bool CategoryExists(string categoryName) + { + return _dbContext.Categories.Any(c => c.Name == categoryName); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/Category.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs similarity index 94% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/Category.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs index 256b997..60080b3 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/Category.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs @@ -2,7 +2,7 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Exceptions; -namespace Modules.Warehouse.Domain.Categories; +namespace Modules.Warehouse.Features.Categories.Domain; public class Category : AggregateRoot { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs similarity index 80% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryByIdSpec.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs index a7c4adf..e052355 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Modules.Warehouse.Domain.Categories; +namespace Modules.Warehouse.Features.Categories.Domain; public class CategoryByIdSpec : Specification, ISingleResultSpecification { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs similarity index 68% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryCreatedEvent.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs index ff8b930..3b2ab22 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs @@ -1,5 +1,5 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Warehouse.Domain.Categories; +namespace Modules.Warehouse.Features.Categories.Domain; public record CategoryCreatedEvent(CategoryId Id, string Name) : DomainEvent; \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs new file mode 100644 index 0000000..c28fe3f --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs @@ -0,0 +1,3 @@ +namespace Modules.Warehouse.Features.Categories.Domain; + +public record CategoryId(Guid Value); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/ICategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs similarity index 59% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/ICategoryService.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs index 172d046..d356b39 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/ICategoryService.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs @@ -1,4 +1,4 @@ -namespace Modules.Warehouse.Domain.Categories; +namespace Modules.Warehouse.Features.Categories.Domain; public interface ICategoryRepository { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/CategoryConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Persistence/CategoryConfiguration.cs similarity index 81% rename from src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/CategoryConfiguration.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Persistence/CategoryConfiguration.cs index 30757ff..3522007 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/CategoryConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Persistence/CategoryConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Warehouse.Domain.Categories; +using Modules.Warehouse.Features.Categories.Domain; -namespace Modules.Warehouse.Infrastructure.Persistence.Configurations; +namespace Modules.Warehouse.Features.Categories.Persistence; internal class CategoryConfiguration : IEntityTypeConfiguration { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Products/Commands/CreateProduct/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs similarity index 65% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Products/Commands/CreateProduct/CreateProductCommand.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs index d5c014a..62b5262 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Products/Commands/CreateProduct/CreateProductCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs @@ -1,19 +1,19 @@ using Common.SharedKernel.Domain.Entities; -using Modules.Warehouse.Application.Common.Interfaces; -using Modules.Warehouse.Domain.Categories; -using Modules.Warehouse.Domain.Products; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Features.Categories.Domain; +using Modules.Warehouse.Features.Products.Domain; -namespace Modules.Warehouse.Application.Products.Commands.CreateProduct; +namespace Modules.Warehouse.Features.Products.Commands.CreateProduct; public record CreateProductCommand(string Name, decimal Amount, string Sku, Guid CategoryId) : IRequest; public class CreateProductCommandHandler : IRequestHandler { - private readonly IWarehouseDbContext _dbContext; + private readonly WarehouseDbContext _dbContext; private readonly IProductRepository _productRepository; - public CreateProductCommandHandler(IWarehouseDbContext dbContext, IProductRepository productRepository, CancellationToken cancellationToken) + public CreateProductCommandHandler(WarehouseDbContext dbContext, IProductRepository productRepository, CancellationToken cancellationToken) { _dbContext = dbContext; _productRepository = productRepository; @@ -23,6 +23,7 @@ public async Task Handle(CreateProductCommand request, CancellationToken cancell { var money = new Money(Currency.Default, request.Amount); var sku = Sku.Create(request.Sku); + ArgumentNullException.ThrowIfNull(sku); var categoryId = new CategoryId(request.CategoryId); var product = Product.Create(request.Name, money, sku, categoryId, _productRepository); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/IProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs similarity index 68% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Products/IProductRepository.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs index b3fe000..5fcb65b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/IProductRepository.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs @@ -1,4 +1,4 @@ -namespace Modules.Warehouse.Domain.Products; +namespace Modules.Warehouse.Features.Products.Domain; public interface IProductRepository { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/LowStockEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs similarity index 74% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Products/LowStockEvent.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs index 66bf314..af591d4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/LowStockEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Identifiers; -namespace Modules.Warehouse.Domain.Products; +namespace Modules.Warehouse.Features.Products.Domain; public record LowStockEvent(ProductId ProductId) : DomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs similarity index 95% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Product.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs index faf43f1..49998cc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs @@ -3,9 +3,9 @@ using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; -using Modules.Warehouse.Domain.Categories; +using Modules.Warehouse.Features.Categories.Domain; -namespace Modules.Warehouse.Domain.Products; +namespace Modules.Warehouse.Features.Products.Domain; public class Product : AggregateRoot { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs similarity index 83% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductByIdSpec.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs index 920d3b0..e7694fa 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs @@ -1,7 +1,7 @@ using Ardalis.Specification; using Common.SharedKernel.Domain.Identifiers; -namespace Modules.Warehouse.Domain.Products; +namespace Modules.Warehouse.Features.Products.Domain; public class ProductByIdSpec : Specification, ISingleResultSpecification { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs similarity index 83% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductCreatedEvent.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs index f3f767b..51719a3 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs @@ -1,7 +1,7 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Identifiers; -namespace Modules.Warehouse.Domain.Products; +namespace Modules.Warehouse.Features.Products.Domain; public record ProductCreatedEvent(ProductId Product, string ProductName) : DomainEvent { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Sku.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs similarity index 86% rename from src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Sku.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs index e5ad954..92806db 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Sku.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs @@ -1,4 +1,4 @@ -namespace Modules.Warehouse.Domain.Products; +namespace Modules.Warehouse.Features.Products.Domain; public record Sku { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/ProductEndpoints.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs similarity index 70% rename from src/Modules/Warehouse/Modules.Warehouse.Endpoints/ProductEndpoints.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs index feabfb0..b4dfe0e 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/ProductEndpoints.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs @@ -1,9 +1,10 @@ -using MediatR; -using Modules.Warehouse.Application.Products.Commands.CreateProduct; -using Modules.Warehouse.Application.Products.Queries.GetProducts; -using Modules.Warehouse.Endpoints.Extensions; +using Common.SharedKernel; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Modules.Warehouse.Features.Products.Commands.CreateProduct; +using Modules.Warehouse.Features.Products.Queries.GetProducts; -namespace Modules.Warehouse.Endpoints; +namespace Modules.Warehouse.Features.Products.Endpoints; public static class ProductEndpoints { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/ProductConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Persistence/ProductConfiguration.cs similarity index 80% rename from src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/ProductConfiguration.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Persistence/ProductConfiguration.cs index dd9a734..bb2577a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/ProductConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Persistence/ProductConfiguration.cs @@ -1,10 +1,10 @@ -using Common.SharedKernel.Domain.Entities; -using Common.SharedKernel.Domain.Identifiers; +using Common.SharedKernel.Domain.Identifiers; +using Common.SharedKernel.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Warehouse.Domain.Products; +using Modules.Warehouse.Features.Products.Domain; -namespace Modules.Warehouse.Infrastructure.Persistence.Configurations; +namespace Modules.Warehouse.Features.Products.Persistence; internal class ProductConfiguration : IEntityTypeConfiguration { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Products/ProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs similarity index 59% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Products/ProductRepository.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs index 9341d0b..4e460e6 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Products/ProductRepository.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs @@ -1,14 +1,14 @@ using Microsoft.EntityFrameworkCore; -using Modules.Warehouse.Application.Common.Interfaces; -using Modules.Warehouse.Domain.Products; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Features.Products.Domain; -namespace Modules.Warehouse.Application.Products; +namespace Modules.Warehouse.Features.Products; public class ProductRepository : IProductRepository { - private readonly IWarehouseDbContext _dbContext; + private readonly WarehouseDbContext _dbContext; - public ProductRepository(IWarehouseDbContext dbContext) + public ProductRepository(WarehouseDbContext dbContext) { _dbContext = dbContext; } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Products/Queries/GetProducts/GetProductsQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs similarity index 67% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Products/Queries/GetProducts/GetProductsQuery.cs rename to src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs index 66cd1c9..74816d9 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Products/Queries/GetProducts/GetProductsQuery.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs @@ -1,8 +1,7 @@ -using MediatR; -using Microsoft.EntityFrameworkCore; -using Modules.Warehouse.Application.Common.Interfaces; +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Common.Persistence; -namespace Modules.Warehouse.Application.Products.Queries.GetProducts; +namespace Modules.Warehouse.Features.Products.Queries.GetProducts; public record GetProductsQuery : IRequest>; @@ -10,9 +9,9 @@ public record ProductDto(Guid Id, string Sku, string Name, decimal Price); public class GetProductsQueryHandler : IRequestHandler> { - private readonly IWarehouseDbContext _dbContext; + private readonly WarehouseDbContext _dbContext; - public GetProductsQueryHandler(IWarehouseDbContext dbContext) + public GetProductsQueryHandler(WarehouseDbContext dbContext) { _dbContext = dbContext; } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/GlobalUsings.cs b/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs similarity index 100% rename from src/Modules/Warehouse/Modules.Warehouse.Application/GlobalUsings.cs rename to src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Modules.Warehouse.Application.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj similarity index 54% rename from src/Modules/Warehouse/Modules.Warehouse.Application/Modules.Warehouse.Application.csproj rename to src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index a5e661e..4c98b37 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Modules.Warehouse.Application.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -13,10 +13,21 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs new file mode 100644 index 0000000..ce78a35 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Features.Products; +using Modules.Warehouse.Features.Products.Domain; +using Modules.Warehouse.Features.Products.Endpoints; + +namespace Modules.Warehouse; + +public static class WarehouseModule +{ + public static void AddWarehouse(this IServiceCollection services, IConfiguration configuration) + { + var applicationAssembly = typeof(WarehouseModule).Assembly; + + services.AddValidatorsFromAssembly(applicationAssembly); + + // TODO: Check we can call this multiple times + services.AddMediatR(config => + { + config.RegisterServicesFromAssembly(applicationAssembly); + }); + + // Todo: Move to feature DI + services.AddTransient(); + } + + public static async Task UseWarehouse(this WebApplication app) + { + // TODO: Refactor to up.ps1 + if (app.Environment.IsDevelopment()) + { + // Initialise and seed database + using var scope = app.Services.CreateScope(); + var initializer = scope.ServiceProvider.GetRequiredService(); + await initializer.InitializeAsync(); + await initializer.SeedAsync(); + } + + // TODO: Move to feature DI + app.MapProductEndpoints(); + } +} diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index e351a2b..54c2614 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,5 +1,6 @@ -using Modules.Orders.Endpoints; -using Modules.Warehouse.Endpoints; +using Common.SharedKernel.Behaviours; +using Module.Orders; +using Modules.Warehouse; var builder = WebApplication.CreateBuilder(args); @@ -8,8 +9,18 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddOrdersServices(); -builder.Services.AddWarehouseServices(builder.Configuration); +// Common MediatR behaviors across all modules +builder.Services.AddMediatR(config => +{ + config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); + config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); + config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); +}); + + + +builder.Services.AddOrders(); +builder.Services.AddWarehouse(builder.Configuration); var app = builder.Build(); @@ -22,7 +33,7 @@ app.UseHttpsRedirection(); -app.UseOrdersModule(); -await app.UseWarehouseModule(); +app.UseOrders(); +await app.UseWarehouse(); app.Run(); diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index aace12e..90140a7 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -15,8 +15,8 @@ - - + + From 5344f0fd11d082f211014da21724e01f40f3d55c Mon Sep 17 00:00:00 2001 From: Daniel Mackay <2636640+danielmackay@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:02:34 +1000 Subject: [PATCH 02/87] Started on module discovery --- src/Common/Common.SharedKernel/IModule.cs | 9 +++++ .../Modules.Warehouse/WarehouseModule.cs | 12 ++++++- src/WebApi/Host/ModuleDiscovery.cs | 34 +++++++++++++++++++ src/WebApi/Program.cs | 17 ++++++---- 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 src/Common/Common.SharedKernel/IModule.cs create mode 100644 src/WebApi/Host/ModuleDiscovery.cs diff --git a/src/Common/Common.SharedKernel/IModule.cs b/src/Common/Common.SharedKernel/IModule.cs new file mode 100644 index 0000000..9dd9f81 --- /dev/null +++ b/src/Common/Common.SharedKernel/IModule.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Common.SharedKernel; + +public interface IModule +{ + void AddServices(IServiceCollection services, IConfiguration configuration); +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index ce78a35..f0ee0e6 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Builder; +using Common.SharedKernel; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -43,3 +44,12 @@ public static async Task UseWarehouse(this WebApplication app) app.MapProductEndpoints(); } } + +public class WarehouseModule2 : IModule +{ + public void AddServices(IServiceCollection services, IConfiguration configuration) + { + services.AddWarehouse(configuration); + } + +} diff --git a/src/WebApi/Host/ModuleDiscovery.cs b/src/WebApi/Host/ModuleDiscovery.cs new file mode 100644 index 0000000..c08fff3 --- /dev/null +++ b/src/WebApi/Host/ModuleDiscovery.cs @@ -0,0 +1,34 @@ +using Common.SharedKernel; +using System.Reflection; + +namespace Web.Host; + +public static class ModuleDiscovery +{ + private static readonly Type ModuleType = typeof(IModule); + + public static void AddModules(this IServiceCollection services, IConfiguration config, params Assembly[] assemblies) + { + if (assemblies.Length == 0) + { + throw new ArgumentException("At least one assembly must be provided.", nameof(assemblies)); + } + + var moduleTypes = GetModuleTypes(assemblies); + + foreach (var type in moduleTypes) + { + var method = GetAddServicesMethod(type); + method?.Invoke(null, [services, config]); + } + } + + private static IEnumerable GetModuleTypes(params Assembly[] assemblies) => + assemblies.SelectMany(x => x.GetTypes()) + .Where(x => ModuleType.IsAssignableFrom(x) && + x is { IsInterface: false, IsAbstract: false }); + + private static MethodInfo? GetAddServicesMethod(Type type) => + type.GetMethod(nameof(IModule.AddServices), + BindingFlags.Static | BindingFlags.Public); +} diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 54c2614..f1e2983 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,8 +1,13 @@ using Common.SharedKernel.Behaviours; using Module.Orders; using Modules.Warehouse; +using System.Reflection; +using Web.Host; +var appAssembly = Assembly.GetExecutingAssembly(); var builder = WebApplication.CreateBuilder(args); +var moduleAssemblies = new[] { typeof(OrdersModule).Assembly, typeof(WarehouseModule).Assembly }; + // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -12,15 +17,15 @@ // Common MediatR behaviors across all modules builder.Services.AddMediatR(config => { + config.RegisterServicesFromAssembly(appAssembly); config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); }); - - -builder.Services.AddOrders(); -builder.Services.AddWarehouse(builder.Configuration); +// builder.Services.AddOrders(); +// builder.Services.AddWarehouse(builder.Configuration); +builder.Services.AddModules(builder.Configuration, moduleAssemblies); var app = builder.Build(); @@ -33,7 +38,7 @@ app.UseHttpsRedirection(); -app.UseOrders(); -await app.UseWarehouse(); +// app.UseOrders(); +// await app.UseWarehouse(); app.Run(); From aa00035fbbd25e13edff3595457de67819c564cd Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:20:42 +1000 Subject: [PATCH 03/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Tidied=20up=20module?= =?UTF-8?q?s=20so=20that=20only=20module=20registration=20code=20is=20publ?= =?UTF-8?q?ic=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tidied up modules so that only module registration code is public --- .../Module.Orders/Features/Customers/Address.cs | 12 ++++++------ .../Module.Orders/Features/Customers/Customer.cs | 6 +++--- .../Features/Customers/CustomerCreatedEvent.cs | 4 ++-- .../Module.Orders/Features/Customers/CustomerId.cs | 2 +- .../Orders/Module.Orders/Features/Orders/LineItem.cs | 2 +- .../Features/Orders/LineItemCreatedEvent.cs | 2 +- .../Module.Orders/Features/Orders/LineItemId.cs | 2 +- .../Orders/Module.Orders/Features/Orders/Order.cs | 2 +- .../Module.Orders/Features/Orders/OrderByIdSpec.cs | 4 ++-- .../Features/Orders/OrderCreatedEvent.cs | 4 ++-- .../Orders/Module.Orders/Features/Orders/OrderId.cs | 3 +-- .../Features/Orders/OrderReadyForShippingEvent.cs | 4 ++-- .../Module.Orders/Features/Orders/OrderSpec.cs | 4 ++-- .../Module.Orders/Features/Orders/OrderStatus.cs | 2 +- .../Common/Persistence/WarehouseDbContext.cs | 2 +- .../Persistence/WarehouseDbContextInitializer.cs | 2 +- .../Features/Categories/CategoryService.cs | 2 +- .../Features/Categories/Domain/Category.cs | 2 +- .../Features/Categories/Domain/CategoryByIdSpec.cs | 2 +- .../Categories/Domain/CategoryCreatedEvent.cs | 2 +- .../Features/Categories/Domain/CategoryId.cs | 2 +- .../Features/Categories/Domain/ICategoryService.cs | 2 +- .../Commands/CreateProduct/CreateProductCommand.cs | 4 ++-- .../Features/Products/Domain/IProductRepository.cs | 2 +- .../Features/Products/Domain/LowStockEvent.cs | 2 +- .../Features/Products/Domain/Product.cs | 2 +- .../Features/Products/Domain/ProductByIdSpec.cs | 4 ++-- .../Features/Products/Domain/ProductCreatedEvent.cs | 2 +- .../Features/Products/Domain/Sku.cs | 2 +- .../Features/Products/Endpoints/ProductEndpoints.cs | 2 +- .../Features/Products/ProductRepository.cs | 2 +- .../Products/Queries/GetProducts/GetProductsQuery.cs | 6 +++--- src/WebApi/Program.cs | 2 -- 33 files changed, 48 insertions(+), 51 deletions(-) diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs b/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs index 28e44f6..9a240a0 100644 --- a/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs +++ b/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs @@ -2,16 +2,16 @@ namespace Module.Orders.Features.Customers; -public record Address +internal record Address { - public string Line1 { get; } - public string? Line2 { get; } - public string City { get; } + internal string Line1 { get; } + internal string? Line2 { get; } + internal string City { get; } public string State { get; } public string ZipCode { get; } public string Country { get; } - public Address(string line1, string? line2, string city, string state, string zipCode, string country) + internal Address(string line1, string? line2, string city, string state, string zipCode, string country) { Guard.Against.NullOrWhiteSpace(line1); Guard.Against.NullOrWhiteSpace(city); @@ -26,4 +26,4 @@ public Address(string line1, string? line2, string city, string state, string zi ZipCode = zipCode; Country = country; } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs b/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs index 4b2e96f..a434c75 100644 --- a/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs +++ b/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs @@ -3,7 +3,7 @@ namespace Module.Orders.Features.Customers; -public class Customer : AggregateRoot +internal class Customer : AggregateRoot { public string Email { get; private set; } = null!; @@ -15,7 +15,7 @@ public class Customer : AggregateRoot private Customer() { } - public static Customer Create(string email, string firstName, string lastName) + internal static Customer Create(string email, string firstName, string lastName) { Guard.Against.NullOrWhiteSpace(email); @@ -40,4 +40,4 @@ public void UpdateAddress(Address address) { Address = address; } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs index 154a5d9..66aaf42 100644 --- a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs @@ -2,8 +2,8 @@ namespace Module.Orders.Features.Customers; -public record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : DomainEvent +internal record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : DomainEvent { public static CustomerCreatedEvent Create(Customer customer) => new CustomerCreatedEvent(customer.Id, customer.FirstName, customer.LastName); -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs index 89496f1..7efb2a1 100644 --- a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs +++ b/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs @@ -1,3 +1,3 @@ namespace Module.Orders.Features.Customers; -public record CustomerId(Guid Value); +internal record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs index 3b5ec4b..7c0645c 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs @@ -6,7 +6,7 @@ namespace Module.Orders.Features.Orders; -public class LineItem : Entity +internal class LineItem : Entity { public required OrderId OrderId { get; init; } diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs index 71cd244..dd5fffb 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs @@ -2,7 +2,7 @@ namespace Module.Orders.Features.Orders; -public record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : DomainEvent +internal record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : DomainEvent { public LineItemCreatedEvent(LineItem lineItem) : this(lineItem.Id, lineItem.OrderId) { } diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs index 61f9d5a..1226a3f 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs @@ -1,3 +1,3 @@ namespace Module.Orders.Features.Orders; -public record LineItemId(Guid Value); +internal record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs index 1e4b23f..1e73279 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs @@ -7,7 +7,7 @@ namespace Module.Orders.Features.Orders; -public class Order : AggregateRoot +internal class Order : AggregateRoot { private readonly List _lineItems = new(); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs index c9b974d..f98f61f 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs @@ -2,10 +2,10 @@ namespace Module.Orders.Features.Orders; -public class OrderByIdSpec : OrderSpec, ISingleResultSpecification +internal class OrderByIdSpec : OrderSpec, ISingleResultSpecification { public OrderByIdSpec(OrderId id) : base() { Query.Where(i => i.Id == id); } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs index f1d8f25..45ed2ed 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs @@ -3,7 +3,7 @@ namespace Module.Orders.Features.Orders; -public record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : DomainEvent +internal record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : DomainEvent { public static OrderCreatedEvent Create(Order order) => new(order.Id, order.CustomerId); -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs index 0cdab95..5efe6d9 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs @@ -1,4 +1,3 @@ namespace Module.Orders.Features.Orders; -public record OrderId(Guid Value); - +internal record OrderId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs index 4cc8e43..780d388 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs @@ -2,7 +2,7 @@ namespace Module.Orders.Features.Orders; -public record OrderReadyForShippingEvent(OrderId OrderId) : DomainEvent +internal record OrderReadyForShippingEvent(OrderId OrderId) : DomainEvent { public static OrderReadyForShippingEvent Create(Order order) => new(order.Id); -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs index e5c2fe3..3d7f580 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs @@ -2,10 +2,10 @@ namespace Module.Orders.Features.Orders; -public class OrderSpec : Specification +internal class OrderSpec : Specification { public OrderSpec() { Query.Include(i => i.LineItems); } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs index dafce7a..91c690c 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs @@ -1,6 +1,6 @@ namespace Module.Orders.Features.Orders; -public enum OrderStatus +internal enum OrderStatus { None = 0, PendingPayment = 1, diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index 608909a..5b590db 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -4,7 +4,7 @@ namespace Modules.Warehouse.Common.Persistence; -public class WarehouseDbContext : DbContext +internal class WarehouseDbContext : DbContext { // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; // private readonly OutboxInterceptor _outboxInterceptor; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs index 2c8fced..19a08b1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs @@ -9,7 +9,7 @@ namespace Modules.Warehouse.Common.Persistence; -public class WarehouseDbContextInitializer +internal class WarehouseDbContextInitializer { private readonly ILogger _logger; private readonly WarehouseDbContext _dbContext; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs index 060e1fd..94a794b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs @@ -3,7 +3,7 @@ namespace Modules.Warehouse.Features.Categories; -public class CategoryRepository : ICategoryRepository +internal class CategoryRepository : ICategoryRepository { private readonly WarehouseDbContext _dbContext; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs index 60080b3..a1796e1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs @@ -4,7 +4,7 @@ namespace Modules.Warehouse.Features.Categories.Domain; -public class Category : AggregateRoot +internal class Category : AggregateRoot { public string Name { get; private set; } = default!; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs index e052355..287d6bf 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs @@ -2,7 +2,7 @@ namespace Modules.Warehouse.Features.Categories.Domain; -public class CategoryByIdSpec : Specification, ISingleResultSpecification +internal class CategoryByIdSpec : Specification, ISingleResultSpecification { public CategoryByIdSpec(CategoryId id) : base() { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs index 3b2ab22..535a412 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs @@ -2,4 +2,4 @@ namespace Modules.Warehouse.Features.Categories.Domain; -public record CategoryCreatedEvent(CategoryId Id, string Name) : DomainEvent; \ No newline at end of file +internal record CategoryCreatedEvent(CategoryId Id, string Name) : DomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs index c28fe3f..8c0bf51 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs @@ -1,3 +1,3 @@ namespace Modules.Warehouse.Features.Categories.Domain; -public record CategoryId(Guid Value); +internal record CategoryId(Guid Value); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs index d356b39..57d9e1e 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs @@ -1,6 +1,6 @@ namespace Modules.Warehouse.Features.Categories.Domain; -public interface ICategoryRepository +internal interface ICategoryRepository { bool CategoryExists(string categoryName); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs index 62b5262..466c5f2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs @@ -5,10 +5,10 @@ namespace Modules.Warehouse.Features.Products.Commands.CreateProduct; -public record CreateProductCommand(string Name, decimal Amount, string Sku, Guid CategoryId) +internal record CreateProductCommand(string Name, decimal Amount, string Sku, Guid CategoryId) : IRequest; -public class CreateProductCommandHandler : IRequestHandler +internal class CreateProductCommandHandler : IRequestHandler { private readonly WarehouseDbContext _dbContext; private readonly IProductRepository _productRepository; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs index 5fcb65b..685b71b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs @@ -1,6 +1,6 @@ namespace Modules.Warehouse.Features.Products.Domain; -public interface IProductRepository +internal interface IProductRepository { public Task SkuExistsAsync(Sku sku); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs index af591d4..0362851 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs @@ -3,4 +3,4 @@ namespace Modules.Warehouse.Features.Products.Domain; -public record LowStockEvent(ProductId ProductId) : DomainEvent; +internal record LowStockEvent(ProductId ProductId) : DomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs index 49998cc..23f7ede 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs @@ -7,7 +7,7 @@ namespace Modules.Warehouse.Features.Products.Domain; -public class Product : AggregateRoot +internal class Product : AggregateRoot { private const int LowStockThreshold = 5; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs index e7694fa..dfd9d79 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs @@ -3,10 +3,10 @@ namespace Modules.Warehouse.Features.Products.Domain; -public class ProductByIdSpec : Specification, ISingleResultSpecification +internal class ProductByIdSpec : Specification, ISingleResultSpecification { public ProductByIdSpec(ProductId id) : base() { Query.Where(i => i.Id == id); } -} \ No newline at end of file +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs index 51719a3..056d003 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs @@ -3,7 +3,7 @@ namespace Modules.Warehouse.Features.Products.Domain; -public record ProductCreatedEvent(ProductId Product, string ProductName) : DomainEvent +internal record ProductCreatedEvent(ProductId Product, string ProductName) : DomainEvent { public static ProductCreatedEvent Create(Product product) => new(product.Id, product.Name); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs index 92806db..1c42acb 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs @@ -1,6 +1,6 @@ namespace Modules.Warehouse.Features.Products.Domain; -public record Sku +internal record Sku { private const int DefaultLength = 8; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs index b4dfe0e..b3573b4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs @@ -6,7 +6,7 @@ namespace Modules.Warehouse.Features.Products.Endpoints; -public static class ProductEndpoints +internal static class ProductEndpoints { public static void MapProductEndpoints(this WebApplication app) { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs index 4e460e6..5c063b1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs @@ -4,7 +4,7 @@ namespace Modules.Warehouse.Features.Products; -public class ProductRepository : IProductRepository +internal class ProductRepository : IProductRepository { private readonly WarehouseDbContext _dbContext; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs index 74816d9..e0d2eca 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs @@ -3,11 +3,11 @@ namespace Modules.Warehouse.Features.Products.Queries.GetProducts; -public record GetProductsQuery : IRequest>; +internal record GetProductsQuery : IRequest>; -public record ProductDto(Guid Id, string Sku, string Name, decimal Price); +internal record ProductDto(Guid Id, string Sku, string Name, decimal Price); -public class GetProductsQueryHandler : IRequestHandler> +internal class GetProductsQueryHandler : IRequestHandler> { private readonly WarehouseDbContext _dbContext; diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 54c2614..4ae5ee6 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -17,8 +17,6 @@ config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); }); - - builder.Services.AddOrders(); builder.Services.AddWarehouse(builder.Configuration); From 83471372a07a1a063bda6dd6f27f34f99cb64fbf Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:49:03 +1000 Subject: [PATCH 04/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Added=20customer=20m?= =?UTF-8?q?odule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ModularMonolith.sln | 15 +++++++++ README.md | 31 +++++++++++++++++++ .../Discovery/EndpointDiscovery.cs | 0 .../Features/Customers/Address.cs | 2 +- .../Features/Customers/Customer.cs | 2 +- .../Customers/CustomerCreatedEvent.cs | 2 +- .../Features/Customers/CustomerId.cs | 3 ++ .../Module.Customers/Module.Customers.csproj | 21 +++++++++++++ .../{Customers => Orders}/CustomerId.cs | 0 .../Module.Orders/Features/Orders/Order.cs | 2 -- 10 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/Common/Common.SharedKernel/Discovery/EndpointDiscovery.cs rename src/Modules/{Orders/Module.Orders => Customers/Module.Customers}/Features/Customers/Address.cs (94%) rename src/Modules/{Orders/Module.Orders => Customers/Module.Customers}/Features/Customers/Customer.cs (95%) rename src/Modules/{Orders/Module.Orders => Customers/Module.Customers}/Features/Customers/CustomerCreatedEvent.cs (86%) create mode 100644 src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs create mode 100644 src/Modules/Customers/Module.Customers/Module.Customers.csproj rename src/Modules/Orders/Module.Orders/Features/{Customers => Orders}/CustomerId.cs (100%) diff --git a/ModularMonolith.sln b/ModularMonolith.sln index e9d7fc4..bab81da 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -21,6 +21,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse", "src\Mo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Orders", "src\Modules\Orders\Module.Orders\Module.Orders.csproj", "{6DBAEEED-701C-4C56-A761-D79986094759}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{DC7140A1-214E-41D5-B856-8E131E2FACA7}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Customers", "Customers", "{41494B34-2A0F-4AF6-96DA-C25AEBAA424C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Customers", "src\Modules\Customers\Module.Customers\Module.Customers.csproj", "{EF044D0C-0014-45B1-95F8-C46F34B9ED1F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -46,6 +55,10 @@ Global {6DBAEEED-701C-4C56-A761-D79986094759}.Debug|Any CPU.Build.0 = Debug|Any CPU {6DBAEEED-701C-4C56-A761-D79986094759}.Release|Any CPU.ActiveCfg = Release|Any CPU {6DBAEEED-701C-4C56-A761-D79986094759}.Release|Any CPU.Build.0 = Release|Any CPU + {EF044D0C-0014-45B1-95F8-C46F34B9ED1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF044D0C-0014-45B1-95F8-C46F34B9ED1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF044D0C-0014-45B1-95F8-C46F34B9ED1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF044D0C-0014-45B1-95F8-C46F34B9ED1F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -56,5 +69,7 @@ Global {C626352C-44BE-412D-B4A3-05E180A39BAF} = {3E4B904F-1D6C-437B-8208-C6D17F995528} {74ED43AC-972C-465B-AF8A-30A5532C408E} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {6DBAEEED-701C-4C56-A761-D79986094759} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} + {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} + {EF044D0C-0014-45B1-95F8-C46F34B9ED1F} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 67f1476..807bb4e 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,34 @@ Responsible for Warehouse and Inventory Management - Restocking - When do we need to restock? - Backorders - When do we put items on back order? - Product Management + +## Business Invariants + +Customers: +- Can register with the website +- Must have a unique email address +- Must have an address + +Orders: +- An order must be associated with a customer +- The order total must always be correct +- The order tax must always be correct +- Payment must be completed for the order to be placed (FUTURE: Consider splitting payments to it's own module) + +Products: +- A customer must be able to search products +- A product can be given one or more categories + +Warehouse: +- Products can be loaded into the warehouse and have their location tracked +- When an order is placed the stock level must be updated +- If there is not enough stock the order must be put on back order +- When stock is below a certain threshold the warehouse will be notified to restock +- An order can be dispatched from the warehouse + +Shipping: +- Once an order is dispatched from the warehouse the shipping company must be notified +- The all 'stops' must be tracked until delivered +- The customer must be able to track their order +- Once delivered the order must be marked as complete and the customer will be notified +- Must be able to calculate the shipping cost based on time to delivery diff --git a/src/Common/Common.SharedKernel/Discovery/EndpointDiscovery.cs b/src/Common/Common.SharedKernel/Discovery/EndpointDiscovery.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs b/src/Modules/Customers/Module.Customers/Features/Customers/Address.cs similarity index 94% rename from src/Modules/Orders/Module.Orders/Features/Customers/Address.cs rename to src/Modules/Customers/Module.Customers/Features/Customers/Address.cs index 9a240a0..71b9d28 100644 --- a/src/Modules/Orders/Module.Orders/Features/Customers/Address.cs +++ b/src/Modules/Customers/Module.Customers/Features/Customers/Address.cs @@ -1,6 +1,6 @@ using Ardalis.GuardClauses; -namespace Module.Orders.Features.Customers; +namespace Module.Customers.Features.Customers; internal record Address { diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs b/src/Modules/Customers/Module.Customers/Features/Customers/Customer.cs similarity index 95% rename from src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs rename to src/Modules/Customers/Module.Customers/Features/Customers/Customer.cs index a434c75..5a94a1d 100644 --- a/src/Modules/Orders/Module.Orders/Features/Customers/Customer.cs +++ b/src/Modules/Customers/Module.Customers/Features/Customers/Customer.cs @@ -1,7 +1,7 @@ using Ardalis.GuardClauses; using Common.SharedKernel.Domain.Base; -namespace Module.Orders.Features.Customers; +namespace Module.Customers.Features.Customers; internal class Customer : AggregateRoot { diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs b/src/Modules/Customers/Module.Customers/Features/Customers/CustomerCreatedEvent.cs similarity index 86% rename from src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs rename to src/Modules/Customers/Module.Customers/Features/Customers/CustomerCreatedEvent.cs index 66aaf42..53043a1 100644 --- a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Customers/Module.Customers/Features/Customers/CustomerCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Orders.Features.Customers; +namespace Module.Customers.Features.Customers; internal record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : DomainEvent { diff --git a/src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs b/src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs new file mode 100644 index 0000000..39e2ec8 --- /dev/null +++ b/src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs @@ -0,0 +1,3 @@ +namespace Module.Customers.Features.Customers; + +internal record CustomerId(Guid Value); diff --git a/src/Modules/Customers/Module.Customers/Module.Customers.csproj b/src/Modules/Customers/Module.Customers/Module.Customers.csproj new file mode 100644 index 0000000..73dc8e7 --- /dev/null +++ b/src/Modules/Customers/Module.Customers/Module.Customers.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/CustomerId.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Features/Customers/CustomerId.cs rename to src/Modules/Orders/Module.Orders/Features/Orders/CustomerId.cs diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs index 1e73279..2ae9e1e 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs @@ -15,8 +15,6 @@ internal class Order : AggregateRoot public required CustomerId CustomerId { get; init; } - public Customer? Customer { get; set; } - // TODO: Check FE overrides this public Money AmountPaid { get; private set; } = null!; From 2435c96742046c216368cc5e86a1f66dc3859c1f Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:01:31 +1000 Subject: [PATCH 05/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20Money=20more?= =?UTF-8?q?=20resilient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common.SharedKernel.csproj | 1 + .../Domain/Entities/Money.cs | 56 +++++++++++++++---- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 351aa61..2a7eec4 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Common/Common.SharedKernel/Domain/Entities/Money.cs b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs index d143962..74407f1 100644 --- a/src/Common/Common.SharedKernel/Domain/Entities/Money.cs +++ b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs @@ -1,4 +1,6 @@ -namespace Common.SharedKernel.Domain.Entities; +using Throw; + +namespace Common.SharedKernel.Domain.Entities; public record Money(Currency Currency, decimal Amount) { @@ -6,15 +8,45 @@ public record Money(Currency Currency, decimal Amount) public static Money Zero => Default; - public static Money operator +(Money left, Money right) => new Money(left.Currency, left.Amount + right.Amount); - - public static Money operator -(Money left, Money right) => new Money(left.Currency, left.Amount - right.Amount); - - public static bool operator <(Money left, Money right) => left.Amount < right.Amount; - - public static bool operator <=(Money left, Money right) => left.Amount <= right.Amount; - - public static bool operator >(Money left, Money right) => left.Amount > right.Amount; - - public static bool operator >=(Money left, Money right) => left.Amount >= right.Amount; + public static Money operator +(Money left, Money right) + { + AssertValidCurrencies(left, right); + return left with { Amount = left.Amount + right.Amount }; + } + + public static Money operator -(Money left, Money right) + { + AssertValidCurrencies(left, right); + return left with { Amount = left.Amount - right.Amount }; + } + + public static bool operator <(Money left, Money right) + { + AssertValidCurrencies(left, right); + return left.Amount < right.Amount; + } + + public static bool operator <=(Money left, Money right) + { + AssertValidCurrencies(left, right); + return left.Amount <= right.Amount; + } + + public static bool operator >(Money left, Money right) + { + return left.Amount > right.Amount; + } + + public static bool operator >=(Money left, Money right) + { + return left.Amount >= right.Amount; + } + + public static Money operator *(Money left, Money right) + { + AssertValidCurrencies(left, right); + return left with { Amount = left.Amount * right.Amount }; + } + + private static void AssertValidCurrencies(Money left, Money right) => left.Throw().IfNotEquals(right); } From 1f75988d7f9cf69aae89d054ddd41907fdabbdc3 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:59:09 +1000 Subject: [PATCH 06/87] =?UTF-8?q?=F0=9F=A7=AA=20Added=20unit=20tests=20for?= =?UTF-8?q?=20LineItem.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ModularMonolith.sln | 7 + .../Domain/Entities/Money.cs | 4 +- .../Module.Orders.Tests/GlobalUsings.cs | 1 + .../Module.Orders.Tests/LineItemTests.cs | 159 ++++++++++++++++++ .../Module.Orders.Tests.csproj | 30 ++++ .../Orders/Module.Orders/AssemblyInfo.cs | 3 + .../Module.Orders/Features/Orders/LineItem.cs | 20 ++- .../Module.Orders/Features/Orders/Order.cs | 2 +- 8 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 src/Modules/Orders/Module.Orders.Tests/GlobalUsings.cs create mode 100644 src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs create mode 100644 src/Modules/Orders/Module.Orders.Tests/Module.Orders.Tests.csproj create mode 100644 src/Modules/Orders/Module.Orders/AssemblyInfo.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index bab81da..8e2ee25 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -30,6 +30,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Customers", "Customers", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Customers", "src\Modules\Customers\Module.Customers\Module.Customers.csproj", "{EF044D0C-0014-45B1-95F8-C46F34B9ED1F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Orders.Tests", "src\Modules\Orders\Module.Orders.Tests\Module.Orders.Tests.csproj", "{4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,10 @@ Global {EF044D0C-0014-45B1-95F8-C46F34B9ED1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF044D0C-0014-45B1-95F8-C46F34B9ED1F}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF044D0C-0014-45B1-95F8-C46F34B9ED1F}.Release|Any CPU.Build.0 = Release|Any CPU + {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -71,5 +77,6 @@ Global {6DBAEEED-701C-4C56-A761-D79986094759} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} {EF044D0C-0014-45B1-95F8-C46F34B9ED1F} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} + {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} EndGlobalSection EndGlobal diff --git a/src/Common/Common.SharedKernel/Domain/Entities/Money.cs b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs index 74407f1..58e49f6 100644 --- a/src/Common/Common.SharedKernel/Domain/Entities/Money.cs +++ b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs @@ -4,6 +4,8 @@ namespace Common.SharedKernel.Domain.Entities; public record Money(Currency Currency, decimal Amount) { + public static Money Create(decimal amount) => new(Currency.Default, amount); + public static Money Default => new(Currency.Default, 0); public static Money Zero => Default; @@ -48,5 +50,5 @@ public record Money(Currency Currency, decimal Amount) return left with { Amount = left.Amount * right.Amount }; } - private static void AssertValidCurrencies(Money left, Money right) => left.Throw().IfNotEquals(right); + private static void AssertValidCurrencies(Money left, Money right) => left.Currency.Throw().IfNotEquals(right.Currency); } diff --git a/src/Modules/Orders/Module.Orders.Tests/GlobalUsings.cs b/src/Modules/Orders/Module.Orders.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/Modules/Orders/Module.Orders.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs new file mode 100644 index 0000000..8b11c42 --- /dev/null +++ b/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs @@ -0,0 +1,159 @@ +using Common.SharedKernel.Domain.Entities; +using Common.SharedKernel.Domain.Identifiers; +using FluentAssertions; +using Module.Orders.Features.Orders; + +namespace Module.Orders.Tests; + +public class LineItemTests +{ + [Fact] + public void Create_ValidParameters_ShouldCreateLineItem() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 2; + + // Act + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Assert + lineItem.OrderId.Should().Be(orderId); + lineItem.ProductId.Should().Be(productId); + lineItem.Price.Should().Be(price); + lineItem.Quantity.Should().Be(quantity); + } + + [Fact] + public void Create_NegativePrice_ShouldThrow() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(-100m); + var quantity = 2; + + // Act + var act = () => LineItem.Create(orderId, productId, price, quantity); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ZeroQuantity_ShouldThrow() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 0; + + // Act + var act = () => LineItem.Create(orderId, productId, price, quantity); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Total_ShouldReturnCorrectAmount() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 2; + + // Act + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Assert + lineItem.Total.Amount.Should().Be(200m); + } + + [Fact] + public void Tax_ShouldReturnCorrectAmount() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 2; + + // Act + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Assert + lineItem.Tax.Amount.Should().Be(20m); + } + + [Fact] + public void TotalIncludingTax_ShouldReturnCorrectAmount() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 2; + + // Act + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Assert + lineItem.TotalIncludingTax.Amount.Should().Be(220m); + } + + [Fact] + public void AddQuantity_ShouldIncreaseQuantity() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 2; + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Act + lineItem.AddQuantity(3); + + // Assert + lineItem.Quantity.Should().Be(5); + } + + [Fact] + public void RemoveQuantity_ShouldDecreaseQuantity() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 5; + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Act + lineItem.RemoveQuantity(3); + + // Assert + lineItem.Quantity.Should().Be(2); + } + + [Fact] + public void RemoveQuantity_TooMany_ShouldThrow() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = Money.Create(100m); + var quantity = 2; + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Act + var act = () => lineItem.RemoveQuantity(3); + + // Assert + act.Should().Throw(); + } +} diff --git a/src/Modules/Orders/Module.Orders.Tests/Module.Orders.Tests.csproj b/src/Modules/Orders/Module.Orders.Tests/Module.Orders.Tests.csproj new file mode 100644 index 0000000..b887391 --- /dev/null +++ b/src/Modules/Orders/Module.Orders.Tests/Module.Orders.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Modules/Orders/Module.Orders/AssemblyInfo.cs b/src/Modules/Orders/Module.Orders/AssemblyInfo.cs new file mode 100644 index 0000000..963c590 --- /dev/null +++ b/src/Modules/Orders/Module.Orders/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Module.Orders.Tests")] diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs index 7c0645c..f82403a 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs @@ -1,34 +1,38 @@ using Ardalis.GuardClauses; using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; -using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; +using Throw; namespace Module.Orders.Features.Orders; internal class LineItem : Entity { + private const decimal TaxRate = 0.1m; + public required OrderId OrderId { get; init; } public required ProductId ProductId { get; init; } - //public Product? Product { get; init; } - - // Detatch price from product to capture the price at the time of purchase + // Detach price from product to capture the price at the time of purchase public required Money Price { get; init; } public int Quantity { get; private set; } - public Money Total => new(Price.Currency, Price.Amount * Quantity); + public Money Total => Price with { Amount = Price.Amount * Quantity }; + + public Money Tax => Total * Total with { Amount = TaxRate }; + + public Money TotalIncludingTax => Total + Tax; private LineItem() { } - // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money) be passed via the constructor + // NOTE: Need to use a factory, as EF does not let owned entities (i.e. Money) be passed via the constructor // Internal so that only the Order can create a LineItem internal static LineItem Create(OrderId orderId, ProductId productId, Money price, int quantity) { - Guard.Against.ZeroOrNegative(price.Amount); - Guard.Against.ZeroOrNegative(quantity); + price.Amount.Throw().IfNegativeOrZero(); + quantity.Throw().IfNegativeOrZero(); var lineItem = new LineItem() { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs index 2ae9e1e..1aa05c1 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs +++ b/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs @@ -9,7 +9,7 @@ namespace Module.Orders.Features.Orders; internal class Order : AggregateRoot { - private readonly List _lineItems = new(); + private readonly List _lineItems = []; public IEnumerable LineItems => _lineItems.AsReadOnly(); From cdff59653593c21bea9b79aa6a8d397dfcf918aa Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:41:01 +1000 Subject: [PATCH 07/87] =?UTF-8?q?=E2=9C=A8=20Added=20Customer=20and=20Cate?= =?UTF-8?q?log=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ModularMonolith.sln | 10 + README.md | 34 ++- .../{Features => }/Customers/Address.cs | 2 +- .../{Features => }/Customers/Customer.cs | 2 +- .../Customers/CustomerCreatedEvent.cs | 2 +- .../Module.Customers/Customers/CustomerId.cs | 3 + .../Features/Customers/CustomerId.cs | 3 - .../Module.Customers/Module.Customers.csproj | 4 - .../Module.Orders.Tests/LineItemTests.cs | 2 +- .../Features/Orders/CustomerId.cs | 3 - .../Features/Orders/LineItemId.cs | 3 - .../Module.Orders/Features/Orders/OrderId.cs | 3 - .../Orders/Module.Orders/Module.Orders.csproj | 4 - .../Orders/Module.Orders/Orders/CustomerId.cs | 3 + .../{Features => }/Orders/LineItem.cs | 2 +- .../Orders/LineItemCreatedEvent.cs | 2 +- .../Orders/Module.Orders/Orders/LineItemId.cs | 3 + .../{Features => }/Orders/Order.cs | 3 +- .../{Features => }/Orders/OrderByIdSpec.cs | 2 +- .../Orders/OrderCreatedEvent.cs | 3 +- .../Orders/Module.Orders/Orders/OrderId.cs | 3 + .../Orders/OrderReadyForShippingEvent.cs | 2 +- .../{Features => }/Orders/OrderSpec.cs | 2 +- .../{Features => }/Orders/OrderStatus.cs | 2 +- .../Categories/CategoryService.cs | 19 ++ .../Categories/Domain/Category.cs | 2 +- .../Categories/Domain/CategoryByIdSpec.cs | 2 +- .../Categories/Domain/CategoryCreatedEvent.cs | 2 +- .../Categories/Domain/CategoryId.cs | 3 + .../Categories/Domain/ICategoryService.cs | 2 +- .../Persistence/CategoryConfiguration.cs | 19 ++ .../Modules.Catelog/Modules.Catelog.csproj | 21 ++ .../Persistence/DepdendencyInjection.cs | 14 +- .../Common/Persistence/WarehouseDbContext.cs | 96 +++---- .../WarehouseDbContextInitializer.cs | 236 +++++++++--------- .../Features/Categories/CategoryService.cs | 19 -- .../Features/Categories/Domain/CategoryId.cs | 3 - .../Persistence/CategoryConfiguration.cs | 19 -- .../CreateProduct/CreateProductCommand.cs | 34 --- .../Features/Products/ProductRepository.cs | 25 -- .../Queries/GetProducts/GetProductsQuery.cs | 25 -- .../Modules.Warehouse.csproj | 4 - .../CreateProduct/CreateProductCommand.cs | 34 +++ .../Products/Domain/IProductRepository.cs | 2 +- .../Products/Domain/LowStockEvent.cs | 2 +- .../{Features => }/Products/Domain/Product.cs | 13 +- .../Products/Domain/ProductByIdSpec.cs | 2 +- .../Products/Domain/ProductCreatedEvent.cs | 2 +- .../{Features => }/Products/Domain/Sku.cs | 2 +- .../Products/Endpoints/ProductEndpoints.cs | 14 +- .../Persistence/ProductConfiguration.cs | 12 +- .../Products/ProductRepository.cs | 25 ++ .../Queries/GetProducts/GetProductsQuery.cs | 25 ++ .../Modules.Warehouse/WarehouseModule.cs | 24 +- 54 files changed, 408 insertions(+), 396 deletions(-) rename src/Modules/Customers/Module.Customers/{Features => }/Customers/Address.cs (94%) rename src/Modules/Customers/Module.Customers/{Features => }/Customers/Customer.cs (95%) rename src/Modules/Customers/Module.Customers/{Features => }/Customers/CustomerCreatedEvent.cs (86%) create mode 100644 src/Modules/Customers/Module.Customers/Customers/CustomerId.cs delete mode 100644 src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs delete mode 100644 src/Modules/Orders/Module.Orders/Features/Orders/CustomerId.cs delete mode 100644 src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs delete mode 100644 src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs create mode 100644 src/Modules/Orders/Module.Orders/Orders/CustomerId.cs rename src/Modules/Orders/Module.Orders/{Features => }/Orders/LineItem.cs (97%) rename src/Modules/Orders/Module.Orders/{Features => }/Orders/LineItemCreatedEvent.cs (89%) create mode 100644 src/Modules/Orders/Module.Orders/Orders/LineItemId.cs rename src/Modules/Orders/Module.Orders/{Features => }/Orders/Order.cs (98%) rename src/Modules/Orders/Module.Orders/{Features => }/Orders/OrderByIdSpec.cs (83%) rename src/Modules/Orders/Module.Orders/{Features => }/Orders/OrderCreatedEvent.cs (73%) create mode 100644 src/Modules/Orders/Module.Orders/Orders/OrderId.cs rename src/Modules/Orders/Module.Orders/{Features => }/Orders/OrderReadyForShippingEvent.cs (83%) rename src/Modules/Orders/Module.Orders/{Features => }/Orders/OrderSpec.cs (79%) rename src/Modules/Orders/Module.Orders/{Features => }/Orders/OrderStatus.cs (71%) create mode 100644 src/Modules/Products/Modules.Catelog/Categories/CategoryService.cs rename src/Modules/{Warehouse/Modules.Warehouse/Features => Products/Modules.Catelog}/Categories/Domain/Category.cs (94%) rename src/Modules/{Warehouse/Modules.Warehouse/Features => Products/Modules.Catelog}/Categories/Domain/CategoryByIdSpec.cs (80%) rename src/Modules/{Warehouse/Modules.Warehouse/Features => Products/Modules.Catelog}/Categories/Domain/CategoryCreatedEvent.cs (68%) create mode 100644 src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs rename src/Modules/{Warehouse/Modules.Warehouse/Features => Products/Modules.Catelog}/Categories/Domain/ICategoryService.cs (60%) create mode 100644 src/Modules/Products/Modules.Catelog/Categories/Persistence/CategoryConfiguration.cs create mode 100644 src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Persistence/CategoryConfiguration.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Domain/IProductRepository.cs (69%) rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Domain/LowStockEvent.cs (74%) rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Domain/Product.cs (86%) rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Domain/ProductByIdSpec.cs (83%) rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Domain/ProductCreatedEvent.cs (83%) rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Domain/Sku.cs (86%) rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Endpoints/ProductEndpoints.cs (58%) rename src/Modules/Warehouse/Modules.Warehouse/{Features => }/Products/Persistence/ProductConfiguration.cs (76%) create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/Queries/GetProducts/GetProductsQuery.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 8e2ee25..5b7a875 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Customers", "src\Mod EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Orders.Tests", "src\Modules\Orders\Module.Orders.Tests\Module.Orders.Tests.csproj", "{4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Catalog", "Catalog", "{1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catelog", "src\Modules\Products\Modules.Catelog\Modules.Catelog.csproj", "{591B271C-4C16-49CA-9549-E51087B591D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,6 +69,10 @@ Global {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}.Release|Any CPU.Build.0 = Release|Any CPU + {591B271C-4C16-49CA-9549-E51087B591D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {591B271C-4C16-49CA-9549-E51087B591D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {591B271C-4C16-49CA-9549-E51087B591D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {591B271C-4C16-49CA-9549-E51087B591D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -78,5 +86,7 @@ Global {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} {EF044D0C-0014-45B1-95F8-C46F34B9ED1F} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} + {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} + {591B271C-4C16-49CA-9549-E51087B591D1} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 807bb4e..cc38afa 100644 --- a/README.md +++ b/README.md @@ -30,31 +30,27 @@ Responsible for Warehouse and Inventory Management ## Business Invariants +Warehouse: +- N/A + +Product Catalog: +- A product must have a name +- A product must have a price +- A product can be given one or more categories +- A product cannot have a negative price +- A product cannot duplicate categories + Customers: -- Can register with the website - Must have a unique email address - Must have an address +Cart: +- Must be associated with a customer +- Must always have the correct price + Orders: - An order must be associated with a customer - The order total must always be correct - The order tax must always be correct +- Shipping must be included in the total price - Payment must be completed for the order to be placed (FUTURE: Consider splitting payments to it's own module) - -Products: -- A customer must be able to search products -- A product can be given one or more categories - -Warehouse: -- Products can be loaded into the warehouse and have their location tracked -- When an order is placed the stock level must be updated -- If there is not enough stock the order must be put on back order -- When stock is below a certain threshold the warehouse will be notified to restock -- An order can be dispatched from the warehouse - -Shipping: -- Once an order is dispatched from the warehouse the shipping company must be notified -- The all 'stops' must be tracked until delivered -- The customer must be able to track their order -- Once delivered the order must be marked as complete and the customer will be notified -- Must be able to calculate the shipping cost based on time to delivery diff --git a/src/Modules/Customers/Module.Customers/Features/Customers/Address.cs b/src/Modules/Customers/Module.Customers/Customers/Address.cs similarity index 94% rename from src/Modules/Customers/Module.Customers/Features/Customers/Address.cs rename to src/Modules/Customers/Module.Customers/Customers/Address.cs index 71b9d28..3384518 100644 --- a/src/Modules/Customers/Module.Customers/Features/Customers/Address.cs +++ b/src/Modules/Customers/Module.Customers/Customers/Address.cs @@ -1,6 +1,6 @@ using Ardalis.GuardClauses; -namespace Module.Customers.Features.Customers; +namespace Module.Customers.Customers; internal record Address { diff --git a/src/Modules/Customers/Module.Customers/Features/Customers/Customer.cs b/src/Modules/Customers/Module.Customers/Customers/Customer.cs similarity index 95% rename from src/Modules/Customers/Module.Customers/Features/Customers/Customer.cs rename to src/Modules/Customers/Module.Customers/Customers/Customer.cs index 5a94a1d..e9693a8 100644 --- a/src/Modules/Customers/Module.Customers/Features/Customers/Customer.cs +++ b/src/Modules/Customers/Module.Customers/Customers/Customer.cs @@ -1,7 +1,7 @@ using Ardalis.GuardClauses; using Common.SharedKernel.Domain.Base; -namespace Module.Customers.Features.Customers; +namespace Module.Customers.Customers; internal class Customer : AggregateRoot { diff --git a/src/Modules/Customers/Module.Customers/Features/Customers/CustomerCreatedEvent.cs b/src/Modules/Customers/Module.Customers/Customers/CustomerCreatedEvent.cs similarity index 86% rename from src/Modules/Customers/Module.Customers/Features/Customers/CustomerCreatedEvent.cs rename to src/Modules/Customers/Module.Customers/Customers/CustomerCreatedEvent.cs index 53043a1..0b52cf9 100644 --- a/src/Modules/Customers/Module.Customers/Features/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Customers/Module.Customers/Customers/CustomerCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Customers.Features.Customers; +namespace Module.Customers.Customers; internal record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : DomainEvent { diff --git a/src/Modules/Customers/Module.Customers/Customers/CustomerId.cs b/src/Modules/Customers/Module.Customers/Customers/CustomerId.cs new file mode 100644 index 0000000..03b6f0d --- /dev/null +++ b/src/Modules/Customers/Module.Customers/Customers/CustomerId.cs @@ -0,0 +1,3 @@ +namespace Module.Customers.Customers; + +internal record CustomerId(Guid Value); diff --git a/src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs b/src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs deleted file mode 100644 index 39e2ec8..0000000 --- a/src/Modules/Customers/Module.Customers/Features/Customers/CustomerId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Module.Customers.Features.Customers; - -internal record CustomerId(Guid Value); diff --git a/src/Modules/Customers/Module.Customers/Module.Customers.csproj b/src/Modules/Customers/Module.Customers/Module.Customers.csproj index 73dc8e7..09cf44a 100644 --- a/src/Modules/Customers/Module.Customers/Module.Customers.csproj +++ b/src/Modules/Customers/Module.Customers/Module.Customers.csproj @@ -6,10 +6,6 @@ enable - - - - diff --git a/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs index 8b11c42..323ad8e 100644 --- a/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs @@ -1,7 +1,7 @@ using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Identifiers; using FluentAssertions; -using Module.Orders.Features.Orders; +using Module.Orders.Orders; namespace Module.Orders.Tests; diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/CustomerId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/CustomerId.cs deleted file mode 100644 index 7efb2a1..0000000 --- a/src/Modules/Orders/Module.Orders/Features/Orders/CustomerId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Module.Orders.Features.Customers; - -internal record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs deleted file mode 100644 index 1226a3f..0000000 --- a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Module.Orders.Features.Orders; - -internal record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs b/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs deleted file mode 100644 index 5efe6d9..0000000 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Module.Orders.Features.Orders; - -internal record OrderId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Module.Orders.csproj b/src/Modules/Orders/Module.Orders/Module.Orders.csproj index d257f01..0104ae8 100644 --- a/src/Modules/Orders/Module.Orders/Module.Orders.csproj +++ b/src/Modules/Orders/Module.Orders/Module.Orders.csproj @@ -17,8 +17,4 @@ - - - - diff --git a/src/Modules/Orders/Module.Orders/Orders/CustomerId.cs b/src/Modules/Orders/Module.Orders/Orders/CustomerId.cs new file mode 100644 index 0000000..e486075 --- /dev/null +++ b/src/Modules/Orders/Module.Orders/Orders/CustomerId.cs @@ -0,0 +1,3 @@ +namespace Module.Orders.Orders; + +internal record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs b/src/Modules/Orders/Module.Orders/Orders/LineItem.cs similarity index 97% rename from src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs rename to src/Modules/Orders/Module.Orders/Orders/LineItem.cs index f82403a..f050eaf 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/LineItem.cs +++ b/src/Modules/Orders/Module.Orders/Orders/LineItem.cs @@ -4,7 +4,7 @@ using Common.SharedKernel.Domain.Identifiers; using Throw; -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal class LineItem : Entity { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Orders/LineItemCreatedEvent.cs similarity index 89% rename from src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs rename to src/Modules/Orders/Module.Orders/Orders/LineItemCreatedEvent.cs index dd5fffb..561c7d9 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Orders/LineItemCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : DomainEvent { diff --git a/src/Modules/Orders/Module.Orders/Orders/LineItemId.cs b/src/Modules/Orders/Module.Orders/Orders/LineItemId.cs new file mode 100644 index 0000000..02de426 --- /dev/null +++ b/src/Modules/Orders/Module.Orders/Orders/LineItemId.cs @@ -0,0 +1,3 @@ +namespace Module.Orders.Orders; + +internal record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs b/src/Modules/Orders/Module.Orders/Orders/Order.cs similarity index 98% rename from src/Modules/Orders/Module.Orders/Features/Orders/Order.cs rename to src/Modules/Orders/Module.Orders/Orders/Order.cs index 1aa05c1..7b61f88 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/Order.cs +++ b/src/Modules/Orders/Module.Orders/Orders/Order.cs @@ -3,9 +3,8 @@ using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; -using Module.Orders.Features.Customers; -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal class Order : AggregateRoot { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs b/src/Modules/Orders/Module.Orders/Orders/OrderByIdSpec.cs similarity index 83% rename from src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs rename to src/Modules/Orders/Module.Orders/Orders/OrderByIdSpec.cs index f98f61f..b34eccb 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderByIdSpec.cs +++ b/src/Modules/Orders/Module.Orders/Orders/OrderByIdSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal class OrderByIdSpec : OrderSpec, ISingleResultSpecification { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs b/src/Modules/Orders/Module.Orders/Orders/OrderCreatedEvent.cs similarity index 73% rename from src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs rename to src/Modules/Orders/Module.Orders/Orders/OrderCreatedEvent.cs index 45ed2ed..32869ef 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Module.Orders/Orders/OrderCreatedEvent.cs @@ -1,7 +1,6 @@ using Common.SharedKernel.Domain.Base; -using Module.Orders.Features.Customers; -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : DomainEvent { diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderId.cs b/src/Modules/Orders/Module.Orders/Orders/OrderId.cs new file mode 100644 index 0000000..400c610 --- /dev/null +++ b/src/Modules/Orders/Module.Orders/Orders/OrderId.cs @@ -0,0 +1,3 @@ +namespace Module.Orders.Orders; + +internal record OrderId(Guid Value); diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Module.Orders/Orders/OrderReadyForShippingEvent.cs similarity index 83% rename from src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs rename to src/Modules/Orders/Module.Orders/Orders/OrderReadyForShippingEvent.cs index 780d388..4ea9570 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Module.Orders/Orders/OrderReadyForShippingEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal record OrderReadyForShippingEvent(OrderId OrderId) : DomainEvent { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs b/src/Modules/Orders/Module.Orders/Orders/OrderSpec.cs similarity index 79% rename from src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs rename to src/Modules/Orders/Module.Orders/Orders/OrderSpec.cs index 3d7f580..49a1d94 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderSpec.cs +++ b/src/Modules/Orders/Module.Orders/Orders/OrderSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal class OrderSpec : Specification { diff --git a/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs b/src/Modules/Orders/Module.Orders/Orders/OrderStatus.cs similarity index 71% rename from src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs rename to src/Modules/Orders/Module.Orders/Orders/OrderStatus.cs index 91c690c..f2b81b2 100644 --- a/src/Modules/Orders/Module.Orders/Features/Orders/OrderStatus.cs +++ b/src/Modules/Orders/Module.Orders/Orders/OrderStatus.cs @@ -1,4 +1,4 @@ -namespace Module.Orders.Features.Orders; +namespace Module.Orders.Orders; internal enum OrderStatus { diff --git a/src/Modules/Products/Modules.Catelog/Categories/CategoryService.cs b/src/Modules/Products/Modules.Catelog/Categories/CategoryService.cs new file mode 100644 index 0000000..b6fde46 --- /dev/null +++ b/src/Modules/Products/Modules.Catelog/Categories/CategoryService.cs @@ -0,0 +1,19 @@ +// using Modules.Warehouse.Common.Persistence; +// using Modules.Warehouse.Features.Categories.Domain; +// +// namespace Modules.Warehouse.Features.Categories; +// +// internal class CategoryRepository : ICategoryRepository +// { +// private readonly WarehouseDbContext _dbContext; +// +// public CategoryRepository(WarehouseDbContext dbContext) +// { +// _dbContext = dbContext; +// } +// +// public bool CategoryExists(string categoryName) +// { +// return _dbContext.Categories.Any(c => c.Name == categoryName); +// } +// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs similarity index 94% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs rename to src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs index a1796e1..9d4cb0a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs @@ -2,7 +2,7 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Exceptions; -namespace Modules.Warehouse.Features.Categories.Domain; +namespace Modules.Catelog.Categories.Domain; internal class Category : AggregateRoot { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs b/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryByIdSpec.cs similarity index 80% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs rename to src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryByIdSpec.cs index 287d6bf..b97cc02 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryByIdSpec.cs +++ b/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryByIdSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Modules.Warehouse.Features.Categories.Domain; +namespace Modules.Catelog.Categories.Domain; internal class CategoryByIdSpec : Specification, ISingleResultSpecification { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs b/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryCreatedEvent.cs similarity index 68% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs rename to src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryCreatedEvent.cs index 535a412..9e98f5f 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryCreatedEvent.cs +++ b/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryCreatedEvent.cs @@ -1,5 +1,5 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Warehouse.Features.Categories.Domain; +namespace Modules.Catelog.Categories.Domain; internal record CategoryCreatedEvent(CategoryId Id, string Name) : DomainEvent; diff --git a/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs b/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs new file mode 100644 index 0000000..e32d94f --- /dev/null +++ b/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs @@ -0,0 +1,3 @@ +namespace Modules.Catelog.Categories.Domain; + +internal record CategoryId(Guid Value); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs b/src/Modules/Products/Modules.Catelog/Categories/Domain/ICategoryService.cs similarity index 60% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs rename to src/Modules/Products/Modules.Catelog/Categories/Domain/ICategoryService.cs index 57d9e1e..d05bfec 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/ICategoryService.cs +++ b/src/Modules/Products/Modules.Catelog/Categories/Domain/ICategoryService.cs @@ -1,4 +1,4 @@ -namespace Modules.Warehouse.Features.Categories.Domain; +namespace Modules.Catelog.Categories.Domain; internal interface ICategoryRepository { diff --git a/src/Modules/Products/Modules.Catelog/Categories/Persistence/CategoryConfiguration.cs b/src/Modules/Products/Modules.Catelog/Categories/Persistence/CategoryConfiguration.cs new file mode 100644 index 0000000..2320358 --- /dev/null +++ b/src/Modules/Products/Modules.Catelog/Categories/Persistence/CategoryConfiguration.cs @@ -0,0 +1,19 @@ +// using Microsoft.EntityFrameworkCore; +// using Microsoft.EntityFrameworkCore.Metadata.Builders; +// using Modules.Warehouse.Features.Categories.Domain; +// +// namespace Modules.Warehouse.Features.Categories.Persistence; +// +// internal class CategoryConfiguration : IEntityTypeConfiguration +// { +// public void Configure(EntityTypeBuilder builder) +// { +// builder.HasKey(p => p.Id); +// +// builder.Property(p => p.Id) +// .HasConversion(categoryId => categoryId.Value, value => new CategoryId(value)); +// +// builder.Property(p => p.Name) +// .HasMaxLength(50); +// } +// } diff --git a/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj b/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj new file mode 100644 index 0000000..fdbd836 --- /dev/null +++ b/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index 06598de..bc002e3 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -9,16 +9,16 @@ internal static class DepdendencyInjection internal static void AddPersistence(this IServiceCollection services, IConfiguration config) { var connectionString = config.GetConnectionString("DefaultConnection"); - services.AddDbContext(options => - options.UseSqlServer(connectionString, builder => - { - builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); - builder.EnableRetryOnFailure(); - })); + // services.AddDbContext(options => + // options.UseSqlServer(connectionString, builder => + // { + // builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); + // builder.EnableRetryOnFailure(); + // })); //services.AddSingleton(); // TODO: Consider moving to up.ps1 - services.AddScoped(); + // services.AddScoped(); // services.AddScoped(); // services.AddScoped(); // services.AddScoped(); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index 5b590db..779d2d1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -1,48 +1,48 @@ -using Microsoft.EntityFrameworkCore; -using Modules.Warehouse.Features.Categories.Domain; -using Modules.Warehouse.Features.Products.Domain; - -namespace Modules.Warehouse.Common.Persistence; - -internal class WarehouseDbContext : DbContext -{ - // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; - // private readonly OutboxInterceptor _outboxInterceptor; - // - // public DbSet Products { get; set; } = default!; - // - // public DbSet Customers { get; set; } = default!; - // - // public DbSet Orders { get; set; } = default!; - // - // public DbSet OutboxMessages { get; set; } = default!; - // - - public DbSet Products => Set(); - - public DbSet Categories => Set(); - - public WarehouseDbContext(DbContextOptions options /*EntitySaveChangesInterceptor saveChangesInterceptor, OutboxInterceptor outboxInterceptor*/) : base(options) - { - // _saveChangesInterceptor = saveChangesInterceptor; - // _outboxInterceptor = outboxInterceptor; - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); - base.OnModelCreating(modelBuilder); - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - // optionsBuilder.AddInterceptors( - // _saveChangesInterceptor, - // _outboxInterceptor); - } - - // public Task SaveChangesAsync() => this - // { - // throw new NotImplementedException(); - // } -} +// using Microsoft.EntityFrameworkCore; +// using Modules.Warehouse.Features.Categories.Domain; +// using Modules.Warehouse.Products.Domain; +// +// namespace Modules.Warehouse.Common.Persistence; +// +// internal class WarehouseDbContext : DbContext +// { +// // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; +// // private readonly OutboxInterceptor _outboxInterceptor; +// // +// // public DbSet Products { get; set; } = default!; +// // +// // public DbSet Customers { get; set; } = default!; +// // +// // public DbSet Orders { get; set; } = default!; +// // +// // public DbSet OutboxMessages { get; set; } = default!; +// // +// +// public DbSet Products => Set(); +// +// public DbSet Categories => Set(); +// +// public WarehouseDbContext(DbContextOptions options /*EntitySaveChangesInterceptor saveChangesInterceptor, OutboxInterceptor outboxInterceptor*/) : base(options) +// { +// // _saveChangesInterceptor = saveChangesInterceptor; +// // _outboxInterceptor = outboxInterceptor; +// } +// +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); +// base.OnModelCreating(modelBuilder); +// } +// +// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +// { +// // optionsBuilder.AddInterceptors( +// // _saveChangesInterceptor, +// // _outboxInterceptor); +// } +// +// // public Task SaveChangesAsync() => this +// // { +// // throw new NotImplementedException(); +// // } +// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs index 19a08b1..54eb642 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs @@ -1,118 +1,118 @@ -using Bogus; -using Common.SharedKernel.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Modules.Warehouse.Features.Categories; -using Modules.Warehouse.Features.Categories.Domain; -using Modules.Warehouse.Features.Products; -using Modules.Warehouse.Features.Products.Domain; - -namespace Modules.Warehouse.Common.Persistence; - -internal class WarehouseDbContextInitializer -{ - private readonly ILogger _logger; - private readonly WarehouseDbContext _dbContext; - - private const int NumProducts = 20; - - private const int NumCategories = 5; - // private const int NumCustomers = 20; - // private const int NumOrders = 20; - - public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) - { - _logger = logger; - _dbContext = dbContext; - } - - public async Task InitializeAsync() - { - try - { - if (_dbContext.Database.IsSqlServer()) - { - //await _dbContext.Database.EnsureDeletedAsync(); - await _dbContext.Database.EnsureCreatedAsync(); - } - } - catch (Exception e) - { - _logger.LogError(e, "An error occurred while migrating or initializing the database"); - throw; - } - } - - public async Task SeedAsync() - { - await SeedCategoriesAsync(); - await SeedProductsAsync(); - // await SeedCustomersAsync(); - // await SeedOrdersAsync(); - } - - // private async Task SeedCustomersAsync() - // { - // if (await _dbContext.Customers.AnyAsync()) - // return; - // - // var customerFaker = new Faker() - // .CustomInstantiator(f => Customer.Create(f.Person.Email, f.Person.FirstName, f.Person.LastName)); - // - // var customers = customerFaker.Generate(NumCustomers); - // _dbContext.Customers.AddRange(customers); - // await _dbContext.SaveChangesAsync(); - // } - - private async Task SeedProductsAsync() - { - if (await _dbContext.Products.AnyAsync()) - return; - - var categories = await _dbContext.Categories.ToListAsync(); - - var moneyFaker = new Faker() - .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); - - var skuFaker = new Faker() - .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); - - var productRepository = new ProductRepository(_dbContext); - - var faker = new Faker() - .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), moneyFaker.Generate(), - skuFaker.Generate(), f.PickRandom(categories).Id, productRepository)); - - var products = faker.Generate(NumProducts); - _dbContext.Products.AddRange(products); - await _dbContext.SaveChangesAsync(); - } - - // private async Task SeedOrdersAsync() - // { - // if (await _dbContext.Orders.AnyAsync()) - // return; - // - // var customerIds = _dbContext.Customers.Select(c => c.Id).ToList(); - // - // var orderFaker = new Faker() - // .CustomInstantiator(f => Order.Create(f.PickRandom(customerIds))); - // - // var orders = orderFaker.Generate(NumOrders); - // _dbContext.Orders.AddRange(orders); - // await _dbContext.SaveChangesAsync(); - // } - - private async Task SeedCategoriesAsync() - { - if (await _dbContext.Categories.AnyAsync()) - return; - - var categoryFaker = new Faker() - .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1)[0], new CategoryRepository(_dbContext))); - - var categories = categoryFaker.Generate(NumCategories); - _dbContext.Categories.AddRange(categories); - await _dbContext.SaveChangesAsync(); - } -} +// using Bogus; +// using Common.SharedKernel.Domain.Entities; +// using Microsoft.EntityFrameworkCore; +// using Microsoft.Extensions.Logging; +// using Modules.Warehouse.Features.Categories; +// using Modules.Warehouse.Features.Categories.Domain; +// using Modules.Warehouse.Products; +// using Modules.Warehouse.Products.Domain; +// +// namespace Modules.Warehouse.Common.Persistence; +// +// internal class WarehouseDbContextInitializer +// { +// private readonly ILogger _logger; +// private readonly WarehouseDbContext _dbContext; +// +// private const int NumProducts = 20; +// +// private const int NumCategories = 5; +// // private const int NumCustomers = 20; +// // private const int NumOrders = 20; +// +// public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) +// { +// _logger = logger; +// _dbContext = dbContext; +// } +// +// public async Task InitializeAsync() +// { +// try +// { +// if (_dbContext.Database.IsSqlServer()) +// { +// //await _dbContext.Database.EnsureDeletedAsync(); +// await _dbContext.Database.EnsureCreatedAsync(); +// } +// } +// catch (Exception e) +// { +// _logger.LogError(e, "An error occurred while migrating or initializing the database"); +// throw; +// } +// } +// +// public async Task SeedAsync() +// { +// await SeedCategoriesAsync(); +// await SeedProductsAsync(); +// // await SeedCustomersAsync(); +// // await SeedOrdersAsync(); +// } +// +// // private async Task SeedCustomersAsync() +// // { +// // if (await _dbContext.Customers.AnyAsync()) +// // return; +// // +// // var customerFaker = new Faker() +// // .CustomInstantiator(f => Customer.Create(f.Person.Email, f.Person.FirstName, f.Person.LastName)); +// // +// // var customers = customerFaker.Generate(NumCustomers); +// // _dbContext.Customers.AddRange(customers); +// // await _dbContext.SaveChangesAsync(); +// // } +// +// private async Task SeedProductsAsync() +// { +// if (await _dbContext.Products.AnyAsync()) +// return; +// +// var categories = await _dbContext.Categories.ToListAsync(); +// +// var moneyFaker = new Faker() +// .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); +// +// var skuFaker = new Faker() +// .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); +// +// var productRepository = new ProductRepository(_dbContext); +// +// var faker = new Faker() +// .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), moneyFaker.Generate(), +// skuFaker.Generate(), f.PickRandom(categories).Id, productRepository)); +// +// var products = faker.Generate(NumProducts); +// _dbContext.Products.AddRange(products); +// await _dbContext.SaveChangesAsync(); +// } +// +// // private async Task SeedOrdersAsync() +// // { +// // if (await _dbContext.Orders.AnyAsync()) +// // return; +// // +// // var customerIds = _dbContext.Customers.Select(c => c.Id).ToList(); +// // +// // var orderFaker = new Faker() +// // .CustomInstantiator(f => Order.Create(f.PickRandom(customerIds))); +// // +// // var orders = orderFaker.Generate(NumOrders); +// // _dbContext.Orders.AddRange(orders); +// // await _dbContext.SaveChangesAsync(); +// // } +// +// private async Task SeedCategoriesAsync() +// { +// if (await _dbContext.Categories.AnyAsync()) +// return; +// +// var categoryFaker = new Faker() +// .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1)[0], new CategoryRepository(_dbContext))); +// +// var categories = categoryFaker.Generate(NumCategories); +// _dbContext.Categories.AddRange(categories); +// await _dbContext.SaveChangesAsync(); +// } +// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs deleted file mode 100644 index 94a794b..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/CategoryService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Features.Categories.Domain; - -namespace Modules.Warehouse.Features.Categories; - -internal class CategoryRepository : ICategoryRepository -{ - private readonly WarehouseDbContext _dbContext; - - public CategoryRepository(WarehouseDbContext dbContext) - { - _dbContext = dbContext; - } - - public bool CategoryExists(string categoryName) - { - return _dbContext.Categories.Any(c => c.Name == categoryName); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs deleted file mode 100644 index 8c0bf51..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Domain/CategoryId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Warehouse.Features.Categories.Domain; - -internal record CategoryId(Guid Value); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Persistence/CategoryConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Persistence/CategoryConfiguration.cs deleted file mode 100644 index 3522007..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Categories/Persistence/CategoryConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Warehouse.Features.Categories.Domain; - -namespace Modules.Warehouse.Features.Categories.Persistence; - -internal class CategoryConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(p => p.Id); - - builder.Property(p => p.Id) - .HasConversion(categoryId => categoryId.Value, value => new CategoryId(value)); - - builder.Property(p => p.Name) - .HasMaxLength(50); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs deleted file mode 100644 index 466c5f2..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Commands/CreateProduct/CreateProductCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Common.SharedKernel.Domain.Entities; -using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Features.Categories.Domain; -using Modules.Warehouse.Features.Products.Domain; - -namespace Modules.Warehouse.Features.Products.Commands.CreateProduct; - -internal record CreateProductCommand(string Name, decimal Amount, string Sku, Guid CategoryId) - : IRequest; - -internal class CreateProductCommandHandler : IRequestHandler -{ - private readonly WarehouseDbContext _dbContext; - private readonly IProductRepository _productRepository; - - public CreateProductCommandHandler(WarehouseDbContext dbContext, IProductRepository productRepository, CancellationToken cancellationToken) - { - _dbContext = dbContext; - _productRepository = productRepository; - } - - public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken) - { - var money = new Money(Currency.Default, request.Amount); - var sku = Sku.Create(request.Sku); - ArgumentNullException.ThrowIfNull(sku); - var categoryId = new CategoryId(request.CategoryId); - var product = Product.Create(request.Name, money, sku, categoryId, _productRepository); - - _dbContext.Products.Add(product); - - await _dbContext.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs deleted file mode 100644 index 5c063b1..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/ProductRepository.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Features.Products.Domain; - -namespace Modules.Warehouse.Features.Products; - -internal class ProductRepository : IProductRepository -{ - private readonly WarehouseDbContext _dbContext; - - public ProductRepository(WarehouseDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task SkuExistsAsync(Sku sku) - { - return await _dbContext.Products.AnyAsync(p => p.Sku == sku); - } - - public bool SkuExists(Sku sku) - { - return _dbContext.Products.Any(p => p.Sku == sku); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs deleted file mode 100644 index e0d2eca..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Queries/GetProducts/GetProductsQuery.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Modules.Warehouse.Common.Persistence; - -namespace Modules.Warehouse.Features.Products.Queries.GetProducts; - -internal record GetProductsQuery : IRequest>; - -internal record ProductDto(Guid Id, string Sku, string Name, decimal Price); - -internal class GetProductsQueryHandler : IRequestHandler> -{ - private readonly WarehouseDbContext _dbContext; - - public GetProductsQueryHandler(WarehouseDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task> Handle(GetProductsQuery request, CancellationToken cancellationToken) - { - return await _dbContext.Products - .Select(p => new ProductDto(p.Id.Value, p.Sku.Value, p.Name, p.Price.Amount)) - .ToListAsync(cancellationToken: cancellationToken); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 4c98b37..1daa73c 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -22,10 +22,6 @@ - - - - diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs new file mode 100644 index 0000000..870169a --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs @@ -0,0 +1,34 @@ +using Common.SharedKernel.Domain.Entities; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Products.Domain; + +namespace Modules.Warehouse.Products.Commands.CreateProduct; + +internal record CreateProductCommand(string Name, decimal Amount, string Sku, Guid CategoryId) + : IRequest; + +internal class CreateProductCommandHandler : IRequestHandler +{ + // private readonly WarehouseDbContext _dbContext; + // private readonly IProductRepository _productRepository; + // + // public CreateProductCommandHandler(WarehouseDbContext dbContext, IProductRepository productRepository, CancellationToken cancellationToken) + // { + // _dbContext = dbContext; + // _productRepository = productRepository; + // } + + public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken) + { + // var money = new Money(Currency.Default, request.Amount); + // var sku = Sku.Create(request.Sku); + // ArgumentNullException.ThrowIfNull(sku); + // var product = Product.Create(request.Name, money, sku, _productRepository); + // + // _dbContext.Products.Add(product); + // + // await _dbContext.SaveChangesAsync(cancellationToken); + + await Task.CompletedTask; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/IProductRepository.cs similarity index 69% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Domain/IProductRepository.cs index 685b71b..ac2ebc6 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/IProductRepository.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/IProductRepository.cs @@ -1,4 +1,4 @@ -namespace Modules.Warehouse.Features.Products.Domain; +namespace Modules.Warehouse.Products.Domain; internal interface IProductRepository { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs similarity index 74% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs index 0362851..c6701b1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/LowStockEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Identifiers; -namespace Modules.Warehouse.Features.Products.Domain; +namespace Modules.Warehouse.Products.Domain; internal record LowStockEvent(ProductId ProductId) : DomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs similarity index 86% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 23f7ede..f16d5cf 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -3,17 +3,16 @@ using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; -using Modules.Warehouse.Features.Categories.Domain; -namespace Modules.Warehouse.Features.Products.Domain; +namespace Modules.Warehouse.Products.Domain; internal class Product : AggregateRoot { private const int LowStockThreshold = 5; - public CategoryId CategoryId { get; set; } = null!; + // public CategoryId CategoryId { get; set; } = null!; - public Category Category { get; set; } = null!; + // public Category Category { get; set; } = null!; public string Name { get; private set; } = null!; @@ -28,18 +27,18 @@ private Product() } // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money & Sku) be passed via the constructor - public static Product Create(string name, Money price, Sku sku, CategoryId categoryId, IProductRepository productRepository) + public static Product Create(string name, Money price, Sku sku, IProductRepository productRepository) { Guard.Against.NullOrWhiteSpace(name); Guard.Against.Null(sku); Guard.Against.Null(price); Guard.Against.ZeroOrNegative(price.Amount); - Guard.Against.Null(categoryId); + // Guard.Against.Null(categoryId); var product = new Product { Id = new ProductId(Guid.NewGuid()), - CategoryId = categoryId, + // CategoryId = categoryId, Name = name, Price = price, StockOnHand = 0 diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs similarity index 83% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs index dfd9d79..da8b18f 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs @@ -1,7 +1,7 @@ using Ardalis.Specification; using Common.SharedKernel.Domain.Identifiers; -namespace Modules.Warehouse.Features.Products.Domain; +namespace Modules.Warehouse.Products.Domain; internal class ProductByIdSpec : Specification, ISingleResultSpecification { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs similarity index 83% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs index 056d003..66cf1b2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/ProductCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs @@ -1,7 +1,7 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Identifiers; -namespace Modules.Warehouse.Features.Products.Domain; +namespace Modules.Warehouse.Products.Domain; internal record ProductCreatedEvent(ProductId Product, string ProductName) : DomainEvent { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs similarity index 86% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs index 1c42acb..d6cf59c 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Domain/Sku.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs @@ -1,4 +1,4 @@ -namespace Modules.Warehouse.Features.Products.Domain; +namespace Modules.Warehouse.Products.Domain; internal record Sku { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Endpoints/ProductEndpoints.cs similarity index 58% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Endpoints/ProductEndpoints.cs index b3573b4..0b5d3ee 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Endpoints/ProductEndpoints.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Endpoints/ProductEndpoints.cs @@ -1,10 +1,10 @@ using Common.SharedKernel; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Modules.Warehouse.Features.Products.Commands.CreateProduct; -using Modules.Warehouse.Features.Products.Queries.GetProducts; +using Modules.Warehouse.Products.Commands.CreateProduct; +// using Modules.Warehouse.Products.Queries.GetProducts; -namespace Modules.Warehouse.Features.Products.Endpoints; +namespace Modules.Warehouse.Products.Endpoints; internal static class ProductEndpoints { @@ -15,10 +15,10 @@ public static void MapProductEndpoints(this WebApplication app) .WithTags("Warehouse") .WithOpenApi(); - group - .MapGet("/", async (ISender sender, CancellationToken ct) => await sender.Send(new GetProductsQuery(), ct)) - .WithName("GetProducts") - .ProducesGet(); + // group + // .MapGet("/", async (ISender sender, CancellationToken ct) => await sender.Send(new GetProductsQuery(), ct)) + // .WithName("GetProducts") + // .ProducesGet(); group .MapPost("/", async (ISender sender, CreateProductCommand command, CancellationToken ct) => await sender.Send(command, ct)) diff --git a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Persistence/ProductConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs similarity index 76% rename from src/Modules/Warehouse/Modules.Warehouse/Features/Products/Persistence/ProductConfiguration.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs index bb2577a..3f7ff79 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Features/Products/Persistence/ProductConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs @@ -2,9 +2,9 @@ using Common.SharedKernel.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Warehouse.Features.Products.Domain; +using Modules.Warehouse.Products.Domain; -namespace Modules.Warehouse.Features.Products.Persistence; +namespace Modules.Warehouse.Products.Persistence; internal class ProductConfiguration : IEntityTypeConfiguration { @@ -23,9 +23,9 @@ public void Configure(EntityTypeBuilder builder) //builder.ComplexProperty(p => p.Price, () => MoneyConfiguration.BuildAction) - builder.HasOne(p => p.Category) - .WithMany() - .HasForeignKey(o => o.CategoryId) - .IsRequired(); + // builder.HasOne(p => p.Category) + // .WithMany() + // .HasForeignKey(o => o.CategoryId) + // .IsRequired(); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs new file mode 100644 index 0000000..d3ae581 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs @@ -0,0 +1,25 @@ +// using Microsoft.EntityFrameworkCore; +// using Modules.Warehouse.Common.Persistence; +// using Modules.Warehouse.Products.Domain; +// +// namespace Modules.Warehouse.Products; +// +// internal class ProductRepository : IProductRepository +// { +// private readonly WarehouseDbContext _dbContext; +// +// public ProductRepository(WarehouseDbContext dbContext) +// { +// _dbContext = dbContext; +// } +// +// public async Task SkuExistsAsync(Sku sku) +// { +// return await _dbContext.Products.AnyAsync(p => p.Sku == sku); +// } +// +// public bool SkuExists(Sku sku) +// { +// return _dbContext.Products.Any(p => p.Sku == sku); +// } +// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Queries/GetProducts/GetProductsQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Queries/GetProducts/GetProductsQuery.cs new file mode 100644 index 0000000..ce9b783 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Queries/GetProducts/GetProductsQuery.cs @@ -0,0 +1,25 @@ +// using Microsoft.EntityFrameworkCore; +// using Modules.Warehouse.Common.Persistence; +// +// namespace Modules.Warehouse.Products.Queries.GetProducts; +// +// internal record GetProductsQuery : IRequest>; +// +// internal record ProductDto(Guid Id, string Sku, string Name, decimal Price); +// +// internal class GetProductsQueryHandler : IRequestHandler> +// { +// private readonly WarehouseDbContext _dbContext; +// +// public GetProductsQueryHandler(WarehouseDbContext dbContext) +// { +// _dbContext = dbContext; +// } +// +// public async Task> Handle(GetProductsQuery request, CancellationToken cancellationToken) +// { +// return await _dbContext.Products +// .Select(p => new ProductDto(p.Id.Value, p.Sku.Value, p.Name, p.Price.Amount)) +// .ToListAsync(cancellationToken: cancellationToken); +// } +// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index ce78a35..6802a41 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -3,9 +3,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Features.Products; -using Modules.Warehouse.Features.Products.Domain; -using Modules.Warehouse.Features.Products.Endpoints; +using Modules.Warehouse.Products; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Products.Endpoints; namespace Modules.Warehouse; @@ -24,20 +24,20 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration }); // Todo: Move to feature DI - services.AddTransient(); + // services.AddTransient(); } public static async Task UseWarehouse(this WebApplication app) { // TODO: Refactor to up.ps1 - if (app.Environment.IsDevelopment()) - { - // Initialise and seed database - using var scope = app.Services.CreateScope(); - var initializer = scope.ServiceProvider.GetRequiredService(); - await initializer.InitializeAsync(); - await initializer.SeedAsync(); - } + // if (app.Environment.IsDevelopment()) + // { + // // Initialise and seed database + // using var scope = app.Services.CreateScope(); + // var initializer = scope.ServiceProvider.GetRequiredService(); + // await initializer.InitializeAsync(); + // await initializer.SeedAsync(); + // } // TODO: Move to feature DI app.MapProductEndpoints(); From d24bdfcb6b0010509287443cba180e0d93495746 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 3 Aug 2024 21:16:00 +1000 Subject: [PATCH 08/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Switched=20from=20Ar?= =?UTF-8?q?dalis.GuardClauses=20to=20ErrorOr=20and=20Throw.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common.SharedKernel.csproj | 1 - .../Domain/Exceptions/GuardClausesExt.cs | 46 ++++++++--------- .../Module.Customers/Customers/Address.cs | 12 ++--- .../Module.Customers/Customers/Customer.cs | 10 ++-- .../Module.Customers/Module.Customers.csproj | 4 +- .../Orders/Module.Orders/Module.Orders.csproj | 3 +- .../Orders/Module.Orders/Orders/LineItem.cs | 6 +-- .../Orders/Module.Orders/Orders/Order.cs | 51 ++++++++++++------- .../Module.Orders/Orders/OrderErrors.cs | 34 +++++++++++++ .../Categories/Domain/Category.cs | 7 +-- .../Products/Domain/Product.cs | 22 +++----- 11 files changed, 120 insertions(+), 76 deletions(-) create mode 100644 src/Modules/Orders/Module.Orders/Orders/OrderErrors.cs diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 2a7eec4..72629e4 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Common/Common.SharedKernel/Domain/Exceptions/GuardClausesExt.cs b/src/Common/Common.SharedKernel/Domain/Exceptions/GuardClausesExt.cs index 71c077c..99ee32e 100644 --- a/src/Common/Common.SharedKernel/Domain/Exceptions/GuardClausesExt.cs +++ b/src/Common/Common.SharedKernel/Domain/Exceptions/GuardClausesExt.cs @@ -1,23 +1,23 @@ -using Ardalis.GuardClauses; -using System.Runtime.CompilerServices; - -namespace Common.SharedKernel.Domain.Exceptions; - -public static class FooGuard -{ - public static void ZeroOrNegative(this IGuardClause guardClause, - int input, - [CallerArgumentExpression("input")] string? parameterName = null) - { - if (input <= 0) - throw new ArgumentException("Cannot be zero or negative", parameterName); - } - - public static void ZeroOrNegative(this IGuardClause guardClause, - decimal input, - [CallerArgumentExpression("input")] string? parameterName = null) - { - if (input <= 0) - throw new ArgumentException("Cannot be zero or negative", parameterName); - } -} \ No newline at end of file +// using Ardalis.GuardClauses; +// using System.Runtime.CompilerServices; +// +// namespace Common.SharedKernel.Domain.Exceptions; +// +// public static class FooGuard +// { +// public static void ZeroOrNegative(this IGuardClause guardClause, +// int input, +// [CallerArgumentExpression("input")] string? parameterName = null) +// { +// if (input <= 0) +// throw new ArgumentException("Cannot be zero or negative", parameterName); +// } +// +// public static void ZeroOrNegative(this IGuardClause guardClause, +// decimal input, +// [CallerArgumentExpression("input")] string? parameterName = null) +// { +// if (input <= 0) +// throw new ArgumentException("Cannot be zero or negative", parameterName); +// } +// } diff --git a/src/Modules/Customers/Module.Customers/Customers/Address.cs b/src/Modules/Customers/Module.Customers/Customers/Address.cs index 3384518..8a52680 100644 --- a/src/Modules/Customers/Module.Customers/Customers/Address.cs +++ b/src/Modules/Customers/Module.Customers/Customers/Address.cs @@ -1,4 +1,4 @@ -using Ardalis.GuardClauses; +using Throw; namespace Module.Customers.Customers; @@ -13,11 +13,11 @@ internal record Address internal Address(string line1, string? line2, string city, string state, string zipCode, string country) { - Guard.Against.NullOrWhiteSpace(line1); - Guard.Against.NullOrWhiteSpace(city); - Guard.Against.NullOrWhiteSpace(state); - Guard.Against.NullOrWhiteSpace(zipCode); - Guard.Against.NullOrWhiteSpace(country); + line1.Throw().IfEmpty(); + city.Throw().IfEmpty(); + state.Throw().IfEmpty(); + zipCode.Throw().IfEmpty(); + country.Throw().IfEmpty(); Line1 = line1; Line2 = line2; diff --git a/src/Modules/Customers/Module.Customers/Customers/Customer.cs b/src/Modules/Customers/Module.Customers/Customers/Customer.cs index e9693a8..9514b4e 100644 --- a/src/Modules/Customers/Module.Customers/Customers/Customer.cs +++ b/src/Modules/Customers/Module.Customers/Customers/Customer.cs @@ -1,5 +1,5 @@ -using Ardalis.GuardClauses; -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; +using Throw; namespace Module.Customers.Customers; @@ -17,7 +17,7 @@ private Customer() { } internal static Customer Create(string email, string firstName, string lastName) { - Guard.Against.NullOrWhiteSpace(email); + email.Throw().IfEmpty(); var customer = new Customer() { Id = new CustomerId(Guid.NewGuid()), Email = email, }; @@ -29,8 +29,8 @@ internal static Customer Create(string email, string firstName, string lastName) public void UpdateName(string firstName, string lastName) { - Guard.Against.NullOrWhiteSpace(firstName); - Guard.Against.NullOrWhiteSpace(lastName); + firstName.Throw().IfEmpty(); + lastName.Throw().IfEmpty(); FirstName = firstName; LastName = lastName; diff --git a/src/Modules/Customers/Module.Customers/Module.Customers.csproj b/src/Modules/Customers/Module.Customers/Module.Customers.csproj index 09cf44a..57715d4 100644 --- a/src/Modules/Customers/Module.Customers/Module.Customers.csproj +++ b/src/Modules/Customers/Module.Customers/Module.Customers.csproj @@ -7,11 +7,11 @@ - + - + diff --git a/src/Modules/Orders/Module.Orders/Module.Orders.csproj b/src/Modules/Orders/Module.Orders/Module.Orders.csproj index 0104ae8..ed52deb 100644 --- a/src/Modules/Orders/Module.Orders/Module.Orders.csproj +++ b/src/Modules/Orders/Module.Orders/Module.Orders.csproj @@ -12,9 +12,10 @@ - + + diff --git a/src/Modules/Orders/Module.Orders/Orders/LineItem.cs b/src/Modules/Orders/Module.Orders/Orders/LineItem.cs index f050eaf..415fd29 100644 --- a/src/Modules/Orders/Module.Orders/Orders/LineItem.cs +++ b/src/Modules/Orders/Module.Orders/Orders/LineItem.cs @@ -1,5 +1,4 @@ -using Ardalis.GuardClauses; -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Identifiers; using Throw; @@ -50,8 +49,7 @@ internal static LineItem Create(OrderId orderId, ProductId productId, Money pric internal void RemoveQuantity(int quantity) { - Guard.Against.Expression(_ => Quantity - quantity <= 0, quantity, - "Can't remove all units. Remove the entire item instead."); + quantity.Throw("Can't remove all units. Remove the entire item instead").IfTrue(Quantity - quantity <= 0); Quantity -= quantity; } } diff --git a/src/Modules/Orders/Module.Orders/Orders/Order.cs b/src/Modules/Orders/Module.Orders/Orders/Order.cs index 7b61f88..d2fc59c 100644 --- a/src/Modules/Orders/Module.Orders/Orders/Order.cs +++ b/src/Modules/Orders/Module.Orders/Orders/Order.cs @@ -1,8 +1,11 @@ -using Ardalis.GuardClauses; -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; +using ErrorOr; +using OneOf.Types; +using Error = ErrorOr.Error; +using Success = ErrorOr.Success; namespace Module.Orders.Orders; @@ -37,7 +40,9 @@ public Money OrderTotal } } - private Order() { } + private Order() + { + } public static Order Create(CustomerId customerId) { @@ -54,14 +59,15 @@ public static Order Create(CustomerId customerId) return order; } - public LineItem AddLineItem(ProductId productId, Money price, int quantity) + public ErrorOr AddLineItem(ProductId productId, Money price, int quantity) { - Guard.Against.Expression(_ => Status != OrderStatus.PendingPayment, Status, - "Can't modify order once payment is done"); + // TODO: Unit test + if (Status == OrderStatus.PendingPayment) + return OrderErrors.CantModifyAfterPayment; + // TODO: Unit test if (OrderCurrency != null && OrderCurrency != price.Currency) - throw new DomainException( - $"Cannot add line item with currency {price.Currency} to and order than already contains a currency of {price.Currency}"); + return OrderErrors.CurrencyMismatch; var existingLineItem = _lineItems.FirstOrDefault(li => li.ProductId == productId); if (existingLineItem != null) @@ -77,19 +83,23 @@ public LineItem AddLineItem(ProductId productId, Money price, int quantity) return lineItem; } - public void RemoveLineItem(ProductId productId) + public ErrorOr RemoveLineItem(ProductId productId) { - Guard.Against.Expression(_ => Status != OrderStatus.PendingPayment, Status, - "Can't modify order once payment is done"); + if (Status == OrderStatus.PendingPayment) + return OrderErrors.CantModifyAfterPayment; var lineItem = _lineItems.RemoveAll(x => x.ProductId == productId); + + return Result.Success; } - public void AddPayment(Money payment) + public ErrorOr AddPayment(Money payment) { - Guard.Against.ZeroOrNegative(payment.Amount); + if (payment.Amount <= 0) + return OrderErrors.PaymentAmountZeroOrNegative; + if (payment > OrderTotal - AmountPaid) - throw new DomainException("Payment can't exceed order total"); + return OrderErrors.PaymentExceedsOrderTotal; // Ensure currency is set on first payment if (AmountPaid.Amount == 0) @@ -102,6 +112,8 @@ public void AddPayment(Money payment) Status = OrderStatus.ReadyForShipping; AddDomainEvent(new OrderReadyForShippingEvent(Id)); } + + return Result.Success; } public void AddQuantity(ProductId productId, int quantity) => @@ -110,15 +122,20 @@ public void AddQuantity(ProductId productId, int quantity) => public void RemoveQuantity(ProductId productId, int quantity) => _lineItems.FirstOrDefault(li => li.ProductId == productId)?.RemoveQuantity(quantity); - public void ShipOrder(TimeProvider timeProvider) + public ErrorOr ShipOrder(TimeProvider timeProvider) { - Guard.Against.Expression(_ => Status == OrderStatus.PendingPayment, Status, "Can't ship an unpaid order"); - Guard.Against.Expression(_ => Status == OrderStatus.InTransit, Status, "Order already shipped to customer"); + if (Status == OrderStatus.PendingPayment) + return OrderErrors.CantShipUnpaidOrder; + + if (Status == OrderStatus.InTransit) + return OrderErrors.OrderAlreadyShipped; if (_lineItems.Sum(li => li.Quantity) <= 0) throw new DomainException("Can't ship an order with no items"); ShippingDate = timeProvider.GetUtcNow(); Status = OrderStatus.InTransit; + + return Result.Success; } } diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderErrors.cs b/src/Modules/Orders/Module.Orders/Orders/OrderErrors.cs new file mode 100644 index 0000000..97ac0f6 --- /dev/null +++ b/src/Modules/Orders/Module.Orders/Orders/OrderErrors.cs @@ -0,0 +1,34 @@ +using ErrorOr; + +namespace Module.Orders.Orders; + +public static class OrderErrors +{ + public static Error CantModifyAfterPayment = Error.Validation( + "Order.CantModifyAfterPayment", + "Order can't be modified after payment"); + + public static Error CurrencyMismatch = Error.Validation( + "Order.CurrencyMismatch", + "Cannot add line item with different currency to an order"); + + // Guard.Against.ZeroOrNegative(payment.Amount); + public static Error PaymentAmountZeroOrNegative = Error.Validation( + "Order.PaymentAmountZeroOrNegative", + "Payment amount must be greater than zero"); + + // "Payment can't exceed order total" + public static Error PaymentExceedsOrderTotal = Error.Validation( + "Order.PaymentExceedsOrderTotal", + "Payment can't exceed order total"); + + // Guard.Against.Expression(_ => Status == OrderStatus.PendingPayment, Status, "Can't ship an unpaid order"); + public static Error CantShipUnpaidOrder = Error.Validation( + "Order.CantShipUnpaidOrder", + "Can't ship an unpaid order"); + + // Guard.Against.Expression(_ => Status == OrderStatus.InTransit, Status, "Order already shipped to customer"); + public static Error OrderAlreadyShipped = Error.Validation( + "Order.OrderAlreadyShipped", + "Order already shipped to customer"); +} diff --git a/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs index 9d4cb0a..bba8d34 100644 --- a/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs @@ -1,6 +1,6 @@ -using Ardalis.GuardClauses; -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Exceptions; +using Throw; namespace Modules.Catelog.Categories.Domain; @@ -27,7 +27,8 @@ public static Category Create(string name, ICategoryRepository categoryRepositor public void UpdateName(string name, ICategoryRepository categoryRepository) { - Guard.Against.NullOrWhiteSpace(name); + name.Throw().IfEmpty(); + // Guard.Against.NullOrWhiteSpace(name); if (categoryRepository.CategoryExists(name)) throw new DomainException($"Category {name} already exists"); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index f16d5cf..8c5b999 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -1,8 +1,8 @@ -using Ardalis.GuardClauses; -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using Common.SharedKernel.Domain.Identifiers; +using Throw; namespace Modules.Warehouse.Products.Domain; @@ -29,11 +29,8 @@ private Product() // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money & Sku) be passed via the constructor public static Product Create(string name, Money price, Sku sku, IProductRepository productRepository) { - Guard.Against.NullOrWhiteSpace(name); - Guard.Against.Null(sku); - Guard.Against.Null(price); - Guard.Against.ZeroOrNegative(price.Amount); - // Guard.Against.Null(categoryId); + name.Throw().IfEmpty(); + price.Throw().IfNegativeOrZero(p => p.Amount); var product = new Product { @@ -53,21 +50,18 @@ public static Product Create(string name, Money price, Sku sku, IProductReposito public void UpdateName(string name) { - Guard.Against.NullOrWhiteSpace(name); + name.Throw().IfEmpty(); Name = name; } public void UpdatePrice(Money price) { - Guard.Against.Null(price); - Guard.Against.ZeroOrNegative(price.Amount); + price.Throw().IfNegativeOrZero(p => p.Amount); Price = price; } public void UpdateSku(Sku sku, IProductRepository productRepository) { - Guard.Against.Null(sku); - if (productRepository.SkuExists(sku)) throw new ArgumentException("Sku already exists"); @@ -76,7 +70,7 @@ public void UpdateSku(Sku sku, IProductRepository productRepository) public void RemoveStock(int quantity) { - Guard.Against.NegativeOrZero(quantity); + quantity.Throw().IfNegativeOrZero(); if (StockOnHand - quantity < 0) throw new DomainException("Cannot adjust stock below zero"); @@ -89,7 +83,7 @@ public void RemoveStock(int quantity) public void AddStock(int quantity) { - Guard.Against.NegativeOrZero(quantity); + quantity.Throw().IfNegativeOrZero(); StockOnHand += quantity; } } From 814c4040f164c4ff943c5a45bcce0413857d0016 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 4 Aug 2024 22:34:03 +1000 Subject: [PATCH 09/87] Added storage to Warehouse domain model --- ModularMonolith.sln | 13 +++-- README.md | 3 +- .../Common.SharedKernel.csproj | 4 ++ .../Domain/Identifiers/ProductId.cs | 4 -- .../Customers/Address.cs | 0 .../Customers/Customer.cs | 0 .../Customers/CustomerCreatedEvent.cs | 0 .../Customers/CustomerId.cs | 0 .../Modules.Customers.csproj} | 1 + .../Orders/Module.Orders/AssemblyInfo.cs | 3 -- .../GlobalUsings.cs | 0 .../LineItemTests.cs | 1 - .../Modules.Orders.Tests.csproj} | 3 +- .../Orders/Modules.Orders/AssemblyInfo.cs | 3 ++ .../Modules.Orders.csproj} | 1 + .../Orders/CustomerId.cs | 0 .../Orders/LineItem.cs | 1 - .../Orders/LineItemCreatedEvent.cs | 0 .../Orders/LineItemId.cs | 0 .../Orders/Order.cs | 3 -- .../Orders/OrderByIdSpec.cs | 0 .../Orders/OrderCreatedEvent.cs | 0 .../Orders/OrderErrors.cs | 4 -- .../Orders/OrderId.cs | 0 .../Orders/OrderReadyForShippingEvent.cs | 0 .../Orders/OrderSpec.cs | 0 .../Orders/OrderStatus.cs | 0 .../Orders/Modules.Orders/Orders/ProductId.cs | 3 ++ .../OrdersModule.cs | 0 .../Modules.Warehouse.Tests/AisleTests.cs | 21 ++++++++ .../Modules.Warehouse.Tests/GlobalUsings.cs | 1 + .../Modules.Warehouse.Tests/ModelTests.cs | 52 +++++++++++++++++++ .../Modules.Warehouse.Tests.csproj | 31 +++++++++++ .../StorageAllocationServiceTests.cs | 50 ++++++++++++++++++ .../Modules.Warehouse/AssemblyInfo.cs | 3 ++ .../Modules.Warehouse.csproj | 4 ++ .../Products/Domain/LowStockEvent.cs | 1 - .../Products/Domain/Product.cs | 7 +-- .../Products/Domain/ProductByIdSpec.cs | 1 - .../Products/Domain/ProductCreatedEvent.cs | 1 - .../Persistence/ProductConfiguration.cs | 3 +- .../Modules.Warehouse/Storage/Domain/Aisle.cs | 40 ++++++++++++++ .../Modules.Warehouse/Storage/Domain/Bay.cs | 38 ++++++++++++++ .../Modules.Warehouse/Storage/Domain/Shelf.cs | 29 +++++++++++ .../Domain/StorageAllocationService.cs | 29 +++++++++++ src/WebApi/WebApi.csproj | 2 +- 46 files changed, 328 insertions(+), 32 deletions(-) delete mode 100644 src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs rename src/Modules/Customers/{Module.Customers => Modules.Customers}/Customers/Address.cs (100%) rename src/Modules/Customers/{Module.Customers => Modules.Customers}/Customers/Customer.cs (100%) rename src/Modules/Customers/{Module.Customers => Modules.Customers}/Customers/CustomerCreatedEvent.cs (100%) rename src/Modules/Customers/{Module.Customers => Modules.Customers}/Customers/CustomerId.cs (100%) rename src/Modules/Customers/{Module.Customers/Module.Customers.csproj => Modules.Customers/Modules.Customers.csproj} (88%) delete mode 100644 src/Modules/Orders/Module.Orders/AssemblyInfo.cs rename src/Modules/Orders/{Module.Orders.Tests => Modules.Orders.Tests}/GlobalUsings.cs (100%) rename src/Modules/Orders/{Module.Orders.Tests => Modules.Orders.Tests}/LineItemTests.cs (98%) rename src/Modules/Orders/{Module.Orders.Tests/Module.Orders.Tests.csproj => Modules.Orders.Tests/Modules.Orders.Tests.csproj} (89%) create mode 100644 src/Modules/Orders/Modules.Orders/AssemblyInfo.cs rename src/Modules/Orders/{Module.Orders/Module.Orders.csproj => Modules.Orders/Modules.Orders.csproj} (92%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/CustomerId.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/LineItem.cs (97%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/LineItemCreatedEvent.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/LineItemId.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/Order.cs (97%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/OrderByIdSpec.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/OrderCreatedEvent.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/OrderErrors.cs (75%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/OrderId.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/OrderReadyForShippingEvent.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/OrderSpec.cs (100%) rename src/Modules/Orders/{Module.Orders => Modules.Orders}/Orders/OrderStatus.cs (100%) create mode 100644 src/Modules/Orders/Modules.Orders/Orders/ProductId.cs rename src/Modules/Orders/{Module.Orders => Modules.Orders}/OrdersModule.cs (100%) create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 5b7a875..149d56b 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -19,7 +19,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.SharedKernel", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse", "src\Modules\Warehouse\Modules.Warehouse\Modules.Warehouse.csproj", "{74ED43AC-972C-465B-AF8A-30A5532C408E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Orders", "src\Modules\Orders\Module.Orders\Module.Orders.csproj", "{6DBAEEED-701C-4C56-A761-D79986094759}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Orders", "src\Modules\Orders\Modules.Orders\Modules.Orders.csproj", "{6DBAEEED-701C-4C56-A761-D79986094759}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{DC7140A1-214E-41D5-B856-8E131E2FACA7}" ProjectSection(SolutionItems) = preProject @@ -28,14 +28,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{DC7140A1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Customers", "Customers", "{41494B34-2A0F-4AF6-96DA-C25AEBAA424C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Customers", "src\Modules\Customers\Module.Customers\Module.Customers.csproj", "{EF044D0C-0014-45B1-95F8-C46F34B9ED1F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Customers", "src\Modules\Customers\Modules.Customers\Modules.Customers.csproj", "{EF044D0C-0014-45B1-95F8-C46F34B9ED1F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Module.Orders.Tests", "src\Modules\Orders\Module.Orders.Tests\Module.Orders.Tests.csproj", "{4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Orders.Tests", "src\Modules\Orders\Modules.Orders.Tests\Modules.Orders.Tests.csproj", "{4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Catalog", "Catalog", "{1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catelog", "src\Modules\Products\Modules.Catelog\Modules.Catelog.csproj", "{591B271C-4C16-49CA-9549-E51087B591D1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Tests", "src\Modules\Warehouse\Modules.Warehouse.Tests\Modules.Warehouse.Tests.csproj", "{C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,10 @@ Global {591B271C-4C16-49CA-9549-E51087B591D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {591B271C-4C16-49CA-9549-E51087B591D1}.Release|Any CPU.ActiveCfg = Release|Any CPU {591B271C-4C16-49CA-9549-E51087B591D1}.Release|Any CPU.Build.0 = Release|Any CPU + {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -88,5 +94,6 @@ Global {4D5FD8B9-2825-4DBC-B952-3F5C76EE9B36} = {92D97012-135E-4AA1-AE1C-8C0803E9F6AC} {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} {591B271C-4C16-49CA-9549-E51087B591D1} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} + {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index cc38afa..9d4bc6f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ Responsible for Warehouse and Inventory Management ## Business Invariants Warehouse: -- N/A +- An Aisle cannot have duplicate bays +- A bay cannot have duplicate shelves Product Catalog: - A product must have a name diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 72629e4..8f6bb87 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs b/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs deleted file mode 100644 index 393c55c..0000000 --- a/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Common.SharedKernel.Domain.Identifiers; - -// TODO: Consider moving into relevant modules -public record ProductId(Guid Value); diff --git a/src/Modules/Customers/Module.Customers/Customers/Address.cs b/src/Modules/Customers/Modules.Customers/Customers/Address.cs similarity index 100% rename from src/Modules/Customers/Module.Customers/Customers/Address.cs rename to src/Modules/Customers/Modules.Customers/Customers/Address.cs diff --git a/src/Modules/Customers/Module.Customers/Customers/Customer.cs b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs similarity index 100% rename from src/Modules/Customers/Module.Customers/Customers/Customer.cs rename to src/Modules/Customers/Modules.Customers/Customers/Customer.cs diff --git a/src/Modules/Customers/Module.Customers/Customers/CustomerCreatedEvent.cs b/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs similarity index 100% rename from src/Modules/Customers/Module.Customers/Customers/CustomerCreatedEvent.cs rename to src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs diff --git a/src/Modules/Customers/Module.Customers/Customers/CustomerId.cs b/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs similarity index 100% rename from src/Modules/Customers/Module.Customers/Customers/CustomerId.cs rename to src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs diff --git a/src/Modules/Customers/Module.Customers/Module.Customers.csproj b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj similarity index 88% rename from src/Modules/Customers/Module.Customers/Module.Customers.csproj rename to src/Modules/Customers/Modules.Customers/Modules.Customers.csproj index 57715d4..c34a68c 100644 --- a/src/Modules/Customers/Module.Customers/Module.Customers.csproj +++ b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + Module.Customers diff --git a/src/Modules/Orders/Module.Orders/AssemblyInfo.cs b/src/Modules/Orders/Module.Orders/AssemblyInfo.cs deleted file mode 100644 index 963c590..0000000 --- a/src/Modules/Orders/Module.Orders/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly:InternalsVisibleTo("Module.Orders.Tests")] diff --git a/src/Modules/Orders/Module.Orders.Tests/GlobalUsings.cs b/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs similarity index 100% rename from src/Modules/Orders/Module.Orders.Tests/GlobalUsings.cs rename to src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs diff --git a/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs similarity index 98% rename from src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs rename to src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs index 323ad8e..ed81b07 100644 --- a/src/Modules/Orders/Module.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs @@ -1,5 +1,4 @@ using Common.SharedKernel.Domain.Entities; -using Common.SharedKernel.Domain.Identifiers; using FluentAssertions; using Module.Orders.Orders; diff --git a/src/Modules/Orders/Module.Orders.Tests/Module.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj similarity index 89% rename from src/Modules/Orders/Module.Orders.Tests/Module.Orders.Tests.csproj rename to src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index b887391..9db5043 100644 --- a/src/Modules/Orders/Module.Orders.Tests/Module.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -7,6 +7,7 @@ false true + Module.Orders.Tests @@ -24,7 +25,7 @@ - + diff --git a/src/Modules/Orders/Modules.Orders/AssemblyInfo.cs b/src/Modules/Orders/Modules.Orders/AssemblyInfo.cs new file mode 100644 index 0000000..2208f78 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Modules.Orders.Tests")] diff --git a/src/Modules/Orders/Module.Orders/Module.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj similarity index 92% rename from src/Modules/Orders/Module.Orders/Module.Orders.csproj rename to src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index ed52deb..512c5dd 100644 --- a/src/Modules/Orders/Module.Orders/Module.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + Module.Orders diff --git a/src/Modules/Orders/Module.Orders/Orders/CustomerId.cs b/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/CustomerId.cs rename to src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/LineItem.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs similarity index 97% rename from src/Modules/Orders/Module.Orders/Orders/LineItem.cs rename to src/Modules/Orders/Modules.Orders/Orders/LineItem.cs index 415fd29..8fb6e99 100644 --- a/src/Modules/Orders/Module.Orders/Orders/LineItem.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs @@ -1,6 +1,5 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; -using Common.SharedKernel.Domain.Identifiers; using Throw; namespace Module.Orders.Orders; diff --git a/src/Modules/Orders/Module.Orders/Orders/LineItemCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/LineItemCreatedEvent.cs rename to src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/LineItemId.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/LineItemId.cs rename to src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order.cs similarity index 97% rename from src/Modules/Orders/Module.Orders/Orders/Order.cs rename to src/Modules/Orders/Modules.Orders/Orders/Order.cs index d2fc59c..073d1ee 100644 --- a/src/Modules/Orders/Module.Orders/Orders/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order.cs @@ -1,10 +1,7 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; -using Common.SharedKernel.Domain.Identifiers; using ErrorOr; -using OneOf.Types; -using Error = ErrorOr.Error; using Success = ErrorOr.Success; namespace Module.Orders.Orders; diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderByIdSpec.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/OrderByIdSpec.cs rename to src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/OrderCreatedEvent.cs rename to src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderErrors.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs similarity index 75% rename from src/Modules/Orders/Module.Orders/Orders/OrderErrors.cs rename to src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs index 97ac0f6..9c49137 100644 --- a/src/Modules/Orders/Module.Orders/Orders/OrderErrors.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs @@ -12,22 +12,18 @@ public static class OrderErrors "Order.CurrencyMismatch", "Cannot add line item with different currency to an order"); - // Guard.Against.ZeroOrNegative(payment.Amount); public static Error PaymentAmountZeroOrNegative = Error.Validation( "Order.PaymentAmountZeroOrNegative", "Payment amount must be greater than zero"); - // "Payment can't exceed order total" public static Error PaymentExceedsOrderTotal = Error.Validation( "Order.PaymentExceedsOrderTotal", "Payment can't exceed order total"); - // Guard.Against.Expression(_ => Status == OrderStatus.PendingPayment, Status, "Can't ship an unpaid order"); public static Error CantShipUnpaidOrder = Error.Validation( "Order.CantShipUnpaidOrder", "Can't ship an unpaid order"); - // Guard.Against.Expression(_ => Status == OrderStatus.InTransit, Status, "Order already shipped to customer"); public static Error OrderAlreadyShipped = Error.Validation( "Order.OrderAlreadyShipped", "Order already shipped to customer"); diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderId.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/OrderId.cs rename to src/Modules/Orders/Modules.Orders/Orders/OrderId.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/OrderReadyForShippingEvent.cs rename to src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderSpec.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/OrderSpec.cs rename to src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs diff --git a/src/Modules/Orders/Module.Orders/Orders/OrderStatus.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/Orders/OrderStatus.cs rename to src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs diff --git a/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs b/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs new file mode 100644 index 0000000..90ec286 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs @@ -0,0 +1,3 @@ +namespace Module.Orders.Orders; + +internal record ProductId(Guid Value); \ No newline at end of file diff --git a/src/Modules/Orders/Module.Orders/OrdersModule.cs b/src/Modules/Orders/Modules.Orders/OrdersModule.cs similarity index 100% rename from src/Modules/Orders/Module.Orders/OrdersModule.cs rename to src/Modules/Orders/Modules.Orders/OrdersModule.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs new file mode 100644 index 0000000..a8d87bb --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs @@ -0,0 +1,21 @@ +using FluentAssertions; +using Modules.Warehouse.Storage.Domain; + +namespace Module.Warehouse.Tests; + +public class AisleTests +{ + [Fact] + public void Create_WithBaysAndShelves_CreatesCorrectNumberOfShelves() + { + // Arrange + var numBays = 2; + var numShelves = 3; + + // Act + var sut = Aisle.Create("Aisle 1", numBays, numShelves); + + // Assert + sut.TotalStorage.Should().Be(numBays * numShelves); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs new file mode 100644 index 0000000..d2c86d7 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; +using Xunit.Abstractions; + +namespace Module.Warehouse.Tests; + +public class ModelTests +{ + private readonly ITestOutputHelper _output; + + public ModelTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void LookingUpProduct_ReturnsAisleBayAndShelf() + { + // Exploratory + var productA = new ProductId(Guid.NewGuid()); + var productB = new ProductId(Guid.NewGuid()); + var aisle = Aisle.Create("Aisle 1", 2, 3); + var service = new StorageAllocationService(); + + service.AllocateStorage(new List { aisle }, productA); + service.AllocateStorage(new List { aisle }, productA); + service.AllocateStorage(new List { aisle }, productA); + service.AllocateStorage(new List { aisle }, productB); + + string aisleName = string.Empty; + string bayName = string.Empty; + string shelfName = string.Empty; + + // NOTE: If look ups like there were to cause performacne problems, two-way relationships could be added between + // Different storage locations (e.g. Shelf->Bay->Aisle) to make lookups more efficient. + foreach (var bay in aisle.Bays) + { + foreach (var shelf in bay.Shelves) + { + if (shelf.ProductId == productB) + { + aisleName = aisle.Name; + bayName = bay.Name; + shelfName = shelf.Name; + } + } + } + + _output.WriteLine($"Product B is in Aisle {aisleName}, Bay {bayName}, Shelf {shelfName}"); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj new file mode 100644 index 0000000..b916712 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + Module.Warehouse.Tests + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs new file mode 100644 index 0000000..4665826 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs @@ -0,0 +1,50 @@ +using FluentAssertions; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Module.Warehouse.Tests; + +public class StorageAllocationServiceTests +{ + [Fact] + public void AllocateStorage_WhenMaxStorageUsed_ShouldHaveNoAvailableStorage() + { + // Arrange + var numBays = 2; + var numShelves = 3; + var aisle = Aisle.Create("Aisle 1", numBays, numShelves); + var sut = new StorageAllocationService(); + var productId = new ProductId(Guid.NewGuid()); + + // Act + for(var i = 0; i < numBays * numShelves; i++) + { + sut.AllocateStorage([aisle], productId); + } + + // Assert + aisle.TotalStorage.Should().Be(numBays * numShelves); + aisle.AvailableStorage.Should().Be(0); + } + + [Fact] + public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() + { + // Arrange + var numBays = 2; + var numShelves = 3; + var aisle = Aisle.Create("Aisle 1", numBays, numShelves); + var sut = new StorageAllocationService(); + var productId = new ProductId(Guid.NewGuid()); + + // Act + for(var i = 0; i < numBays * numShelves; i++) + { + sut.AllocateStorage([aisle], productId); + } + + // Assert + var act = () => sut.AllocateStorage([aisle], productId); + act.Should().Throw(); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs b/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs new file mode 100644 index 0000000..59a528a --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Modules.Warehouse.Tests")] diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 1daa73c..a6e2171 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -26,4 +26,8 @@ + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs index c6701b1..34d9921 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs @@ -1,5 +1,4 @@ using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Identifiers; namespace Modules.Warehouse.Products.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 8c5b999..6c016c1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -1,19 +1,16 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; -using Common.SharedKernel.Domain.Identifiers; using Throw; namespace Modules.Warehouse.Products.Domain; +internal record ProductId(Guid Value); + internal class Product : AggregateRoot { private const int LowStockThreshold = 5; - // public CategoryId CategoryId { get; set; } = null!; - - // public Category Category { get; set; } = null!; - public string Name { get; private set; } = null!; public Money Price { get; private set; } = null!; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs index da8b18f..84764a2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs @@ -1,5 +1,4 @@ using Ardalis.Specification; -using Common.SharedKernel.Domain.Identifiers; namespace Modules.Warehouse.Products.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs index 66cf1b2..3205759 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs @@ -1,5 +1,4 @@ using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Identifiers; namespace Modules.Warehouse.Products.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs index 3f7ff79..4036436 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs @@ -1,5 +1,4 @@ -using Common.SharedKernel.Domain.Identifiers; -using Common.SharedKernel.Persistence; +using Common.SharedKernel.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Modules.Warehouse.Products.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs new file mode 100644 index 0000000..8f712fe --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs @@ -0,0 +1,40 @@ +using Common.SharedKernel.Domain.Base; +using Throw; + +namespace Modules.Warehouse.Storage.Domain; + +internal record AisleId(Guid Value); + +internal class Aisle : AggregateRoot +{ + public string Name { get; private set; } = null!; + + public int TotalStorage => _bays.Sum(b => b.TotalStorage); + + public int AvailableStorage => _bays.Sum(b => b.AvailableStorage); + + private readonly List _bays = []; + + public IReadOnlyList Bays => _bays.AsReadOnly(); + + private Aisle() { } + + public static Aisle Create(string name, int numBays, int numShelves) + { + numBays.Throw().IfNegativeOrZero(); + + var aisle = new Aisle + { + Id = new AisleId(Guid.NewGuid()), + Name = name + }; + + for (var i = 0; i < numBays; i++) + { + var bay = Bay.Create(i + 1, numShelves); + aisle._bays.Add(bay); + } + + return aisle; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs new file mode 100644 index 0000000..36c0cec --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs @@ -0,0 +1,38 @@ +using Common.SharedKernel.Domain.Base; +using Throw; + +namespace Modules.Warehouse.Storage.Domain; + +// internal record BayId(Guid Value); + +internal class Bay : Entity +{ + private readonly List _shelves = []; + + public IReadOnlyList Shelves => _shelves.AsReadOnly(); + + public int AvailableStorage => _shelves.Count(s => s.IsEmpty); + + public int TotalStorage => _shelves.Count; + + public string Name { get; private set; } = null!; + + public static Bay Create(int id, int numShelves) + { + numShelves.Throw().IfNegativeOrZero(); + + var bay = new Bay + { + Id = id, + Name = $"Bay {id}" + }; + + for (var j = 0; j < numShelves; j++) + { + var shelf = Shelf.Create(j); + bay._shelves.Add(shelf); + } + + return bay; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs new file mode 100644 index 0000000..ecc5c8b --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs @@ -0,0 +1,29 @@ +using Common.SharedKernel.Domain.Base; +using Modules.Warehouse.Products.Domain; + +namespace Modules.Warehouse.Storage.Domain; + +internal class Shelf : Entity +{ + public string Name { get; private set; } = null!; + + // private int _number => Id; + + public ProductId? ProductId { get; private set; } + + public bool IsEmpty => ProductId is null; + + public static Shelf Create(int number) + { + return new Shelf() + { + Id = number, + Name = $"Shelf {number}" + }; + } + + public void AssignProduct(ProductId productId) + { + ProductId = productId; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs new file mode 100644 index 0000000..1ec2d64 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs @@ -0,0 +1,29 @@ +using Modules.Warehouse.Products.Domain; + +namespace Modules.Warehouse.Storage.Domain; + +/// +/// Simple storage algorithm that naively assumes 1 product per shelf. +/// +internal class StorageAllocationService +{ + internal void AllocateStorage(IEnumerable aisles, ProductId productId) + { + foreach (var aisle in aisles) + { + foreach (var bay in aisle.Bays) + { + foreach (var shelf in bay.Shelves) + { + if (!shelf.IsEmpty) + continue; + + shelf.AssignProduct(productId); + return; + } + } + } + + throw new Exception("No available storage"); + } +} diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 90140a7..944aa5d 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -15,7 +15,7 @@ - + From 80a8acd455cacb1887ccf0c7b99e1246b324a311 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 6 Aug 2024 07:01:14 +1000 Subject: [PATCH 10/87] =?UTF-8?q?=F0=9F=8E=A8=20consolidate=20csproj=20set?= =?UTF-8?q?tings=20by=20using=20Directory.Build.props?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 14 ++++++++++++++ .../Common.SharedKernel/Common.SharedKernel.csproj | 6 ------ .../Modules.Customers/Modules.Customers.csproj | 7 ------- .../Modules.Orders.Tests.csproj | 5 ----- .../Orders/Modules.Orders/Modules.Orders.csproj | 7 ------- .../Modules.Catelog/Modules.Catelog.csproj | 6 ------ .../Modules.Warehouse.Tests.csproj | 5 ----- .../Modules.Warehouse/Modules.Warehouse.csproj | 6 ------ .../Warehouse/Modules.Warehouse/WarehouseModule.cs | 4 +++- src/WebApi/WebApi.csproj | 5 ----- 10 files changed, 17 insertions(+), 48 deletions(-) create mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..869b6a4 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,14 @@ + + + net8.0 + enable + enable + + + latest + Default + false + false + true + + diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 8f6bb87..2d55942 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -1,11 +1,5 @@  - - net8.0 - enable - enable - - diff --git a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj index c34a68c..ba34f7a 100644 --- a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj +++ b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj @@ -1,12 +1,5 @@  - - net8.0 - enable - enable - Module.Customers - - diff --git a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index 9db5043..a7189e3 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -1,13 +1,8 @@ - net8.0 - enable - enable - false true - Module.Orders.Tests diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index 512c5dd..45c47f7 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -1,12 +1,5 @@  - - net8.0 - enable - enable - Module.Orders - - diff --git a/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj b/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj index fdbd836..25c9da1 100644 --- a/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj +++ b/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj @@ -1,11 +1,5 @@  - - net8.0 - enable - enable - - diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj index b916712..5c94342 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj @@ -1,13 +1,8 @@ - net8.0 - enable - enable - false true - Module.Warehouse.Tests diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index a6e2171..1f12734 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -1,11 +1,5 @@  - - net8.0 - enable - enable - - diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 6802a41..4abf6cf 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -27,7 +27,7 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration // services.AddTransient(); } - public static async Task UseWarehouse(this WebApplication app) + public static Task UseWarehouse(this WebApplication app) { // TODO: Refactor to up.ps1 // if (app.Environment.IsDevelopment()) @@ -41,5 +41,7 @@ public static async Task UseWarehouse(this WebApplication app) // TODO: Move to feature DI app.MapProductEndpoints(); + + return Task.CompletedTask; } } diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 944aa5d..b4830f2 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -1,11 +1,6 @@ - net8.0 - enable - enable - - Web 992be0e9-0906-4010-8fca-76a03c001d19 From 7d1c5c7a17f10bb2f1f90482a198be2d4774a969 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 11 Aug 2024 15:48:54 +1000 Subject: [PATCH 11/87] Bump static code analysis to minimum --- Directory.Build.props | 6 +++--- .../Behaviours/ValidationBehaviour.cs | 4 ++-- .../Orders/Modules.Orders/Orders/OrderErrors.cs | 12 ++++++------ .../Warehouse/Modules.Warehouse.Tests/ModelTests.cs | 8 ++++---- .../StorageAllocationServiceTests.cs | 6 +++--- .../Storage/Domain/StorageAllocationService.cs | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 869b6a4..1bae2cd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,9 +6,9 @@ latest - Default - false - false + Minimum + true + true true diff --git a/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs b/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs index 7443f40..a5c2c8f 100644 --- a/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs +++ b/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs @@ -18,11 +18,11 @@ public async Task Handle(TRequest request, RequestHandlerDelegate r.Errors.Any()) + .Where(r => r.Errors.Count != 0) .SelectMany(r => r.Errors) .ToList(); - if (failures.Any()) + if (failures.Count != 0) throw new Exceptions.ValidationException(failures); } return await next(); diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs index 9c49137..ce56d01 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs @@ -4,27 +4,27 @@ namespace Module.Orders.Orders; public static class OrderErrors { - public static Error CantModifyAfterPayment = Error.Validation( + public static readonly Error CantModifyAfterPayment = Error.Validation( "Order.CantModifyAfterPayment", "Order can't be modified after payment"); - public static Error CurrencyMismatch = Error.Validation( + public static readonly Error CurrencyMismatch = Error.Validation( "Order.CurrencyMismatch", "Cannot add line item with different currency to an order"); - public static Error PaymentAmountZeroOrNegative = Error.Validation( + public static readonly Error PaymentAmountZeroOrNegative = Error.Validation( "Order.PaymentAmountZeroOrNegative", "Payment amount must be greater than zero"); - public static Error PaymentExceedsOrderTotal = Error.Validation( + public static readonly Error PaymentExceedsOrderTotal = Error.Validation( "Order.PaymentExceedsOrderTotal", "Payment can't exceed order total"); - public static Error CantShipUnpaidOrder = Error.Validation( + public static readonly Error CantShipUnpaidOrder = Error.Validation( "Order.CantShipUnpaidOrder", "Can't ship an unpaid order"); - public static Error OrderAlreadyShipped = Error.Validation( + public static readonly Error OrderAlreadyShipped = Error.Validation( "Order.OrderAlreadyShipped", "Order already shipped to customer"); } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs index d2c86d7..34b5a6a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs @@ -23,10 +23,10 @@ public void LookingUpProduct_ReturnsAisleBayAndShelf() var aisle = Aisle.Create("Aisle 1", 2, 3); var service = new StorageAllocationService(); - service.AllocateStorage(new List { aisle }, productA); - service.AllocateStorage(new List { aisle }, productA); - service.AllocateStorage(new List { aisle }, productA); - service.AllocateStorage(new List { aisle }, productB); + StorageAllocationService.AllocateStorage(new List { aisle }, productA); + StorageAllocationService.AllocateStorage(new List { aisle }, productA); + StorageAllocationService.AllocateStorage(new List { aisle }, productA); + StorageAllocationService.AllocateStorage(new List { aisle }, productB); string aisleName = string.Empty; string bayName = string.Empty; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs index 4665826..d73fe41 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs @@ -19,7 +19,7 @@ public void AllocateStorage_WhenMaxStorageUsed_ShouldHaveNoAvailableStorage() // Act for(var i = 0; i < numBays * numShelves; i++) { - sut.AllocateStorage([aisle], productId); + StorageAllocationService.AllocateStorage([aisle], productId); } // Assert @@ -40,11 +40,11 @@ public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() // Act for(var i = 0; i < numBays * numShelves; i++) { - sut.AllocateStorage([aisle], productId); + StorageAllocationService.AllocateStorage([aisle], productId); } // Assert - var act = () => sut.AllocateStorage([aisle], productId); + var act = () => StorageAllocationService.AllocateStorage([aisle], productId); act.Should().Throw(); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs index 1ec2d64..e2d5e68 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs @@ -7,7 +7,7 @@ namespace Modules.Warehouse.Storage.Domain; /// internal class StorageAllocationService { - internal void AllocateStorage(IEnumerable aisles, ProductId productId) + internal static void AllocateStorage(IEnumerable aisles, ProductId productId) { foreach (var aisle in aisles) { From 45b1e40cf47c1e1a865cbb1c2f6b261938b2a8e5 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 11 Aug 2024 16:24:39 +1000 Subject: [PATCH 12/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Tidy=20up=20namespac?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Modules/Customers/Modules.Customers/Customers/Address.cs | 2 +- src/Modules/Customers/Modules.Customers/Customers/Customer.cs | 2 +- .../Modules.Customers/Customers/CustomerCreatedEvent.cs | 2 +- .../Customers/Modules.Customers/Customers/CustomerId.cs | 2 +- src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs | 4 ++-- src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/LineItem.cs | 2 +- .../Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/Order.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/OrderId.cs | 2 +- .../Modules.Orders/Orders/OrderReadyForShippingEvent.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs | 2 +- src/Modules/Orders/Modules.Orders/Orders/ProductId.cs | 2 +- src/Modules/Orders/Modules.Orders/OrdersModule.cs | 2 +- src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs | 2 +- src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs | 3 +-- .../Modules.Warehouse.Tests/StorageAllocationServiceTests.cs | 2 +- src/WebApi/Program.cs | 2 +- 23 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/Modules/Customers/Modules.Customers/Customers/Address.cs b/src/Modules/Customers/Modules.Customers/Customers/Address.cs index 8a52680..e82e27b 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Address.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Address.cs @@ -1,6 +1,6 @@ using Throw; -namespace Module.Customers.Customers; +namespace Modules.Customers.Customers; internal record Address { diff --git a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs index 9514b4e..b8285fa 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs @@ -1,7 +1,7 @@ using Common.SharedKernel.Domain.Base; using Throw; -namespace Module.Customers.Customers; +namespace Modules.Customers.Customers; internal class Customer : AggregateRoot { diff --git a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs b/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs index 0b52cf9..84815a5 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Customers.Customers; +namespace Modules.Customers.Customers; internal record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : DomainEvent { diff --git a/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs b/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs index 03b6f0d..287a622 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs @@ -1,3 +1,3 @@ -namespace Module.Customers.Customers; +namespace Modules.Customers.Customers; internal record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs index ed81b07..238bb4c 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs @@ -1,8 +1,8 @@ using Common.SharedKernel.Domain.Entities; using FluentAssertions; -using Module.Orders.Orders; +using Modules.Orders.Orders; -namespace Module.Orders.Tests; +namespace Modules.Orders.Tests; public class LineItemTests { diff --git a/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs b/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs index e486075..25a20ef 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs @@ -1,3 +1,3 @@ -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs index 8fb6e99..90943ac 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs @@ -2,7 +2,7 @@ using Common.SharedKernel.Domain.Entities; using Throw; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal class LineItem : Entity { diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs index 561c7d9..81589e6 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : DomainEvent { diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs index 02de426..4bfcb46 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs @@ -1,3 +1,3 @@ -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order.cs index 073d1ee..35cfa2c 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order.cs @@ -4,7 +4,7 @@ using ErrorOr; using Success = ErrorOr.Success; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal class Order : AggregateRoot { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs index b34eccb..dd2c4dc 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal class OrderByIdSpec : OrderSpec, ISingleResultSpecification { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs index 32869ef..c74128b 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : DomainEvent { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs index ce56d01..f451804 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs @@ -1,6 +1,6 @@ using ErrorOr; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; public static class OrderErrors { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs index 400c610..52b1115 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs @@ -1,3 +1,3 @@ -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal record OrderId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs index 4ea9570..eb960b8 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal record OrderReadyForShippingEvent(OrderId OrderId) : DomainEvent { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs index 49a1d94..aaaf918 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal class OrderSpec : Specification { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs index f2b81b2..fdf6020 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs @@ -1,4 +1,4 @@ -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal enum OrderStatus { diff --git a/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs b/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs index 90ec286..40bb0a8 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs @@ -1,3 +1,3 @@ -namespace Module.Orders.Orders; +namespace Modules.Orders.Orders; internal record ProductId(Guid Value); \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/OrdersModule.cs b/src/Modules/Orders/Modules.Orders/OrdersModule.cs index 830ae48..8a224e4 100644 --- a/src/Modules/Orders/Modules.Orders/OrdersModule.cs +++ b/src/Modules/Orders/Modules.Orders/OrdersModule.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace Module.Orders; +namespace Modules.Orders; public static class OrdersModule { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs index a8d87bb..ab689a0 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs @@ -1,7 +1,7 @@ using FluentAssertions; using Modules.Warehouse.Storage.Domain; -namespace Module.Warehouse.Tests; +namespace Modules.Warehouse.Tests; public class AisleTests { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs index 34b5a6a..f281ce1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs @@ -1,9 +1,8 @@ -using Microsoft.AspNetCore.Mvc.TagHelpers; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; using Xunit.Abstractions; -namespace Module.Warehouse.Tests; +namespace Modules.Warehouse.Tests; public class ModelTests { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs index d73fe41..094e0c9 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs @@ -2,7 +2,7 @@ using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; -namespace Module.Warehouse.Tests; +namespace Modules.Warehouse.Tests; public class StorageAllocationServiceTests { diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 4ae5ee6..73bbb95 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,5 +1,5 @@ using Common.SharedKernel.Behaviours; -using Module.Orders; +using Modules.Orders; using Modules.Warehouse; var builder = WebApplication.CreateBuilder(args); From db391e1a18028c37a3426ce3719dcbde2a38fff9 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 11 Aug 2024 16:58:01 +1000 Subject: [PATCH 13/87] =?UTF-8?q?=E2=9C=A8=20Added=20Catalog=20Product?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ModularMonolith.sln | 2 +- .../Categories/CategoryService.cs | 0 .../Categories/Domain/Category.cs | 2 +- .../Categories/Domain/CategoryByIdSpec.cs | 2 +- .../Categories/Domain/CategoryCreatedEvent.cs | 2 +- .../Categories/Domain/CategoryId.cs | 3 +++ .../Categories/Domain/ICategoryService.cs | 2 +- .../Persistence/CategoryConfiguration.cs | 0 .../Modules.Catalog.csproj} | 4 ---- .../Modules.Catalog/Products/Product.cs | 18 ++++++++++++++++++ .../Categories/Domain/CategoryId.cs | 3 --- .../Products/Domain/Product.cs | 14 ++------------ .../Persistence/ProductConfiguration.cs | 2 +- 13 files changed, 29 insertions(+), 25 deletions(-) rename src/Modules/Products/{Modules.Catelog => Modules.Catalog}/Categories/CategoryService.cs (100%) rename src/Modules/Products/{Modules.Catelog => Modules.Catalog}/Categories/Domain/Category.cs (95%) rename src/Modules/Products/{Modules.Catelog => Modules.Catalog}/Categories/Domain/CategoryByIdSpec.cs (83%) rename src/Modules/Products/{Modules.Catelog => Modules.Catalog}/Categories/Domain/CategoryCreatedEvent.cs (73%) create mode 100644 src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs rename src/Modules/Products/{Modules.Catelog => Modules.Catalog}/Categories/Domain/ICategoryService.cs (65%) rename src/Modules/Products/{Modules.Catelog => Modules.Catalog}/Categories/Persistence/CategoryConfiguration.cs (100%) rename src/Modules/Products/{Modules.Catelog/Modules.Catelog.csproj => Modules.Catalog/Modules.Catalog.csproj} (80%) create mode 100644 src/Modules/Products/Modules.Catalog/Products/Product.cs delete mode 100644 src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 149d56b..37fb751 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -34,7 +34,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Orders.Tests", "src EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Catalog", "Catalog", "{1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catelog", "src\Modules\Products\Modules.Catelog\Modules.Catelog.csproj", "{591B271C-4C16-49CA-9549-E51087B591D1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catalog", "src\Modules\Products\Modules.Catalog\Modules.Catalog.csproj", "{591B271C-4C16-49CA-9549-E51087B591D1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Tests", "src\Modules\Warehouse\Modules.Warehouse.Tests\Modules.Warehouse.Tests.csproj", "{C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}" EndProject diff --git a/src/Modules/Products/Modules.Catelog/Categories/CategoryService.cs b/src/Modules/Products/Modules.Catalog/Categories/CategoryService.cs similarity index 100% rename from src/Modules/Products/Modules.Catelog/Categories/CategoryService.cs rename to src/Modules/Products/Modules.Catalog/Categories/CategoryService.cs diff --git a/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs similarity index 95% rename from src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs rename to src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs index bba8d34..704aee6 100644 --- a/src/Modules/Products/Modules.Catelog/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs @@ -2,7 +2,7 @@ using Common.SharedKernel.Domain.Exceptions; using Throw; -namespace Modules.Catelog.Categories.Domain; +namespace Modules.Catalog.Categories.Domain; internal class Category : AggregateRoot { diff --git a/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryByIdSpec.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryByIdSpec.cs similarity index 83% rename from src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryByIdSpec.cs rename to src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryByIdSpec.cs index b97cc02..f8512ad 100644 --- a/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryByIdSpec.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryByIdSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Modules.Catelog.Categories.Domain; +namespace Modules.Catalog.Categories.Domain; internal class CategoryByIdSpec : Specification, ISingleResultSpecification { diff --git a/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryCreatedEvent.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs similarity index 73% rename from src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryCreatedEvent.cs rename to src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs index 9e98f5f..5eac8c0 100644 --- a/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryCreatedEvent.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs @@ -1,5 +1,5 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Catelog.Categories.Domain; +namespace Modules.Catalog.Categories.Domain; internal record CategoryCreatedEvent(CategoryId Id, string Name) : DomainEvent; diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs new file mode 100644 index 0000000..65bb098 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs @@ -0,0 +1,3 @@ +namespace Modules.Catalog.Categories.Domain; + +internal record CategoryId(Guid Value); diff --git a/src/Modules/Products/Modules.Catelog/Categories/Domain/ICategoryService.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/ICategoryService.cs similarity index 65% rename from src/Modules/Products/Modules.Catelog/Categories/Domain/ICategoryService.cs rename to src/Modules/Products/Modules.Catalog/Categories/Domain/ICategoryService.cs index d05bfec..09015c0 100644 --- a/src/Modules/Products/Modules.Catelog/Categories/Domain/ICategoryService.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/ICategoryService.cs @@ -1,4 +1,4 @@ -namespace Modules.Catelog.Categories.Domain; +namespace Modules.Catalog.Categories.Domain; internal interface ICategoryRepository { diff --git a/src/Modules/Products/Modules.Catelog/Categories/Persistence/CategoryConfiguration.cs b/src/Modules/Products/Modules.Catalog/Categories/Persistence/CategoryConfiguration.cs similarity index 100% rename from src/Modules/Products/Modules.Catelog/Categories/Persistence/CategoryConfiguration.cs rename to src/Modules/Products/Modules.Catalog/Categories/Persistence/CategoryConfiguration.cs diff --git a/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj similarity index 80% rename from src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj rename to src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 25c9da1..7c721e5 100644 --- a/src/Modules/Products/Modules.Catelog/Modules.Catelog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -8,8 +8,4 @@ - - - - diff --git a/src/Modules/Products/Modules.Catalog/Products/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Product.cs new file mode 100644 index 0000000..68b2897 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/Product.cs @@ -0,0 +1,18 @@ +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Entities; +using Modules.Catalog.Categories.Domain; + +namespace Modules.Catalog.Products; + +internal record ProductId(Guid Value); + +internal class Product : AggregateRoot +{ + private string _name = string.Empty; + + private string _sku = string.Empty; + + private Money _price = Money.Default; + + private List _categories = []; +} diff --git a/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs b/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs deleted file mode 100644 index e32d94f..0000000 --- a/src/Modules/Products/Modules.Catelog/Categories/Domain/CategoryId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Catelog.Categories.Domain; - -internal record CategoryId(Guid Value); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 6c016c1..474f8d6 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -13,8 +13,6 @@ internal class Product : AggregateRoot public string Name { get; private set; } = null!; - public Money Price { get; private set; } = null!; - public Sku Sku { get; private set; } = null!; public int StockOnHand { get; private set; } @@ -23,7 +21,7 @@ private Product() { } - // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money & Sku) be passed via the constructor + // NOTE: Need to use a factory, as EF does not let owned entities (i.e. Money & Sku) be passed via the constructor public static Product Create(string name, Money price, Sku sku, IProductRepository productRepository) { name.Throw().IfEmpty(); @@ -32,12 +30,10 @@ public static Product Create(string name, Money price, Sku sku, IProductReposito var product = new Product { Id = new ProductId(Guid.NewGuid()), - // CategoryId = categoryId, - Name = name, - Price = price, StockOnHand = 0 }; + product.UpdateName(name); product.UpdateSku(sku, productRepository); product.AddDomainEvent(ProductCreatedEvent.Create(product)); @@ -51,12 +47,6 @@ public void UpdateName(string name) Name = name; } - public void UpdatePrice(Money price) - { - price.Throw().IfNegativeOrZero(p => p.Amount); - Price = price; - } - public void UpdateSku(Sku sku, IProductRepository productRepository) { if (productRepository.SkuExists(sku)) diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs index 4036436..f0a3ce0 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs @@ -18,7 +18,7 @@ public void Configure(EntityTypeBuilder builder) .HasConversion(sku => sku.Value, value => Sku.Create(value)!) .HasMaxLength(50); - builder.ComplexProperty(p => p.Price, MoneyConfiguration.BuildAction); + // builder.ComplexProperty(p => p.Price, MoneyConfiguration.BuildAction); //builder.ComplexProperty(p => p.Price, () => MoneyConfiguration.BuildAction) From 8c1c49c0fd10d7ebb1780ad5c0b76aeb2d199f28 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 11 Aug 2024 17:20:12 +1000 Subject: [PATCH 14/87] =?UTF-8?q?=E2=9C=A8=20Added=20Payment=20to=20Order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Orders/Modules.Orders/Carts/Cart.cs | 38 +++++++++++++++++++ .../{Orders => Common}/ProductId.cs | 0 .../Orders/Modules.Orders/Orders/Order.cs | 25 ++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 src/Modules/Orders/Modules.Orders/Carts/Cart.cs rename src/Modules/Orders/Modules.Orders/{Orders => Common}/ProductId.cs (100%) diff --git a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs new file mode 100644 index 0000000..b119593 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs @@ -0,0 +1,38 @@ +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Entities; +using Modules.Orders.Orders; + +namespace Modules.Orders.Carts; + +internal record CartId(Guid Value); + +internal class Cart : AggregateRoot +{ + private List _items = new(); + + public void AddItem(CartItem item) + { + _items.Add(item); + } + + public void RemoveItem(CartItem item) + { + _items.Remove(item); + } +} + +internal record CartItemId(Guid Value); + +internal class CartItem : Entity +{ + private ProductId _productId; + private int _quantity; + private Money _price; + + public CartItem(ProductId productId, int quantity, Money price) + { + _productId = productId; + _quantity = quantity; + _price = price; + } +} diff --git a/src/Modules/Orders/Modules.Orders/Orders/ProductId.cs b/src/Modules/Orders/Modules.Orders/Common/ProductId.cs similarity index 100% rename from src/Modules/Orders/Modules.Orders/Orders/ProductId.cs rename to src/Modules/Orders/Modules.Orders/Common/ProductId.cs diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order.cs index 35cfa2c..ed19670 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order.cs @@ -17,6 +17,8 @@ internal class Order : AggregateRoot // TODO: Check FE overrides this public Money AmountPaid { get; private set; } = null!; + private Payment _payment = null!; + public OrderStatus Status { get; private set; } public DateTimeOffset ShippingDate { get; private set; } @@ -136,3 +138,26 @@ public ErrorOr ShipOrder(TimeProvider timeProvider) return Result.Success; } } + +internal record PaymentId(Guid Value); + +internal class Payment : Entity +{ + public Money Amount { get; private set; } + + public PaymentType PaymentType { get; private set; } + + public Payment(Money amount, PaymentType paymentType) + { + Amount = amount; + PaymentType = paymentType; + } +} + +// TODO: Convert to Smart Enums +internal enum PaymentType +{ + CreditCard = 1, + PayPal = 2, + Cash = 3 +} From 0b7c95547232a2802d3688ead57e29eff16d0731 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 11 Aug 2024 17:47:29 +1000 Subject: [PATCH 15/87] Added Warehouse BackOrder. Also introduced Ardalis.SmartEnum --- .../Modules.Orders/Modules.Orders.csproj | 1 + .../Orders/Modules.Orders/Orders/Order.cs | 16 ++++--- .../Modules.Warehouse/BackOrders/BackOrder.cs | 46 +++++++++++++++++++ .../Modules.Warehouse.csproj | 5 +- 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index 45c47f7..4e8a635 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order.cs index ed19670..1f2ef69 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order.cs @@ -1,4 +1,5 @@ -using Common.SharedKernel.Domain.Base; +using Ardalis.SmartEnum; +using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using ErrorOr; @@ -154,10 +155,13 @@ public Payment(Money amount, PaymentType paymentType) } } -// TODO: Convert to Smart Enums -internal enum PaymentType +internal class PaymentType : SmartEnum { - CreditCard = 1, - PayPal = 2, - Cash = 3 + public static readonly PaymentType CreditCard = new(1, "CreditCard"); + public static readonly PaymentType PayPal = new(2, "PayPal"); + public static readonly PaymentType Cash = new(3, "Cash"); + + private PaymentType(int id, string name) : base(name, id) + { + } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs new file mode 100644 index 0000000..6aa8a7f --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs @@ -0,0 +1,46 @@ +using Ardalis.SmartEnum; +using Common.SharedKernel.Domain.Base; +using Modules.Warehouse.Products.Domain; + +namespace Modules.Warehouse.BackOrders; + +internal record BackOrderId(Guid Value); + +internal class BackOrder : AggregateRoot +{ + private ProductId _productId = null!; + + private int _quantityOrdered; + + private int _quantityReceived; + + public BackOrderStatus Status { get; private set; } = null!; + + private BackOrder() + { + } + + public static BackOrder Create(ProductId productId, int quantityOrdered) + { + var backOrder = new BackOrder + { + Id = new BackOrderId(Guid.NewGuid()), + _productId = productId, + _quantityOrdered = quantityOrdered, + _quantityReceived = 0, + Status = BackOrderStatus.Pending + }; + + return backOrder; + } +} + +internal class BackOrderStatus : SmartEnum +{ + public static readonly BackOrderStatus Pending = new(1, "Pending"); + public static readonly BackOrderStatus Received = new(2, "Received"); + + private BackOrderStatus(int id, string name) : base(name, id) + { + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 1f12734..f2b7d09 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -1,6 +1,7 @@  + @@ -20,8 +21,4 @@ - - - - From a1a611558db1b015ec5cb0a0a448a3891f0e4242 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 11 Aug 2024 20:28:58 +1000 Subject: [PATCH 16/87] =?UTF-8?q?=F0=9F=93=9D=20Added=20Event=20Storming?= =?UTF-8?q?=20and=20Bounded=20Contexts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 9d4bc6f..4d9bc8b 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,26 @@ Orders: - The order tax must always be correct - Shipping must be included in the total price - Payment must be completed for the order to be placed (FUTURE: Consider splitting payments to it's own module) + +## Event Storming + +![image](https://github.com/user-attachments/assets/63ceadd7-428e-4cac-9937-377e46ae384a) + +## Bounded Contexts + +### Warehouse Context + +![image](https://github.com/user-attachments/assets/f7d5a522-246b-4ecf-88cf-e5cb0738f0a0) + +### Catalog Context + +![image](https://github.com/user-attachments/assets/a08d2964-c3fa-417f-8b6a-598d2a2fe511) + +### Customer Context + +![image](https://github.com/user-attachments/assets/d104aff7-2d5d-4308-af97-fca0c298d0b7) + +### Orders Context + +![image](https://github.com/user-attachments/assets/3c731981-1f98-42ca-9f74-c955a31a5790) + From c566b7f837c97db65997ef0b65a8a0e303d37606 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:27:40 +1000 Subject: [PATCH 17/87] =?UTF-8?q?=E2=9C=A8=2026=20catalog=20create=20invar?= =?UTF-8?q?iants=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update product catalog and add product creation tests Adjusted README to remove redundant price requirement. Refactored the Product and Category classes for better encapsulation and added unit tests for product creation in the Catalog module. * Add unit tests for Category creation in Catalog module Introduce tests to ensure Category creation throws exceptions for null, empty, and whitespace names. Also, verify correct Category creation with valid name. * Refactor out validation method calls. Replaced custom validation methods with built-in `ArgumentOutOfRangeException.ThrowIfNegativeOrZero` for better clarity and consistency. Also adjusted loop indices to start from 1 instead of 0 in `Bay` and `Aisle` class methods. --- ModularMonolith.sln | 7 + README.md | 2 - .../Modules.Catalog.Tests/CatalogTests.cs | 51 +++++ .../Modules.Catalog.Tests/GlobalUsings.cs | 1 + .../Modules.Catalog.Tests.csproj | 30 +++ .../Modules.Catalog.Tests/ProductTests.cs | 180 ++++++++++++++++++ .../Products/Modules.Catalog/AssemblyInfo.cs | 3 + .../Categories/Domain/Category.cs | 18 +- .../Modules.Catalog/Modules.Catalog.csproj | 1 + .../Modules.Catalog/Products/Product.cs | 51 ++++- .../Products/Domain/Product.cs | 4 +- .../Modules.Warehouse/Storage/Domain/Aisle.cs | 6 +- .../Modules.Warehouse/Storage/Domain/Bay.cs | 9 +- .../Modules.Warehouse/Storage/Domain/Shelf.cs | 7 +- 14 files changed, 340 insertions(+), 30 deletions(-) create mode 100644 src/Modules/Products/Modules.Catalog.Tests/CatalogTests.cs create mode 100644 src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs create mode 100644 src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj create mode 100644 src/Modules/Products/Modules.Catalog.Tests/ProductTests.cs create mode 100644 src/Modules/Products/Modules.Catalog/AssemblyInfo.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 37fb751..9c10421 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catalog", "src\Modu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Tests", "src\Modules\Warehouse\Modules.Warehouse.Tests\Modules.Warehouse.Tests.csproj", "{C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catalog.Tests", "src\Modules\Products\Modules.Catalog.Tests\Modules.Catalog.Tests.csproj", "{11925734-961D-4761-B209-BF601E59EB95}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,10 @@ Global {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0}.Release|Any CPU.Build.0 = Release|Any CPU + {11925734-961D-4761-B209-BF601E59EB95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11925734-961D-4761-B209-BF601E59EB95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11925734-961D-4761-B209-BF601E59EB95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11925734-961D-4761-B209-BF601E59EB95}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -95,5 +101,6 @@ Global {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} = {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} {591B271C-4C16-49CA-9549-E51087B591D1} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} + {11925734-961D-4761-B209-BF601E59EB95} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 4d9bc8b..c0e5c33 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ Warehouse: Product Catalog: - A product must have a name -- A product must have a price - A product can be given one or more categories - A product cannot have a negative price - A product cannot duplicate categories @@ -77,4 +76,3 @@ Orders: ### Orders Context ![image](https://github.com/user-attachments/assets/3c731981-1f98-42ca-9f74-c955a31a5790) - diff --git a/src/Modules/Products/Modules.Catalog.Tests/CatalogTests.cs b/src/Modules/Products/Modules.Catalog.Tests/CatalogTests.cs new file mode 100644 index 0000000..e5449be --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Tests/CatalogTests.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using Modules.Catalog.Categories.Domain; + +namespace Modules.Catalog.Tests; + +public class CatalogTests +{ + [Fact] + public void Create_ShouldThrowException_WhenNameIsNull() + { + // Arrange + Action act = () => Category.Create(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenNameIsEmpty() + { + // Arrange + Action act = () => Category.Create(string.Empty); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenNameIsWhiteSpace() + { + // Arrange + Action act = () => Category.Create(" "); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldReturnCategory_WhenNameIsValid() + { + // Arrange + var name = "ValidName"; + + // Act + var category = Category.Create(name); + + // Assert + category.Name.Should().Be(name); + category.Id.Should().NotBeNull(); + } +} diff --git a/src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs b/src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj new file mode 100644 index 0000000..f5522ae --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Modules/Products/Modules.Catalog.Tests/ProductTests.cs b/src/Modules/Products/Modules.Catalog.Tests/ProductTests.cs new file mode 100644 index 0000000..eb0a705 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Tests/ProductTests.cs @@ -0,0 +1,180 @@ +using Common.SharedKernel.Domain.Entities; +using FluentAssertions; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Products; + +namespace Modules.Catalog.Tests; + +public class ProductTests +{ + [Fact] + public void Create_ShouldThrowException_WhenNameIsNull() + { + // Act + var act = () => Product.Create(null!, "ValidSku"); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenNameIsEmpty() + { + // Act + var act = () => Product.Create(string.Empty, "ValidSku"); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenNameIsWhiteSpace() + { + // Act + var act = () => Product.Create(" ", "ValidSku"); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenSkuIsNull() + { + // Act + var act = () => Product.Create("ValidName", null!); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenSkuIsEmpty() + { + // Act + var act = () => Product.Create("ValidName", string.Empty); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenSkuIsWhiteSpace() + { + // Act + var act = () => Product.Create("ValidName", " "); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldReturnProduct_WhenNameAndSkuAreValid() + { + // Act + var product = Product.Create("ValidName", "ValidSku"); + + // Assert + product.Name.Should().Be("ValidName"); + product.Sku.Should().Be("ValidSku"); + product.Id.Should().NotBeNull(); + } + + [Fact] + public void UpdatePrice_ShouldThrowException_WhenPriceIsNegative() + { + // Arrange + var product = Product.Create("ValidName", "ValidSku"); + + // Act + Action act = () => product.UpdatePrice(Money.Create(-1)); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void UpdatePrice_ShouldThrowException_WhenPriceIsZero() + { + // Arrange + var product = Product.Create("ValidName", "ValidSku"); + + // Act + var act = () => product.UpdatePrice(Money.Create(0)); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void UpdatePrice_ShouldUpdatePrice_WhenPriceIsPositive() + { + // Arrange + var product = Product.Create("ValidName", "ValidSku"); + var newPrice = Money.Create(100); + + // Act + product.UpdatePrice(newPrice); + + // Assert + product.Price.Should().Be(newPrice); + } + + [Fact] + public void AddCategory_ShouldAddCategory_WhenCategoryIsNotDuplicate() + { + // Arrage + var product = Product.Create("ValidName", "ValidSku"); + var category = Category.Create("Category1"); + + // Act + product.AddCategory(category); + + // Assert + product.Categories.Should().Contain(category); + } + + [Fact] + public void AddCategory_ShouldNotAddCategory_WhenCategoryIsDuplicate() + { + // Arrange + var product = Product.Create("ValidName", "ValidSku"); + var category = Category.Create("Category1"); + + // Act + product.AddCategory(category); + product.AddCategory(category); + + // Assert + product.Categories.Count.Should().Be(1); + } + + [Fact] + public void RemoveCategory_ShouldRemoveCategory_WhenCategoryExists() + { + // Arrange + var product = Product.Create("ValidName", "ValidSku"); + var category = Category.Create("Category1"); + + // Act + product.AddCategory(category); + product.RemoveCategory(category); + + // Assert + product.Categories.Should().NotContain(category); + product.Categories.Count.Should().Be(0); + } + + [Fact] + public void RemoveCategory_ShouldNotThrowException_WhenCategoryDoesNotExist() + { + // Arrange + var product = Product.Create("ValidName", "ValidSku"); + var category = Category.Create("Category1"); + + // Act + Action act = () => product.RemoveCategory(category); + + // Assert + act.Should().NotThrow(); + } +} diff --git a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs new file mode 100644 index 0000000..bd5a405 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Modules.Catalog.Tests")] diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs index 704aee6..f2962b2 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs @@ -1,37 +1,33 @@ using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Exceptions; -using Throw; namespace Modules.Catalog.Categories.Domain; internal class Category : AggregateRoot { + /// + /// Name should be unique + /// public string Name { get; private set; } = default!; private Category() { } // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money & Sku) be passed via the constructor - public static Category Create(string name, ICategoryRepository categoryRepository) + public static Category Create(string name) { var category = new Category { Id = new CategoryId(Guid.NewGuid()), }; - category.UpdateName(name, categoryRepository); - + category.UpdateName(name); category.AddDomainEvent(new CategoryCreatedEvent(category.Id, category.Name)); return category; } - public void UpdateName(string name, ICategoryRepository categoryRepository) + private void UpdateName(string name) { - name.Throw().IfEmpty(); - // Guard.Against.NullOrWhiteSpace(name); - - if (categoryRepository.CategoryExists(name)) - throw new DomainException($"Category {name} already exists"); + ArgumentException.ThrowIfNullOrWhiteSpace(name); Name = name; } diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 7c721e5..1187a11 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Modules/Products/Modules.Catalog/Products/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Product.cs index 68b2897..464ff33 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Product.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Product.cs @@ -8,11 +8,54 @@ internal record ProductId(Guid Value); internal class Product : AggregateRoot { - private string _name = string.Empty; + public string Name { get; private set; } = null!; - private string _sku = string.Empty; + public string Sku { get; private set; } = null!; - private Money _price = Money.Default; + public Money Price { get; private set; } = Money.Default; - private List _categories = []; + private readonly List _categories = []; + + public IReadOnlyList Categories => _categories.AsReadOnly(); + + private Product() + { + } + + public static Product Create(string name, string sku) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentException.ThrowIfNullOrWhiteSpace(sku); + + var product = new Product + { + Name = name, + Sku = sku, + Id = new ProductId(Guid.NewGuid()) + }; + + return product; + } + + public void UpdatePrice(Money price) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price.Amount); + Price = price; + } + + public void AddCategory(Category category) + { + if (_categories.Contains(category)) + return; + + _categories.Add(category); + } + + public void RemoveCategory(Category category) + { + if (!_categories.Contains(category)) + return; + + _categories.Remove(category); + } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 474f8d6..686ddfd 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -41,13 +41,13 @@ public static Product Create(string name, Money price, Sku sku, IProductReposito return product; } - public void UpdateName(string name) + private void UpdateName(string name) { name.Throw().IfEmpty(); Name = name; } - public void UpdateSku(Sku sku, IProductRepository productRepository) + private void UpdateSku(Sku sku, IProductRepository productRepository) { if (productRepository.SkuExists(sku)) throw new ArgumentException("Sku already exists"); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs index 8f712fe..b007970 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs @@ -21,7 +21,7 @@ private Aisle() { } public static Aisle Create(string name, int numBays, int numShelves) { - numBays.Throw().IfNegativeOrZero(); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(numBays); var aisle = new Aisle { @@ -29,9 +29,9 @@ public static Aisle Create(string name, int numBays, int numShelves) Name = name }; - for (var i = 0; i < numBays; i++) + for (var i = 1; i <= numBays; i++) { - var bay = Bay.Create(i + 1, numShelves); + var bay = Bay.Create(i, numShelves); aisle._bays.Add(bay); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs index 36c0cec..bfd4c33 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs @@ -3,8 +3,6 @@ namespace Modules.Warehouse.Storage.Domain; -// internal record BayId(Guid Value); - internal class Bay : Entity { private readonly List _shelves = []; @@ -19,7 +17,8 @@ internal class Bay : Entity public static Bay Create(int id, int numShelves) { - numShelves.Throw().IfNegativeOrZero(); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(numShelves); var bay = new Bay { @@ -27,9 +26,9 @@ public static Bay Create(int id, int numShelves) Name = $"Bay {id}" }; - for (var j = 0; j < numShelves; j++) + for (var i = 1; i <= numShelves; i++) { - var shelf = Shelf.Create(j); + var shelf = Shelf.Create(i); bay._shelves.Add(shelf); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs index ecc5c8b..92bce7f 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs @@ -1,5 +1,6 @@ using Common.SharedKernel.Domain.Base; using Modules.Warehouse.Products.Domain; +using Throw; namespace Modules.Warehouse.Storage.Domain; @@ -7,15 +8,15 @@ internal class Shelf : Entity { public string Name { get; private set; } = null!; - // private int _number => Id; - public ProductId? ProductId { get; private set; } public bool IsEmpty => ProductId is null; public static Shelf Create(int number) { - return new Shelf() + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(number); + + return new Shelf { Id = number, Name = $"Shelf {number}" From 9878a20706ae4f2e05a0accca751894fb0d7e270 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:14:48 +1000 Subject: [PATCH 18/87] =?UTF-8?q?=E2=9C=A8=20Added=20CartItem=20logic=20an?= =?UTF-8?q?d=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored the Cart and CartItem classes to include methods for adding, removing, and updating items, along with maintaining the total price. Modified the warehouse middleware initialization to be synchronous, and added unit tests for CartItem. --- Directory.Build.props | 2 + .../Common.SharedKernel.csproj | 1 + .../Cart/CartItemTests.cs | 107 ++++++++++++++++++ .../Modules.Orders.Tests.csproj | 4 + .../Orders/Modules.Orders/Carts/Cart.cs | 53 ++++++--- .../Orders/Modules.Orders/Carts/CartItem.cs | 63 +++++++++++ .../Modules.Warehouse/WarehouseModule.cs | 8 +- src/WebApi/Program.cs | 3 +- 8 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs create mode 100644 src/Modules/Orders/Modules.Orders/Carts/CartItem.cs diff --git a/Directory.Build.props b/Directory.Build.props index 1bae2cd..3785898 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,9 +6,11 @@ latest + Minimum true true true + diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 2d55942..400247e 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs new file mode 100644 index 0000000..fecbe61 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs @@ -0,0 +1,107 @@ +using Common.SharedKernel.Domain.Entities; +using FluentAssertions; +using Modules.Orders.Carts; +using Modules.Orders.Orders; + +namespace Modules.Orders.Tests.Cart; + +public class CartItemTests +{ + [Fact] + public void Create_ValidParameters_ShouldCreateCartItem() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var quantity = 2; + var unitPrice = Money.Create(100m); + + // Act + var cartItem = CartItem.Create(productId, quantity, unitPrice); + + // Assert + cartItem.ProductId.Should().Be(productId); + cartItem.Quantity.Should().Be(quantity); + cartItem.UnitPrice.Should().Be(unitPrice); + cartItem.LinePrice.Amount.Should().Be(200m); + } + + [Fact] + public void Create_NegativeQuantity_ShouldThrow() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var quantity = -1; + var unitPrice = Money.Create(100m); + + // Act + var act = () => CartItem.Create(productId, quantity, unitPrice); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Create_NegativeUnitPrice_ShouldThrow() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var quantity = 2; + var unitPrice = Money.Create(-100m); + + // Act + var act = () => CartItem.Create(productId, quantity, unitPrice); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void IncreaseQuantity_ShouldIncreaseQuantity() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var quantity = 2; + var unitPrice = Money.Create(100m); + var cartItem = CartItem.Create(productId, quantity, unitPrice); + + // Act + cartItem.IncreaseQuantity(3); + + // Assert + cartItem.Quantity.Should().Be(5); + cartItem.LinePrice.Amount.Should().Be(500m); + } + + [Fact] + public void DecreaseQuantity_ShouldDecreaseQuantity() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var quantity = 5; + var unitPrice = Money.Create(100m); + var cartItem = CartItem.Create(productId, quantity, unitPrice); + + // Act + cartItem.DecreaseQuantity(3); + + // Assert + cartItem.Quantity.Should().Be(2); + cartItem.LinePrice.Amount.Should().Be(200m); + } + + [Fact] + public void DecreaseQuantity_TooMany_ShouldThrow() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var quantity = 2; + var unitPrice = Money.Create(100m); + var cartItem = CartItem.Create(productId, quantity, unitPrice); + + // Act + var act = () => cartItem.DecreaseQuantity(3); + + // Assert + act.Should().Throw(); + } +} diff --git a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index a7189e3..7499846 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -23,4 +23,8 @@ + + + + diff --git a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs index b119593..2b47e75 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs @@ -8,31 +8,52 @@ internal record CartId(Guid Value); internal class Cart : AggregateRoot { - private List _items = new(); + private List _items = []; - public void AddItem(CartItem item) + public Money TotalPrice { get; private set; } = null!; + + public static Cart Create(ProductId productId, int quantity, Money unitPrice) { - _items.Add(item); + var cart = new Cart + { + Id = new CartId(Guid.NewGuid()) + }; + + cart.AddItem(productId, quantity, unitPrice); + + return cart; } - public void RemoveItem(CartItem item) + public void AddItem(ProductId productId, int quantity, Money unitPrice) { - _items.Remove(item); + var item = _items.FirstOrDefault(i => i.ProductId == productId); + if (item is not null) + { + item.IncreaseQuantity(quantity); + } + else + { + var cartItem = CartItem.Create(productId, quantity, unitPrice); + _items.Add(cartItem); + } + + UpdateTotal(); } -} -internal record CartItemId(Guid Value); + public void RemoveItem(ProductId productId) + { + var item = _items.FirstOrDefault(i => i.ProductId == productId); + if (item is null) + return; -internal class CartItem : Entity -{ - private ProductId _productId; - private int _quantity; - private Money _price; + _items.Remove(item); + UpdateTotal(); + } - public CartItem(ProductId productId, int quantity, Money price) + private void UpdateTotal() { - _productId = productId; - _quantity = quantity; - _price = price; + var currency = _items[0].UnitPrice.Currency; + var total = _items.Sum(i => i.LinePrice.Amount); + TotalPrice = new Money(currency, total); } } diff --git a/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs b/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs new file mode 100644 index 0000000..cd59ae0 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs @@ -0,0 +1,63 @@ +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Entities; +using Modules.Orders.Orders; + +namespace Modules.Orders.Carts; + +internal record CartItemId(Guid Value); + +internal class CartItem : Entity +{ + public ProductId ProductId { get; private set; } = null!; + + public int Quantity { get; private set; } + + public Money UnitPrice { get; private set; } = null!; + + public Money LinePrice { get; private set; } = null!; + + public static CartItem Create(ProductId productId, int quantity, Money unitPrice) + { + ArgumentNullException.ThrowIfNull(productId); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(unitPrice.Amount); + + var cartItem = new CartItem + { + Id = new CartItemId(Guid.NewGuid()), + ProductId = productId, + Quantity = quantity, + UnitPrice = unitPrice, + }; + + cartItem.UpdateLinePrice(); + + return cartItem; + } + + public void IncreaseQuantity(int quantity) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); + + Quantity += quantity; + UpdateLinePrice(); + } + + public void DecreaseQuantity(int quantity) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); + + if (Quantity - quantity < 0) + throw new ArgumentOutOfRangeException(nameof(quantity), "Cannot remove more items than the cart contains."); + + Quantity -= quantity; + UpdateLinePrice(); + } + + private void UpdateLinePrice() => LinePrice = UnitPrice with { Amount = UnitPrice.Amount * Quantity }; + + private CartItem() + { + } + +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 4abf6cf..cf8f4db 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -1,10 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Products; -using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Products.Endpoints; namespace Modules.Warehouse; @@ -27,7 +23,7 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration // services.AddTransient(); } - public static Task UseWarehouse(this WebApplication app) + public static void UseWarehouse(this WebApplication app) { // TODO: Refactor to up.ps1 // if (app.Environment.IsDevelopment()) @@ -41,7 +37,5 @@ public static Task UseWarehouse(this WebApplication app) // TODO: Move to feature DI app.MapProductEndpoints(); - - return Task.CompletedTask; } } diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 73bbb95..a044675 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -31,7 +31,6 @@ app.UseHttpsRedirection(); -app.UseOrders(); -await app.UseWarehouse(); +app.UseOrders(); app.UseWarehouse(); app.Run(); From baef97ddbf5ed09b0b1ba9b096d217e273abbccb Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:34:05 +1000 Subject: [PATCH 19/87] Add readonly property and unit tests to Cart class Introduced a read-only property for cart items and extended the `UpdateTotal()` method to handle empty carts. Additionally, created unit tests covering add, remove, and update operations to ensure cart functionality works as expected. #28 --- README.md | 1 - .../Modules.Orders.Tests/Cart/CartTests.cs | 94 +++++++++++++++++++ .../Orders/Modules.Orders/Carts/Cart.cs | 8 ++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs diff --git a/README.md b/README.md index c0e5c33..496920e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ Customers: - Must have an address Cart: -- Must be associated with a customer - Must always have the correct price Orders: diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs new file mode 100644 index 0000000..6dfbf7b --- /dev/null +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs @@ -0,0 +1,94 @@ +using Common.SharedKernel.Domain.Entities; +using FluentAssertions; +using Modules.Orders.Orders; + +namespace Modules.Orders.Tests.Cart; + +public class CartTests +{ + [Fact] + public void AddItem_ShouldIncreaseQuantity_WhenItemAlreadyExists() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var unitPrice = new Money(Currency.Default, 10); + var cart = Carts.Cart.Create(productId, 1, unitPrice); + + // Act + cart.AddItem(productId, 2, unitPrice); + + // Assert + var item = cart.Items.First(i => i.ProductId == productId); + item.Quantity.Should().Be(3); + item.LinePrice.Amount.Should().Be(30); + } + + [Fact] + public void AddItem_ShouldAddNewItem_WhenItemDoesNotExist() + { + // Arrange + var productId1 = new ProductId(Guid.NewGuid()); + var productId2 = new ProductId(Guid.NewGuid()); + var unitPrice = new Money(Currency.Default, 10); + var cart = Carts.Cart.Create(productId1, 1, unitPrice); + + // Act + cart.AddItem(productId2, 2, unitPrice); + + // Assert + cart.Items.Count.Should().Be(2); + var item = cart.Items.First(i => i.ProductId == productId2); + item.Quantity.Should().Be(2); + item.LinePrice.Amount.Should().Be(20); + } + + [Fact] + public void RemoveItem_ShouldRemoveItem_WhenItemExists() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var unitPrice = new Money(Currency.Default, 10); + var cart = Carts.Cart.Create(productId, 1, unitPrice); + + // Act + cart.RemoveItem(productId); + + // Assert + cart.Items.Should().BeEmpty(); + cart.TotalPrice.Amount.Should().Be(0); + } + + [Fact] + public void RemoveItem_ShouldDoNothing_WhenItemDoesNotExist() + { + // Arrange + var productId1 = new ProductId(Guid.NewGuid()); + var productId2 = new ProductId(Guid.NewGuid()); + var unitPrice = new Money(Currency.Default, 10); + var cart = Carts.Cart.Create(productId1, 1, unitPrice); + + // Act + cart.RemoveItem(productId2); + + // Assert + cart.Items.Should().HaveCount(1); + cart.TotalPrice.Amount.Should().Be(10); + } + + [Fact] + public void UpdateTotal_ShouldCalculateTotalPriceCorrectly() + { + // Arrange + var productId1 = new ProductId(Guid.NewGuid()); + var productId2 = new ProductId(Guid.NewGuid()); + var unitPrice1 = new Money(Currency.Default, 10); + var unitPrice2 = new Money(Currency.Default, 20); + var cart = Carts.Cart.Create(productId1, 1, unitPrice1); + + // Act + cart.AddItem(productId2, 2, unitPrice2); + + // Assert + cart.TotalPrice.Amount.Should().Be(50); + } +} diff --git a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs index 2b47e75..f3a8727 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs @@ -10,6 +10,8 @@ internal class Cart : AggregateRoot { private List _items = []; + public IReadOnlyList Items => _items.AsReadOnly(); + public Money TotalPrice { get; private set; } = null!; public static Cart Create(ProductId productId, int quantity, Money unitPrice) @@ -52,6 +54,12 @@ public void RemoveItem(ProductId productId) private void UpdateTotal() { + if (_items.Count == 0) + { + TotalPrice = Money.Default; + return; + } + var currency = _items[0].UnitPrice.Currency; var total = _items.Sum(i => i.LinePrice.Amount); TotalPrice = new Money(currency, total); From 85999b66fe961dc76191b24b1e929dcdc9cd1351 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 22 Aug 2024 06:15:26 +1000 Subject: [PATCH 20/87] =?UTF-8?q?=E2=9C=A8=20Add=20Payment=20and=20Payment?= =?UTF-8?q?Type=20entities;=20restructure=20Order=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced new Payment and PaymentType classes to handle order payments. Updated the Order class with comprehensive methods for handling subtotals, shipping, and tax. Reorganized the directory structure for better modularity and clarity. #28 --- README.md | 14 +-- .../Modules.Orders.Tests/LineItemTests.cs | 2 + .../Modules.Orders/Orders/CustomerId.cs | 3 - .../Orders/{ => LineItem}/LineItem.cs | 3 +- .../{ => LineItem}/LineItemCreatedEvent.cs | 3 +- .../Orders/LineItem/LineItemId.cs | 3 + .../Modules.Orders/Orders/LineItemId.cs | 3 - .../Modules.Orders/Orders/Order/CustomerId.cs | 3 + .../Orders/{ => Order}/Order.cs | 113 ++++++++++-------- .../Orders/{ => Order}/OrderByIdSpec.cs | 2 +- .../Orders/{ => Order}/OrderCreatedEvent.cs | 2 +- .../Orders/{ => Order}/OrderErrors.cs | 2 +- .../Modules.Orders/Orders/Order/OrderId.cs | 3 + .../{ => Order}/OrderReadyForShippingEvent.cs | 2 +- .../Orders/{ => Order}/OrderSpec.cs | 2 +- .../Orders/Order/OrderStatus.cs | 24 ++++ .../Orders/Modules.Orders/Orders/OrderId.cs | 3 - .../Modules.Orders/Orders/OrderStatus.cs | 9 -- .../Modules.Orders/Orders/Payment/Payment.cs | 19 +++ .../Orders/Payment/PaymentType.cs | 14 +++ 20 files changed, 147 insertions(+), 82 deletions(-) delete mode 100644 src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs rename src/Modules/Orders/Modules.Orders/Orders/{ => LineItem}/LineItem.cs (95%) rename src/Modules/Orders/Modules.Orders/Orders/{ => LineItem}/LineItemCreatedEvent.cs (81%) create mode 100644 src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemId.cs delete mode 100644 src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs create mode 100644 src/Modules/Orders/Modules.Orders/Orders/Order/CustomerId.cs rename src/Modules/Orders/Modules.Orders/Orders/{ => Order}/Order.cs (56%) rename src/Modules/Orders/Modules.Orders/Orders/{ => Order}/OrderByIdSpec.cs (84%) rename src/Modules/Orders/Modules.Orders/Orders/{ => Order}/OrderCreatedEvent.cs (85%) rename src/Modules/Orders/Modules.Orders/Orders/{ => Order}/OrderErrors.cs (96%) create mode 100644 src/Modules/Orders/Modules.Orders/Orders/Order/OrderId.cs rename src/Modules/Orders/Modules.Orders/Orders/{ => Order}/OrderReadyForShippingEvent.cs (84%) rename src/Modules/Orders/Modules.Orders/Orders/{ => Order}/OrderSpec.cs (80%) create mode 100644 src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs delete mode 100644 src/Modules/Orders/Modules.Orders/Orders/OrderId.cs delete mode 100644 src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs create mode 100644 src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs create mode 100644 src/Modules/Orders/Modules.Orders/Orders/Payment/PaymentType.cs diff --git a/README.md b/README.md index 496920e..7b4aa79 100644 --- a/README.md +++ b/README.md @@ -31,21 +31,21 @@ Responsible for Warehouse and Inventory Management ## Business Invariants Warehouse: -- An Aisle cannot have duplicate bays -- A bay cannot have duplicate shelves +- ✅ An Aisle cannot have duplicate bays +- ✅ A bay cannot have duplicate shelves Product Catalog: -- A product must have a name -- A product can be given one or more categories -- A product cannot have a negative price -- A product cannot duplicate categories +- ✅ A product must have a name +- ✅ A product can be given one or more categories +- ✅ A product cannot have a negative price +- ✅ A product cannot duplicate categories Customers: - Must have a unique email address - Must have an address Cart: -- Must always have the correct price +- ✅ Must always have the correct price Orders: - An order must be associated with a customer diff --git a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs index 238bb4c..1579bbc 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs @@ -1,6 +1,8 @@ using Common.SharedKernel.Domain.Entities; using FluentAssertions; using Modules.Orders.Orders; +using Modules.Orders.Orders.LineItem; +using Modules.Orders.Orders.Order; namespace Modules.Orders.Tests; diff --git a/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs b/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs deleted file mode 100644 index 25a20ef..0000000 --- a/src/Modules/Orders/Modules.Orders/Orders/CustomerId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Orders.Orders; - -internal record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs similarity index 95% rename from src/Modules/Orders/Modules.Orders/Orders/LineItem.cs rename to src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs index 90943ac..d52f7a1 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItem.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs @@ -1,8 +1,9 @@ using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; +using Modules.Orders.Orders.Order; using Throw; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.LineItem; internal class LineItem : Entity { diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs similarity index 81% rename from src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs rename to src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs index 81589e6..26b0a61 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs @@ -1,6 +1,7 @@ using Common.SharedKernel.Domain.Base; +using Modules.Orders.Orders.Order; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.LineItem; internal record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : DomainEvent { diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemId.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemId.cs new file mode 100644 index 0000000..bc00446 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemId.cs @@ -0,0 +1,3 @@ +namespace Modules.Orders.Orders.LineItem; + +internal record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs deleted file mode 100644 index 4bfcb46..0000000 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItemId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Orders.Orders; - -internal record LineItemId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/CustomerId.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/CustomerId.cs new file mode 100644 index 0000000..fc60879 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/CustomerId.cs @@ -0,0 +1,3 @@ +namespace Modules.Orders.Orders.Order; + +internal record CustomerId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs similarity index 56% rename from src/Modules/Orders/Modules.Orders/Orders/Order.cs rename to src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs index 1f2ef69..e042a69 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs @@ -1,24 +1,34 @@ -using Ardalis.SmartEnum; -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using ErrorOr; +using Modules.Orders.Orders.LineItem; using Success = ErrorOr.Success; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.Order; + +/* + * An order must be associated with a customer - DONE + * The order total must always be correct - DONE + * The order tax must always be correct - DONE + * Shipping must be included in the total price - DONE + * Payment must be completed for the order to be placed - DONE + */ internal class Order : AggregateRoot { - private readonly List _lineItems = []; + // 10% tax rate + private const decimal TaxRate = 0.1m; + + private readonly List _lineItems = []; - public IEnumerable LineItems => _lineItems.AsReadOnly(); + public IEnumerable LineItems => _lineItems.AsReadOnly(); public required CustomerId CustomerId { get; init; } - // TODO: Check FE overrides this public Money AmountPaid { get; private set; } = null!; - private Payment _payment = null!; + private Payment.Payment _payment = null!; public OrderStatus Status { get; private set; } @@ -26,19 +36,28 @@ internal class Order : AggregateRoot public Currency? OrderCurrency => _lineItems.FirstOrDefault()?.Price.Currency; - public Money OrderTotal - { - get - { - if (_lineItems.Count == 0) - return Money.Default; + /// + /// Total of all line items (including quantities). Excludes tax and shipping. + /// + public Money OrderSubTotal { get; private set; } = null!; + + + /// + /// Shipping total. Excludes tax. + /// + public Money ShippingTotal {get; private set; } = null!; + + /// + /// Tax of the order. Calculated on the OrderSubTotal and ShippingTotal. + /// + public Money TaxTotal { get; private set; } = null!; + + /// + /// OrderSubTotal + ShippingTotal + TaxTotal + /// + public Money OrderTotal => OrderSubTotal + ShippingTotal + TaxTotal; - var amount = _lineItems.Sum(li => li.Price.Amount * li.Quantity); - var currency = _lineItems[0].Price.Currency; - return new Money(currency, amount); - } - } private Order() { @@ -46,11 +65,14 @@ private Order() public static Order Create(CustomerId customerId) { - var order = new Order() + var order = new Order { Id = new OrderId(Guid.NewGuid()), CustomerId = customerId, - AmountPaid = Money.Default, + AmountPaid = Money.Zero, + OrderSubTotal = Money.Zero, + ShippingTotal = Money.Zero, + TaxTotal = Money.Zero, Status = OrderStatus.PendingPayment }; @@ -59,7 +81,7 @@ public static Order Create(CustomerId customerId) return order; } - public ErrorOr AddLineItem(ProductId productId, Money price, int quantity) + public ErrorOr AddLineItem(ProductId productId, Money price, int quantity) { // TODO: Unit test if (Status == OrderStatus.PendingPayment) @@ -76,9 +98,10 @@ public ErrorOr AddLineItem(ProductId productId, Money price, int quant return existingLineItem; } - var lineItem = LineItem.Create(Id, productId, price, quantity); + var lineItem = LineItem.LineItem.Create(Id, productId, price, quantity); AddDomainEvent(new LineItemCreatedEvent(lineItem.Id, lineItem.OrderId)); _lineItems.Add(lineItem); + UpdateOrderTotal(); return lineItem; } @@ -88,11 +111,18 @@ public ErrorOr RemoveLineItem(ProductId productId) if (Status == OrderStatus.PendingPayment) return OrderErrors.CantModifyAfterPayment; - var lineItem = _lineItems.RemoveAll(x => x.ProductId == productId); + _lineItems.RemoveAll(x => x.ProductId == productId); + UpdateOrderTotal(); return Result.Success; } + public void AddShipping(Money shipping) + { + // TODO: Do we need to check an order status here? + ShippingTotal = shipping; + } + public ErrorOr AddPayment(Money payment) { if (payment.Amount <= 0) @@ -116,12 +146,6 @@ public ErrorOr AddPayment(Money payment) return Result.Success; } - public void AddQuantity(ProductId productId, int quantity) => - _lineItems.FirstOrDefault(li => li.ProductId == productId)?.AddQuantity(quantity); - - public void RemoveQuantity(ProductId productId, int quantity) => - _lineItems.FirstOrDefault(li => li.ProductId == productId)?.RemoveQuantity(quantity); - public ErrorOr ShipOrder(TimeProvider timeProvider) { if (Status == OrderStatus.PendingPayment) @@ -138,30 +162,19 @@ public ErrorOr ShipOrder(TimeProvider timeProvider) return Result.Success; } -} - -internal record PaymentId(Guid Value); - -internal class Payment : Entity -{ - public Money Amount { get; private set; } - - public PaymentType PaymentType { get; private set; } - public Payment(Money amount, PaymentType paymentType) + private void UpdateOrderTotal() { - Amount = amount; - PaymentType = paymentType; - } -} + if (_lineItems.Count == 0) + { + OrderSubTotal = Money.Zero; + return; + } -internal class PaymentType : SmartEnum -{ - public static readonly PaymentType CreditCard = new(1, "CreditCard"); - public static readonly PaymentType PayPal = new(2, "PayPal"); - public static readonly PaymentType Cash = new(3, "Cash"); + var amount = _lineItems.Sum(li => li.Price.Amount * li.Quantity); + var currency = OrderCurrency!; - private PaymentType(int id, string name) : base(name, id) - { + OrderSubTotal = new Money(currency, amount); + TaxTotal = new Money(currency, OrderSubTotal.Amount * TaxRate); } } diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderByIdSpec.cs similarity index 84% rename from src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs rename to src/Modules/Orders/Modules.Orders/Orders/Order/OrderByIdSpec.cs index dd2c4dc..c70c18b 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderByIdSpec.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderByIdSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.Order; internal class OrderByIdSpec : OrderSpec, ISingleResultSpecification { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs similarity index 85% rename from src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs rename to src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs index c74128b..afb3490 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.Order; internal record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : DomainEvent { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderErrors.cs similarity index 96% rename from src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs rename to src/Modules/Orders/Modules.Orders/Orders/Order/OrderErrors.cs index f451804..6a2b5de 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderErrors.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderErrors.cs @@ -1,6 +1,6 @@ using ErrorOr; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.Order; public static class OrderErrors { diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderId.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderId.cs new file mode 100644 index 0000000..a66a8dd --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderId.cs @@ -0,0 +1,3 @@ +namespace Modules.Orders.Orders.Order; + +internal record OrderId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs similarity index 84% rename from src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs rename to src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs index eb960b8..007dac6 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Base; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.Order; internal record OrderReadyForShippingEvent(OrderId OrderId) : DomainEvent { diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderSpec.cs similarity index 80% rename from src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs rename to src/Modules/Orders/Modules.Orders/Orders/Order/OrderSpec.cs index aaaf918..a8124f8 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderSpec.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderSpec.cs @@ -1,6 +1,6 @@ using Ardalis.Specification; -namespace Modules.Orders.Orders; +namespace Modules.Orders.Orders.Order; internal class OrderSpec : Specification { diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs new file mode 100644 index 0000000..1852672 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs @@ -0,0 +1,24 @@ +using Ardalis.SmartEnum; + +namespace Modules.Orders.Orders.Order; + +// internal enum OrderStatus +// { +// None = 0, +// PendingPayment = 1, +// ReadyForShipping = 2, +// InTransit = 3 +// } + +internal class OrderStatus : SmartEnum +{ + public static readonly OrderStatus None = new(0, "None"); + public static readonly OrderStatus PendingPayment = new(1, "PendingPayment"); + public static readonly OrderStatus ReadyForShipping = new(2, "ReadyForShipping"); + public static readonly OrderStatus InTransit = new(3, "InTransit"); + public static readonly OrderStatus Delivered = new(4, "Delivered"); + + private OrderStatus(int id, string name) : base(name, id) + { + } +} diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs deleted file mode 100644 index 52b1115..0000000 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Orders.Orders; - -internal record OrderId(Guid Value); diff --git a/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs b/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs deleted file mode 100644 index fdf6020..0000000 --- a/src/Modules/Orders/Modules.Orders/Orders/OrderStatus.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Modules.Orders.Orders; - -internal enum OrderStatus -{ - None = 0, - PendingPayment = 1, - ReadyForShipping = 2, - InTransit = 3 -} diff --git a/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs new file mode 100644 index 0000000..14911e8 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs @@ -0,0 +1,19 @@ +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Entities; + +namespace Modules.Orders.Orders.Payment; + +internal record PaymentId(Guid Value); + +internal class Payment : Entity +{ + public Money Amount { get; private set; } + + public PaymentType PaymentType { get; private set; } + + public Payment(Money amount, PaymentType paymentType) + { + Amount = amount; + PaymentType = paymentType; + } +} diff --git a/src/Modules/Orders/Modules.Orders/Orders/Payment/PaymentType.cs b/src/Modules/Orders/Modules.Orders/Orders/Payment/PaymentType.cs new file mode 100644 index 0000000..c415d17 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Orders/Payment/PaymentType.cs @@ -0,0 +1,14 @@ +using Ardalis.SmartEnum; + +namespace Modules.Orders.Orders.Payment; + +internal class PaymentType : SmartEnum +{ + public static readonly PaymentType CreditCard = new(1, "CreditCard"); + public static readonly PaymentType PayPal = new(2, "PayPal"); + public static readonly PaymentType Cash = new(3, "Cash"); + + private PaymentType(int id, string name) : base(name, id) + { + } +} From 46337fbcd44494a47b5952506b5996a7d58842ad Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 22 Aug 2024 06:47:38 +1000 Subject: [PATCH 21/87] =?UTF-8?q?=F0=9F=A7=AA=20Add=20comprehensive=20unit?= =?UTF-8?q?=20tests=20for=20Orders=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented detailed unit tests for LineItem, Payment, and Order classes to ensure correct behavior and error handling. Also, updated OrderStatus enum and enhanced payment creation logic for improved validation. #28 --- .../Modules.Orders.Tests/GlobalUsings.cs | 3 +- .../Modules.Orders.Tests.csproj | 4 - .../Orders/LineItemTests.cs | 109 ++++++++++++++++++ .../Modules.Orders.Tests/Orders/OrderTests.cs | 106 +++++++++++++++++ .../Orders/PaymentTests.cs | 59 ++++++++++ .../Modules.Orders/Orders/Order/Order.cs | 13 ++- .../Orders/Order/OrderStatus.cs | 10 +- .../Modules.Orders/Orders/Payment/Payment.cs | 23 +++- 8 files changed, 307 insertions(+), 20 deletions(-) create mode 100644 src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs create mode 100644 src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs create mode 100644 src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs diff --git a/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs b/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs index 8c927eb..91743bb 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs @@ -1 +1,2 @@ -global using Xunit; \ No newline at end of file +global using Xunit; +global using FluentAssertions; diff --git a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index 7499846..a7189e3 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -23,8 +23,4 @@ - - - - diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs new file mode 100644 index 0000000..c2b7d2d --- /dev/null +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs @@ -0,0 +1,109 @@ +using Common.SharedKernel.Domain.Entities; +using Modules.Orders.Orders; +using Modules.Orders.Orders.LineItem; +using Modules.Orders.Orders.Order; + +namespace Modules.Orders.Tests.Orders; + +public class LineItemTests +{ + [Fact] + public void Create_ShouldInitializeLineItemWithCorrectValues() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.Default, 100); + var quantity = 2; + + // Act + var lineItem = LineItem.Create(orderId, productId, price, quantity); + + // Assert + lineItem.OrderId.Should().Be(orderId); + lineItem.ProductId.Should().Be(productId); + lineItem.Price.Should().Be(price); + lineItem.Quantity.Should().Be(quantity); + lineItem.Id.Should().NotBeNull(); + } + + [Fact] + public void Create_ShouldThrowException_WhenPriceIsNegativeOrZero() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.Default, 0); + var quantity = 2; + Action act = () => LineItem.Create(orderId, productId, price, quantity); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenQuantityIsNegativeOrZero() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.Default, 100); + var quantity = 0; + Action act = () => LineItem.Create(orderId, productId, price, quantity); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void AddQuantity_ShouldIncreaseQuantity() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.Default, 100); + var quantity = 2; + var lineItem = LineItem.Create(orderId, productId, price, quantity); + var additionalQuantity = 3; + + // Act + lineItem.AddQuantity(additionalQuantity); + + // Assert + lineItem.Quantity.Should().Be(quantity + additionalQuantity); + } + + [Fact] + public void RemoveQuantity_ShouldDecreaseQuantity() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.Default, 100); + var quantity = 5; + var lineItem = LineItem.Create(orderId, productId, price, quantity); + var removeQuantity = 3; + + // Act + lineItem.RemoveQuantity(removeQuantity); + + // Assert + lineItem.Quantity.Should().Be(quantity - removeQuantity); + } + + [Fact] + public void RemoveQuantity_ShouldThrowException_WhenRemovingMoreThanAvailable() + { + // Arrange + var orderId = new OrderId(Guid.NewGuid()); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.Default, 100); + var quantity = 2; + var lineItem = LineItem.Create(orderId, productId, price, quantity); + var removeQuantity = 3; + Action act = () => lineItem.RemoveQuantity(removeQuantity); + + // Act & Assert + act.Should().Throw(); + } +} diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs new file mode 100644 index 0000000..2bc2dc6 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs @@ -0,0 +1,106 @@ +using Common.SharedKernel.Domain.Entities; +using Modules.Orders.Orders; +using Modules.Orders.Orders.Order; + +namespace Modules.Orders.Tests.Orders +{ + public class OrderTests + { + [Fact] + public void AddLineItem_ShouldAddNewItem_WhenProductDoesNotExist() + { + // Arrange + var order = Order.Create(new CustomerId(Guid.NewGuid())); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.USD, 100); + var quantity = 1; + + // Act + var result = order.AddLineItem(productId, price, quantity); + + // Assert + result.IsError.Should().BeFalse(); + order.LineItems.Should().HaveCount(1); + order.LineItems.First().ProductId.Should().Be(productId); + } + + [Fact] + public void AddLineItem_ShouldIncreaseQuantity_WhenProductExists() + { + // Arrange + var order = Order.Create(new CustomerId(Guid.NewGuid())); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.USD, 100); + var quantity = 1; + order.AddLineItem(productId, price, quantity); + + // Act + var result = order.AddLineItem(productId, price, quantity); + + // Assert + result.IsError.Should().BeFalse(); + order.LineItems.Should().HaveCount(1); + order.LineItems.First().Quantity.Should().Be(2); + } + + [Fact] + public void RemoveLineItem_ShouldRemoveItem_WhenProductExists() + { + // Arrange + var order = Order.Create(new CustomerId(Guid.NewGuid())); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.USD, 100); + var quantity = 1; + order.AddLineItem(productId, price, quantity); + + // Act + var result = order.RemoveLineItem(productId); + + // Assert + result.IsError.Should().BeFalse(); + order.LineItems.Should().BeEmpty(); + } + + [Fact] + public void AddPayment_ShouldUpdateAmountPaid_WhenPaymentIsValid() + { + // Arrange + var order = Order.Create(new CustomerId(Guid.NewGuid())); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.USD, 100); + var quantity = 1; + order.AddLineItem(productId, price, quantity); + order.AddShipping(new Money(Currency.USD, 10)); + + // Act + var result = order.AddPayment(new Money(Currency.USD, 110)); + + // Assert + result.IsError.Should().BeFalse(); + order.AmountPaid.Amount.Should().Be(110); + order.Status.Should().Be(OrderStatus.PaymentReceived); + } + + [Fact] + public void ShipOrder_ShouldUpdateStatusAndShippingDate_WhenOrderIsReadyForShipping() + { + // Arrange + var order = Order.Create(new CustomerId(Guid.NewGuid())); + var productId = new ProductId(Guid.NewGuid()); + var price = new Money(Currency.USD, 100); + var quantity = 1; + order.AddLineItem(productId, price, quantity); + order.AddShipping(new Money(Currency.USD, 10)); + order.AddPayment(new Money(Currency.USD, 110)); + var timeProvider = TimeProvider.System; + + // Act + var result = order.ShipOrder(timeProvider); + + // Assert + result.IsError.Should().BeFalse(); + order.Status.Should().Be(OrderStatus.InTransit); + order.ShippingDate.Should().BeCloseTo(timeProvider.GetUtcNow(), TimeSpan.FromSeconds(1)); + } + } +} diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs new file mode 100644 index 0000000..1fe619c --- /dev/null +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs @@ -0,0 +1,59 @@ +using Common.SharedKernel.Domain.Entities; +using Modules.Orders.Orders.Payment; + +namespace Modules.Orders.Tests.Orders; + +public class PaymentTests +{ + [Fact] + public void Create_ShouldInitializePaymentWithCorrectValues() + { + // Arrange + var amount = new Money(Currency.Default, 100); + var paymentType = PaymentType.CreditCard; + + // Act + var payment = Payment.Create(amount, paymentType); + + // Assert + payment.Amount.Should().Be(amount); + payment.PaymentType.Should().Be(paymentType); + payment.Id.Should().NotBeNull(); + } + + [Fact] + public void Create_ShouldGenerateUniquePaymentId() + { + // Arrange + var amount = new Money(Currency.Default, 100); + var paymentType = PaymentType.CreditCard; + + // Act + var payment1 = Payment.Create(amount, paymentType); + var payment2 = Payment.Create(amount, paymentType); + + // Assert + payment1.Id.Should().NotBe(payment2.Id); + } + + [Fact] + public void Create_ShouldThrowException_WhenAmountIsNull() + { + // Arrange + Action act = () => Payment.Create(null!, PaymentType.CreditCard); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrowException_WhenPaymentTypeIsNull() + { + // Arrange + var amount = new Money(Currency.Default, 100); + Action act = () => Payment.Create(amount, null!); + + // Act & Assert + act.Should().Throw(); + } +} diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs index e042a69..3bc23ac 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs @@ -73,7 +73,7 @@ public static Order Create(CustomerId customerId) OrderSubTotal = Money.Zero, ShippingTotal = Money.Zero, TaxTotal = Money.Zero, - Status = OrderStatus.PendingPayment + Status = OrderStatus.New }; order.AddDomainEvent(OrderCreatedEvent.Create(order)); @@ -84,7 +84,7 @@ public static Order Create(CustomerId customerId) public ErrorOr AddLineItem(ProductId productId, Money price, int quantity) { // TODO: Unit test - if (Status == OrderStatus.PendingPayment) + if (Status == OrderStatus.PaymentReceived) return OrderErrors.CantModifyAfterPayment; // TODO: Unit test @@ -108,7 +108,7 @@ public static Order Create(CustomerId customerId) public ErrorOr RemoveLineItem(ProductId productId) { - if (Status == OrderStatus.PendingPayment) + if (Status == OrderStatus.PaymentReceived) return OrderErrors.CantModifyAfterPayment; _lineItems.RemoveAll(x => x.ProductId == productId); @@ -128,7 +128,8 @@ public ErrorOr AddPayment(Money payment) if (payment.Amount <= 0) return OrderErrors.PaymentAmountZeroOrNegative; - if (payment > OrderTotal - AmountPaid) + // Compare raw amounts to avoid error with default currency (i.e. AUD on $0 amounts) + if (payment.Amount > OrderTotal.Amount - AmountPaid.Amount) return OrderErrors.PaymentExceedsOrderTotal; // Ensure currency is set on first payment @@ -137,6 +138,8 @@ public ErrorOr AddPayment(Money payment) else AmountPaid += payment; + Status = OrderStatus.PaymentReceived; + if (AmountPaid >= OrderTotal) { Status = OrderStatus.ReadyForShipping; @@ -148,7 +151,7 @@ public ErrorOr AddPayment(Money payment) public ErrorOr ShipOrder(TimeProvider timeProvider) { - if (Status == OrderStatus.PendingPayment) + if (Status == OrderStatus.New) return OrderErrors.CantShipUnpaidOrder; if (Status == OrderStatus.InTransit) diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs index 1852672..d77790a 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderStatus.cs @@ -12,11 +12,11 @@ namespace Modules.Orders.Orders.Order; internal class OrderStatus : SmartEnum { - public static readonly OrderStatus None = new(0, "None"); - public static readonly OrderStatus PendingPayment = new(1, "PendingPayment"); - public static readonly OrderStatus ReadyForShipping = new(2, "ReadyForShipping"); - public static readonly OrderStatus InTransit = new(3, "InTransit"); - public static readonly OrderStatus Delivered = new(4, "Delivered"); + public static readonly OrderStatus New = new(1, "New"); + public static readonly OrderStatus PaymentReceived = new(2, "PaymentReceived"); + public static readonly OrderStatus ReadyForShipping = new(3, "ReadyForShipping"); + public static readonly OrderStatus InTransit = new(4, "InTransit"); + public static readonly OrderStatus Delivered = new(5, "Delivered"); private OrderStatus(int id, string name) : base(name, id) { diff --git a/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs index 14911e8..36bf70e 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs @@ -7,13 +7,26 @@ internal record PaymentId(Guid Value); internal class Payment : Entity { - public Money Amount { get; private set; } + public Money Amount { get; private set; } = null!; - public PaymentType PaymentType { get; private set; } + public PaymentType PaymentType { get; private set; } = null!; - public Payment(Money amount, PaymentType paymentType) + private Payment() { - Amount = amount; - PaymentType = paymentType; + } + + public static Payment Create (Money amount, PaymentType paymentType) + { + ArgumentNullException.ThrowIfNull(amount); + ArgumentNullException.ThrowIfNull(paymentType); + + var payment = new Payment + { + Id = new PaymentId(Guid.NewGuid()), + Amount = amount, + PaymentType = paymentType + }; + + return payment; } } From 7ffcef71a79296529395cdc66e30650967493545 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 22 Aug 2024 06:58:48 +1000 Subject: [PATCH 22/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Enhance=20validation?= =?UTF-8?q?=20for=20`Address`=20and=20`Customer`=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improved validation by replacing custom `Throw().IfEmpty()` calls with standard library methods like `ArgumentException.ThrowIfNullOrWhiteSpace()`. Added invariants for the `Customer` class and made email and name update methods private, ensuring encapsulation. Updated the README for clarity and consistency. #27 --- README.md | 10 ++++---- .../Modules.Customers/Customers/Address.cs | 14 +++++------ .../Modules.Customers/Customers/Customer.cs | 25 ++++++++++++------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7b4aa79..e3c5128 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,11 @@ Cart: - ✅ Must always have the correct price Orders: -- An order must be associated with a customer -- The order total must always be correct -- The order tax must always be correct -- Shipping must be included in the total price -- Payment must be completed for the order to be placed (FUTURE: Consider splitting payments to it's own module) +- ✅ An order must be associated with a customer +- ✅ The order total must always be correct +- ✅ The order tax must always be correct +- ✅ Shipping must be included in the total price +- ✅ Payment must be completed for the order to be placed (FUTURE: Consider splitting payments to its own module) ## Event Storming diff --git a/src/Modules/Customers/Modules.Customers/Customers/Address.cs b/src/Modules/Customers/Modules.Customers/Customers/Address.cs index e82e27b..693cf58 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Address.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Address.cs @@ -1,6 +1,4 @@ -using Throw; - -namespace Modules.Customers.Customers; +namespace Modules.Customers.Customers; internal record Address { @@ -13,11 +11,11 @@ internal record Address internal Address(string line1, string? line2, string city, string state, string zipCode, string country) { - line1.Throw().IfEmpty(); - city.Throw().IfEmpty(); - state.Throw().IfEmpty(); - zipCode.Throw().IfEmpty(); - country.Throw().IfEmpty(); + ArgumentException.ThrowIfNullOrWhiteSpace(line1); + ArgumentException.ThrowIfNullOrWhiteSpace(city); + ArgumentException.ThrowIfNullOrWhiteSpace(state); + ArgumentException.ThrowIfNullOrWhiteSpace(zipCode); + ArgumentException.ThrowIfNullOrWhiteSpace(country); Line1 = line1; Line2 = line2; diff --git a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs index b8285fa..1420b21 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs @@ -1,8 +1,11 @@ using Common.SharedKernel.Domain.Base; -using Throw; namespace Modules.Customers.Customers; +/* Invariants: + * - Must have a unique email address (handled by application) + * - Must have an address + */ internal class Customer : AggregateRoot { public string Email { get; private set; } = null!; @@ -17,27 +20,31 @@ private Customer() { } internal static Customer Create(string email, string firstName, string lastName) { - email.Throw().IfEmpty(); - - var customer = new Customer() { Id = new CustomerId(Guid.NewGuid()), Email = email, }; - + var customer = new Customer { Id = new CustomerId(Guid.NewGuid()) }; + customer.UpdateEmail(email); customer.UpdateName(firstName, lastName); customer.AddDomainEvent(CustomerCreatedEvent.Create(customer)); return customer; } - public void UpdateName(string firstName, string lastName) + private void UpdateName(string firstName, string lastName) { - firstName.Throw().IfEmpty(); - lastName.Throw().IfEmpty(); - + ArgumentException.ThrowIfNullOrWhiteSpace(firstName); + ArgumentException.ThrowIfNullOrWhiteSpace(lastName); FirstName = firstName; LastName = lastName; } + private void UpdateEmail(string email) + { + ArgumentException.ThrowIfNullOrWhiteSpace(email); + Email = email; + } + public void UpdateAddress(Address address) { + ArgumentNullException.ThrowIfNull(address); Address = address; } } From ce14a6c9b0b22eef55c12bd300f0e84ba0cd2e84 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 22 Aug 2024 07:10:38 +1000 Subject: [PATCH 23/87] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20for?= =?UTF-8?q?=20Customer=20and=20Address=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made `UpdateName` and `UpdateEmail` methods public to facilitate testing. Added various tests for customer creation, name update, email update, and address update scenarios. Included new AddressTests to validate address initialization and error handling. --- ModularMonolith.sln | 7 ++ .../Modules.Customers.Tests/AddressTests.cs | 114 ++++++++++++++++++ .../Modules.Customers.Tests/CustomerTests.cs | 68 +++++++++++ .../Modules.Customers.Tests/GlobalUsings.cs | 2 + .../Modules.Customers.Tests.csproj | 26 ++++ .../Modules.Customers/AssemblyInfo.cs | 3 + .../Modules.Customers/Customers/Customer.cs | 4 +- .../Modules.Catalog.Tests.csproj | 4 - 8 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs create mode 100644 src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs create mode 100644 src/Modules/Customers/Modules.Customers.Tests/GlobalUsings.cs create mode 100644 src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj create mode 100644 src/Modules/Customers/Modules.Customers/AssemblyInfo.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 9c10421..7d6141b 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -40,6 +40,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Tests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catalog.Tests", "src\Modules\Products\Modules.Catalog.Tests\Modules.Catalog.Tests.csproj", "{11925734-961D-4761-B209-BF601E59EB95}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Customers.Tests", "src\Modules\Customers\Modules.Customers.Tests\Modules.Customers.Tests.csproj", "{50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,6 +87,10 @@ Global {11925734-961D-4761-B209-BF601E59EB95}.Debug|Any CPU.Build.0 = Debug|Any CPU {11925734-961D-4761-B209-BF601E59EB95}.Release|Any CPU.ActiveCfg = Release|Any CPU {11925734-961D-4761-B209-BF601E59EB95}.Release|Any CPU.Build.0 = Release|Any CPU + {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -102,5 +108,6 @@ Global {591B271C-4C16-49CA-9549-E51087B591D1} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {11925734-961D-4761-B209-BF601E59EB95} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} + {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} EndGlobalSection EndGlobal diff --git a/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs b/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs new file mode 100644 index 0000000..bac17ad --- /dev/null +++ b/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs @@ -0,0 +1,114 @@ +using Modules.Customers.Customers; + +namespace Modules.Customers.Tests; + +public class AddressTests +{ + [Fact] + public void Constructor_ShouldInitializeProperties_WhenValidArguments() + { + // Arrange + var line1 = "123 Main St"; + var line2 = "Apt 4B"; + var city = "Anytown"; + var state = "CA"; + var zipCode = "12345"; + var country = "USA"; + + // Act + var address = new Address(line1, line2, city, state, zipCode, country); + + // Assert + address.Line1.Should().Be(line1); + address.Line2.Should().Be(line2); + address.City.Should().Be(city); + address.State.Should().Be(state); + address.ZipCode.Should().Be(zipCode); + address.Country.Should().Be(country); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenLine1IsNullOrWhiteSpace() + { + // Arrange + var line1 = ""; + var city = "Anytown"; + var state = "CA"; + var zipCode = "12345"; + var country = "USA"; + + // Act + Action act = () => _ = new Address(line1, null, city, state, zipCode, country); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenCityIsNullOrWhiteSpace() + { + // Arrange + var line1 = "123 Main St"; + var city = ""; + var state = "CA"; + var zipCode = "12345"; + var country = "USA"; + + // Act + Action act = () => _ = new Address(line1, null, city, state, zipCode, country); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenStateIsNullOrWhiteSpace() + { + // Arrange + var line1 = "123 Main St"; + var city = "Anytown"; + var state = ""; + var zipCode = "12345"; + var country = "USA"; + + // Act + Action act = () => _ = new Address(line1, null, city, state, zipCode, country); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenZipCodeIsNullOrWhiteSpace() + { + // Arrange + var line1 = "123 Main St"; + var city = "Anytown"; + var state = "CA"; + var zipCode = ""; + var country = "USA"; + + // Act + Action act = () => _ = new Address(line1, null, city, state, zipCode, country); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenCountryIsNullOrWhiteSpace() + { + // Arrange + var line1 = "123 Main St"; + var city = "Anytown"; + var state = "CA"; + var zipCode = "12345"; + var country = ""; + + // Act + Action act = () => _ = new Address(line1, null, city, state, zipCode, country); + + // Assert + act.Should().Throw(); + } +} diff --git a/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs b/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs new file mode 100644 index 0000000..d33d53e --- /dev/null +++ b/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs @@ -0,0 +1,68 @@ +using Modules.Customers.Customers; + +namespace Modules.Customers.Tests; + +public class CustomerTests +{ + [Fact] + public void Create_ShouldInitializeCustomerWithGivenValues() + { + // Arrange + var email = "test@example.com"; + var firstName = "John"; + var lastName = "Doe"; + + // Act + var customer = Customer.Create(email, firstName, lastName); + + // Assert + customer.Email.Should().Be(email); + customer.FirstName.Should().Be(firstName); + customer.LastName.Should().Be(lastName); + customer.Address.Should().BeNull(); + } + + [Fact] + public void UpdateName_ShouldUpdateFirstNameAndLastName() + { + // Arrange + var customer = Customer.Create("test@example.com", "John", "Doe"); + var newFirstName = "Jane"; + var newLastName = "Smith"; + + // Act + customer.UpdateName(newFirstName, newLastName); + + // Assert + customer.FirstName.Should().Be(newFirstName); + customer.LastName.Should().Be(newLastName); + } + + [Fact] + public void UpdateEmail_ShouldUpdateEmail() + { + // Arrange + var customer = Customer.Create("test@example.com", "John", "Doe"); + var newEmail = "new@example.com"; + + // Act + customer.UpdateEmail(newEmail); + + // Assert + customer.Email.Should().Be(newEmail); + } + + [Fact] + public void UpdateAddress_ShouldUpdateAddress() + { + // Arrange + var customer = Customer.Create("test@example.com", "John", "Doe"); + var address = new Address("123 Main St", null,"City", "State", "12345", "US"); + + // Act + customer.UpdateAddress(address); + + // Assert + customer.Address.Should().Be(address); + } +} diff --git a/src/Modules/Customers/Modules.Customers.Tests/GlobalUsings.cs b/src/Modules/Customers/Modules.Customers.Tests/GlobalUsings.cs new file mode 100644 index 0000000..91743bb --- /dev/null +++ b/src/Modules/Customers/Modules.Customers.Tests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using FluentAssertions; diff --git a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj new file mode 100644 index 0000000..1f97651 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj @@ -0,0 +1,26 @@ + + + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Modules/Customers/Modules.Customers/AssemblyInfo.cs b/src/Modules/Customers/Modules.Customers/AssemblyInfo.cs new file mode 100644 index 0000000..e5ee732 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Modules.Customers.Tests")] diff --git a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs index 1420b21..88c4027 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs @@ -28,7 +28,7 @@ internal static Customer Create(string email, string firstName, string lastName) return customer; } - private void UpdateName(string firstName, string lastName) + public void UpdateName(string firstName, string lastName) { ArgumentException.ThrowIfNullOrWhiteSpace(firstName); ArgumentException.ThrowIfNullOrWhiteSpace(lastName); @@ -36,7 +36,7 @@ private void UpdateName(string firstName, string lastName) LastName = lastName; } - private void UpdateEmail(string email) + public void UpdateEmail(string email) { ArgumentException.ThrowIfNullOrWhiteSpace(email); Email = email; diff --git a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj index f5522ae..e0d3a67 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj +++ b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - false true From 699f58a6b57a44190f91b36d4bab9e666ec19923 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Fri, 23 Aug 2024 07:33:45 +1000 Subject: [PATCH 24/87] =?UTF-8?q?=F0=9F=A7=AA=20Consolidate=20AisleTests?= =?UTF-8?q?=20into=20ModelTests=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged the tests from `AisleTests.cs` into `ModelTests.cs` and renamed it to `Storage/AisleTests.cs` for better organization. Deleted the old `AisleTests.cs` file to avoid duplication. --- .../Modules.Warehouse.Tests/AisleTests.cs | 21 ------------------- .../{ModelTests.cs => Storage/AisleTests.cs} | 19 +++++++++++++++-- 2 files changed, 17 insertions(+), 23 deletions(-) delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs rename src/Modules/Warehouse/Modules.Warehouse.Tests/{ModelTests.cs => Storage/AisleTests.cs} (79%) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs deleted file mode 100644 index ab689a0..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/AisleTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentAssertions; -using Modules.Warehouse.Storage.Domain; - -namespace Modules.Warehouse.Tests; - -public class AisleTests -{ - [Fact] - public void Create_WithBaysAndShelves_CreatesCorrectNumberOfShelves() - { - // Arrange - var numBays = 2; - var numShelves = 3; - - // Act - var sut = Aisle.Create("Aisle 1", numBays, numShelves); - - // Assert - sut.TotalStorage.Should().Be(numBays * numShelves); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs similarity index 79% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs rename to src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs index f281ce1..b6316d9 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/ModelTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs @@ -1,14 +1,15 @@ +using FluentAssertions; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; using Xunit.Abstractions; namespace Modules.Warehouse.Tests; -public class ModelTests +public class AisleTests { private readonly ITestOutputHelper _output; - public ModelTests(ITestOutputHelper output) + public AisleTests(ITestOutputHelper output) { _output = output; } @@ -48,4 +49,18 @@ public void LookingUpProduct_ReturnsAisleBayAndShelf() _output.WriteLine($"Product B is in Aisle {aisleName}, Bay {bayName}, Shelf {shelfName}"); } + + [Fact] + public void Create_WithBaysAndShelves_CreatesCorrectNumberOfShelves() + { + // Arrange + var numBays = 2; + var numShelves = 3; + + // Act + var sut = Aisle.Create("Aisle 1", numBays, numShelves); + + // Assert + sut.TotalStorage.Should().Be(numBays * numShelves); + } } From 6058c79e78e48573b923f5ce67d8d665cb13f462 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:43:54 +1000 Subject: [PATCH 25/87] =?UTF-8?q?=F0=9F=8C=B1=20Up=20script=20=20and=20tes?= =?UTF-8?q?t=20database=20seed=20data=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add custom Aisle configuration and update database setup Refactor database seeding to include new Aisle configurations and custom storage command. Moving the database initializer to a new tools project enhances modularity and clarity in database seeding procedures. Additionally, some configurations in existing entities and properties have been updated accordingly. * ✨ Add StronglyTypedId support to warehouse module Introduced StronglyTypedId conversion and configuration in the warehouse module. Updated entity creation methods and DbContext to use StronglyTypedId. Enhanced entity configurations and added Product DbSet to WarehouseDbContext. * ✨ Enable product seeding and remove commented-out code Re-enabled the `SeedProductsAsync` method to allow seeding products in the database. Removed commented-out code for clarity and maintainability. Simplified the aisle seeding logic for better readability. --- ModularMonolith.sln | 15 +++ docker-compose.yml | 12 +- .../Common.SharedKernel/Domain/Base/Entity.cs | 5 + .../Categories/Domain/Category.cs | 1 + .../Modules.Catalog/Products/Product.cs | 2 +- .../Products/ProductTests.cs | 19 +++ .../Modules.Warehouse/AssemblyInfo.cs | 1 + .../Configuration/AisleConfiguraiton.cs | 44 +++++++ .../Configuration/BayConfiguration.cs | 26 ++++ .../Configuration/ShelfConfiguration.cs | 31 +++++ .../Persistence/DepdendencyInjection.cs | 14 +-- .../Extensions/PropertyBuilderExtensions.cs | 13 ++ .../Extensions/StronglyTypedIdConverter.cs | 16 +++ .../Common/Persistence/WarehouseDbContext.cs | 89 ++++++------- .../WarehouseDbContextInitializer.cs | 118 ------------------ .../Products/Domain/Product.cs | 15 ++- .../Modules.Warehouse/Storage/Domain/Aisle.cs | 5 +- .../Modules.Warehouse/Storage/Domain/Bay.cs | 15 +-- .../Modules.Warehouse/Storage/Domain/Shelf.cs | 13 +- .../Storage/UseCases/CreateStorageCommand.cs | 60 +++++++++ src/WebApi/Program.cs | 51 ++++---- tools/Database/Database.csproj | 16 +++ .../MockCurrentUserServiceProvider.cs | 8 ++ tools/Database/Program.cs | 33 +++++ .../Database/WarehouseDbContextInitializer.cs | 86 +++++++++++++ tools/Database/appsettings.json | 11 ++ up.ps1 | 10 ++ 27 files changed, 502 insertions(+), 227 deletions(-) create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/PropertyBuilderExtensions.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/StronglyTypedIdConverter.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs create mode 100644 tools/Database/Database.csproj create mode 100644 tools/Database/MockCurrentUserServiceProvider.cs create mode 100644 tools/Database/Program.cs create mode 100644 tools/Database/WarehouseDbContextInitializer.cs create mode 100644 tools/Database/appsettings.json create mode 100644 up.ps1 diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 7d6141b..6a9f6e9 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -42,6 +42,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catalog.Tests", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Customers.Tests", "src\Modules\Customers\Modules.Customers.Tests\Modules.Customers.Tests.csproj", "{50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7915FF68-4EC9-497B-BD33-1A3DA7D6B457}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".infra", ".infra", "{758DF8D4-BEBB-4AD8-9B9E-84F613420CB2}" + ProjectSection(SolutionItems) = preProject + up.ps1 = up.ps1 + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "tools\Database\Database.csproj", "{9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -91,6 +101,10 @@ Global {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Debug|Any CPU.Build.0 = Debug|Any CPU {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Release|Any CPU.ActiveCfg = Release|Any CPU {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Release|Any CPU.Build.0 = Release|Any CPU + {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -109,5 +123,6 @@ Global {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {11925734-961D-4761-B209-BF601E59EB95} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} + {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB} = {7915FF68-4EC9-497B-BD33-1A3DA7D6B457} EndGlobalSection EndGlobal diff --git a/docker-compose.yml b/docker-compose.yml index fc5d16e..c8bcf6d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,21 @@ # docker run --cap-add SYS_PTRACE -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 --name azuresqledge -d mcr.microsoft.com/azure-sql-edge +name: modular-monolith services: db: environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "yourStrong(!)Password" - #container_name: "ef-core-samples" + SA_PASSWORD: "Password123" + container_name: modular-monolith-db + platform: linux/amd64 # NOTE: can't use azure-sql-edge as it doesn't support .NET CLR. image: mcr.microsoft.com/mssql/server ports: # {{exposed}}:{{internal}} - you'll need to contain the exposed ports if you have more than one DB server running at a time - - 1433:1433 - restart: always + - 1800:1433 + restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P yourStrong(!)Password -Q 'SELECT 1' || exit 1"] + test: [ "CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P yourStrong(!)Password -Q 'SELECT 1' || exit 1" ] interval: 10s retries: 10 start_period: 10s diff --git a/src/Common/Common.SharedKernel/Domain/Base/Entity.cs b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs index 2650d43..36e6a18 100644 --- a/src/Common/Common.SharedKernel/Domain/Base/Entity.cs +++ b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs @@ -22,3 +22,8 @@ public void Updated(DateTimeOffset dateTime, string? user) UpdatedBy = user; } } + +public interface IStronglyTypedId +{ + T Value { get; } +} diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs index f2962b2..c6591a6 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs @@ -27,6 +27,7 @@ public static Category Create(string name) private void UpdateName(string name) { + // name.ThrowIfNull(); ArgumentException.ThrowIfNullOrWhiteSpace(name); Name = name; diff --git a/src/Modules/Products/Modules.Catalog/Products/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Product.cs index 464ff33..5454792 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Product.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Product.cs @@ -4,7 +4,7 @@ namespace Modules.Catalog.Products; -internal record ProductId(Guid Value); +internal record ProductId(Guid Value) : IStronglyTypedId; internal class Product : AggregateRoot { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs new file mode 100644 index 0000000..87eedc4 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs @@ -0,0 +1,19 @@ +namespace Modules.Warehouse.Tests.Products; + +public class ProductTests +{ + // [Fact] + // public void Create_WithValidData_ShouldSucceed() + // { + // // Arrange + // var name = "Product 1"; + // var sku = "SKU-1"; + // + // // Act + // var product = Product.Create(name, sku); + // + // // Assert + // product.Name.Should().Be(name); + // product.Sku.Should().Be(sku); + // } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs b/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs index 59a528a..6025fb5 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Modules.Warehouse.Tests")] +[assembly: InternalsVisibleTo("Database")] diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs new file mode 100644 index 0000000..27027f2 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Warehouse.Common.Persistence.Extensions; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Common.Persistence.Configuration; + +internal class AisleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(m => m.Id); + + builder + .Property(m => m.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.HasMany(t => t.Bays) + .WithOne() + .IsRequired(); + + builder.Property(m => m.Name) + .IsRequired(); + + // builder.OwnsMany(m => m.Bays, b => + // { + // // b.HasKey(m => m.Id); + // // b.Property(m => m.Id) + // // .HasConversion(x => x.Value, + // // x => new BayId(x)) + // // .ValueGeneratedNever(); + // b.OwnsMany(m => m.Shelves, s => + // { + // // s.HasKey(m => m.Id); + // s + // .Property(m => m.ProductId)! + // .HasStronglyTypedId() + // // .HasConversion>() + // .ValueGeneratedNever(); + // }); + // }); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs new file mode 100644 index 0000000..b3f8054 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Warehouse.Common.Persistence.Extensions; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Common.Persistence.Configuration; + +internal class BayConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(m => m.Id); + + builder + .Property(m => m.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.Property(m => m.Name) + .IsRequired(); + + builder.HasMany(m => m.Shelves) + .WithOne() + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs new file mode 100644 index 0000000..3eaa6b3 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Warehouse.Common.Persistence.Extensions; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Common.Persistence.Configuration; + +internal class ShelfConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(m => m.Id); + + builder + .Property(m => m.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.Property(m => m.Name) + .IsRequired(); + + builder.Property(m => m.ProductId)! + .HasStronglyTypedId() + .IsRequired(false); + + builder.HasOne() + .WithMany() + .HasForeignKey(m => m.ProductId); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index bc002e3..426e476 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -8,13 +8,13 @@ internal static class DepdendencyInjection { internal static void AddPersistence(this IServiceCollection services, IConfiguration config) { - var connectionString = config.GetConnectionString("DefaultConnection"); - // services.AddDbContext(options => - // options.UseSqlServer(connectionString, builder => - // { - // builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); - // builder.EnableRetryOnFailure(); - // })); + var connectionString = config.GetConnectionString("Warehouse"); + services.AddDbContext(options => + options.UseSqlServer(connectionString, builder => + { + builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); + builder.EnableRetryOnFailure(); + })); //services.AddSingleton(); // TODO: Consider moving to up.ps1 diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/PropertyBuilderExtensions.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/PropertyBuilderExtensions.cs new file mode 100644 index 0000000..65a96c8 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/PropertyBuilderExtensions.cs @@ -0,0 +1,13 @@ +using Common.SharedKernel.Domain.Base; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Modules.Warehouse.Common.Persistence.Extensions; + +internal static class PropertyBuilderExtensions +{ + public static PropertyBuilder HasStronglyTypedId(this PropertyBuilder propertyBuilder) + where TId : IStronglyTypedId + { + return propertyBuilder.HasConversion(new StronglyTypedIdConverter()); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/StronglyTypedIdConverter.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/StronglyTypedIdConverter.cs new file mode 100644 index 0000000..ec4a1b8 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/StronglyTypedIdConverter.cs @@ -0,0 +1,16 @@ +using Common.SharedKernel.Domain.Base; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Modules.Warehouse.Common.Persistence.Extensions; + +internal class StronglyTypedIdConverter : ValueConverter + where TId : IStronglyTypedId +{ + public StronglyTypedIdConverter(ConverterMappingHints? mappingHints = null) + : base( + id => id.Value, + value => (TId)Activator.CreateInstance(typeof(TId), value)!, + mappingHints) + { + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index 779d2d1..e20498f 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -1,48 +1,41 @@ -// using Microsoft.EntityFrameworkCore; -// using Modules.Warehouse.Features.Categories.Domain; -// using Modules.Warehouse.Products.Domain; -// -// namespace Modules.Warehouse.Common.Persistence; -// -// internal class WarehouseDbContext : DbContext -// { -// // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; -// // private readonly OutboxInterceptor _outboxInterceptor; -// // -// // public DbSet Products { get; set; } = default!; -// // -// // public DbSet Customers { get; set; } = default!; -// // -// // public DbSet Orders { get; set; } = default!; -// // -// // public DbSet OutboxMessages { get; set; } = default!; -// // -// -// public DbSet Products => Set(); -// -// public DbSet Categories => Set(); -// -// public WarehouseDbContext(DbContextOptions options /*EntitySaveChangesInterceptor saveChangesInterceptor, OutboxInterceptor outboxInterceptor*/) : base(options) -// { -// // _saveChangesInterceptor = saveChangesInterceptor; -// // _outboxInterceptor = outboxInterceptor; -// } -// -// protected override void OnModelCreating(ModelBuilder modelBuilder) -// { -// modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); -// base.OnModelCreating(modelBuilder); -// } -// -// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) -// { -// // optionsBuilder.AddInterceptors( -// // _saveChangesInterceptor, -// // _outboxInterceptor); -// } -// -// // public Task SaveChangesAsync() => this -// // { -// // throw new NotImplementedException(); -// // } -// } +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Common.Persistence; + +internal class WarehouseDbContext : DbContext +{ + // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; + // private readonly OutboxInterceptor _outboxInterceptor; + + internal DbSet Aisles => Set(); + + internal DbSet Products => Set(); + + // Needs to be public for the Database project + public WarehouseDbContext(DbContextOptions options /*EntitySaveChangesInterceptor saveChangesInterceptor, OutboxInterceptor outboxInterceptor*/) : base(options) + { + // _saveChangesInterceptor = saveChangesInterceptor; + // _outboxInterceptor = outboxInterceptor; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("warehouse"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // optionsBuilder.AddInterceptors( + // _saveChangesInterceptor, + // _outboxInterceptor); + } + + // public Task SaveChangesAsync() => this + // { + // throw new NotImplementedException(); + // } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs deleted file mode 100644 index 54eb642..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContextInitializer.cs +++ /dev/null @@ -1,118 +0,0 @@ -// using Bogus; -// using Common.SharedKernel.Domain.Entities; -// using Microsoft.EntityFrameworkCore; -// using Microsoft.Extensions.Logging; -// using Modules.Warehouse.Features.Categories; -// using Modules.Warehouse.Features.Categories.Domain; -// using Modules.Warehouse.Products; -// using Modules.Warehouse.Products.Domain; -// -// namespace Modules.Warehouse.Common.Persistence; -// -// internal class WarehouseDbContextInitializer -// { -// private readonly ILogger _logger; -// private readonly WarehouseDbContext _dbContext; -// -// private const int NumProducts = 20; -// -// private const int NumCategories = 5; -// // private const int NumCustomers = 20; -// // private const int NumOrders = 20; -// -// public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) -// { -// _logger = logger; -// _dbContext = dbContext; -// } -// -// public async Task InitializeAsync() -// { -// try -// { -// if (_dbContext.Database.IsSqlServer()) -// { -// //await _dbContext.Database.EnsureDeletedAsync(); -// await _dbContext.Database.EnsureCreatedAsync(); -// } -// } -// catch (Exception e) -// { -// _logger.LogError(e, "An error occurred while migrating or initializing the database"); -// throw; -// } -// } -// -// public async Task SeedAsync() -// { -// await SeedCategoriesAsync(); -// await SeedProductsAsync(); -// // await SeedCustomersAsync(); -// // await SeedOrdersAsync(); -// } -// -// // private async Task SeedCustomersAsync() -// // { -// // if (await _dbContext.Customers.AnyAsync()) -// // return; -// // -// // var customerFaker = new Faker() -// // .CustomInstantiator(f => Customer.Create(f.Person.Email, f.Person.FirstName, f.Person.LastName)); -// // -// // var customers = customerFaker.Generate(NumCustomers); -// // _dbContext.Customers.AddRange(customers); -// // await _dbContext.SaveChangesAsync(); -// // } -// -// private async Task SeedProductsAsync() -// { -// if (await _dbContext.Products.AnyAsync()) -// return; -// -// var categories = await _dbContext.Categories.ToListAsync(); -// -// var moneyFaker = new Faker() -// .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); -// -// var skuFaker = new Faker() -// .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); -// -// var productRepository = new ProductRepository(_dbContext); -// -// var faker = new Faker() -// .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), moneyFaker.Generate(), -// skuFaker.Generate(), f.PickRandom(categories).Id, productRepository)); -// -// var products = faker.Generate(NumProducts); -// _dbContext.Products.AddRange(products); -// await _dbContext.SaveChangesAsync(); -// } -// -// // private async Task SeedOrdersAsync() -// // { -// // if (await _dbContext.Orders.AnyAsync()) -// // return; -// // -// // var customerIds = _dbContext.Customers.Select(c => c.Id).ToList(); -// // -// // var orderFaker = new Faker() -// // .CustomInstantiator(f => Order.Create(f.PickRandom(customerIds))); -// // -// // var orders = orderFaker.Generate(NumOrders); -// // _dbContext.Orders.AddRange(orders); -// // await _dbContext.SaveChangesAsync(); -// // } -// -// private async Task SeedCategoriesAsync() -// { -// if (await _dbContext.Categories.AnyAsync()) -// return; -// -// var categoryFaker = new Faker() -// .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1)[0], new CategoryRepository(_dbContext))); -// -// var categories = categoryFaker.Generate(NumCategories); -// _dbContext.Categories.AddRange(categories); -// await _dbContext.SaveChangesAsync(); -// } -// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 686ddfd..0b03fcf 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -5,7 +5,7 @@ namespace Modules.Warehouse.Products.Domain; -internal record ProductId(Guid Value); +internal record ProductId(Guid Value) : IStronglyTypedId; internal class Product : AggregateRoot { @@ -22,9 +22,11 @@ private Product() } // NOTE: Need to use a factory, as EF does not let owned entities (i.e. Money & Sku) be passed via the constructor - public static Product Create(string name, Money price, Sku sku, IProductRepository productRepository) + public static Product Create(string name, Money price, Sku sku) { - name.Throw().IfEmpty(); + // TODO: Check for SKU uniqueness in Application + + name.ThrowIfNull(); price.Throw().IfNegativeOrZero(p => p.Amount); var product = new Product @@ -34,7 +36,7 @@ public static Product Create(string name, Money price, Sku sku, IProductReposito }; product.UpdateName(name); - product.UpdateSku(sku, productRepository); + product.UpdateSku(sku); product.AddDomainEvent(ProductCreatedEvent.Create(product)); @@ -47,11 +49,8 @@ private void UpdateName(string name) Name = name; } - private void UpdateSku(Sku sku, IProductRepository productRepository) + private void UpdateSku(Sku sku) { - if (productRepository.SkuExists(sku)) - throw new ArgumentException("Sku already exists"); - Sku = sku; } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs index b007970..443274e 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs @@ -1,9 +1,8 @@ using Common.SharedKernel.Domain.Base; -using Throw; namespace Modules.Warehouse.Storage.Domain; -internal record AisleId(Guid Value); +internal record AisleId(Guid Value) : IStronglyTypedId; internal class Aisle : AggregateRoot { @@ -31,7 +30,7 @@ public static Aisle Create(string name, int numBays, int numShelves) for (var i = 1; i <= numBays; i++) { - var bay = Bay.Create(i, numShelves); + var bay = Bay.Create($"Bay {i}", numShelves); aisle._bays.Add(bay); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs index bfd4c33..d37dfda 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs @@ -1,9 +1,10 @@ using Common.SharedKernel.Domain.Base; -using Throw; namespace Modules.Warehouse.Storage.Domain; -internal class Bay : Entity +public record BayId(Guid Value) : IStronglyTypedId; + +internal class Bay : Entity { private readonly List _shelves = []; @@ -15,20 +16,20 @@ internal class Bay : Entity public string Name { get; private set; } = null!; - public static Bay Create(int id, int numShelves) + public static Bay Create(string name, int numShelves) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id); + ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(numShelves); var bay = new Bay { - Id = id, - Name = $"Bay {id}" + Id = new BayId(Guid.NewGuid()), + Name = name }; for (var i = 1; i <= numShelves; i++) { - var shelf = Shelf.Create(i); + var shelf = Shelf.Create($"Shelf {i}"); bay._shelves.Add(shelf); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs index 92bce7f..fcf31cc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs @@ -1,10 +1,11 @@ using Common.SharedKernel.Domain.Base; using Modules.Warehouse.Products.Domain; -using Throw; namespace Modules.Warehouse.Storage.Domain; -internal class Shelf : Entity +internal record ShelfId(Guid Value) : IStronglyTypedId; + +internal class Shelf : Entity { public string Name { get; private set; } = null!; @@ -12,14 +13,14 @@ internal class Shelf : Entity public bool IsEmpty => ProductId is null; - public static Shelf Create(int number) + public static Shelf Create(string name) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(number); + ArgumentException.ThrowIfNullOrWhiteSpace(name); return new Shelf { - Id = number, - Name = $"Shelf {number}" + Id = new ShelfId(Guid.NewGuid()), + Name = name }; } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs new file mode 100644 index 0000000..3fb9869 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Storage.UseCases; + +internal static class CreateStorageCommand +{ + internal record Request(string Name, int NumBays, int NumShelves) : IRequest; + + internal static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/storage", async (Request request, ISender sender) => await sender.Send(request)) + .WithName("CreateStorage") + .WithTags("Storage") + .WithOpenApi(); + } + } + + internal class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.Name).NotEmpty(); + RuleFor(r => r.NumBays).GreaterThan(0); + RuleFor(r => r.NumShelves).GreaterThan(0); + } + } + + internal class Handler : IRequestHandler + { + public async Task Handle(Request request, CancellationToken cancellationToken) + { + var storage = Aisle.Create(request.Name, request.NumBays, request.NumShelves); + + + } + } +} + +// public record CreateStorageCommand : IRequest; +// +// public class CreateStorageValidator : AbstractValidator +// { +// public CreateStorageValidator() +// { +// // TODO: Add rules +// } +// } +// +// public class CreateStorageCommandHandler : IRequestHandler +// { +// public async Task Handle(CreateStorageCommand request, CancellationToken cancellationToken) +// { +// +// } +// } diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index a044675..cc8ee8d 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -3,34 +3,37 @@ using Modules.Warehouse; var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -// Common MediatR behaviors across all modules -builder.Services.AddMediatR(config => { - config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); - config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); - config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); -}); - -builder.Services.AddOrders(); -builder.Services.AddWarehouse(builder.Configuration); + // Add services to the container. + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + // Common MediatR behaviors across all modules + builder.Services.AddMediatR(config => + { + config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); + config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); + config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); + }); + + builder.Services.AddOrders(); + builder.Services.AddWarehouse(builder.Configuration); +} var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); -} + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } -app.UseHttpsRedirection(); + app.UseHttpsRedirection(); -app.UseOrders(); app.UseWarehouse(); + app.UseOrders(); + app.UseWarehouse(); -app.Run(); + app.Run(); +} diff --git a/tools/Database/Database.csproj b/tools/Database/Database.csproj new file mode 100644 index 0000000..62ecc99 --- /dev/null +++ b/tools/Database/Database.csproj @@ -0,0 +1,16 @@ + + + + Exe + + + + + + + + + + + + diff --git a/tools/Database/MockCurrentUserServiceProvider.cs b/tools/Database/MockCurrentUserServiceProvider.cs new file mode 100644 index 0000000..aebb3b6 --- /dev/null +++ b/tools/Database/MockCurrentUserServiceProvider.cs @@ -0,0 +1,8 @@ +// using SSW.CleanArchitecture.Application.Common.Interfaces; +// +// namespace SSW.CleanArchitecture.Database; +// +// public class MockCurrentUserService : ICurrentUserService +// { +// public string UserId => "00000000-0000-0000-0000-000000000000"; +// } diff --git a/tools/Database/Program.cs b/tools/Database/Program.cs new file mode 100644 index 0000000..f4d558a --- /dev/null +++ b/tools/Database/Program.cs @@ -0,0 +1,33 @@ +using Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Modules.Warehouse; +using Modules.Warehouse.Common.Persistence; + +var builder = Host.CreateDefaultBuilder(args); + +builder.ConfigureServices((context, services) => +{ + services.AddSingleton(TimeProvider.System); + + services.AddDbContext(options => + { + options.UseSqlServer(context.Configuration.GetConnectionString("Warehouse"), opt => + { + opt.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); + }); + }); + + services.AddScoped(); +}); + +var app = builder.Build(); +app.Start(); + +// Initialise and seed database +using var scope = app.Services.CreateScope(); +var initializer = scope.ServiceProvider.GetRequiredService(); +await initializer.InitializeAsync(); +await initializer.SeedAsync(); diff --git a/tools/Database/WarehouseDbContextInitializer.cs b/tools/Database/WarehouseDbContextInitializer.cs new file mode 100644 index 0000000..44e407f --- /dev/null +++ b/tools/Database/WarehouseDbContextInitializer.cs @@ -0,0 +1,86 @@ +using Bogus; +using Common.SharedKernel.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Database; + +internal class WarehouseDbContextInitializer +{ + private readonly ILogger _logger; + private readonly WarehouseDbContext _dbContext; + + private const int NumProducts = 20; + private const int NumAisles = 10; + private const int NumShelves = 5; + private const int NumBays = 20; + + public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) + { + _logger = logger; + _dbContext = dbContext; + } + + internal async Task InitializeAsync() + { + try + { + if (_dbContext.Database.IsSqlServer()) + { + // TODO: Move to migrations + await _dbContext.Database.EnsureDeletedAsync(); + await _dbContext.Database.EnsureCreatedAsync(); + } + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while migrating or initializing the database"); + throw; + } + } + + public async Task SeedAsync() + { + await SeedAisles(); + await SeedProductsAsync(); + } + + private async Task SeedAisles() + { + if (await _dbContext.Aisles.AnyAsync()) + return; + + + + for (int i = 1; i <= NumAisles; i++) + { + var aisle = Aisle.Create($"Aisle {i}", NumBays, NumShelves); + _dbContext.Aisles.Add(aisle); + } + + await _dbContext.SaveChangesAsync(); + } + + private async Task SeedProductsAsync() + { + if (await _dbContext.Products.AnyAsync()) + return; + + var moneyFaker = new Faker() + .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); + + var skuFaker = new Faker() + .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); + + var faker = new Faker() + .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), moneyFaker.Generate(), + skuFaker.Generate())); + + var products = faker.Generate(NumProducts); + _dbContext.Products.AddRange(products); + await _dbContext.SaveChangesAsync(); + } +} diff --git a/tools/Database/appsettings.json b/tools/Database/appsettings.json new file mode 100644 index 0000000..b445d7c --- /dev/null +++ b/tools/Database/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "Warehouse": "Server=localhost,1800;Initial Catalog=Warehouse;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;" + } +} diff --git a/up.ps1 b/up.ps1 new file mode 100644 index 0000000..5981474 --- /dev/null +++ b/up.ps1 @@ -0,0 +1,10 @@ +Write-Host "🚢 Starting Docker Compose" -ForegroundColor Green +docker compose up -d + +$upScriptPath = $Script:MyInvocation.MyCommand.Path | Split-Path + +Write-Host "🚀 Creating and Seeding Database" -ForegroundColor Green +Set-Location ./tools/Database/ +dotnet run + +Set-Location $upScriptPath From 2eaaa463c09169b9839ba36fbf9a46c67cc2d9b3 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 25 Aug 2024 15:20:48 +1000 Subject: [PATCH 26/87] =?UTF-8?q?=E2=9C=A8=20Add=20integration=20test=20se?= =?UTF-8?q?tup=20for=20Aisle=20creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added necessary dependencies and configurations for integration testing in the Warehouse module. Implemented a new integration test for creating aisles, ensuring correct behavior and database interactions. #29 --- .../Common/DatabaseContainer.cs | 25 ++++++++ .../Common/Extensions/ServiceCollectionExt.cs | 36 +++++++++++ .../Common/IntegrationTestBase.cs | 60 +++++++++++++++++++ .../Common/TestingDatabaseFixture.cs | 58 ++++++++++++++++++ .../Common/WebApiTestFactory.cs | 41 +++++++++++++ .../Modules.Warehouse.Tests.csproj | 9 +++ .../Storage/AisleIntegrationTests.cs | 40 +++++++++++++ .../Storage/AisleTests.cs | 2 +- .../Storage/Domain/GetAllAislesSpec.cs | 13 ++++ .../Storage/UseCases/CreateStorageCommand.cs | 43 ++++++------- .../Modules.Warehouse/WarehouseModule.cs | 17 +----- src/WebApi/Extensions/MediatRExtensions.cs | 25 ++++++++ src/WebApi/IWebApiMarker.cs | 3 + src/WebApi/Program.cs | 10 +--- .../Database/WarehouseDbContextInitializer.cs | 4 +- 15 files changed, 334 insertions(+), 52 deletions(-) create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Common/TestingDatabaseFixture.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WebApiTestFactory.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/GetAllAislesSpec.cs create mode 100644 src/WebApi/Extensions/MediatRExtensions.cs create mode 100644 src/WebApi/IWebApiMarker.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs new file mode 100644 index 0000000..aca4e7f --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs @@ -0,0 +1,25 @@ +using Testcontainers.SqlEdge; + +namespace Modules.Warehouse.Tests.Common; + +/// +/// Wraper for SQL edge container +/// +public class DatabaseContainer +{ + private readonly SqlEdgeContainer _container = new SqlEdgeBuilder() + .WithName("Modular-Monolith-IntegrationTests-DbContainer") + .WithPassword("Password123") + .WithAutoRemove(true) + .Build(); + + public string? ConnectionString { get; private set; } + + public async Task InitializeAsync() + { + await _container.StartAsync(); + ConnectionString = _container.GetConnectionString(); + } + + public Task DisposeAsync() => _container.StopAsync() ?? Task.CompletedTask; +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs new file mode 100644 index 0000000..c4cdb2b --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Diagnostics; + +namespace Modules.Warehouse.Tests.Common.Extensions; + +internal static class ServiceCollectionExt +{ + /// + /// Replaces the DbContext with a new instance using the provided database container + /// + internal static IServiceCollection ReplaceDbContext( + this IServiceCollection services, + DatabaseContainer databaseContainer) where T : DbContext + { + services + .RemoveAll>() + .RemoveAll() + .AddDbContext((_, options) => + { + options.UseSqlServer(databaseContainer.ConnectionString, + b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); + + options.LogTo(m => Debug.WriteLine(m)); + options.EnableSensitiveDataLogging(); + + // options.AddInterceptors( + // services.BuildServiceProvider().GetRequiredService(), + // services.BuildServiceProvider().GetRequiredService() + // ); + }); + + return services; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs new file mode 100644 index 0000000..dfb95f9 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs @@ -0,0 +1,60 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Persistence; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Common; + +/// +/// Integration tests inherit from this to access helper classes +/// +[Collection(TestingDatabaseFixtureCollection.Name)] +public abstract class IntegrationTestBase : IAsyncLifetime +{ + private readonly IServiceScope _scope; + + private readonly TestingDatabaseFixture _fixture; + + protected IMediator Mediator { get; } + + protected IQueryable GetQueryable() where T : class => DbContext.Set().AsNoTracking(); + + internal WarehouseDbContext DbContext { get; } + + protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _fixture.SetOutput(output); + + _scope = _fixture.ScopeFactory.CreateScope(); + Mediator = _scope.ServiceProvider.GetRequiredService(); + DbContext = _scope.ServiceProvider.GetRequiredService(); + } + + + // protected async Task AddEntityAsync(T entity, CancellationToken cancellationToken = default) where T : class + // { + // await Context.Set().AddAsync(entity, cancellationToken); + // await Context.SaveChangesAsync(cancellationToken); + // } + // + // protected async Task AddEntitiesAsync(IEnumerable entities, CancellationToken cancellationToken = default) where T : class + // { + // await Context.Set().AddRangeAsync(entities, cancellationToken); + // await Context.SaveChangesAsync(cancellationToken); + // } + + public async Task InitializeAsync() + { + await _fixture.ResetState(); + } + + protected HttpClient GetAnonymousClient() => _fixture.Factory.AnonymousClient.Value; + + public Task DisposeAsync() + { + _scope.Dispose(); + return Task.CompletedTask; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/TestingDatabaseFixture.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/TestingDatabaseFixture.cs new file mode 100644 index 0000000..7484acd --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/TestingDatabaseFixture.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Persistence; +using Respawn; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Common; + +/// +/// Initializes and resets the database before and after each test +/// +// ReSharper disable once ClassNeverInstantiated.Global +public class TestingDatabaseFixture : IAsyncLifetime +{ + private string ConnectionString => Factory.Database.ConnectionString!; + + private Respawner _checkpoint = default!; + + public IServiceScopeFactory ScopeFactory { get; private set; } = null!; + + public WebApiTestFactory Factory { get; } = new(); + + public async Task InitializeAsync() + { + // Initialize DB Container + await Factory.Database.InitializeAsync(); + ScopeFactory = Factory.Services.GetRequiredService(); + + // Create and seed database + using var scope = ScopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + await dbContext.Database.EnsureCreatedAsync(); + + // NOTE: If there are any tables you want to skip being reset, they can be configured here + _checkpoint = await Respawner.CreateAsync(ConnectionString); + } + + public async Task DisposeAsync() + { + await Factory.Database.DisposeAsync(); + } + + public async Task ResetState() + { + await _checkpoint.ResetAsync(ConnectionString); + } + + public void SetOutput(ITestOutputHelper output) => Factory.Output = output; +} + +[CollectionDefinition(Name)] +public class TestingDatabaseFixtureCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + + public const string Name = nameof(TestingDatabaseFixtureCollection); +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WebApiTestFactory.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WebApiTestFactory.cs new file mode 100644 index 0000000..bda2feb --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WebApiTestFactory.cs @@ -0,0 +1,41 @@ +using Meziantou.Extensions.Logging.Xunit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Tests.Common.Extensions; +using WebApi; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Common; + +/// +/// Host builder (services, DI, and configuration) for integration tests +/// +public class WebApiTestFactory : WebApplicationFactory +{ + public DatabaseContainer Database { get; } = new(); + + public ITestOutputHelper? Output { private get; set; } + + public Lazy AnonymousClient => new(CreateClient()); + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // Redirect application logging to test output + builder.ConfigureLogging(x => + { + x.ClearProviders(); + x.AddFilter(level => level >= LogLevel.Information); + x.Services.AddSingleton(new XUnitLoggerProvider(Output!)); + }); + + // Override default DB registration to use out Test Container instead + builder.ConfigureTestServices(services => + { + services.ReplaceDbContext(Database); + }); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj index 5c94342..652cf4d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj @@ -8,6 +8,14 @@ + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,6 +28,7 @@ + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs new file mode 100644 index 0000000..c6c7de5 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs @@ -0,0 +1,40 @@ +using Ardalis.Specification.EntityFrameworkCore; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Storage.Domain; +using Modules.Warehouse.Storage.UseCases; +using Modules.Warehouse.Tests.Common; +using System.Net; +using System.Net.Http.Json; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Storage; + +public class AisleIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) + : IntegrationTestBase(fixture, output) +{ + [Fact] + public async Task CreateAisle_ValidRequest_ReturnsCreatedAisle() + { + // Arrange + var client = GetAnonymousClient(); + var request = new CreateAisleCommand.Request("Name", 2, 2); + + // Act + var response = await client.PostAsJsonAsync("/api/aisles", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + var aisles = await GetQueryable().WithSpecification(new GetAllAislesSpec()).ToListAsync(); + aisles.Should().HaveCount(1); + + var aisle = aisles.First(); + // aisle.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + // aisle.CreatedBy.Should().NotBeNullOrWhiteSpace(); + aisle.Name.Should().Be(request.Name); + aisle.Bays.Count.Should().Be(request.NumBays); + + var shelves = aisles.First().Bays.SelectMany(b => b.Shelves).ToList(); + shelves.Count.Should().Be(request.NumBays * request.NumShelves); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs index b6316d9..a571d5c 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs @@ -3,7 +3,7 @@ using Modules.Warehouse.Storage.Domain; using Xunit.Abstractions; -namespace Modules.Warehouse.Tests; +namespace Modules.Warehouse.Tests.Storage; public class AisleTests { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/GetAllAislesSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/GetAllAislesSpec.cs new file mode 100644 index 0000000..397228e --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/GetAllAislesSpec.cs @@ -0,0 +1,13 @@ +using Ardalis.Specification; + +namespace Modules.Warehouse.Storage.Domain; + +internal class GetAllAislesSpec : Specification +{ + public GetAllAislesSpec() + { + Query + .Include(a => a.Bays) + .ThenInclude(b => b.Shelves); + } +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs index 3fb9869..10d528b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs @@ -1,20 +1,21 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Modules.Warehouse.Common.Persistence; using Modules.Warehouse.Storage.Domain; namespace Modules.Warehouse.Storage.UseCases; -internal static class CreateStorageCommand +internal static class CreateAisleCommand { - internal record Request(string Name, int NumBays, int NumShelves) : IRequest; + internal record Request(string Name, int NumBays, int NumShelves) : IRequest; internal static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/storage", async (Request request, ISender sender) => await sender.Send(request)) - .WithName("CreateStorage") + app.MapPost("/api/aisles", async (Request request, ISender sender) => await sender.Send(request)) + .WithName("CreateAisle") .WithTags("Storage") .WithOpenApi(); } @@ -30,31 +31,21 @@ public Validator() } } - internal class Handler : IRequestHandler + internal class Handler : IRequestHandler { - public async Task Handle(Request request, CancellationToken cancellationToken) - { - var storage = Aisle.Create(request.Name, request.NumBays, request.NumShelves); + private readonly WarehouseDbContext _context; + public Handler(WarehouseDbContext context) + { + _context = context; + } + public async Task Handle(Request request, CancellationToken cancellationToken) + { + var aisle = Aisle.Create(request.Name, request.NumBays, request.NumShelves); + await _context.Aisles.AddAsync(aisle, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return TypedResults.Created(); } } } - -// public record CreateStorageCommand : IRequest; -// -// public class CreateStorageValidator : AbstractValidator -// { -// public CreateStorageValidator() -// { -// // TODO: Add rules -// } -// } -// -// public class CreateStorageCommandHandler : IRequestHandler -// { -// public async Task Handle(CreateStorageCommand request, CancellationToken cancellationToken) -// { -// -// } -// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index cf8f4db..3f949f6 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Modules.Warehouse.Products.Endpoints; +using Modules.Warehouse.Storage.UseCases; namespace Modules.Warehouse; @@ -13,27 +14,13 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration services.AddValidatorsFromAssembly(applicationAssembly); - // TODO: Check we can call this multiple times - services.AddMediatR(config => - { - config.RegisterServicesFromAssembly(applicationAssembly); - }); - // Todo: Move to feature DI // services.AddTransient(); } public static void UseWarehouse(this WebApplication app) { - // TODO: Refactor to up.ps1 - // if (app.Environment.IsDevelopment()) - // { - // // Initialise and seed database - // using var scope = app.Services.CreateScope(); - // var initializer = scope.ServiceProvider.GetRequiredService(); - // await initializer.InitializeAsync(); - // await initializer.SeedAsync(); - // } + CreateAisleCommand.Endpoint.MapEndpoint(app); // TODO: Move to feature DI app.MapProductEndpoints(); diff --git a/src/WebApi/Extensions/MediatRExtensions.cs b/src/WebApi/Extensions/MediatRExtensions.cs new file mode 100644 index 0000000..c4fa839 --- /dev/null +++ b/src/WebApi/Extensions/MediatRExtensions.cs @@ -0,0 +1,25 @@ +using Common.SharedKernel.Behaviours; +using Modules.Warehouse; +using System.Reflection; + +namespace WebApi.Extensions; + +public static class MediatRExtensions +{ + private static readonly Assembly[] _assemblies = + [ + typeof(WarehouseModule).Assembly + ]; + + public static void AddMediatR(this IServiceCollection services) + { + // Common MediatR behaviors across all modules + services.AddMediatR(config => + { + config.RegisterServicesFromAssemblies(_assemblies); + config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); + config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); + config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); + }); + } +} diff --git a/src/WebApi/IWebApiMarker.cs b/src/WebApi/IWebApiMarker.cs new file mode 100644 index 0000000..044040c --- /dev/null +++ b/src/WebApi/IWebApiMarker.cs @@ -0,0 +1,3 @@ +namespace WebApi; + +public interface IWebApiMarker; diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index cc8ee8d..d6248d7 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,6 +1,6 @@ -using Common.SharedKernel.Behaviours; using Modules.Orders; using Modules.Warehouse; +using WebApi.Extensions; var builder = WebApplication.CreateBuilder(args); { @@ -9,13 +9,7 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - // Common MediatR behaviors across all modules - builder.Services.AddMediatR(config => - { - config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); - config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); - config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); - }); + builder.Services.AddMediatR(); builder.Services.AddOrders(); builder.Services.AddWarehouse(builder.Configuration); diff --git a/tools/Database/WarehouseDbContextInitializer.cs b/tools/Database/WarehouseDbContextInitializer.cs index 44e407f..013323e 100644 --- a/tools/Database/WarehouseDbContextInitializer.cs +++ b/tools/Database/WarehouseDbContextInitializer.cs @@ -18,7 +18,7 @@ internal class WarehouseDbContextInitializer private const int NumShelves = 5; private const int NumBays = 20; - public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) + internal WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) { _logger = logger; _dbContext = dbContext; @@ -42,7 +42,7 @@ internal async Task InitializeAsync() } } - public async Task SeedAsync() + internal async Task SeedAsync() { await SeedAisles(); await SeedProductsAsync(); From f46321a97bb55cb875d5ccd2f907bdba0b0c3bf1 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:11:04 +1000 Subject: [PATCH 27/87] =?UTF-8?q?=E2=9C=A8=20We=20now=20set=20IAuditable?= =?UTF-8?q?=20properties=20when=20persisting=20entities.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added `ICurrentUserService` and `EntitySaveChangesInterceptor` to enhance audit logging in the persistence layer. Integrated these into the application by updating dependencies and ensuring proper initialization in DI setup. --- .../Common.SharedKernel.csproj | 4 -- .../DependencyInjection.cs | 13 +++++ .../Common.SharedKernel/Domain/Base/Entity.cs | 2 +- .../{IAuditableEntity.cs => IAuditable.cs} | 4 +- .../Identity/CurrentUserService.cs | 7 +++ .../Identity/ICurrentUserService.cs | 6 +++ .../Common/Extensions/ServiceCollectionExt.cs | 9 ++-- .../Storage/AisleIntegrationTests.cs | 4 +- .../Persistence/DepdendencyInjection.cs | 10 +++- .../EntitySaveChangesInterceptor.cs | 53 +++++++++++++++++++ .../Modules.Warehouse/WarehouseModule.cs | 6 +-- src/WebApi/Program.cs | 3 ++ 12 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 src/Common/Common.SharedKernel/DependencyInjection.cs rename src/Common/Common.SharedKernel/Domain/Interfaces/{IAuditableEntity.cs => IAuditable.cs} (82%) create mode 100644 src/Common/Common.SharedKernel/Identity/CurrentUserService.cs create mode 100644 src/Common/Common.SharedKernel/Identity/ICurrentUserService.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/EntitySaveChangesInterceptor.cs diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 400247e..414a302 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -11,8 +11,4 @@ - - - - diff --git a/src/Common/Common.SharedKernel/DependencyInjection.cs b/src/Common/Common.SharedKernel/DependencyInjection.cs new file mode 100644 index 0000000..a1856d5 --- /dev/null +++ b/src/Common/Common.SharedKernel/DependencyInjection.cs @@ -0,0 +1,13 @@ +using Common.SharedKernel.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace Common.SharedKernel; + +public static class DependencyInjection +{ + public static void AddCommon(this IServiceCollection services) + { + services.AddScoped(); + services.AddSingleton(TimeProvider.System); + } +} diff --git a/src/Common/Common.SharedKernel/Domain/Base/Entity.cs b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs index 36e6a18..7d236e5 100644 --- a/src/Common/Common.SharedKernel/Domain/Base/Entity.cs +++ b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs @@ -2,7 +2,7 @@ namespace Common.SharedKernel.Domain.Base; -public abstract class Entity : IAuditableEntity +public abstract class Entity : IAuditable { public required TId Id { get; init; } public DateTimeOffset CreatedAt { get; private set; } diff --git a/src/Common/Common.SharedKernel/Domain/Interfaces/IAuditableEntity.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IAuditable.cs similarity index 82% rename from src/Common/Common.SharedKernel/Domain/Interfaces/IAuditableEntity.cs rename to src/Common/Common.SharedKernel/Domain/Interfaces/IAuditable.cs index 49e9340..33ef3df 100644 --- a/src/Common/Common.SharedKernel/Domain/Interfaces/IAuditableEntity.cs +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IAuditable.cs @@ -1,8 +1,8 @@ namespace Common.SharedKernel.Domain.Interfaces; -public interface IAuditableEntity +public interface IAuditable { void Created(DateTimeOffset dateTime, string? user); void Updated(DateTimeOffset dateTime, string? user); -} \ No newline at end of file +} diff --git a/src/Common/Common.SharedKernel/Identity/CurrentUserService.cs b/src/Common/Common.SharedKernel/Identity/CurrentUserService.cs new file mode 100644 index 0000000..0dcbcb9 --- /dev/null +++ b/src/Common/Common.SharedKernel/Identity/CurrentUserService.cs @@ -0,0 +1,7 @@ +namespace Common.SharedKernel.Identity; + +// This should be implemented based on your configured identity provider +public class CurrentUserService : ICurrentUserService +{ + public string? UserId => "Admin"; +} diff --git a/src/Common/Common.SharedKernel/Identity/ICurrentUserService.cs b/src/Common/Common.SharedKernel/Identity/ICurrentUserService.cs new file mode 100644 index 0000000..a1bfb29 --- /dev/null +++ b/src/Common/Common.SharedKernel/Identity/ICurrentUserService.cs @@ -0,0 +1,6 @@ +namespace Common.SharedKernel.Identity; + +public interface ICurrentUserService +{ + public string? UserId { get; } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs index c4cdb2b..1113b80 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Modules.Warehouse.Common.Persistence.Interceptors; using System.Diagnostics; namespace Modules.Warehouse.Tests.Common.Extensions; @@ -25,10 +26,10 @@ internal static IServiceCollection ReplaceDbContext( options.LogTo(m => Debug.WriteLine(m)); options.EnableSensitiveDataLogging(); - // options.AddInterceptors( - // services.BuildServiceProvider().GetRequiredService(), - // services.BuildServiceProvider().GetRequiredService() - // ); + options.AddInterceptors( + services.BuildServiceProvider().GetRequiredService() + // services.BuildServiceProvider().GetRequiredService() + ); }); return services; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs index c6c7de5..7459511 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs @@ -29,8 +29,8 @@ public async Task CreateAisle_ValidRequest_ReturnsCreatedAisle() aisles.Should().HaveCount(1); var aisle = aisles.First(); - // aisle.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); - // aisle.CreatedBy.Should().NotBeNullOrWhiteSpace(); + aisle.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + aisle.CreatedBy.Should().NotBeNullOrWhiteSpace(); aisle.Name.Should().Be(request.Name); aisle.Bays.Count.Should().Be(request.NumBays); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index 426e476..5927465 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Persistence.Interceptors; namespace Modules.Warehouse.Common.Persistence; @@ -10,16 +11,21 @@ internal static void AddPersistence(this IServiceCollection services, IConfigura { var connectionString = config.GetConnectionString("Warehouse"); services.AddDbContext(options => + { options.UseSqlServer(connectionString, builder => { builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); builder.EnableRetryOnFailure(); - })); + }); + + options.AddInterceptors(services.BuildServiceProvider().GetRequiredService()); + }); + //services.AddSingleton(); // TODO: Consider moving to up.ps1 // services.AddScoped(); - // services.AddScoped(); + services.AddScoped(); // services.AddScoped(); // services.AddScoped(); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/EntitySaveChangesInterceptor.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/EntitySaveChangesInterceptor.cs new file mode 100644 index 0000000..2f1400a --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/EntitySaveChangesInterceptor.cs @@ -0,0 +1,53 @@ +using Common.SharedKernel.Domain.Interfaces; +using Common.SharedKernel.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Modules.Warehouse.Common.Persistence.Interceptors; + +public class EntitySaveChangesInterceptor(ICurrentUserService currentUserService, TimeProvider timeProvider) + : SaveChangesInterceptor +{ + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + UpdateEntities(eventData.Context); + + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + UpdateEntities(eventData.Context); + + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void UpdateEntities(DbContext? context) + { + if (context is null) + return; + + foreach (var entry in context.ChangeTracker.Entries()) + { + if (entry.State is EntityState.Added) + { + entry.Entity.Created(timeProvider.GetUtcNow(), currentUserService.UserId); + } + else if (entry.State is EntityState.Added or EntityState.Modified || + entry.HasChangedOwnedEntities()) + { + entry.Entity.Updated(timeProvider.GetUtcNow(), currentUserService.UserId); + } + } + } +} + +public static class Extensions +{ + public static bool HasChangedOwnedEntities(this EntityEntry entry) => + entry.References.Any(r => + r.TargetEntry != null && + r.TargetEntry.Metadata.IsOwned() && + r.TargetEntry.State is EntityState.Added or EntityState.Modified); +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 3f949f6..e50438b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Persistence; using Modules.Warehouse.Products.Endpoints; using Modules.Warehouse.Storage.UseCases; @@ -14,15 +15,12 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration services.AddValidatorsFromAssembly(applicationAssembly); - // Todo: Move to feature DI - // services.AddTransient(); + services.AddPersistence(configuration); } public static void UseWarehouse(this WebApplication app) { CreateAisleCommand.Endpoint.MapEndpoint(app); - - // TODO: Move to feature DI app.MapProductEndpoints(); } } diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index d6248d7..bbac73a 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,3 +1,4 @@ +using Common.SharedKernel; using Modules.Orders; using Modules.Warehouse; using WebApi.Extensions; @@ -9,6 +10,8 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + builder.Services.AddCommon(); + builder.Services.AddMediatR(); builder.Services.AddOrders(); From 53ea3d889d6e9558315af799cdda9319896adc66 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:12:04 +1000 Subject: [PATCH 28/87] =?UTF-8?q?=E2=9C=A8=20Integrate=20ErrorOr=20package?= =?UTF-8?q?=20and=20enhance=20validation=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated MediatR behaviors to include `ResultValidationBehavior`, replaced `ValidationBehaviour`, and integrated the ErrorOr package across the solution. Added new tests for invalid requests and refactored existing classes to support the ErrorOr type. #29 --- .../Behaviours/ResultValidationBehaviour.cs | 32 ++++++++++++++ .../Behaviours/ValidationBehaviour.cs | 33 ++++++++------ .../Common.SharedKernel.csproj | 3 +- .../Storage/AisleIntegrationTests.cs | 22 ++++++++++ .../Configuration/AisleConfiguraiton.cs | 18 -------- .../Modules.Warehouse.csproj | 1 + ...torageCommand.cs => CreateAisleCommand.cs} | 21 +++++---- .../Storage/UseCases/ErrorOrExt.cs | 43 +++++++++++++++++++ src/WebApi/Extensions/MediatRExtensions.cs | 3 +- 9 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 src/Common/Common.SharedKernel/Behaviours/ResultValidationBehaviour.cs rename src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/{CreateStorageCommand.cs => CreateAisleCommand.cs} (63%) create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs diff --git a/src/Common/Common.SharedKernel/Behaviours/ResultValidationBehaviour.cs b/src/Common/Common.SharedKernel/Behaviours/ResultValidationBehaviour.cs new file mode 100644 index 0000000..8a3d24c --- /dev/null +++ b/src/Common/Common.SharedKernel/Behaviours/ResultValidationBehaviour.cs @@ -0,0 +1,32 @@ +using ErrorOr; +using FluentValidation; +using MediatR; + +namespace Common.SharedKernel.Behaviours; + +public class ResultValidationBehavior(IValidator? validator = null) + : IPipelineBehavior + where TRequest : IRequest + where TResponse : IErrorOr +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + if (validator is null) + return await next(); + + var validationResult = await validator.ValidateAsync(request, cancellationToken); + + if (validationResult.IsValid) + return await next(); + + var errors = validationResult.Errors + .ConvertAll(error => Error.Validation( + code: error.PropertyName, + description: error.ErrorMessage)); + + return (dynamic)errors; + } +} diff --git a/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs b/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs index a5c2c8f..aad7471 100644 --- a/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs +++ b/src/Common/Common.SharedKernel/Behaviours/ValidationBehaviour.cs @@ -1,5 +1,6 @@ using FluentValidation; using MediatR; +using ValidationException = Common.SharedKernel.Exceptions.ValidationException; namespace Common.SharedKernel.Behaviours; @@ -7,24 +8,28 @@ public class ValidationBehaviour(IEnumerable where TRequest : notnull { - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) { - if (validators.Any()) - { - var context = new ValidationContext(request); + if (!validators.Any()) + return await next(); - var validationResults = await Task.WhenAll( - validators.Select(v => - v.ValidateAsync(context, cancellationToken))); + var context = new ValidationContext(request); - var failures = validationResults - .Where(r => r.Errors.Count != 0) - .SelectMany(r => r.Errors) - .ToList(); + var validationResults = await Task.WhenAll( + validators.Select(v => + v.ValidateAsync(context, cancellationToken))); + + var failures = validationResults + .Where(r => r.Errors.Count > 0) + .SelectMany(r => r.Errors) + .ToList(); + + if (failures.Count != 0) + throw new ValidationException(failures); - if (failures.Count != 0) - throw new Exceptions.ValidationException(failures); - } return await next(); } } diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 414a302..9fdee9e 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -6,9 +6,8 @@ - + - diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs index 7459511..98d4ad1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs @@ -37,4 +37,26 @@ public async Task CreateAisle_ValidRequest_ReturnsCreatedAisle() var shelves = aisles.First().Bays.SelectMany(b => b.Shelves).ToList(); shelves.Count.Should().Be(request.NumBays * request.NumShelves); } + + [Theory] + [InlineData("name", 0, 0)] + [InlineData("name", 0, 1)] + [InlineData("name", 1, 0)] + [InlineData("", 1, 1)] + [InlineData(" ", 1, 1)] + [InlineData(null, 1, 1)] + public async Task CreateAisle_WithInvalidRequest_Throws(string name, int numBays, int numShelves) + { + // Arrange + var client = GetAnonymousClient(); + var request = new CreateAisleCommand.Request(name, numBays, numShelves); + + // Act + var response = await client.PostAsJsonAsync("/api/aisles", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var content = await response.Content.ReadAsStringAsync(); + output.WriteLine(content); + } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs index 27027f2..21c99bc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs @@ -22,23 +22,5 @@ public void Configure(EntityTypeBuilder builder) builder.Property(m => m.Name) .IsRequired(); - - // builder.OwnsMany(m => m.Bays, b => - // { - // // b.HasKey(m => m.Id); - // // b.Property(m => m.Id) - // // .HasConversion(x => x.Value, - // // x => new BayId(x)) - // // .ValueGeneratedNever(); - // b.OwnsMany(m => m.Shelves, s => - // { - // // s.HasKey(m => m.Id); - // s - // .Property(m => m.ProductId)! - // .HasStronglyTypedId() - // // .HasConversion>() - // .ValueGeneratedNever(); - // }); - // }); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index f2b7d09..437371a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -3,6 +3,7 @@ + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs similarity index 63% rename from src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs rename to src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs index 10d528b..f4e4adc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateStorageCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs @@ -1,3 +1,4 @@ +using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -6,22 +7,26 @@ namespace Modules.Warehouse.Storage.UseCases; -internal static class CreateAisleCommand +public static class CreateAisleCommand { - internal record Request(string Name, int NumBays, int NumShelves) : IRequest; + public record Request(string Name, int NumBays, int NumShelves) : IRequest>; - internal static class Endpoint + public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/aisles", async (Request request, ISender sender) => await sender.Send(request)) + app.MapPost("/api/aisles", async (Request request, ISender sender) => + { + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Created(); + }) .WithName("CreateAisle") .WithTags("Storage") .WithOpenApi(); } } - internal class Validator : AbstractValidator + public class Validator : AbstractValidator { public Validator() { @@ -31,7 +36,7 @@ public Validator() } } - internal class Handler : IRequestHandler + internal class Handler : IRequestHandler> { private readonly WarehouseDbContext _context; @@ -40,12 +45,12 @@ public Handler(WarehouseDbContext context) _context = context; } - public async Task Handle(Request request, CancellationToken cancellationToken) + public async Task> Handle(Request request, CancellationToken cancellationToken) { var aisle = Aisle.Create(request.Name, request.NumBays, request.NumShelves); await _context.Aisles.AddAsync(aisle, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - return TypedResults.Created(); + return Result.Success; } } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs new file mode 100644 index 0000000..a04eb52 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs @@ -0,0 +1,43 @@ +using ErrorOr; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace Modules.Warehouse.Storage.UseCases; + +public static class ErrorOrExt +{ + public static IResult Problem(this IErrorOr error) + { + ArgumentOutOfRangeException.ThrowIfEqual(error.IsError, false); + + if (error.Errors is null) + return Problem(Error.Unexpected()); + + if (error.Errors.All(e => e.Type == ErrorType.Validation)) + return ValidationProblem(error); + + return Problem(error.Errors[0]); + } + + private static ProblemHttpResult Problem(Error error) + { + var statusCode = error.Type switch + { + ErrorType.Conflict => StatusCodes.Status409Conflict, + ErrorType.Validation => StatusCodes.Status400BadRequest, + ErrorType.NotFound => StatusCodes.Status404NotFound, + _ => StatusCodes.Status500InternalServerError, + }; + + return TypedResults.Problem(statusCode: statusCode, title: error.Description); + } + + private static ValidationProblem ValidationProblem(IErrorOr error) + { + var errors = new Dictionary(); + foreach (var e in error.Errors!) + errors.Add(e.Code, [e.Description]); + + return TypedResults.ValidationProblem(errors, title: "One or more validation errors occurred."); + } +} \ No newline at end of file diff --git a/src/WebApi/Extensions/MediatRExtensions.cs b/src/WebApi/Extensions/MediatRExtensions.cs index c4fa839..dc0e975 100644 --- a/src/WebApi/Extensions/MediatRExtensions.cs +++ b/src/WebApi/Extensions/MediatRExtensions.cs @@ -18,7 +18,8 @@ public static void AddMediatR(this IServiceCollection services) { config.RegisterServicesFromAssemblies(_assemblies); config.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>)); - config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); + config.AddOpenBehavior(typeof(ResultValidationBehavior<,>)); + // config.AddOpenBehavior(typeof(ValidationBehaviour<,>)); config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); }); } From cfe2778817601f37241652b4cd56a7bcb4a31c81 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 26 Aug 2024 06:17:11 +1000 Subject: [PATCH 29/87] =?UTF-8?q?=E2=9C=A8=20Added=20Create=20Product=20Co?= =?UTF-8?q?mmand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored the product creation logic and moved endpoint mappings from the Products module to a new UseCases module. Removed unused repository and command files. Improved validation and error handling for product creation. --- .../Common/IntegrationTestBase.cs | 3 + .../Configuration}/ProductConfiguration.cs | 12 +--- .../CreateProduct/CreateProductCommand.cs | 34 ----------- .../Products/Domain/IProductRepository.cs | 8 --- .../Products/Domain/Product.cs | 7 +-- .../Products/Domain/ProductByIdSpec.cs | 2 +- .../Modules.Warehouse/Products/Domain/Sku.cs | 17 +++--- .../Products/Endpoints/ProductEndpoints.cs | 28 --------- .../Products/ProductRepository.cs | 25 -------- .../Products/UseCases/CreateProductCommand.cs | 61 +++++++++++++++++++ .../GetProductsQuery.cs | 0 .../Modules.Warehouse/WarehouseModule.cs | 5 +- .../Database/WarehouseDbContextInitializer.cs | 8 +-- 13 files changed, 82 insertions(+), 128 deletions(-) rename src/Modules/Warehouse/Modules.Warehouse/{Products/Persistence => Common/Persistence/Configuration}/ProductConfiguration.cs (60%) delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/Domain/IProductRepository.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/Endpoints/ProductEndpoints.cs delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs rename src/Modules/Warehouse/Modules.Warehouse/Products/{Queries/GetProducts => UseCases}/GetProductsQuery.cs (100%) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs index dfb95f9..9a9b5fc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs @@ -45,6 +45,9 @@ protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper // await Context.SaveChangesAsync(cancellationToken); // } + /// + /// Gets called between each test to reset the state of the database + /// public async Task InitializeAsync() { await _fixture.ResetState(); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ProductConfiguration.cs similarity index 60% rename from src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs rename to src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ProductConfiguration.cs index f0a3ce0..5bf3368 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Persistence/ProductConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ProductConfiguration.cs @@ -1,5 +1,4 @@ -using Common.SharedKernel.Persistence; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Modules.Warehouse.Products.Domain; @@ -17,14 +16,5 @@ public void Configure(EntityTypeBuilder builder) builder.Property(p => p.Sku) .HasConversion(sku => sku.Value, value => Sku.Create(value)!) .HasMaxLength(50); - - // builder.ComplexProperty(p => p.Price, MoneyConfiguration.BuildAction); - - //builder.ComplexProperty(p => p.Price, () => MoneyConfiguration.BuildAction) - - // builder.HasOne(p => p.Category) - // .WithMany() - // .HasForeignKey(o => o.CategoryId) - // .IsRequired(); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs deleted file mode 100644 index 870169a..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Commands/CreateProduct/CreateProductCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Common.SharedKernel.Domain.Entities; -using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Products.Domain; - -namespace Modules.Warehouse.Products.Commands.CreateProduct; - -internal record CreateProductCommand(string Name, decimal Amount, string Sku, Guid CategoryId) - : IRequest; - -internal class CreateProductCommandHandler : IRequestHandler -{ - // private readonly WarehouseDbContext _dbContext; - // private readonly IProductRepository _productRepository; - // - // public CreateProductCommandHandler(WarehouseDbContext dbContext, IProductRepository productRepository, CancellationToken cancellationToken) - // { - // _dbContext = dbContext; - // _productRepository = productRepository; - // } - - public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken) - { - // var money = new Money(Currency.Default, request.Amount); - // var sku = Sku.Create(request.Sku); - // ArgumentNullException.ThrowIfNull(sku); - // var product = Product.Create(request.Name, money, sku, _productRepository); - // - // _dbContext.Products.Add(product); - // - // await _dbContext.SaveChangesAsync(cancellationToken); - - await Task.CompletedTask; - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/IProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/IProductRepository.cs deleted file mode 100644 index ac2ebc6..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/IProductRepository.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Modules.Warehouse.Products.Domain; - -internal interface IProductRepository -{ - public Task SkuExistsAsync(Sku sku); - - public bool SkuExists(Sku sku); -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 0b03fcf..61b496b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -1,5 +1,4 @@ using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Entities; using Common.SharedKernel.Domain.Exceptions; using Throw; @@ -22,12 +21,10 @@ private Product() } // NOTE: Need to use a factory, as EF does not let owned entities (i.e. Money & Sku) be passed via the constructor - public static Product Create(string name, Money price, Sku sku) + public static Product Create(string name, Sku sku) { // TODO: Check for SKU uniqueness in Application - - name.ThrowIfNull(); - price.Throw().IfNegativeOrZero(p => p.Amount); + ArgumentException.ThrowIfNullOrWhiteSpace(name); var product = new Product { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs index 84764a2..8edf70b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductByIdSpec.cs @@ -2,7 +2,7 @@ namespace Modules.Warehouse.Products.Domain; -internal class ProductByIdSpec : Specification, ISingleResultSpecification +internal class ProductByIdSpec : Specification { public ProductByIdSpec(ProductId id) : base() { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs index d6cf59c..45110c0 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs @@ -1,20 +1,19 @@ -namespace Modules.Warehouse.Products.Domain; +using Common.SharedKernel.Domain.Base; -internal record Sku +namespace Modules.Warehouse.Products.Domain; + +internal record Sku : ValueObject { - private const int DefaultLength = 8; + internal const int DefaultLength = 8; public string Value { get; } private Sku(string value) => Value = value; - public static Sku? Create(string value) + public static Sku Create(string value) { - if (string.IsNullOrWhiteSpace(value)) - return null; - - if (value.Length != DefaultLength) - return null; + ArgumentException.ThrowIfNullOrWhiteSpace(value); + ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, DefaultLength); return new Sku(value); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Endpoints/ProductEndpoints.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Endpoints/ProductEndpoints.cs deleted file mode 100644 index 0b5d3ee..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Endpoints/ProductEndpoints.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Common.SharedKernel; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Modules.Warehouse.Products.Commands.CreateProduct; -// using Modules.Warehouse.Products.Queries.GetProducts; - -namespace Modules.Warehouse.Products.Endpoints; - -internal static class ProductEndpoints -{ - public static void MapProductEndpoints(this WebApplication app) - { - var group = app - .MapGroup("api/products") - .WithTags("Warehouse") - .WithOpenApi(); - - // group - // .MapGet("/", async (ISender sender, CancellationToken ct) => await sender.Send(new GetProductsQuery(), ct)) - // .WithName("GetProducts") - // .ProducesGet(); - - group - .MapPost("/", async (ISender sender, CreateProductCommand command, CancellationToken ct) => await sender.Send(command, ct)) - .WithName("CreateProduct") - .ProducesPost(); - } -} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs deleted file mode 100644 index d3ae581..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/ProductRepository.cs +++ /dev/null @@ -1,25 +0,0 @@ -// using Microsoft.EntityFrameworkCore; -// using Modules.Warehouse.Common.Persistence; -// using Modules.Warehouse.Products.Domain; -// -// namespace Modules.Warehouse.Products; -// -// internal class ProductRepository : IProductRepository -// { -// private readonly WarehouseDbContext _dbContext; -// -// public ProductRepository(WarehouseDbContext dbContext) -// { -// _dbContext = dbContext; -// } -// -// public async Task SkuExistsAsync(Sku sku) -// { -// return await _dbContext.Products.AnyAsync(p => p.Sku == sku); -// } -// -// public bool SkuExists(Sku sku) -// { -// return _dbContext.Products.Any(p => p.Sku == sku); -// } -// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs new file mode 100644 index 0000000..e4c87a8 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs @@ -0,0 +1,61 @@ +using ErrorOr; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.UseCases; + +namespace Modules.Warehouse.Products.UseCases; + +public static class CreateProductCommand +{ + public record Request(string Name, string Sku) : IRequest>; + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/products", async (Request request, ISender sender) => + { + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Created(); + }) + .WithName("Create Product") + .WithTags("Warehouse") + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.Name).NotEmpty(); + RuleFor(r => r.Sku) + .NotEmpty() + .Length(Sku.DefaultLength); + } + } + + internal class Handler : IRequestHandler> + { + private readonly WarehouseDbContext _dbDbContext; + + public Handler(WarehouseDbContext dbContext) + { + _dbDbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var sku = Sku.Create(request.Sku); + + var product = Product.Create(request.Name, sku); + _dbDbContext.Products.Add(product); + await _dbDbContext.SaveChangesAsync(cancellationToken); + + return Result.Success; + } + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Queries/GetProducts/GetProductsQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/GetProductsQuery.cs similarity index 100% rename from src/Modules/Warehouse/Modules.Warehouse/Products/Queries/GetProducts/GetProductsQuery.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/GetProductsQuery.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index e50438b..8fe3a9d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Products.Endpoints; +using Modules.Warehouse.Products.UseCases; using Modules.Warehouse.Storage.UseCases; namespace Modules.Warehouse; @@ -20,7 +20,8 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration public static void UseWarehouse(this WebApplication app) { + // TODO: Consider source generation or reflection for endpoint mapping CreateAisleCommand.Endpoint.MapEndpoint(app); - app.MapProductEndpoints(); + CreateProductCommand.Endpoint.MapEndpoint(app); } } diff --git a/tools/Database/WarehouseDbContextInitializer.cs b/tools/Database/WarehouseDbContextInitializer.cs index 013323e..52fa5e5 100644 --- a/tools/Database/WarehouseDbContextInitializer.cs +++ b/tools/Database/WarehouseDbContextInitializer.cs @@ -1,5 +1,4 @@ using Bogus; -using Common.SharedKernel.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Modules.Warehouse.Common.Persistence; @@ -69,15 +68,14 @@ private async Task SeedProductsAsync() if (await _dbContext.Products.AnyAsync()) return; - var moneyFaker = new Faker() - .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); + // var moneyFaker = new Faker() + // .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); var skuFaker = new Faker() .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); var faker = new Faker() - .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), moneyFaker.Generate(), - skuFaker.Generate())); + .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), skuFaker.Generate())); var products = faker.Generate(NumProducts); _dbContext.Products.AddRange(products); From 67dd3c9a4a38be7649926e80b48547ebfc738e4c Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 26 Aug 2024 06:25:46 +1000 Subject: [PATCH 30/87] =?UTF-8?q?=F0=9F=A7=AA=20Add=20product=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement methods for adding and removing stock in the `Product` class, returning an error when attempting to remove more stock than available. Add unit tests to ensure proper functionality and error handling, ensuring product initialization and stock adjustments are covered. #30 --- .../Products/ProductTests.cs | 78 +++++++++++++++---- .../Products/Domain/Product.cs | 15 +++- .../Database/WarehouseDbContextInitializer.cs | 2 - 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs index 87eedc4..93a95a7 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs @@ -1,19 +1,69 @@ +using FluentAssertions; +using Modules.Warehouse.Products.Domain; + namespace Modules.Warehouse.Tests.Products; public class ProductTests { - // [Fact] - // public void Create_WithValidData_ShouldSucceed() - // { - // // Arrange - // var name = "Product 1"; - // var sku = "SKU-1"; - // - // // Act - // var product = Product.Create(name, sku); - // - // // Assert - // product.Name.Should().Be(name); - // product.Sku.Should().Be(sku); - // } + [Fact] + public void Create_ShouldInitializeProductCorrectly() + { + // Arrange + var name = "Test Product"; + var sku = Sku.Create("12345678"); + + // Act + var product = Product.Create(name, sku); + + // Assert + product.Name.Should().Be(name); + product.Sku.Should().Be(sku); + product.StockOnHand.Should().Be(0); + product.Id.Should().NotBeNull(); + } + + [Fact] + public void RemoveStock_ShouldDecreaseStockOnHand() + { + // Arrange + var product = Product.Create("Test Product", Sku.Create("12345678")); + product.AddStock(10); + var quantityToRemove = 5; + + // Act + product.RemoveStock(quantityToRemove); + + // Assert + product.StockOnHand.Should().Be(5); + } + + [Fact] + public void RemoveStock_ShouldThrowException_WhenStockGoesBelowZero() + { + // Arrange + var product = Product.Create("Test Product", Sku.Create("12345678")); + product.AddStock(5); + var quantityToRemove = 10; + + // Act + var result = product.RemoveStock(quantityToRemove); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Should().Be(ProductErrors.CantRemoveMoreStockThanExists); + } + + [Fact] + public void AddStock_ShouldIncreaseStockOnHand() + { + // Arrange + var product = Product.Create("Test Product", Sku.Create("12345678")); + var quantityToAdd = 5; + + // Act + product.AddStock(quantityToAdd); + + // Assert + product.StockOnHand.Should().Be(5); + } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 61b496b..80f014d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -1,5 +1,5 @@ using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Exceptions; +using ErrorOr; using Throw; namespace Modules.Warehouse.Products.Domain; @@ -51,17 +51,19 @@ private void UpdateSku(Sku sku) Sku = sku; } - public void RemoveStock(int quantity) + public ErrorOr RemoveStock(int quantity) { quantity.Throw().IfNegativeOrZero(); if (StockOnHand - quantity < 0) - throw new DomainException("Cannot adjust stock below zero"); + return ProductErrors.CantRemoveMoreStockThanExists; StockOnHand -= quantity; if (StockOnHand <= LowStockThreshold) AddDomainEvent(new LowStockEvent(Id)); + + return Result.Success; } public void AddStock(int quantity) @@ -70,3 +72,10 @@ public void AddStock(int quantity) StockOnHand += quantity; } } + +public static class ProductErrors +{ + public static readonly Error CantRemoveMoreStockThanExists = Error.Validation( + "Product.CantRemoveMoreStockThanExists", + "Can't remove more stock than the warehouse has on hand"); +} diff --git a/tools/Database/WarehouseDbContextInitializer.cs b/tools/Database/WarehouseDbContextInitializer.cs index 52fa5e5..c906f66 100644 --- a/tools/Database/WarehouseDbContextInitializer.cs +++ b/tools/Database/WarehouseDbContextInitializer.cs @@ -52,8 +52,6 @@ private async Task SeedAisles() if (await _dbContext.Aisles.AnyAsync()) return; - - for (int i = 1; i <= NumAisles; i++) { var aisle = Aisle.Create($"Aisle {i}", NumBays, NumShelves); From 286393ded590ff9f9417234e2be84a3a70f0c4ab Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 26 Aug 2024 06:57:00 +1000 Subject: [PATCH 31/87] =?UTF-8?q?=E2=9C=A8=20Add=20ProductErrors=20class?= =?UTF-8?q?=20and=20product=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `ProductErrors` class to handle validation errors related to products. Also, implement integration tests to verify product creation and handle invalid requests properly. --- .../Products/ProductIntegrationTests.cs | 58 +++++++++++++++++++ .../Products/Domain/Product.cs | 13 +++-- .../Products/Domain/ProductErrors.cs | 10 ++++ .../Storage/UseCases/ErrorOrExt.cs | 10 +++- 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductErrors.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs new file mode 100644 index 0000000..2650692 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Products.UseCases; +using Modules.Warehouse.Tests.Common; +using System.Net; +using System.Net.Http.Json; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Products; + +public class ProductIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) + : IntegrationTestBase(fixture, output) +{ + [Fact] + public async Task CreateProduct_ValidRequest_ReturnsCreatedProduct() + { + // Arrange + var client = GetAnonymousClient(); + var request = new CreateProductCommand.Request("Name", "12345678"); + + // Act + var response = await client.PostAsJsonAsync("/api/products", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + var products = await GetQueryable().ToListAsync(); + products.Should().HaveCount(1); + + var product = products.First(); + product.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + product.CreatedBy.Should().NotBeNullOrWhiteSpace(); + product.Name.Should().Be(request.Name); + product.Sku.Value.Should().Be(request.Sku); + } + + [Theory] + [InlineData(null, "12345678")] + [InlineData("", "12345678")] + [InlineData(" ", "12345678")] + [InlineData("name", null)] + [InlineData("name", "")] + [InlineData("name", "123")] + public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, string sku) + { + // Arrange + var client = GetAnonymousClient(); + var request = new CreateProductCommand.Request(name, sku); + + // Act + var response = await client.PostAsJsonAsync("/api/products", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var content = await response.Content.ReadAsStringAsync(); + output.WriteLine(content); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 80f014d..596dc6f 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -73,9 +73,10 @@ public void AddStock(int quantity) } } -public static class ProductErrors -{ - public static readonly Error CantRemoveMoreStockThanExists = Error.Validation( - "Product.CantRemoveMoreStockThanExists", - "Can't remove more stock than the warehouse has on hand"); -} +// internal class GetAllProductsSpecification : Specification +// { +// public GetAllProductsSpecification() +// { +// +// } +// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductErrors.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductErrors.cs new file mode 100644 index 0000000..695aa70 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductErrors.cs @@ -0,0 +1,10 @@ +using ErrorOr; + +namespace Modules.Warehouse.Products.Domain; + +public static class ProductErrors +{ + public static readonly Error CantRemoveMoreStockThanExists = Error.Validation( + "Product.CantRemoveMoreStockThanExists", + "Can't remove more stock than the warehouse has on hand"); +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs index a04eb52..c588545 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs @@ -36,8 +36,14 @@ private static ValidationProblem ValidationProblem(IErrorOr error) { var errors = new Dictionary(); foreach (var e in error.Errors!) - errors.Add(e.Code, [e.Description]); + { + if (errors.Remove(e.Code, out string[]? value)) + errors.Add(e.Description, [..value, e.Description]); + else + errors.Add(e.Code, [e.Description]); + + } return TypedResults.ValidationProblem(errors, title: "One or more validation errors occurred."); } -} \ No newline at end of file +} From d833f1ff82f8aa68b14da19b7d50b656f4e6f18f Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 26 Aug 2024 07:40:50 +1000 Subject: [PATCH 32/87] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20swagger=20and=20co?= =?UTF-8?q?nnection=20string=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored endpoint mappings from command-based names to simpler class names. Removed old commented-out code and renamed CreateProductCommand and CreateAisleCommand to CreateProduct and CreateAisle respectively. Also added a new connection string for the Warehouse database in the development environment settings. --- .../Products/ProductIntegrationTests.cs | 4 ++-- .../Storage/AisleIntegrationTests.cs | 4 ++-- .../Common/Persistence/WarehouseDbContext.cs | 19 +------------------ ...eateProductCommand.cs => CreateProduct.cs} | 16 ++++++++-------- .../{CreateAisleCommand.cs => CreateAisle.cs} | 16 ++++++++-------- .../Storage/UseCases/ErrorOrExt.cs | 3 +-- .../Modules.Warehouse/WarehouseModule.cs | 4 ++-- src/WebApi/appsettings.Development.json | 3 +++ 8 files changed, 27 insertions(+), 42 deletions(-) rename src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/{CreateProductCommand.cs => CreateProduct.cs} (66%) rename src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/{CreateAisleCommand.cs => CreateAisle.cs} (64%) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs index 2650692..b67ec00 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs @@ -17,7 +17,7 @@ public async Task CreateProduct_ValidRequest_ReturnsCreatedProduct() { // Arrange var client = GetAnonymousClient(); - var request = new CreateProductCommand.Request("Name", "12345678"); + var request = new CreateProduct.CreateProductCommand("Name", "12345678"); // Act var response = await client.PostAsJsonAsync("/api/products", request); @@ -45,7 +45,7 @@ public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, st { // Arrange var client = GetAnonymousClient(); - var request = new CreateProductCommand.Request(name, sku); + var request = new CreateProduct.CreateProductCommand(name, sku); // Act var response = await client.PostAsJsonAsync("/api/products", request); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs index 98d4ad1..54f3f85 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs @@ -18,7 +18,7 @@ public async Task CreateAisle_ValidRequest_ReturnsCreatedAisle() { // Arrange var client = GetAnonymousClient(); - var request = new CreateAisleCommand.Request("Name", 2, 2); + var request = new CreateAisle.CreateAisleCommand("Name", 2, 2); // Act var response = await client.PostAsJsonAsync("/api/aisles", request); @@ -49,7 +49,7 @@ public async Task CreateAisle_WithInvalidRequest_Throws(string name, int numBays { // Arrange var client = GetAnonymousClient(); - var request = new CreateAisleCommand.Request(name, numBays, numShelves); + var request = new CreateAisle.CreateAisleCommand(name, numBays, numShelves); // Act var response = await client.PostAsJsonAsync("/api/aisles", request); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index e20498f..106e594 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -6,18 +6,13 @@ namespace Modules.Warehouse.Common.Persistence; internal class WarehouseDbContext : DbContext { - // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; - // private readonly OutboxInterceptor _outboxInterceptor; - internal DbSet Aisles => Set(); internal DbSet Products => Set(); // Needs to be public for the Database project - public WarehouseDbContext(DbContextOptions options /*EntitySaveChangesInterceptor saveChangesInterceptor, OutboxInterceptor outboxInterceptor*/) : base(options) + public WarehouseDbContext(DbContextOptions options) : base(options) { - // _saveChangesInterceptor = saveChangesInterceptor; - // _outboxInterceptor = outboxInterceptor; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -26,16 +21,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); base.OnModelCreating(modelBuilder); } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - // optionsBuilder.AddInterceptors( - // _saveChangesInterceptor, - // _outboxInterceptor); - } - - // public Task SaveChangesAsync() => this - // { - // throw new NotImplementedException(); - // } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProduct.cs similarity index 66% rename from src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProduct.cs index e4c87a8..7c37ded 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProduct.cs @@ -8,15 +8,15 @@ namespace Modules.Warehouse.Products.UseCases; -public static class CreateProductCommand +public static class CreateProduct { - public record Request(string Name, string Sku) : IRequest>; + public record CreateProductCommand(string Name, string Sku) : IRequest>; public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/products", async (Request request, ISender sender) => + app.MapPost("/api/products", async (CreateProductCommand request, ISender sender) => { var response = await sender.Send(request); return response.IsError ? response.Problem() : TypedResults.Created(); @@ -27,7 +27,7 @@ public static void MapEndpoint(IEndpointRouteBuilder app) } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { public Validator() { @@ -38,7 +38,7 @@ public Validator() } } - internal class Handler : IRequestHandler> + internal class Handler : IRequestHandler> { private readonly WarehouseDbContext _dbDbContext; @@ -47,11 +47,11 @@ public Handler(WarehouseDbContext dbContext) _dbDbContext = dbContext; } - public async Task> Handle(Request request, CancellationToken cancellationToken) + public async Task> Handle(CreateProductCommand createProductCommand, CancellationToken cancellationToken) { - var sku = Sku.Create(request.Sku); + var sku = Sku.Create(createProductCommand.Sku); - var product = Product.Create(request.Name, sku); + var product = Product.Create(createProductCommand.Name, sku); _dbDbContext.Products.Add(product); await _dbDbContext.SaveChangesAsync(cancellationToken); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisle.cs similarity index 64% rename from src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs rename to src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisle.cs index f4e4adc..112b9e9 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisle.cs @@ -7,26 +7,26 @@ namespace Modules.Warehouse.Storage.UseCases; -public static class CreateAisleCommand +public static class CreateAisle { - public record Request(string Name, int NumBays, int NumShelves) : IRequest>; + public record CreateAisleCommand(string Name, int NumBays, int NumShelves) : IRequest>; public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/aisles", async (Request request, ISender sender) => + app.MapPost("/api/aisles", async (CreateAisleCommand request, ISender sender) => { var response = await sender.Send(request); return response.IsError ? response.Problem() : TypedResults.Created(); }) .WithName("CreateAisle") - .WithTags("Storage") + .WithTags("Warehouse") .WithOpenApi(); } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { public Validator() { @@ -36,7 +36,7 @@ public Validator() } } - internal class Handler : IRequestHandler> + internal class Handler : IRequestHandler> { private readonly WarehouseDbContext _context; @@ -45,9 +45,9 @@ public Handler(WarehouseDbContext context) _context = context; } - public async Task> Handle(Request request, CancellationToken cancellationToken) + public async Task> Handle(CreateAisleCommand createAisleCommand, CancellationToken cancellationToken) { - var aisle = Aisle.Create(request.Name, request.NumBays, request.NumShelves); + var aisle = Aisle.Create(createAisleCommand.Name, createAisleCommand.NumBays, createAisleCommand.NumShelves); await _context.Aisles.AddAsync(aisle, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return Result.Success; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs index c588545..c01d9fd 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs @@ -38,10 +38,9 @@ private static ValidationProblem ValidationProblem(IErrorOr error) foreach (var e in error.Errors!) { if (errors.Remove(e.Code, out string[]? value)) - errors.Add(e.Description, [..value, e.Description]); + errors.Add(e.Code, [..value, e.Description]); else errors.Add(e.Code, [e.Description]); - } return TypedResults.ValidationProblem(errors, title: "One or more validation errors occurred."); diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 8fe3a9d..49a4a54 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -21,7 +21,7 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration public static void UseWarehouse(this WebApplication app) { // TODO: Consider source generation or reflection for endpoint mapping - CreateAisleCommand.Endpoint.MapEndpoint(app); - CreateProductCommand.Endpoint.MapEndpoint(app); + CreateAisle.Endpoint.MapEndpoint(app); + CreateProduct.Endpoint.MapEndpoint(app); } } diff --git a/src/WebApi/appsettings.Development.json b/src/WebApi/appsettings.Development.json index 0c208ae..b445d7c 100644 --- a/src/WebApi/appsettings.Development.json +++ b/src/WebApi/appsettings.Development.json @@ -4,5 +4,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ConnectionStrings": { + "Warehouse": "Server=localhost,1800;Initial Catalog=Warehouse;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;" } } From a7b2dc61711c598fddc10d12ff879e10422d024e Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:07:05 +1000 Subject: [PATCH 33/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20UseCases=20?= =?UTF-8?q?and=20Enhance=20Swagger=20Configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed UseCases to follow the 'Command' naming convention for clarity. Added OpenApiExt for better Swagger configuration and moved Swagger setup into this extension in `Program.cs`. --- .../Products/ProductIntegrationTests.cs | 4 +- .../Storage/AisleIntegrationTests.cs | 4 +- ...eateProduct.cs => CreateProductCommand.cs} | 18 +++++---- .../{CreateAisle.cs => CreateAisleCommand.cs} | 14 +++---- .../Modules.Warehouse/WarehouseModule.cs | 4 +- src/WebApi/Extensions/OpenApiExt.cs | 40 +++++++++++++++++++ src/WebApi/Program.cs | 5 +-- 7 files changed, 64 insertions(+), 25 deletions(-) rename src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/{CreateProduct.cs => CreateProductCommand.cs} (66%) rename src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/{CreateAisle.cs => CreateAisleCommand.cs} (66%) create mode 100644 src/WebApi/Extensions/OpenApiExt.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs index b67ec00..2650692 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs @@ -17,7 +17,7 @@ public async Task CreateProduct_ValidRequest_ReturnsCreatedProduct() { // Arrange var client = GetAnonymousClient(); - var request = new CreateProduct.CreateProductCommand("Name", "12345678"); + var request = new CreateProductCommand.Request("Name", "12345678"); // Act var response = await client.PostAsJsonAsync("/api/products", request); @@ -45,7 +45,7 @@ public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, st { // Arrange var client = GetAnonymousClient(); - var request = new CreateProduct.CreateProductCommand(name, sku); + var request = new CreateProductCommand.Request(name, sku); // Act var response = await client.PostAsJsonAsync("/api/products", request); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs index 54f3f85..98d4ad1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs @@ -18,7 +18,7 @@ public async Task CreateAisle_ValidRequest_ReturnsCreatedAisle() { // Arrange var client = GetAnonymousClient(); - var request = new CreateAisle.CreateAisleCommand("Name", 2, 2); + var request = new CreateAisleCommand.Request("Name", 2, 2); // Act var response = await client.PostAsJsonAsync("/api/aisles", request); @@ -49,7 +49,7 @@ public async Task CreateAisle_WithInvalidRequest_Throws(string name, int numBays { // Arrange var client = GetAnonymousClient(); - var request = new CreateAisle.CreateAisleCommand(name, numBays, numShelves); + var request = new CreateAisleCommand.Request(name, numBays, numShelves); // Act var response = await client.PostAsJsonAsync("/api/aisles", request); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProduct.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs similarity index 66% rename from src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProduct.cs rename to src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs index 7c37ded..26d9056 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProduct.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs @@ -1,3 +1,4 @@ +using Common.SharedKernel; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -8,26 +9,27 @@ namespace Modules.Warehouse.Products.UseCases; -public static class CreateProduct +public static class CreateProductCommand { - public record CreateProductCommand(string Name, string Sku) : IRequest>; + public record Request(string Name, string Sku) : IRequest>; public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/products", async (CreateProductCommand request, ISender sender) => + app.MapPost("/api/products", async (Request request, ISender sender) => { var response = await sender.Send(request); return response.IsError ? response.Problem() : TypedResults.Created(); }) .WithName("Create Product") .WithTags("Warehouse") + .ProducesPost() .WithOpenApi(); } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { public Validator() { @@ -38,7 +40,7 @@ public Validator() } } - internal class Handler : IRequestHandler> + internal class Handler : IRequestHandler> { private readonly WarehouseDbContext _dbDbContext; @@ -47,11 +49,11 @@ public Handler(WarehouseDbContext dbContext) _dbDbContext = dbContext; } - public async Task> Handle(CreateProductCommand createProductCommand, CancellationToken cancellationToken) + public async Task> Handle(Request request, CancellationToken cancellationToken) { - var sku = Sku.Create(createProductCommand.Sku); + var sku = Sku.Create(request.Sku); - var product = Product.Create(createProductCommand.Name, sku); + var product = Product.Create(request.Name, sku); _dbDbContext.Products.Add(product); await _dbDbContext.SaveChangesAsync(cancellationToken); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisle.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs similarity index 66% rename from src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisle.cs rename to src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs index 112b9e9..8674ed4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisle.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs @@ -7,15 +7,15 @@ namespace Modules.Warehouse.Storage.UseCases; -public static class CreateAisle +public static class CreateAisleCommand { - public record CreateAisleCommand(string Name, int NumBays, int NumShelves) : IRequest>; + public record Request(string Name, int NumBays, int NumShelves) : IRequest>; public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/aisles", async (CreateAisleCommand request, ISender sender) => + app.MapPost("/api/aisles", async (Request request, ISender sender) => { var response = await sender.Send(request); return response.IsError ? response.Problem() : TypedResults.Created(); @@ -26,7 +26,7 @@ public static void MapEndpoint(IEndpointRouteBuilder app) } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { public Validator() { @@ -36,7 +36,7 @@ public Validator() } } - internal class Handler : IRequestHandler> + internal class Handler : IRequestHandler> { private readonly WarehouseDbContext _context; @@ -45,9 +45,9 @@ public Handler(WarehouseDbContext context) _context = context; } - public async Task> Handle(CreateAisleCommand createAisleCommand, CancellationToken cancellationToken) + public async Task> Handle(Request request, CancellationToken cancellationToken) { - var aisle = Aisle.Create(createAisleCommand.Name, createAisleCommand.NumBays, createAisleCommand.NumShelves); + var aisle = Aisle.Create(request.Name, request.NumBays, request.NumShelves); await _context.Aisles.AddAsync(aisle, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return Result.Success; diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 49a4a54..8fe3a9d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -21,7 +21,7 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration public static void UseWarehouse(this WebApplication app) { // TODO: Consider source generation or reflection for endpoint mapping - CreateAisle.Endpoint.MapEndpoint(app); - CreateProduct.Endpoint.MapEndpoint(app); + CreateAisleCommand.Endpoint.MapEndpoint(app); + CreateProductCommand.Endpoint.MapEndpoint(app); } } diff --git a/src/WebApi/Extensions/OpenApiExt.cs b/src/WebApi/Extensions/OpenApiExt.cs new file mode 100644 index 0000000..fe8c262 --- /dev/null +++ b/src/WebApi/Extensions/OpenApiExt.cs @@ -0,0 +1,40 @@ +using Microsoft.OpenApi.Models; + +namespace WebApi.Extensions; + +public static class OpenApiExt +{ + public static void AddSwagger(this IServiceCollection services) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(setup => + { + setup.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Modular Monolith API", + Version = "v1", + }); + + // TechDebt: Enable swagger documentation enrichment from XML Docs + // Set the comments path for the Swagger JSON and UI. + //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + //var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + //setup.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); + + // Needed to support nested types in the schema + setup.CustomSchemaIds(x => x.FullName?.Replace("+", ".", StringComparison.Ordinal)); + + // setup.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + // { + // Type = SecuritySchemeType.Http, + // Name = JwtBearerDefaults.AuthenticationScheme, + // Scheme = JwtBearerDefaults.AuthenticationScheme, + // Reference = new() + // { + // Type = ReferenceType.SecurityScheme, + // Id = JwtBearerDefaults.AuthenticationScheme + // } + // }); + }); + } +} diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index bbac73a..e1e93a7 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -5,10 +5,7 @@ var builder = WebApplication.CreateBuilder(args); { - // Add services to the container. - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); + builder.Services.AddSwagger(); builder.Services.AddCommon(); From d4b89409f21f65784cb47a5925588445a1060216 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:01:10 +1000 Subject: [PATCH 34/87] =?UTF-8?q?=E2=9C=A8=20Add=20storage=20allocation=20?= =?UTF-8?q?feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented `AllocateStorageCommand` to handle storage allocation requests. Added integration and unit tests to ensure proper functionality and error handling. #32 --- .../Common/IntegrationTestBase.cs | 12 ++-- .../StorageAllocationIntegrationTests.cs | 32 +++++++++ .../Storage/StorageAllocationServiceTests.cs | 46 +++++++++++++ .../Domain/StorageAllocationService.cs | 12 +++- .../UseCases/AllocateStorageCommand.cs | 69 +++++++++++++++++++ .../Modules.Warehouse/WarehouseModule.cs | 1 + 6 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationIntegrationTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs index 9a9b5fc..331f5b0 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs @@ -33,12 +33,12 @@ protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper } - // protected async Task AddEntityAsync(T entity, CancellationToken cancellationToken = default) where T : class - // { - // await Context.Set().AddAsync(entity, cancellationToken); - // await Context.SaveChangesAsync(cancellationToken); - // } - // + protected async Task AddEntityAsync(T entity, CancellationToken cancellationToken = default) where T : class + { + await DbContext.Set().AddAsync(entity, cancellationToken); + await DbContext.SaveChangesAsync(cancellationToken); + } + // protected async Task AddEntitiesAsync(IEnumerable entities, CancellationToken cancellationToken = default) where T : class // { // await Context.Set().AddRangeAsync(entities, cancellationToken); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationIntegrationTests.cs new file mode 100644 index 0000000..ad905c9 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationIntegrationTests.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; +using Modules.Warehouse.Storage.UseCases; +using Modules.Warehouse.Tests.Common; +using System.Net; +using System.Net.Http.Json; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Storage; + +public class StorageAllocationIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) + : IntegrationTestBase(fixture, output) +{ + [Fact] + public async Task AllocateStorage_ValidRequest_ReturnsOk() + { + // Arrange + var client = GetAnonymousClient(); + var product = Product.Create("Name", Sku.Create("12345678")); + await AddEntityAsync(product); + var aisle = Aisle.Create("Name", 2, 2); + await AddEntityAsync(aisle); + var request = new AllocateStorageCommand.Request(product.Id.Value); + + // Act + var response = await client.PostAsJsonAsync("/api/aisles/allocate-storage", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs new file mode 100644 index 0000000..55fd111 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Tests.Storage +{ + public class StorageAllocationServiceTests + { + [Fact] + public void AllocateStorage_ShouldAssignProductToFirstEmptyShelf() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var aisles = new List + { + Aisle.Create("name", 2, 2) + }; + + // Act + var result = StorageAllocationService.AllocateStorage(aisles, productId); + + // Assert + result.IsError.Should().BeFalse(); + aisles[0].Bays[0].Shelves[0].ProductId.Should().Be(productId); + } + + [Fact] + public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelfIsAvailable() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var aisles = new List + { + Aisle.Create("name", 1, 1) + }; + StorageAllocationService.AllocateStorage(aisles, productId); + + // Act + var result = StorageAllocationService.AllocateStorage(aisles, productId); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Should().Be(StorageAllocationErrors.NoAvailableStorage); + } + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs index e2d5e68..d2b4517 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs @@ -1,3 +1,4 @@ +using ErrorOr; using Modules.Warehouse.Products.Domain; namespace Modules.Warehouse.Storage.Domain; @@ -7,7 +8,7 @@ namespace Modules.Warehouse.Storage.Domain; /// internal class StorageAllocationService { - internal static void AllocateStorage(IEnumerable aisles, ProductId productId) + internal static ErrorOr AllocateStorage(IEnumerable aisles, ProductId productId) { foreach (var aisle in aisles) { @@ -19,11 +20,16 @@ internal static void AllocateStorage(IEnumerable aisles, ProductId produc continue; shelf.AssignProduct(productId); - return; + return Result.Success; } } } - throw new Exception("No available storage"); + return StorageAllocationErrors.NoAvailableStorage; } } + +public static class StorageAllocationErrors +{ + public static readonly Error NoAvailableStorage = Error.Failure("No available storage"); +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs new file mode 100644 index 0000000..8705410 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs @@ -0,0 +1,69 @@ +using ErrorOr; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Storage.UseCases; + +public static class AllocateStorageCommand +{ + public record Request(Guid ProductId) : IRequest>; + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/aisles/allocate-storage", async (Request request, ISender sender) => + { + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Ok(); + }) + .WithName("Allocate Storage") + .WithTags("Warehouse") + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.ProductId).NotEmpty(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly WarehouseDbContext _dbContext; + + public Handler(WarehouseDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var aisles = await _dbContext.Aisles + .WithSpecification(new GetAllAislesSpec()) + .ToListAsync(cancellationToken); + + var product = await _dbContext.Products + .WithSpecification(new ProductByIdSpec(new ProductId(request.ProductId))) + .FirstOrDefaultAsync(cancellationToken); + + if (product == null) + return Error.NotFound("Product not found"); + + var result = StorageAllocationService.AllocateStorage(aisles, product.Id); + if (result.IsError) + return result; + + await _dbContext.SaveChangesAsync(cancellationToken); + return Result.Success; + } + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 8fe3a9d..12825e4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -23,5 +23,6 @@ public static void UseWarehouse(this WebApplication app) // TODO: Consider source generation or reflection for endpoint mapping CreateAisleCommand.Endpoint.MapEndpoint(app); CreateProductCommand.Endpoint.MapEndpoint(app); + AllocateStorageCommand.Endpoint.MapEndpoint(app); } } From f9ff7224b007af56d34cc3b163891968edfb168a Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:08:07 +1000 Subject: [PATCH 35/87] =?UTF-8?q?=F0=9F=A7=AA=20Consolidate=20StorageAlloc?= =?UTF-8?q?ationServiceTests=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed redundant StorageAllocationServiceTests.cs and merged its contents into the existing Storage/StorageAllocationServiceTests.cs. This cleanup reduces duplication and improves file organization in test modules. #32 --- .../Storage/StorageAllocationServiceTests.cs | 109 ++++++++++++------ .../StorageAllocationServiceTests.cs | 50 -------- 2 files changed, 76 insertions(+), 83 deletions(-) delete mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs index 55fd111..c46ec63 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs @@ -2,45 +2,88 @@ using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; -namespace Modules.Warehouse.Tests.Storage +namespace Modules.Warehouse.Tests.Storage; + +public class StorageAllocationServiceTests { - public class StorageAllocationServiceTests + [Fact] + public void AllocateStorage_ShouldAssignProductToFirstEmptyShelf() { - [Fact] - public void AllocateStorage_ShouldAssignProductToFirstEmptyShelf() + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var aisles = new List { - // Arrange - var productId = new ProductId(Guid.NewGuid()); - var aisles = new List - { - Aisle.Create("name", 2, 2) - }; - - // Act - var result = StorageAllocationService.AllocateStorage(aisles, productId); - - // Assert - result.IsError.Should().BeFalse(); - aisles[0].Bays[0].Shelves[0].ProductId.Should().Be(productId); + Aisle.Create("name", 2, 2) + }; + + // Act + var result = StorageAllocationService.AllocateStorage(aisles, productId); + + // Assert + result.IsError.Should().BeFalse(); + aisles[0].Bays[0].Shelves[0].ProductId.Should().Be(productId); + } + + [Fact] + public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelfIsAvailable() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var aisles = new List + { + Aisle.Create("name", 1, 1) + }; + StorageAllocationService.AllocateStorage(aisles, productId); + + // Act + var result = StorageAllocationService.AllocateStorage(aisles, productId); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Should().Be(StorageAllocationErrors.NoAvailableStorage); + } + + + [Fact] + public void AllocateStorage_WhenMaxStorageUsed_ShouldHaveNoAvailableStorage() + { + // Arrange + var numBays = 2; + var numShelves = 3; + var aisle = Aisle.Create("Aisle 1", numBays, numShelves); + var sut = new StorageAllocationService(); + var productId = new ProductId(Guid.NewGuid()); + + // Act + for (var i = 0; i < numBays * numShelves; i++) + { + StorageAllocationService.AllocateStorage([aisle], productId); } - [Fact] - public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelfIsAvailable() + // Assert + aisle.TotalStorage.Should().Be(numBays * numShelves); + aisle.AvailableStorage.Should().Be(0); + } + + + [Fact] + public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() + { + // Arrange + var numBays = 2; + var numShelves = 3; + var aisle = Aisle.Create("Aisle 1", numBays, numShelves); + var sut = new StorageAllocationService(); + var productId = new ProductId(Guid.NewGuid()); + + // Act + for(var i = 0; i < numBays * numShelves; i++) { - // Arrange - var productId = new ProductId(Guid.NewGuid()); - var aisles = new List - { - Aisle.Create("name", 1, 1) - }; - StorageAllocationService.AllocateStorage(aisles, productId); - - // Act - var result = StorageAllocationService.AllocateStorage(aisles, productId); - - // Assert - result.IsError.Should().BeTrue(); - result.FirstError.Should().Be(StorageAllocationErrors.NoAvailableStorage); + StorageAllocationService.AllocateStorage([aisle], productId); } + + // Assert + var result = StorageAllocationService.AllocateStorage([aisle], productId); + result.IsError.Should().BeTrue(); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs deleted file mode 100644 index 094e0c9..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/StorageAllocationServiceTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using FluentAssertions; -using Modules.Warehouse.Products.Domain; -using Modules.Warehouse.Storage.Domain; - -namespace Modules.Warehouse.Tests; - -public class StorageAllocationServiceTests -{ - [Fact] - public void AllocateStorage_WhenMaxStorageUsed_ShouldHaveNoAvailableStorage() - { - // Arrange - var numBays = 2; - var numShelves = 3; - var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var sut = new StorageAllocationService(); - var productId = new ProductId(Guid.NewGuid()); - - // Act - for(var i = 0; i < numBays * numShelves; i++) - { - StorageAllocationService.AllocateStorage([aisle], productId); - } - - // Assert - aisle.TotalStorage.Should().Be(numBays * numShelves); - aisle.AvailableStorage.Should().Be(0); - } - - [Fact] - public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() - { - // Arrange - var numBays = 2; - var numShelves = 3; - var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var sut = new StorageAllocationService(); - var productId = new ProductId(Guid.NewGuid()); - - // Act - for(var i = 0; i < numBays * numShelves; i++) - { - StorageAllocationService.AllocateStorage([aisle], productId); - } - - // Assert - var act = () => StorageAllocationService.AllocateStorage([aisle], productId); - act.Should().Throw(); - } -} From 6f23e7bdcfcac37c600ddb4a593ffa305a845bb4 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 27 Aug 2024 05:57:47 +1000 Subject: [PATCH 36/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20namespace?= =?UTF-8?q?=20and=20update=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed `ErrorOrExt` and moved it to `Common.SharedKernel.Api`. Updated relevant files to reflect the new namespace and added necessary imports to accommodate the change. --- .../UseCases => Common/Common.SharedKernel/Api}/ErrorOrExt.cs | 2 +- .../Modules.Warehouse/Products/UseCases/CreateProductCommand.cs | 2 +- .../Storage/UseCases/AllocateStorageCommand.cs | 1 + .../Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) rename src/{Modules/Warehouse/Modules.Warehouse/Storage/UseCases => Common/Common.SharedKernel/Api}/ErrorOrExt.cs (97%) diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs b/src/Common/Common.SharedKernel/Api/ErrorOrExt.cs similarity index 97% rename from src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs rename to src/Common/Common.SharedKernel/Api/ErrorOrExt.cs index c01d9fd..802929e 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/ErrorOrExt.cs +++ b/src/Common/Common.SharedKernel/Api/ErrorOrExt.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; -namespace Modules.Warehouse.Storage.UseCases; +namespace Common.SharedKernel.Api; public static class ErrorOrExt { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs index 26d9056..564530c 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs @@ -1,11 +1,11 @@ using Common.SharedKernel; +using Common.SharedKernel.Api; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Modules.Warehouse.Common.Persistence; using Modules.Warehouse.Products.Domain; -using Modules.Warehouse.Storage.UseCases; namespace Modules.Warehouse.Products.UseCases; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs index 8705410..ced8fe3 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs @@ -1,3 +1,4 @@ +using Common.SharedKernel.Api; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs index 8674ed4..af186bc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs @@ -1,3 +1,4 @@ +using Common.SharedKernel.Api; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; From 5171f5149f499b8e02f10136955e7230c1f80c76 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 27 Aug 2024 06:30:32 +1000 Subject: [PATCH 37/87] =?UTF-8?q?=E2=9C=A8=20add=20item=20location=20endpo?= =?UTF-8?q?int?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the GetProductsQuery.cs file and its associated query to streamline the codebase. Introduced GetItemLocationQuery.cs for fetching the location of specific items within the warehouse. Updated the solution file to reflect these changes. #33 --- Warehouse.slnf | 12 +++ .../Products/UseCases/GetProductsQuery.cs | 25 ------ .../Storage/UseCases/GetItemLocationQuery.cs | 77 +++++++++++++++++++ 3 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 Warehouse.slnf delete mode 100644 src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/GetProductsQuery.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs diff --git a/Warehouse.slnf b/Warehouse.slnf new file mode 100644 index 0000000..ffc9f78 --- /dev/null +++ b/Warehouse.slnf @@ -0,0 +1,12 @@ +{ + "solution": { + "path": "ModularMonolith.sln", + "projects": [ + "src\\Common\\Common.SharedKernel\\Common.SharedKernel.csproj", + "src\\Modules\\Warehouse\\Modules.Warehouse.Tests\\Modules.Warehouse.Tests.csproj", + "src\\Modules\\Warehouse\\Modules.Warehouse\\Modules.Warehouse.csproj", + "src\\WebApi\\WebApi.csproj", + "tools\\Database\\Database.csproj" + ] + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/GetProductsQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/GetProductsQuery.cs deleted file mode 100644 index ce9b783..0000000 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/GetProductsQuery.cs +++ /dev/null @@ -1,25 +0,0 @@ -// using Microsoft.EntityFrameworkCore; -// using Modules.Warehouse.Common.Persistence; -// -// namespace Modules.Warehouse.Products.Queries.GetProducts; -// -// internal record GetProductsQuery : IRequest>; -// -// internal record ProductDto(Guid Id, string Sku, string Name, decimal Price); -// -// internal class GetProductsQueryHandler : IRequestHandler> -// { -// private readonly WarehouseDbContext _dbContext; -// -// public GetProductsQueryHandler(WarehouseDbContext dbContext) -// { -// _dbContext = dbContext; -// } -// -// public async Task> Handle(GetProductsQuery request, CancellationToken cancellationToken) -// { -// return await _dbContext.Products -// .Select(p => new ProductDto(p.Id.Value, p.Sku.Value, p.Name, p.Price.Amount)) -// .ToListAsync(cancellationToken: cancellationToken); -// } -// } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs new file mode 100644 index 0000000..e2c7616 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs @@ -0,0 +1,77 @@ +using Common.SharedKernel; +using Common.SharedKernel.Api; +using ErrorOr; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Storage.UseCases; + +public static class GetItemLocationQuery +{ + public record Request(Guid ProductId) : IRequest>; + + public record Response(string AisleName, string BayName, string ShelfName); + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("/api/aisles/products/{productID:guid}", async (Request request, ISender sender) => + { + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Ok(response.Value); + }) + .WithName("FindProductLocation") + .WithTags("Warehouse") + .ProducesGet() + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.ProductId).NotEmpty(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly WarehouseDbContext _dbContext; + + public Handler(WarehouseDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var aisles = await _dbContext.Aisles + .WithSpecification(new GetAllAislesSpec()) + .ToListAsync(cancellationToken); + + var productId = new ProductId(request.ProductId); + + // Consider adjusting the model to make the query more efficient if needed + foreach (var aisle in aisles) + { + foreach (var bay in aisle.Bays) + { + foreach (var shelf in bay.Shelves) + { + if (shelf.ProductId == productId) + return new Response(aisle.Name, bay.Name, shelf.Name); + } + } + } + + return Error.NotFound(description: "Product not found"); + } + } +} From fb1d4c6f79bbbe852ba9b130134149ad2e92b46b Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 27 Aug 2024 06:33:02 +1000 Subject: [PATCH 38/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20test=20fi?= =?UTF-8?q?le=20structure=20for=20clearer=20module=20separation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocated integration test files to the 'UseCases' subdirectory and domain service test files to the 'Domain' subdirectory for better organization. Updated namespaces and class names to reflect new folder structure, ensuring consistency and clarity in file organization. --- .../Storage/{ => Domain}/AisleTests.cs | 2 +- .../Storage/{ => Domain}/StorageAllocationServiceTests.cs | 2 +- .../AllocateStorageCommandIntegrationTests.cs} | 4 ++-- .../CreateAisleCommandIntegrationTests.cs} | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/{ => Domain}/AisleTests.cs (97%) rename src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/{ => Domain}/StorageAllocationServiceTests.cs (98%) rename src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/{StorageAllocationIntegrationTests.cs => UseCases/AllocateStorageCommandIntegrationTests.cs} (84%) rename src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/{AisleIntegrationTests.cs => UseCases/CreateAisleCommandIntegrationTests.cs} (92%) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs similarity index 97% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs rename to src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs index a571d5c..f9ad7d7 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs @@ -3,7 +3,7 @@ using Modules.Warehouse.Storage.Domain; using Xunit.Abstractions; -namespace Modules.Warehouse.Tests.Storage; +namespace Modules.Warehouse.Tests.Storage.Domain; public class AisleTests { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs similarity index 98% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs rename to src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs index c46ec63..391842d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationServiceTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs @@ -2,7 +2,7 @@ using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; -namespace Modules.Warehouse.Tests.Storage; +namespace Modules.Warehouse.Tests.Storage.Domain; public class StorageAllocationServiceTests { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs similarity index 84% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationIntegrationTests.cs rename to src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs index ad905c9..70f74a4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/StorageAllocationIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs @@ -7,9 +7,9 @@ using System.Net.Http.Json; using Xunit.Abstractions; -namespace Modules.Warehouse.Tests.Storage; +namespace Modules.Warehouse.Tests.Storage.UseCases; -public class StorageAllocationIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) +public class AllocateStorageCommandIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { [Fact] diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs similarity index 92% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs rename to src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs index 98d4ad1..c93b010 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/AisleIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs @@ -8,9 +8,9 @@ using System.Net.Http.Json; using Xunit.Abstractions; -namespace Modules.Warehouse.Tests.Storage; +namespace Modules.Warehouse.Tests.Storage.UseCases; -public class AisleIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) +public class CreateAisleCommandIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { [Fact] From f00a33fc29261add7b7c2dcd8513748f49af6ee6 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 27 Aug 2024 06:53:28 +1000 Subject: [PATCH 39/87] =?UTF-8?q?=F0=9F=A7=AA=20Add=20GetItemLocationQuery?= =?UTF-8?q?=20endpoint=20and=20integration=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced the GetItemLocationQuery endpoint to the Warehouse module. Added integration tests to ensure it returns correct item locations, and modified the IntegrationTestBase to support saving changes asynchronously. --- .../Common/IntegrationTestBase.cs | 5 +++ .../GetItemLocationQueryIntegrationTests.cs | 39 +++++++++++++++++++ .../Storage/UseCases/GetItemLocationQuery.cs | 5 ++- .../Modules.Warehouse/WarehouseModule.cs | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs index 331f5b0..680f14c 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs @@ -39,6 +39,11 @@ protected async Task AddEntityAsync(T entity, CancellationToken cancellationT await DbContext.SaveChangesAsync(cancellationToken); } + protected async Task SaveAsync(CancellationToken cancellationToken = default) + { + await DbContext.SaveChangesAsync(cancellationToken); + } + // protected async Task AddEntitiesAsync(IEnumerable entities, CancellationToken cancellationToken = default) where T : class // { // await Context.Set().AddRangeAsync(entities, cancellationToken); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs new file mode 100644 index 0000000..ad24cb1 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; +using Modules.Warehouse.Storage.UseCases; +using Modules.Warehouse.Tests.Common; +using System.Net; +using System.Net.Http.Json; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Storage.UseCases; + +public class GetItemLocationQueryIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) + : IntegrationTestBase(fixture, output) +{ + [Fact] + public async Task Query_ValidRequest_ReturnsOk() + { + // Arrange + var client = GetAnonymousClient(); + var product = Product.Create("Name", Sku.Create("12345678")); + await AddEntityAsync(product); + var aisle = Aisle.Create("Name", 2, 2); + await AddEntityAsync(aisle); + StorageAllocationService.AllocateStorage([aisle], product.Id); + await SaveAsync(); + // var request = new GetItemLocationQuery.Request(product.Id.Value); + + // Act + var response = await client.GetFromJsonAsync($"/api/aisles/products/{product.Id.Value}"); + // var response = await client.GetAsync($"/api/aisles/products/{product.Id.Value}"); + // var content = response.Content.ReadAsStringAsync(); + + // Assert + response.Should().NotBeNull(); + response!.AisleName.Should().Be(aisle.Name); + response.BayName.Should().Be("Bay 1"); + response.ShelfName.Should().Be("Shelf 1"); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs index e2c7616..39f30ec 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs @@ -3,6 +3,7 @@ using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Common.Persistence; @@ -21,9 +22,9 @@ public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapGet("/api/aisles/products/{productID:guid}", async (Request request, ISender sender) => + app.MapGet("/api/aisles/products/{productId:guid}", async (Guid productId, ISender sender) => { - var response = await sender.Send(request); + var response = await sender.Send(new Request(productId)); return response.IsError ? response.Problem() : TypedResults.Ok(response.Value); }) .WithName("FindProductLocation") diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 12825e4..9e5aa55 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -24,5 +24,6 @@ public static void UseWarehouse(this WebApplication app) CreateAisleCommand.Endpoint.MapEndpoint(app); CreateProductCommand.Endpoint.MapEndpoint(app); AllocateStorageCommand.Endpoint.MapEndpoint(app); + GetItemLocationQuery.Endpoint.MapEndpoint(app); } } From 03065276ace22924874394ff209a89a3e8158d79 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:59:36 +1000 Subject: [PATCH 40/87] =?UTF-8?q?=E2=9C=A8=20Introduce=20event=20dispatchi?= =?UTF-8?q?ng=20and=20IDomainEvent=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the `DomainEvent` class with an `IDomainEvent` interface, and added a new interceptor for dispatching domain events post-save. Updated the `AggregateRoot` class and related domain events to incorporate these changes. Added middleware for event handling and eventual consistency support. #35 --- .../Domain/Base/AggregateRoot.cs | 20 ++--- .../Domain/Base/DomainEvent.cs | 5 -- .../Domain/Interfaces/IAggregateRoot.cs | 16 ++++ .../Domain/Interfaces/IDomainEvent.cs | 7 ++ .../Domain/Interfaces/IDomainEvents.cs | 14 --- .../Customers/CustomerCreatedEvent.cs | 2 +- .../Orders/LineItem/LineItemCreatedEvent.cs | 2 +- .../Orders/Order/OrderCreatedEvent.cs | 2 +- .../Order/OrderReadyForShippingEvent.cs | 2 +- .../Categories/Domain/CategoryCreatedEvent.cs | 2 +- .../Middleware/EventualConsistencyError.cs | 13 +++ .../EventualConsistencyException.cs | 15 ++++ .../EventualConsistencyMiddleware.cs | 47 ++++++++++ .../Persistence/DepdendencyInjection.cs | 19 ++-- .../DispatchDomainEventsInterceptor.cs | 89 +++++++++++++++++++ .../Modules.Warehouse.csproj | 4 + .../Products/Domain/LowStockEvent.cs | 2 +- .../Products/Domain/ProductCreatedEvent.cs | 2 +- .../Modules.Warehouse/Storage/Domain/Aisle.cs | 20 +++++ .../Storage/Domain/ProductStoredEvent.cs | 6 ++ .../Modules.Warehouse/Storage/Domain/Shelf.cs | 1 + .../Domain/StorageAllocationService.cs | 15 +--- .../UseCases/AllocateStorageCommand.cs | 2 +- .../Modules.Warehouse/WarehouseModule.cs | 4 + 24 files changed, 258 insertions(+), 53 deletions(-) delete mode 100644 src/Common/Common.SharedKernel/Domain/Base/DomainEvent.cs create mode 100644 src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs create mode 100644 src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs delete mode 100644 src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvents.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyError.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyException.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs diff --git a/src/Common/Common.SharedKernel/Domain/Base/AggregateRoot.cs b/src/Common/Common.SharedKernel/Domain/Base/AggregateRoot.cs index bc43c18..8bbd20b 100644 --- a/src/Common/Common.SharedKernel/Domain/Base/AggregateRoot.cs +++ b/src/Common/Common.SharedKernel/Domain/Base/AggregateRoot.cs @@ -1,18 +1,18 @@ using Common.SharedKernel.Domain.Interfaces; -using System.ComponentModel.DataAnnotations.Schema; namespace Common.SharedKernel.Domain.Base; -public abstract class AggregateRoot : Entity, IDomainEvents +public abstract class AggregateRoot : Entity, IAggregateRoot { - private readonly List _domainEvents = new(); + private readonly List _domainEvents = []; - [NotMapped] - public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); + public void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent); - public void AddDomainEvent(DomainEvent domainEvent) => _domainEvents.Add(domainEvent); + public IReadOnlyList PopDomainEvents() + { + var copy = _domainEvents.ToList().AsReadOnly(); + _domainEvents.Clear(); - public void RemoveDomainEvent(DomainEvent domainEvent) => _domainEvents.Remove(domainEvent); - - public void ClearDomainEvents() => _domainEvents.Clear(); -} \ No newline at end of file + return copy; + } +} diff --git a/src/Common/Common.SharedKernel/Domain/Base/DomainEvent.cs b/src/Common/Common.SharedKernel/Domain/Base/DomainEvent.cs deleted file mode 100644 index 7f2bf3e..0000000 --- a/src/Common/Common.SharedKernel/Domain/Base/DomainEvent.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Common.SharedKernel.Domain.Base; - -//public record DomainEvent : INotification { } - -public record DomainEvent; \ No newline at end of file diff --git a/src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs new file mode 100644 index 0000000..da4eba6 --- /dev/null +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs @@ -0,0 +1,16 @@ +using Common.SharedKernel.Domain.Base; + +namespace Common.SharedKernel.Domain.Interfaces; + +public interface IAggregateRoot +{ + // IReadOnlyList DomainEvents { get; } + + void AddDomainEvent(IDomainEvent domainEvent); + + // void RemoveDomainEvent(IDomainEvent domainEvent); + + // void ClearDomainEvents(); + + IReadOnlyList PopDomainEvents(); +} diff --git a/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs new file mode 100644 index 0000000..220c80a --- /dev/null +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Common.SharedKernel.Domain.Base; + +public interface IDomainEvent : INotification { } + +// public record DomainEvent; diff --git a/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvents.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvents.cs deleted file mode 100644 index 974c2c3..0000000 --- a/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvents.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Common.SharedKernel.Domain.Base; - -namespace Common.SharedKernel.Domain.Interfaces; - -public interface IDomainEvents -{ - IReadOnlyList DomainEvents { get; } - - void AddDomainEvent(DomainEvent domainEvent); - - void RemoveDomainEvent(DomainEvent domainEvent); - - void ClearDomainEvents(); -} diff --git a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs b/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs index 84815a5..a711100 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs @@ -2,7 +2,7 @@ namespace Modules.Customers.Customers; -internal record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : DomainEvent +internal record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : IDomainEvent { public static CustomerCreatedEvent Create(Customer customer) => new CustomerCreatedEvent(customer.Id, customer.FirstName, customer.LastName); diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs index 26b0a61..b82e8aa 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs @@ -3,7 +3,7 @@ namespace Modules.Orders.Orders.LineItem; -internal record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : DomainEvent +internal record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : IDomainEvent { public LineItemCreatedEvent(LineItem lineItem) : this(lineItem.Id, lineItem.OrderId) { } diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs index afb3490..feb8297 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs @@ -2,7 +2,7 @@ namespace Modules.Orders.Orders.Order; -internal record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : DomainEvent +internal record OrderCreatedEvent(OrderId OrderId, CustomerId CustomerId) : IDomainEvent { public static OrderCreatedEvent Create(Order order) => new(order.Id, order.CustomerId); } diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs index 007dac6..6c78824 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs @@ -2,7 +2,7 @@ namespace Modules.Orders.Orders.Order; -internal record OrderReadyForShippingEvent(OrderId OrderId) : DomainEvent +internal record OrderReadyForShippingEvent(OrderId OrderId) : IDomainEvent { public static OrderReadyForShippingEvent Create(Order order) => new(order.Id); } diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs index 5eac8c0..68fa16c 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs @@ -2,4 +2,4 @@ namespace Modules.Catalog.Categories.Domain; -internal record CategoryCreatedEvent(CategoryId Id, string Name) : DomainEvent; +internal record CategoryCreatedEvent(CategoryId Id, string Name) : IDomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyError.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyError.cs new file mode 100644 index 0000000..bb09881 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyError.cs @@ -0,0 +1,13 @@ +using ErrorOr; + +namespace Modules.Warehouse.Common.Middleware; + +public static class EventualConsistencyError +{ + public const int EventualConsistencyType = 100; + + public static Error From(string code, string description) + { + return Error.Custom(EventualConsistencyType, code, description); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyException.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyException.cs new file mode 100644 index 0000000..12a23f0 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyException.cs @@ -0,0 +1,15 @@ +using ErrorOr; + +namespace Modules.Warehouse.Common.Middleware; + +public class EventualConsistencyException : Exception +{ + public Error EventualConsistencyError { get; } + public List UnderlyingErrors { get; } + + public EventualConsistencyException(Error eventualConsistencyError, List? underlyingErrors = null) : base(message: eventualConsistencyError.Description) + { + EventualConsistencyError = eventualConsistencyError; + UnderlyingErrors = underlyingErrors ?? new(); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs new file mode 100644 index 0000000..608c2a0 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs @@ -0,0 +1,47 @@ +using Common.SharedKernel.Domain.Base; +using Microsoft.AspNetCore.Http; +using Modules.Warehouse.Common.Persistence; + +namespace Modules.Warehouse.Common.Middleware; + +internal class EventualConsistencyMiddleware +{ + public const string DomainEventsKey = "DomainEventsKey"; + + private readonly RequestDelegate _next; + + public EventualConsistencyMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, IPublisher publisher, WarehouseDbContext dbContext) + { + var transaction = await dbContext.Database.BeginTransactionAsync(); + context.Response.OnCompleted(async () => + { + try + { + if (context.Items.TryGetValue(DomainEventsKey, out var value) && value is Queue domainEvents) + { + while (domainEvents.TryDequeue(out var nextEvent)) + { + await publisher.Publish(nextEvent); + } + } + + await transaction.CommitAsync(); + } + catch (EventualConsistencyException) + { + // handle eventual consistency exception + } + finally + { + await transaction.DisposeAsync(); + } + }); + + await _next(context); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index 5927465..7d15c75 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -1,6 +1,8 @@ +using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Middleware; using Modules.Warehouse.Common.Persistence.Interceptors; namespace Modules.Warehouse.Common.Persistence; @@ -18,15 +20,22 @@ internal static void AddPersistence(this IServiceCollection services, IConfigura builder.EnableRetryOnFailure(); }); - options.AddInterceptors(services.BuildServiceProvider().GetRequiredService()); + var serviceProvider = services.BuildServiceProvider(); + + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); }); - //services.AddSingleton(); - // TODO: Consider moving to up.ps1 - // services.AddScoped(); services.AddScoped(); - // services.AddScoped(); + services.AddScoped(); // services.AddScoped(); } + + public static IApplicationBuilder UseInfrastructureMiddleware(this IApplicationBuilder app) + { + app.UseMiddleware(); + return app; + } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs new file mode 100644 index 0000000..b28f024 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs @@ -0,0 +1,89 @@ +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Modules.Warehouse.Common.Middleware; + +namespace Modules.Warehouse.Common.Persistence.Interceptors; + +public class DispatchDomainEventsInterceptor : SaveChangesInterceptor +{ + private readonly IPublisher _publisher; + private readonly IHttpContextAccessor _httpContextAccessor; + + public DispatchDomainEventsInterceptor(IPublisher publisher, IHttpContextAccessor httpContextAccessor) + { + _publisher = publisher; + _httpContextAccessor = httpContextAccessor; + } + + // NOTE: There are two options for dispatching domain events: + // Option 1. Before changes are saved to the database (SavingChanges) + // Option 2. After changes are saved to the database (SavedChanges) + // + // We are using Option 2, for several reasons: + // - Event handlers can query the DB and expect the data to be up-to-date + // - Event handlers that fire integration events that affect other systems, will only do + // so if the changes are successfully saved to the database + // + // The downside of this is that we may have multiple calls to save changes in a single DB request. + // This means we no longer have a single write to the DB, so may need to wrap the entire operation + // in a transaction to ensure consistency. + public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) + { + DispatchDomainEvents(eventData.Context).ConfigureAwait(false).GetAwaiter().GetResult(); + return base.SavedChanges(eventData, result); + } + + public override async ValueTask SavedChangesAsync( + SaveChangesCompletedEventData eventData, + int result, + CancellationToken cancellationToken = new()) + { + await DispatchDomainEvents(eventData.Context); + return await base.SavedChangesAsync(eventData, result, cancellationToken); + } + + private async Task DispatchDomainEvents(DbContext? context) + { + if (context is null) + return; + + var domainEvents = context.ChangeTracker.Entries() + .Select(entry => entry.Entity.PopDomainEvents()) + .SelectMany(x => x); + + if (IsUserWaitingOnline()) + { + AddDomainEventsToOfflineProcessingQueue(domainEvents); + } + else + { + await PublishDomainEvents(domainEvents); + } + } + + private bool IsUserWaitingOnline() => _httpContextAccessor.HttpContext is not null; + + private async Task PublishDomainEvents(IEnumerable domainEvents) + { + foreach (var domainEvent in domainEvents) + { + await _publisher.Publish(domainEvent); + } + } + + private void AddDomainEventsToOfflineProcessingQueue(IEnumerable domainEvents) + { + var domainEventsQueue = _httpContextAccessor.HttpContext!.Items.TryGetValue(EventualConsistencyMiddleware.DomainEventsKey, out var value) && + value is Queue existingDomainEvents + ? existingDomainEvents + : new Queue(); + + foreach (var domainEvent in domainEvents) + domainEventsQueue.Enqueue(domainEvent); + + _httpContextAccessor.HttpContext.Items[EventualConsistencyMiddleware.DomainEventsKey] = domainEventsQueue; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 437371a..82864c4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -22,4 +22,8 @@ + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs index 34d9921..fc34e16 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs @@ -2,4 +2,4 @@ namespace Modules.Warehouse.Products.Domain; -internal record LowStockEvent(ProductId ProductId) : DomainEvent; +internal record LowStockEvent(ProductId ProductId) : IDomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs index 3205759..89ca6a2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs @@ -2,7 +2,7 @@ namespace Modules.Warehouse.Products.Domain; -internal record ProductCreatedEvent(ProductId Product, string ProductName) : DomainEvent +internal record ProductCreatedEvent(ProductId Product, string ProductName) : IDomainEvent { public static ProductCreatedEvent Create(Product product) => new(product.Id, product.Name); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs index 443274e..d2b8873 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs @@ -1,4 +1,6 @@ using Common.SharedKernel.Domain.Base; +using ErrorOr; +using Modules.Warehouse.Products.Domain; namespace Modules.Warehouse.Storage.Domain; @@ -36,4 +38,22 @@ public static Aisle Create(string name, int numBays, int numShelves) return aisle; } + + public ErrorOr AssignProduct(ProductId productId) + { + ArgumentNullException.ThrowIfNull(productId); + + var shelf = _bays + .SelectMany(b => b.Shelves) + .FirstOrDefault(s => s.IsEmpty); + + if (AvailableStorage == 0 || shelf is null) + return StorageAllocationErrors.NoAvailableStorage; + + shelf.AssignProduct(productId); + + AddDomainEvent(new ProductStoredEvent(productId)); + + return shelf; + } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs new file mode 100644 index 0000000..7256627 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs @@ -0,0 +1,6 @@ +using Common.SharedKernel.Domain.Base; +using Modules.Warehouse.Products.Domain; + +namespace Modules.Warehouse.Storage.Domain; + +internal record ProductStoredEvent(ProductId ProductId) : IDomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs index fcf31cc..7e79d38 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs @@ -26,6 +26,7 @@ public static Shelf Create(string name) public void AssignProduct(ProductId productId) { + ArgumentNullException.ThrowIfNull(productId); ProductId = productId; } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs index d2b4517..9f2970a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/StorageAllocationService.cs @@ -8,21 +8,14 @@ namespace Modules.Warehouse.Storage.Domain; /// internal class StorageAllocationService { - internal static ErrorOr AllocateStorage(IEnumerable aisles, ProductId productId) + internal static ErrorOr AllocateStorage(IEnumerable aisles, ProductId productId) { foreach (var aisle in aisles) { - foreach (var bay in aisle.Bays) - { - foreach (var shelf in bay.Shelves) - { - if (!shelf.IsEmpty) - continue; + if (aisle.AvailableStorage == 0) + continue; - shelf.AssignProduct(productId); - return Result.Success; - } - } + return aisle.AssignProduct(productId); } return StorageAllocationErrors.NoAvailableStorage; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs index ced8fe3..2d32130 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs @@ -61,7 +61,7 @@ public async Task> Handle(Request request, CancellationToken ca var result = StorageAllocationService.AllocateStorage(aisles, product.Id); if (result.IsError) - return result; + return result.FirstError; await _dbContext.SaveChangesAsync(cancellationToken); return Result.Success; diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 9e5aa55..dfc6411 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -13,6 +13,8 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration { var applicationAssembly = typeof(WarehouseModule).Assembly; + services.AddHttpContextAccessor(); + services.AddValidatorsFromAssembly(applicationAssembly); services.AddPersistence(configuration); @@ -20,6 +22,8 @@ public static void AddWarehouse(this IServiceCollection services, IConfiguration public static void UseWarehouse(this WebApplication app) { + app.UseInfrastructureMiddleware(); + // TODO: Consider source generation or reflection for endpoint mapping CreateAisleCommand.Endpoint.MapEndpoint(app); CreateProductCommand.Endpoint.MapEndpoint(app); From 0e1c72951ed846b6d8e94bf70ba01d9744dd96ab Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 28 Aug 2024 06:03:36 +1000 Subject: [PATCH 41/87] =?UTF-8?q?=E2=9C=A8=20Implement=20product=20storage?= =?UTF-8?q?=20integration=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added `ProductStoredIntegrationEvent` to handle product storage notifications between Warehouse and Products modules. Created corresponding event handlers in both modules to publish and process these events, ensuring proper logging and setup in the solution. #35 --- ModularMonolith.sln | 7 ++++ .../Modules.Catalog/Modules.Catalog.csproj | 1 + .../ProductStoredIntegrationEventHandler.cs | 33 +++++++++++++++++++ .../Modules.Catalog/Products/Product.cs | 4 +-- .../Modules.Warehouse.Messages.csproj | 5 +++ .../ProductStoredIntegrationEvent.cs | 5 +++ .../Modules.Warehouse.csproj | 5 +-- .../Events/ProductStoredEventHandler.cs | 28 ++++++++++++++++ 8 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Messages/Modules.Warehouse.Messages.csproj create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Messages/ProductStoredIntegrationEvent.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Storage/Events/ProductStoredEventHandler.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 6a9f6e9..d16c0bb 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -52,6 +52,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".infra", ".infra", "{758DF8 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "tools\Database\Database.csproj", "{9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Messages", "src\Modules\Warehouse\Modules.Warehouse.Messages\Modules.Warehouse.Messages.csproj", "{A105835C-C285-4AA5-AADF-CCA4BCC933B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -105,6 +107,10 @@ Global {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Release|Any CPU.Build.0 = Release|Any CPU + {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -124,5 +130,6 @@ Global {11925734-961D-4761-B209-BF601E59EB95} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB} = {7915FF68-4EC9-497B-BD33-1A3DA7D6B457} + {A105835C-C285-4AA5-AADF-CCA4BCC933B1} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} EndGlobalSection EndGlobal diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 1187a11..414c654 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs b/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs new file mode 100644 index 0000000..65ff456 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs @@ -0,0 +1,33 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using Modules.Warehouse.Messages; + +namespace Modules.Catalog.Products.IntegrationEvents; + +internal class ProductStoredIntegrationEventHandler : INotificationHandler +{ + private readonly ILogger _logger; + + public ProductStoredIntegrationEventHandler(ILogger logger) + { + _logger = logger; + } + + public async Task Handle(ProductStoredIntegrationEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation("Product stored integration event received"); + + var productId = new ProductId(notification.ProductId); + var name = notification.ProductName; + var sku = notification.productSku; + + var product = Product.Create(name, sku, productId); + + // TODO: Save product to DB + + _logger.LogInformation("Product stored integration event processed"); + + await Task.CompletedTask; + + } +} diff --git a/src/Modules/Products/Modules.Catalog/Products/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Product.cs index 5454792..5c79c69 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Product.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Product.cs @@ -22,7 +22,7 @@ private Product() { } - public static Product Create(string name, string sku) + public static Product Create(string name, string sku, ProductId? id = null) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(sku); @@ -31,7 +31,7 @@ public static Product Create(string name, string sku) { Name = name, Sku = sku, - Id = new ProductId(Guid.NewGuid()) + Id = id ?? new ProductId(Guid.NewGuid()) }; return product; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Messages/Modules.Warehouse.Messages.csproj b/src/Modules/Warehouse/Modules.Warehouse.Messages/Modules.Warehouse.Messages.csproj new file mode 100644 index 0000000..07e7417 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Messages/Modules.Warehouse.Messages.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Messages/ProductStoredIntegrationEvent.cs b/src/Modules/Warehouse/Modules.Warehouse.Messages/ProductStoredIntegrationEvent.cs new file mode 100644 index 0000000..ed4250f --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Messages/ProductStoredIntegrationEvent.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace Modules.Warehouse.Messages; + +public record ProductStoredIntegrationEvent(Guid ProductId, string ProductName, string productSku) : INotification; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 82864c4..e8d549a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -20,10 +20,7 @@ - - - - + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Events/ProductStoredEventHandler.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Events/ProductStoredEventHandler.cs new file mode 100644 index 0000000..dd1523e --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Events/ProductStoredEventHandler.cs @@ -0,0 +1,28 @@ +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Messages; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace Modules.Warehouse.Storage.Events; + +internal class ProductStoredEventHandler : INotificationHandler +{ + private readonly IPublisher _publisher; + private readonly WarehouseDbContext _dbContext; + + public ProductStoredEventHandler(IPublisher publisher, WarehouseDbContext dbContext) + { + _publisher = publisher; + _dbContext = dbContext; + } + + public async Task Handle(ProductStoredEvent notification, CancellationToken cancellationToken) + { + var product = _dbContext.Products + .WithSpecification(new ProductByIdSpec(notification.ProductId)) + .First(); + + var integrationEvent = new ProductStoredIntegrationEvent(product.Id.Value, product.Name, product.Sku.Value); + await _publisher.Publish(integrationEvent, cancellationToken); + } +} From bf7c161e5cfadf3c8bf127d4fbe593ba945376ac Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 28 Aug 2024 07:22:30 +1000 Subject: [PATCH 42/87] =?UTF-8?q?=E2=9C=A8=20Add=20Catalog=20module=20and?= =?UTF-8?q?=20update=20domain=20events=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced the new Catalog module and integrated it into the WebApi project. Removed the deprecated MockCurrentUserServiceProvider. Made enhancements to event handling and domain interfaces. #35 --- .../Domain/Interfaces/IDomainEvent.cs | 6 ++-- .../Customers/CustomerCreatedEvent.cs | 2 +- .../Orders/LineItem/LineItemCreatedEvent.cs | 2 +- .../Orders/Order/OrderCreatedEvent.cs | 2 +- .../Order/OrderReadyForShippingEvent.cs | 2 +- .../Products/Modules.Catalog/CatalogModule.cs | 31 +++++++++++++++++++ .../Categories/Domain/CategoryCreatedEvent.cs | 2 +- .../Common/DatabaseContainer.cs | 9 ++++++ .../Common/Extensions/ServiceCollectionExt.cs | 6 ++-- .../EventualConsistencyMiddleware.cs | 2 +- .../Persistence/DepdendencyInjection.cs | 2 +- .../DispatchDomainEventsInterceptor.cs | 6 ++-- .../Products/Domain/LowStockEvent.cs | 2 +- .../Products/Domain/ProductCreatedEvent.cs | 2 +- .../Storage/Domain/ProductStoredEvent.cs | 2 +- src/WebApi/Extensions/MediatRExtensions.cs | 4 ++- src/WebApi/Program.cs | 3 ++ src/WebApi/WebApi.csproj | 1 + .../MockCurrentUserServiceProvider.cs | 8 ----- .../Database/WarehouseDbContextInitializer.cs | 6 ++-- 20 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 src/Modules/Products/Modules.Catalog/CatalogModule.cs delete mode 100644 tools/Database/MockCurrentUserServiceProvider.cs diff --git a/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs index 220c80a..2986450 100644 --- a/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvent.cs @@ -1,7 +1,5 @@ using MediatR; -namespace Common.SharedKernel.Domain.Base; +namespace Common.SharedKernel.Domain.Interfaces; -public interface IDomainEvent : INotification { } - -// public record DomainEvent; +public interface IDomainEvent : INotification; diff --git a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs b/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs index a711100..eb49653 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; namespace Modules.Customers.Customers; diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs index b82e8aa..33bdb6f 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItemCreatedEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; using Modules.Orders.Orders.Order; namespace Modules.Orders.Orders.LineItem; diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs index feb8297..5aee9d3 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderCreatedEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; namespace Modules.Orders.Orders.Order; diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs index 6c78824..1c18c58 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/OrderReadyForShippingEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; namespace Modules.Orders.Orders.Order; diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs new file mode 100644 index 0000000..6491e60 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -0,0 +1,31 @@ +using FluentValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Modules.Catalog; + +public static class CatalogModule +{ + public static void AddCatalog(this IServiceCollection services, IConfiguration configuration) + { + var applicationAssembly = typeof(CatalogModule).Assembly; + + services.AddHttpContextAccessor(); + + services.AddValidatorsFromAssembly(applicationAssembly); + + // services.AddPersistence(configuration); + } + + public static void UseCatalog(this WebApplication app) + { + // app.UseInfrastructureMiddleware(); + + // // TODO: Consider source generation or reflection for endpoint mapping + // CreateAisleCommand.Endpoint.MapEndpoint(app); + // CreateProductCommand.Endpoint.MapEndpoint(app); + // AllocateStorageCommand.Endpoint.MapEndpoint(app); + // GetItemLocationQuery.Endpoint.MapEndpoint(app); + } +} diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs index 68fa16c..b0d385f 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryCreatedEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; namespace Modules.Catalog.Categories.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs index aca4e7f..dcb4b42 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs @@ -1,3 +1,4 @@ +using DotNet.Testcontainers.Builders; using Testcontainers.SqlEdge; namespace Modules.Warehouse.Tests.Common; @@ -7,10 +8,18 @@ namespace Modules.Warehouse.Tests.Common; /// public class DatabaseContainer { + // private static readonly int _exposedPort = Random.Shared.Next(10000, 60000); + private static readonly int _exposedPort = 20_000; + private static readonly int _internalPort = 1433; + + private readonly SqlEdgeContainer _container = new SqlEdgeBuilder() .WithName("Modular-Monolith-IntegrationTests-DbContainer") .WithPassword("Password123") .WithAutoRemove(true) + .WithPortBinding(_internalPort) + // .WithExposedPort(_exposedPort) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(_internalPort)) .Build(); public string? ConnectionString { get; private set; } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs index 1113b80..baedfd7 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs @@ -26,9 +26,11 @@ internal static IServiceCollection ReplaceDbContext( options.LogTo(m => Debug.WriteLine(m)); options.EnableSensitiveDataLogging(); + var serviceProvider = services.BuildServiceProvider(); + options.AddInterceptors( - services.BuildServiceProvider().GetRequiredService() - // services.BuildServiceProvider().GetRequiredService() + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() ); }); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs index 608c2a0..3f5eab4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; using Microsoft.AspNetCore.Http; using Modules.Warehouse.Common.Persistence; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index 7d15c75..abb129f 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -17,7 +17,7 @@ internal static void AddPersistence(this IServiceCollection services, IConfigura options.UseSqlServer(connectionString, builder => { builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); - builder.EnableRetryOnFailure(); + // builder.EnableRetryOnFailure(); }); var serviceProvider = services.BuildServiceProvider(); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs index b28f024..d7ca4db 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs @@ -1,5 +1,4 @@ -using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Interfaces; +using Common.SharedKernel.Domain.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -52,7 +51,8 @@ private async Task DispatchDomainEvents(DbContext? context) var domainEvents = context.ChangeTracker.Entries() .Select(entry => entry.Entity.PopDomainEvents()) - .SelectMany(x => x); + .SelectMany(x => x) + .ToList(); if (IsUserWaitingOnline()) { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs index fc34e16..e31c100 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/LowStockEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; namespace Modules.Warehouse.Products.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs index 89ca6a2..972115d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/ProductCreatedEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; namespace Modules.Warehouse.Products.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs index 7256627..46c361a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/ProductStoredEvent.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; using Modules.Warehouse.Products.Domain; namespace Modules.Warehouse.Storage.Domain; diff --git a/src/WebApi/Extensions/MediatRExtensions.cs b/src/WebApi/Extensions/MediatRExtensions.cs index dc0e975..ba78fa2 100644 --- a/src/WebApi/Extensions/MediatRExtensions.cs +++ b/src/WebApi/Extensions/MediatRExtensions.cs @@ -1,4 +1,5 @@ using Common.SharedKernel.Behaviours; +using Modules.Catalog; using Modules.Warehouse; using System.Reflection; @@ -8,7 +9,8 @@ public static class MediatRExtensions { private static readonly Assembly[] _assemblies = [ - typeof(WarehouseModule).Assembly + typeof(WarehouseModule).Assembly, + typeof(CatalogModule).Assembly, ]; public static void AddMediatR(this IServiceCollection services) diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index e1e93a7..aa80165 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,4 +1,5 @@ using Common.SharedKernel; +using Modules.Catalog; using Modules.Orders; using Modules.Warehouse; using WebApi.Extensions; @@ -13,6 +14,7 @@ builder.Services.AddOrders(); builder.Services.AddWarehouse(builder.Configuration); + builder.Services.AddCatalog(builder.Configuration); } var app = builder.Build(); @@ -28,6 +30,7 @@ app.UseOrders(); app.UseWarehouse(); + app.UseCatalog(); app.Run(); } diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index b4830f2..b676224 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -11,6 +11,7 @@ + diff --git a/tools/Database/MockCurrentUserServiceProvider.cs b/tools/Database/MockCurrentUserServiceProvider.cs deleted file mode 100644 index aebb3b6..0000000 --- a/tools/Database/MockCurrentUserServiceProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -// using SSW.CleanArchitecture.Application.Common.Interfaces; -// -// namespace SSW.CleanArchitecture.Database; -// -// public class MockCurrentUserService : ICurrentUserService -// { -// public string UserId => "00000000-0000-0000-0000-000000000000"; -// } diff --git a/tools/Database/WarehouseDbContextInitializer.cs b/tools/Database/WarehouseDbContextInitializer.cs index c906f66..aeaabd3 100644 --- a/tools/Database/WarehouseDbContextInitializer.cs +++ b/tools/Database/WarehouseDbContextInitializer.cs @@ -17,7 +17,8 @@ internal class WarehouseDbContextInitializer private const int NumShelves = 5; private const int NumBays = 20; - internal WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) + // public constructor needed for DI + public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) { _logger = logger; _dbContext = dbContext; @@ -66,8 +67,7 @@ private async Task SeedProductsAsync() if (await _dbContext.Products.AnyAsync()) return; - // var moneyFaker = new Faker() - // .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); + // TODO: Consider how to handle integration events that get raised and handled var skuFaker = new Faker() .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); From 77598b2a384094261a1f7bc3a0c43099e63691fb Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 28 Aug 2024 07:28:07 +1000 Subject: [PATCH 43/87] =?UTF-8?q?=E2=9C=A8=20Add=20new=20unit=20tests=20fo?= =?UTF-8?q?r=20Aisle=20class=20in=20warehouse=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce tests to verify the creation of bays and shelves, product assignment, and error handling when storage is full. This enhances the robustness of the storage allocation logic in the Aisle class. #35 --- .../Storage/Domain/AisleTests.cs | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs index f9ad7d7..af6c066 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs @@ -21,7 +21,6 @@ public void LookingUpProduct_ReturnsAisleBayAndShelf() var productA = new ProductId(Guid.NewGuid()); var productB = new ProductId(Guid.NewGuid()); var aisle = Aisle.Create("Aisle 1", 2, 3); - var service = new StorageAllocationService(); StorageAllocationService.AllocateStorage(new List { aisle }, productA); StorageAllocationService.AllocateStorage(new List { aisle }, productA); @@ -63,4 +62,65 @@ public void Create_WithBaysAndShelves_CreatesCorrectNumberOfShelves() // Assert sut.TotalStorage.Should().Be(numBays * numShelves); } + + [Fact] + public void Create_WithBaysAndShelves_CreatesCorrectNumberOfBays() + { + // Arrange + var numBays = 2; + var numShelves = 3; + + // Act + var sut = Aisle.Create("Aisle 1", numBays, numShelves); + + // Assert + sut.Bays.Should().HaveCount(numBays); + } + + [Fact] + public void Create_WithBaysAndShelves_CreatesCorrectNumberOfShelvesPerBay() + { + // Arrange + var numBays = 2; + var numShelves = 3; + + // Act + var sut = Aisle.Create("Aisle 1", numBays, numShelves); + + // Assert + sut.Bays.Should().OnlyContain(bay => bay.Shelves.Count == numShelves); + } + + [Fact] + public void AssignProduct_WithAvailableStorage_AssignsProductToShelf() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var sut = Aisle.Create("Aisle 1", 1, 1); + + // Act + var result = sut.AssignProduct(productId); + + // Assert + result.IsError.Should().BeFalse(); + result.Value.ProductId.Should().Be(productId); + var events = sut.PopDomainEvents(); + events.Should().ContainSingle(); + events[0].Should().BeOfType(); + } + + [Fact] + public void AssignProduct_WithNoAvailableStorage_ReturnsError() + { + // Arrange + var productId = new ProductId(Guid.NewGuid()); + var sut = Aisle.Create("Aisle 1", 1, 1); + sut.AssignProduct(productId); + + // Act + var result = sut.AssignProduct(productId); + + // Assert + result.IsError.Should().BeTrue(); + } } From 91519de54545873e0aa9b653632fd163e47129d8 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 29 Aug 2024 07:27:16 +1000 Subject: [PATCH 44/87] =?UTF-8?q?=E2=9C=A8=20Restructure=20Catalog=20and?= =?UTF-8?q?=20Warehouse,=20enhance=20persistence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed redundant Category and Service files and renamed key configuration files for consistency. Introduced `DependencyInjection` in the `Catalog` module and updated database initialization to propagate products between `Warehouse` and `Catalog`. Simplified strongly-typed ID handling and made middleware generic. --- ADRs.md | 20 ++++++ .../Extensions/PropertyBuilderExtensions.cs | 4 +- .../Extensions/StronglyTypedIdConverter.cs | 4 +- .../DispatchDomainEventsInterceptor.cs | 9 +-- .../EntitySaveChangesInterceptor.cs | 0 .../CategoryTests.cs} | 4 +- .../{ => Products}/ProductTests.cs | 2 +- .../Products/Modules.Catalog/AssemblyInfo.cs | 1 + .../Products/Modules.Catalog/CatalogModule.cs | 3 +- .../Categories/CategoryService.cs | 19 ------ .../Categories/Domain/Category.cs | 3 +- .../Categories/Domain/CategoryId.cs | 3 - .../Categories/Domain/ICategoryService.cs | 6 -- .../Persistence/CategoryConfiguration.cs | 19 ------ .../Common/Persistence/CatalogDbContext.cs | 21 +++++++ .../Configuration/CategoryConfiguration.cs | 21 +++++++ .../Configuration/ProductConfiguration.cs | 31 +++++++++ .../Persistence/DepdendencyInjection.cs | 41 ++++++++++++ .../Modules.Catalog/Modules.Catalog.csproj | 20 ++++-- .../Common/Extensions/ServiceCollectionExt.cs | 3 +- .../EventualConsistencyMiddleware.cs | 2 + .../Configuration/AisleConfiguraiton.cs | 2 +- .../Configuration/BayConfiguration.cs | 4 +- .../Configuration/ProductConfiguration.cs | 2 +- .../Configuration/ShelfConfiguration.cs | 2 +- .../Persistence/DepdendencyInjection.cs | 1 + .../Common/Persistence/WarehouseDbContext.cs | 2 +- .../Modules.Warehouse.csproj | 5 +- src/WebApi/appsettings.Development.json | 3 +- tools/Database/Database.csproj | 1 + .../CatalogDbContextInitialiser.cs | 63 +++++++++++++++++++ .../WarehouseDbContextInitialiser.cs} | 18 +++--- tools/Database/Program.cs | 25 ++++++-- tools/Database/appsettings.json | 3 +- 34 files changed, 280 insertions(+), 87 deletions(-) create mode 100644 ADRs.md rename src/{Modules/Warehouse/Modules.Warehouse/Common => Common/Common.SharedKernel}/Persistence/Extensions/PropertyBuilderExtensions.cs (77%) rename src/{Modules/Warehouse/Modules.Warehouse/Common => Common/Common.SharedKernel}/Persistence/Extensions/StronglyTypedIdConverter.cs (72%) rename src/{Modules/Warehouse/Modules.Warehouse/Common => Common/Common.SharedKernel}/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs (91%) rename src/{Modules/Warehouse/Modules.Warehouse/Common => Common/Common.SharedKernel}/Persistence/Interceptors/EntitySaveChangesInterceptor.cs (100%) rename src/Modules/Products/Modules.Catalog.Tests/{CatalogTests.cs => Categories/CategoryTests.cs} (93%) rename src/Modules/Products/Modules.Catalog.Tests/{ => Products}/ProductTests.cs (99%) delete mode 100644 src/Modules/Products/Modules.Catalog/Categories/CategoryService.cs delete mode 100644 src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs delete mode 100644 src/Modules/Products/Modules.Catalog/Categories/Domain/ICategoryService.cs delete mode 100644 src/Modules/Products/Modules.Catalog/Categories/Persistence/CategoryConfiguration.cs create mode 100644 src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs create mode 100644 src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/CategoryConfiguration.cs create mode 100644 src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs create mode 100644 src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs create mode 100644 tools/Database/Initialisers/CatalogDbContextInitialiser.cs rename tools/Database/{WarehouseDbContextInitializer.cs => Initialisers/WarehouseDbContextInitialiser.cs} (80%) diff --git a/ADRs.md b/ADRs.md new file mode 100644 index 0000000..e44631d --- /dev/null +++ b/ADRs.md @@ -0,0 +1,20 @@ +# Architectural Decision Records + +## ADR1 - Ensure Related Data is Created When Seeding the DB + +### Context + +In normal execution of the application DB updates will cause domain events to fire. These domain events can have side affects that cause other DB updates. + +There are several pieces of complicated infrastructure that make this work. + +We need to ensure that related data is updated when seeding the DB via the following: + +- `WarehouseDbContextInitializer` +- `CatalogDbContextInitializer` + +### Decision + +Instead of emulating the domain event propagating infrastructure, we will instead pass the data between initializers. This means a bit more manual work, but is a much simpler solution. + +If this turns out to be too complex in future we will need to revist this. diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/PropertyBuilderExtensions.cs b/src/Common/Common.SharedKernel/Persistence/Extensions/PropertyBuilderExtensions.cs similarity index 77% rename from src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/PropertyBuilderExtensions.cs rename to src/Common/Common.SharedKernel/Persistence/Extensions/PropertyBuilderExtensions.cs index 65a96c8..4eb1895 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/PropertyBuilderExtensions.cs +++ b/src/Common/Common.SharedKernel/Persistence/Extensions/PropertyBuilderExtensions.cs @@ -1,9 +1,9 @@ using Common.SharedKernel.Domain.Base; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Modules.Warehouse.Common.Persistence.Extensions; +namespace Common.SharedKernel.Persistence.Extensions; -internal static class PropertyBuilderExtensions +public static class PropertyBuilderExtensions { public static PropertyBuilder HasStronglyTypedId(this PropertyBuilder propertyBuilder) where TId : IStronglyTypedId diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/StronglyTypedIdConverter.cs b/src/Common/Common.SharedKernel/Persistence/Extensions/StronglyTypedIdConverter.cs similarity index 72% rename from src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/StronglyTypedIdConverter.cs rename to src/Common/Common.SharedKernel/Persistence/Extensions/StronglyTypedIdConverter.cs index ec4a1b8..0536da1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Extensions/StronglyTypedIdConverter.cs +++ b/src/Common/Common.SharedKernel/Persistence/Extensions/StronglyTypedIdConverter.cs @@ -1,9 +1,9 @@ using Common.SharedKernel.Domain.Base; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Modules.Warehouse.Common.Persistence.Extensions; +namespace Common.SharedKernel.Persistence.Extensions; -internal class StronglyTypedIdConverter : ValueConverter +public class StronglyTypedIdConverter : ValueConverter where TId : IStronglyTypedId { public StronglyTypedIdConverter(ConverterMappingHints? mappingHints = null) diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs b/src/Common/Common.SharedKernel/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs similarity index 91% rename from src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs rename to src/Common/Common.SharedKernel/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs index d7ca4db..23fdaac 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs +++ b/src/Common/Common.SharedKernel/Persistence/Interceptors/DispatchDomainEventsInterceptor.cs @@ -1,10 +1,10 @@ using Common.SharedKernel.Domain.Interfaces; +using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; -using Modules.Warehouse.Common.Middleware; -namespace Modules.Warehouse.Common.Persistence.Interceptors; +namespace Common.SharedKernel.Persistence.Interceptors; public class DispatchDomainEventsInterceptor : SaveChangesInterceptor { @@ -76,7 +76,8 @@ private async Task PublishDomainEvents(IEnumerable domainEvents) private void AddDomainEventsToOfflineProcessingQueue(IEnumerable domainEvents) { - var domainEventsQueue = _httpContextAccessor.HttpContext!.Items.TryGetValue(EventualConsistencyMiddleware.DomainEventsKey, out var value) && + // TODO: This queue will need to be module specific + var domainEventsQueue = _httpContextAccessor.HttpContext!.Items.TryGetValue("DomainEventsKey", out var value) && value is Queue existingDomainEvents ? existingDomainEvents : new Queue(); @@ -84,6 +85,6 @@ value is Queue existingDomainEvents foreach (var domainEvent in domainEvents) domainEventsQueue.Enqueue(domainEvent); - _httpContextAccessor.HttpContext.Items[EventualConsistencyMiddleware.DomainEventsKey] = domainEventsQueue; + _httpContextAccessor.HttpContext.Items["DomainEventsKey"] = domainEventsQueue; } } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/EntitySaveChangesInterceptor.cs b/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs similarity index 100% rename from src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Interceptors/EntitySaveChangesInterceptor.cs rename to src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs diff --git a/src/Modules/Products/Modules.Catalog.Tests/CatalogTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryTests.cs similarity index 93% rename from src/Modules/Products/Modules.Catalog.Tests/CatalogTests.cs rename to src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryTests.cs index e5449be..856ade2 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/CatalogTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; using Modules.Catalog.Categories.Domain; -namespace Modules.Catalog.Tests; +namespace Modules.Catalog.Tests.Categories; -public class CatalogTests +public class CategoryTests { [Fact] public void Create_ShouldThrowException_WhenNameIsNull() diff --git a/src/Modules/Products/Modules.Catalog.Tests/ProductTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductTests.cs similarity index 99% rename from src/Modules/Products/Modules.Catalog.Tests/ProductTests.cs rename to src/Modules/Products/Modules.Catalog.Tests/Products/ProductTests.cs index eb0a705..ffbf86e 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/ProductTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductTests.cs @@ -3,7 +3,7 @@ using Modules.Catalog.Categories.Domain; using Modules.Catalog.Products; -namespace Modules.Catalog.Tests; +namespace Modules.Catalog.Tests.Products; public class ProductTests { diff --git a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs index bd5a405..8391513 100644 --- a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs +++ b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("Modules.Catalog.Tests")] +[assembly: InternalsVisibleTo("Database")] diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 6491e60..53050ce 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Modules.Catalog.Common.Persistence; namespace Modules.Catalog; @@ -15,7 +16,7 @@ public static void AddCatalog(this IServiceCollection services, IConfiguration c services.AddValidatorsFromAssembly(applicationAssembly); - // services.AddPersistence(configuration); + services.AddPersistence(configuration); } public static void UseCatalog(this WebApplication app) diff --git a/src/Modules/Products/Modules.Catalog/Categories/CategoryService.cs b/src/Modules/Products/Modules.Catalog/Categories/CategoryService.cs deleted file mode 100644 index b6fde46..0000000 --- a/src/Modules/Products/Modules.Catalog/Categories/CategoryService.cs +++ /dev/null @@ -1,19 +0,0 @@ -// using Modules.Warehouse.Common.Persistence; -// using Modules.Warehouse.Features.Categories.Domain; -// -// namespace Modules.Warehouse.Features.Categories; -// -// internal class CategoryRepository : ICategoryRepository -// { -// private readonly WarehouseDbContext _dbContext; -// -// public CategoryRepository(WarehouseDbContext dbContext) -// { -// _dbContext = dbContext; -// } -// -// public bool CategoryExists(string categoryName) -// { -// return _dbContext.Categories.Any(c => c.Name == categoryName); -// } -// } diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs index c6591a6..67e5860 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs @@ -2,6 +2,8 @@ namespace Modules.Catalog.Categories.Domain; +internal record CategoryId(Guid Value) : IStronglyTypedId; + internal class Category : AggregateRoot { /// @@ -27,7 +29,6 @@ public static Category Create(string name) private void UpdateName(string name) { - // name.ThrowIfNull(); ArgumentException.ThrowIfNullOrWhiteSpace(name); Name = name; diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs deleted file mode 100644 index 65bb098..0000000 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Catalog.Categories.Domain; - -internal record CategoryId(Guid Value); diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/ICategoryService.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/ICategoryService.cs deleted file mode 100644 index 09015c0..0000000 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/ICategoryService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Modules.Catalog.Categories.Domain; - -internal interface ICategoryRepository -{ - bool CategoryExists(string categoryName); -} diff --git a/src/Modules/Products/Modules.Catalog/Categories/Persistence/CategoryConfiguration.cs b/src/Modules/Products/Modules.Catalog/Categories/Persistence/CategoryConfiguration.cs deleted file mode 100644 index 2320358..0000000 --- a/src/Modules/Products/Modules.Catalog/Categories/Persistence/CategoryConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -// using Microsoft.EntityFrameworkCore; -// using Microsoft.EntityFrameworkCore.Metadata.Builders; -// using Modules.Warehouse.Features.Categories.Domain; -// -// namespace Modules.Warehouse.Features.Categories.Persistence; -// -// internal class CategoryConfiguration : IEntityTypeConfiguration -// { -// public void Configure(EntityTypeBuilder builder) -// { -// builder.HasKey(p => p.Id); -// -// builder.Property(p => p.Id) -// .HasConversion(categoryId => categoryId.Value, value => new CategoryId(value)); -// -// builder.Property(p => p.Name) -// .HasMaxLength(50); -// } -// } diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs new file mode 100644 index 0000000..15b7f21 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Products; + +namespace Modules.Catalog.Common.Persistence; + +internal class CatalogDbContext : DbContext +{ + internal DbSet Products => Set(); + + // Needs to be public for the Database project + public CatalogDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("catalog"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } +} diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/CategoryConfiguration.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/CategoryConfiguration.cs new file mode 100644 index 0000000..b4f9058 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/CategoryConfiguration.cs @@ -0,0 +1,21 @@ +using Common.SharedKernel.Persistence.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Catalog.Categories.Domain; + +namespace Modules.Catalog.Common.Persistence.Configuration; + +internal class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(p => p.Id); + + builder.Property(p => p.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.Property(p => p.Name) + .HasMaxLength(50); + } +} diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs new file mode 100644 index 0000000..90ceffb --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs @@ -0,0 +1,31 @@ +using Common.SharedKernel.Persistence; +using Common.SharedKernel.Persistence.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Catalog.Products; + +namespace Modules.Catalog.Common.Persistence.Configuration; + +internal class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(m => m.Id); + + builder + .Property(m => m.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.HasMany(t => t.Categories) + .WithOne(); + + builder.Property(m => m.Name) + .IsRequired(); + + builder.Property(m => m.Sku) + .IsRequired(); + + builder.ComplexProperty(m => m.Price, MoneyConfiguration.BuildAction); + } +} diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs new file mode 100644 index 0000000..bd0c6f8 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs @@ -0,0 +1,41 @@ +using Common.SharedKernel.Persistence.Interceptors; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Persistence.Interceptors; + +namespace Modules.Catalog.Common.Persistence; + +internal static class DependencyInjection +{ + internal static void AddPersistence(this IServiceCollection services, IConfiguration config) + { + var connectionString = config.GetConnectionString("Catalog"); + services.AddDbContext(options => + { + options.UseSqlServer(connectionString, builder => + { + builder.MigrationsAssembly(typeof(CatalogModule).Assembly.FullName); + // builder.EnableRetryOnFailure(); + }); + + var serviceProvider = services.BuildServiceProvider(); + + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + }); + + services.AddScoped(); + services.AddScoped(); + // services.AddScoped(); + } + + public static IApplicationBuilder UseInfrastructureMiddleware(this IApplicationBuilder app) + { + // TODO: Will need to add this when any events are fired + // app.UseMiddleware(); + return app; + } +} diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 414c654..1bd9ff1 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -1,13 +1,25 @@  - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - + + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs index baedfd7..ec1965f 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using Common.SharedKernel.Persistence.Interceptors; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Modules.Warehouse.Common.Persistence.Interceptors; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs index 3f5eab4..cd0f0c2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs @@ -6,6 +6,7 @@ namespace Modules.Warehouse.Common.Middleware; internal class EventualConsistencyMiddleware { + // TODO: Make the key specific to each module public const string DomainEventsKey = "DomainEventsKey"; private readonly RequestDelegate _next; @@ -15,6 +16,7 @@ public EventualConsistencyMiddleware(RequestDelegate next) _next = next; } + // TODO: See if we can make this middleware generic public async Task InvokeAsync(HttpContext context, IPublisher publisher, WarehouseDbContext dbContext) { var transaction = await dbContext.Database.BeginTransactionAsync(); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs index 21c99bc..72177a8 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/AisleConfiguraiton.cs @@ -1,6 +1,6 @@ +using Common.SharedKernel.Persistence.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Warehouse.Common.Persistence.Extensions; using Modules.Warehouse.Storage.Domain; namespace Modules.Warehouse.Common.Persistence.Configuration; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs index b3f8054..38c9bdb 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/BayConfiguration.cs @@ -1,6 +1,6 @@ +using Common.SharedKernel.Persistence.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Warehouse.Common.Persistence.Extensions; using Modules.Warehouse.Storage.Domain; namespace Modules.Warehouse.Common.Persistence.Configuration; @@ -23,4 +23,4 @@ public void Configure(EntityTypeBuilder builder) .WithOne() .IsRequired(); } -} \ No newline at end of file +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ProductConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ProductConfiguration.cs index 5bf3368..599f012 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ProductConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ProductConfiguration.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Modules.Warehouse.Products.Domain; -namespace Modules.Warehouse.Products.Persistence; +namespace Modules.Warehouse.Common.Persistence.Configuration; internal class ProductConfiguration : IEntityTypeConfiguration { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs index 3eaa6b3..0dc1623 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Configuration/ShelfConfiguration.cs @@ -1,6 +1,6 @@ +using Common.SharedKernel.Persistence.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Warehouse.Common.Persistence.Extensions; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index abb129f..09ce416 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -1,3 +1,4 @@ +using Common.SharedKernel.Persistence.Interceptors; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index 106e594..b75c406 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -11,7 +11,7 @@ internal class WarehouseDbContext : DbContext internal DbSet Products => Set(); // Needs to be public for the Database project - public WarehouseDbContext(DbContextOptions options) : base(options) + public WarehouseDbContext(DbContextOptions options) : base(options) { } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index e8d549a..268f719 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -9,7 +9,6 @@ - all @@ -23,4 +22,8 @@ + + + + diff --git a/src/WebApi/appsettings.Development.json b/src/WebApi/appsettings.Development.json index b445d7c..a07a4ba 100644 --- a/src/WebApi/appsettings.Development.json +++ b/src/WebApi/appsettings.Development.json @@ -6,6 +6,7 @@ } }, "ConnectionStrings": { - "Warehouse": "Server=localhost,1800;Initial Catalog=Warehouse;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;" + "Warehouse": "Server=localhost,1800;Initial Catalog=Warehouse;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;", + "Catalog": "Server=localhost,1800;Initial Catalog=Catalog;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;" } } diff --git a/tools/Database/Database.csproj b/tools/Database/Database.csproj index 62ecc99..ee9ef1e 100644 --- a/tools/Database/Database.csproj +++ b/tools/Database/Database.csproj @@ -10,6 +10,7 @@ + diff --git a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs new file mode 100644 index 0000000..e0dd847 --- /dev/null +++ b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Modules.Catalog.Common.Persistence; +using Modules.Warehouse.Products.Domain; +using ProductId = Modules.Catalog.Products.ProductId; + +namespace Database.Initialisers; + +internal class CatalogDbContextInitialiser +{ + private readonly ILogger _logger; + private readonly CatalogDbContext _dbContext; + + // public constructor needed for DI + public CatalogDbContextInitialiser(ILogger logger, CatalogDbContext dbContext) + { + _logger = logger; + _dbContext = dbContext; + } + + internal async Task InitializeAsync() + { + try + { + if (_dbContext.Database.IsSqlServer()) + { + // TODO: Move to migrations + await _dbContext.Database.EnsureDeletedAsync(); + await _dbContext.Database.EnsureCreatedAsync(); + } + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while migrating or initializing the database"); + throw; + } + } + + internal async Task SeedAsync(IEnumerable warehouseProducts) + { + await SeedProductsAsync(warehouseProducts); + } + + private async Task SeedProductsAsync(IEnumerable warehouseProducts) + { + if (await _dbContext.Products.AnyAsync()) + return; + + // Usually integration events would propagate products to the catalog + // However, to simplify test data seed, we'll manually pass products into the catalog + foreach (var warehouseProduct in warehouseProducts) + { + var catalogProduct = Modules.Catalog.Products.Product.Create( + warehouseProduct.Name, + warehouseProduct.Sku.Value, + new ProductId(warehouseProduct.Id.Value)); + + _dbContext.Products.Add(catalogProduct); + } + + await _dbContext.SaveChangesAsync(); + } +} diff --git a/tools/Database/WarehouseDbContextInitializer.cs b/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs similarity index 80% rename from tools/Database/WarehouseDbContextInitializer.cs rename to tools/Database/Initialisers/WarehouseDbContextInitialiser.cs index aeaabd3..b202966 100644 --- a/tools/Database/WarehouseDbContextInitializer.cs +++ b/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs @@ -5,11 +5,11 @@ using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; -namespace Database; +namespace Database.Initialisers; -internal class WarehouseDbContextInitializer +internal class WarehouseDbContextInitialiser { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly WarehouseDbContext _dbContext; private const int NumProducts = 20; @@ -18,7 +18,7 @@ internal class WarehouseDbContextInitializer private const int NumBays = 20; // public constructor needed for DI - public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) + public WarehouseDbContextInitialiser(ILogger logger, WarehouseDbContext dbContext) { _logger = logger; _dbContext = dbContext; @@ -42,10 +42,10 @@ internal async Task InitializeAsync() } } - internal async Task SeedAsync() + internal async Task> SeedAsync() { await SeedAisles(); - await SeedProductsAsync(); + return await SeedProductsAsync(); } private async Task SeedAisles() @@ -62,10 +62,10 @@ private async Task SeedAisles() await _dbContext.SaveChangesAsync(); } - private async Task SeedProductsAsync() + private async Task> SeedProductsAsync() { if (await _dbContext.Products.AnyAsync()) - return; + return []; // TODO: Consider how to handle integration events that get raised and handled @@ -78,5 +78,7 @@ private async Task SeedProductsAsync() var products = faker.Generate(NumProducts); _dbContext.Products.AddRange(products); await _dbContext.SaveChangesAsync(); + + return products; } } diff --git a/tools/Database/Program.cs b/tools/Database/Program.cs index f4d558a..eb164da 100644 --- a/tools/Database/Program.cs +++ b/tools/Database/Program.cs @@ -1,8 +1,10 @@ -using Database; +using Database.Initialisers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Modules.Catalog; +using Modules.Catalog.Common.Persistence; using Modules.Warehouse; using Modules.Warehouse.Common.Persistence; @@ -20,7 +22,16 @@ }); }); - services.AddScoped(); + services.AddDbContext(options => + { + options.UseSqlServer(context.Configuration.GetConnectionString("Catalog"), opt => + { + opt.MigrationsAssembly(typeof(CatalogModule).Assembly.FullName); + }); + }); + + services.AddScoped(); + services.AddScoped(); }); var app = builder.Build(); @@ -28,6 +39,10 @@ // Initialise and seed database using var scope = app.Services.CreateScope(); -var initializer = scope.ServiceProvider.GetRequiredService(); -await initializer.InitializeAsync(); -await initializer.SeedAsync(); +var warehouse = scope.ServiceProvider.GetRequiredService(); +await warehouse.InitializeAsync(); +var warehouseProducts = await warehouse.SeedAsync(); + +var catalog = scope.ServiceProvider.GetRequiredService(); +await catalog.InitializeAsync(); +await catalog.SeedAsync(warehouseProducts); diff --git a/tools/Database/appsettings.json b/tools/Database/appsettings.json index b445d7c..a07a4ba 100644 --- a/tools/Database/appsettings.json +++ b/tools/Database/appsettings.json @@ -6,6 +6,7 @@ } }, "ConnectionStrings": { - "Warehouse": "Server=localhost,1800;Initial Catalog=Warehouse;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;" + "Warehouse": "Server=localhost,1800;Initial Catalog=Warehouse;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;", + "Catalog": "Server=localhost,1800;Initial Catalog=Catalog;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;" } } From b31a9c303f65dea94bf8192bd50aba42097b9fef Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 29 Aug 2024 07:39:54 +1000 Subject: [PATCH 45/87] =?UTF-8?q?=F0=9F=8C=B1=20Add=20categories=20seeding?= =?UTF-8?q?=20to=20CatalogDbContext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored the product configuration to handle many-to-many relationships. Introduced seeding for categories within the CatalogDbContextInitialiser using the Bogus library. Added a new method to seed categories and updated the product seeding to assign random categories. --- .../Common/Persistence/CatalogDbContext.cs | 3 ++ .../Configuration/ProductConfiguration.cs | 2 +- .../CatalogDbContextInitialiser.cs | 30 +++++++++++++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs index 15b7f21..afd9054 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Categories.Domain; using Modules.Catalog.Products; namespace Modules.Catalog.Common.Persistence; @@ -7,6 +8,8 @@ internal class CatalogDbContext : DbContext { internal DbSet Products => Set(); + internal DbSet Categories => Set(); + // Needs to be public for the Database project public CatalogDbContext(DbContextOptions options) : base(options) { diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs index 90ceffb..72dcd07 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs @@ -18,7 +18,7 @@ public void Configure(EntityTypeBuilder builder) .ValueGeneratedNever(); builder.HasMany(t => t.Categories) - .WithOne(); + .WithMany(); builder.Property(m => m.Name) .IsRequired(); diff --git a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs index e0dd847..d25b149 100644 --- a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs +++ b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs @@ -1,5 +1,7 @@ +using Bogus; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Modules.Catalog.Categories.Domain; using Modules.Catalog.Common.Persistence; using Modules.Warehouse.Products.Domain; using ProductId = Modules.Catalog.Products.ProductId; @@ -11,6 +13,8 @@ internal class CatalogDbContextInitialiser private readonly ILogger _logger; private readonly CatalogDbContext _dbContext; + private const int NumCategories = 10; + // public constructor needed for DI public CatalogDbContextInitialiser(ILogger logger, CatalogDbContext dbContext) { @@ -38,14 +42,33 @@ internal async Task InitializeAsync() internal async Task SeedAsync(IEnumerable warehouseProducts) { - await SeedProductsAsync(warehouseProducts); + var categories = await SeedCategories(); + await SeedProductsAsync(warehouseProducts, categories); + } + + private async Task> SeedCategories() + { + if (await _dbContext.Categories.AnyAsync()) + return []; + + var categoryFaker = new Faker() + .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1).First()!)); + + var categories = categoryFaker.Generate(NumCategories); + _dbContext.Categories.AddRange(categories); + await _dbContext.SaveChangesAsync(); + + return categories; } - private async Task SeedProductsAsync(IEnumerable warehouseProducts) + private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEnumerable categories) { if (await _dbContext.Products.AnyAsync()) return; + var categoryFaker = new Faker() + .CustomInstantiator(f => f.PickRandom(categories)); + // Usually integration events would propagate products to the catalog // However, to simplify test data seed, we'll manually pass products into the catalog foreach (var warehouseProduct in warehouseProducts) @@ -55,6 +78,9 @@ private async Task SeedProductsAsync(IEnumerable warehouseProducts) warehouseProduct.Sku.Value, new ProductId(warehouseProduct.Id.Value)); + var productCategory = categoryFaker.Generate(); + catalogProduct.AddCategory(productCategory); + _dbContext.Products.Add(catalogProduct); } From 102bb82adcfa62e9544b00c25f951557370f96bc Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:05:49 +1000 Subject: [PATCH 46/87] =?UTF-8?q?=E2=9C=A8=20Add=20database=20saving=20log?= =?UTF-8?q?ic=20for=20Product=20in=20event=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated CatalogDbContext into ProductStoredIntegrationEventHandler to save new products to the database. This change ensures that products are persistently stored upon handling the integration event. --- .../ProductStoredIntegrationEventHandler.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs b/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs index 65ff456..e74e907 100644 --- a/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs +++ b/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs @@ -1,5 +1,6 @@ using MediatR; using Microsoft.Extensions.Logging; +using Modules.Catalog.Common.Persistence; using Modules.Warehouse.Messages; namespace Modules.Catalog.Products.IntegrationEvents; @@ -7,10 +8,12 @@ namespace Modules.Catalog.Products.IntegrationEvents; internal class ProductStoredIntegrationEventHandler : INotificationHandler { private readonly ILogger _logger; + private readonly CatalogDbContext _dbContext; - public ProductStoredIntegrationEventHandler(ILogger logger) + public ProductStoredIntegrationEventHandler(ILogger logger, CatalogDbContext dbContext) { _logger = logger; + _dbContext = dbContext; } public async Task Handle(ProductStoredIntegrationEvent notification, CancellationToken cancellationToken) @@ -23,7 +26,9 @@ public async Task Handle(ProductStoredIntegrationEvent notification, Cancellatio var product = Product.Create(name, sku, productId); - // TODO: Save product to DB + _dbContext.Products.Add(product); + + await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Product stored integration event processed"); From 8fa4917606fe009335e91fdb4314ef77715b0e17 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:06:17 +1000 Subject: [PATCH 47/87] =?UTF-8?q?=F0=9F=A7=AA=20Add=20WebApi.Tests=20proje?= =?UTF-8?q?ct=20to=20the=20solution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a new test project, WebApi.Tests, to the solution to include integration tests. This project includes initial setup with xUnit and references necessary testing packages. --- ModularMonolith.sln | 7 ++++++ src/WebApi.Tests/GlobalUsings.cs | 1 + src/WebApi.Tests/SystemIntegrationTests.cs | 14 ++++++++++++ src/WebApi.Tests/WebApi.Tests.csproj | 25 ++++++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 src/WebApi.Tests/GlobalUsings.cs create mode 100644 src/WebApi.Tests/SystemIntegrationTests.cs create mode 100644 src/WebApi.Tests/WebApi.Tests.csproj diff --git a/ModularMonolith.sln b/ModularMonolith.sln index d16c0bb..d087eae 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "tools\Database\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Messages", "src\Modules\Warehouse\Modules.Warehouse.Messages\Modules.Warehouse.Messages.csproj", "{A105835C-C285-4AA5-AADF-CCA4BCC933B1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Tests", "src\WebApi.Tests\WebApi.Tests.csproj", "{13094B62-3DBC-4C6F-9ECC-D2DC433C319F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,6 +113,10 @@ Global {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Release|Any CPU.Build.0 = Release|Any CPU + {13094B62-3DBC-4C6F-9ECC-D2DC433C319F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13094B62-3DBC-4C6F-9ECC-D2DC433C319F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13094B62-3DBC-4C6F-9ECC-D2DC433C319F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13094B62-3DBC-4C6F-9ECC-D2DC433C319F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -131,5 +137,6 @@ Global {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB} = {7915FF68-4EC9-497B-BD33-1A3DA7D6B457} {A105835C-C285-4AA5-AADF-CCA4BCC933B1} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} + {13094B62-3DBC-4C6F-9ECC-D2DC433C319F} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} EndGlobalSection EndGlobal diff --git a/src/WebApi.Tests/GlobalUsings.cs b/src/WebApi.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/WebApi.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/WebApi.Tests/SystemIntegrationTests.cs b/src/WebApi.Tests/SystemIntegrationTests.cs new file mode 100644 index 0000000..f9a70c2 --- /dev/null +++ b/src/WebApi.Tests/SystemIntegrationTests.cs @@ -0,0 +1,14 @@ +namespace WebApi.Tests; + +public class SystemIntegrationTests +{ + [Fact] + public void Test1() + { + // TODO: Warehouse - Create Product + + // TODO: Warehouse - Allocate Storage + + // TODO: Catalog - Assert Product Created + } +} diff --git a/src/WebApi.Tests/WebApi.Tests.csproj b/src/WebApi.Tests/WebApi.Tests.csproj new file mode 100644 index 0000000..120fbeb --- /dev/null +++ b/src/WebApi.Tests/WebApi.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + From 5ff7223c9bb2f2cad4bf0ca3001a3fe2245a262a Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:35:28 +1000 Subject: [PATCH 48/87] =?UTF-8?q?=F0=9F=A7=AA=20Refactor=20test=20infrastr?= =?UTF-8?q?ucture=20for=20modularity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved common test utilities to a new `Common.Tests` project to enhance modularity and reuse. Adjusted references and namespaces accordingly, updated `WarehouseDbContext` visibility, and ensured all fixtures and base classes use generic types for flexibility. #35 --- ModularMonolith.sln | 7 ++++ src/Common/Common.Tests/Common.Tests.csproj | 32 +++++++++++++++++++ .../Common.Tests}/Common/DatabaseContainer.cs | 16 ++++------ .../Common/IntegrationTestBase.cs | 16 ++++------ .../Common/TestingDatabaseFixture.cs | 21 ++++-------- .../Common.Tests}/Common/WebApiTestFactory.cs | 10 +++--- .../Extensions/ServiceCollectionExt.cs | 3 +- .../Common/WarehouseIntegrationTestBase.cs | 23 +++++++++++++ .../Modules.Warehouse.Tests.csproj | 1 + .../Products/ProductIntegrationTests.cs | 8 +++-- .../AllocateStorageCommandIntegrationTests.cs | 4 +-- .../CreateAisleCommandIntegrationTests.cs | 8 +++-- .../GetItemLocationQueryIntegrationTests.cs | 5 ++- .../Common/Persistence/WarehouseDbContext.cs | 3 +- 14 files changed, 106 insertions(+), 51 deletions(-) create mode 100644 src/Common/Common.Tests/Common.Tests.csproj rename src/{Modules/Warehouse/Modules.Warehouse.Tests => Common/Common.Tests}/Common/DatabaseContainer.cs (63%) rename src/{Modules/Warehouse/Modules.Warehouse.Tests => Common/Common.Tests}/Common/IntegrationTestBase.cs (78%) rename src/{Modules/Warehouse/Modules.Warehouse.Tests => Common/Common.Tests}/Common/TestingDatabaseFixture.cs (68%) rename src/{Modules/Warehouse/Modules.Warehouse.Tests => Common/Common.Tests}/Common/WebApiTestFactory.cs (80%) rename src/{Modules/Warehouse/Modules.Warehouse.Tests/Common => Common/Common.Tests}/Extensions/ServiceCollectionExt.cs (95%) create mode 100644 src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs diff --git a/ModularMonolith.sln b/ModularMonolith.sln index d087eae..14b9baf 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -56,6 +56,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Messages" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Tests", "src\WebApi.Tests\WebApi.Tests.csproj", "{13094B62-3DBC-4C6F-9ECC-D2DC433C319F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "src\Common\Common.Tests\Common.Tests.csproj", "{51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -117,6 +119,10 @@ Global {13094B62-3DBC-4C6F-9ECC-D2DC433C319F}.Debug|Any CPU.Build.0 = Debug|Any CPU {13094B62-3DBC-4C6F-9ECC-D2DC433C319F}.Release|Any CPU.ActiveCfg = Release|Any CPU {13094B62-3DBC-4C6F-9ECC-D2DC433C319F}.Release|Any CPU.Build.0 = Release|Any CPU + {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -138,5 +144,6 @@ Global {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB} = {7915FF68-4EC9-497B-BD33-1A3DA7D6B457} {A105835C-C285-4AA5-AADF-CCA4BCC933B1} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {13094B62-3DBC-4C6F-9ECC-D2DC433C319F} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} + {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9} = {3E4B904F-1D6C-437B-8208-C6D17F995528} EndGlobalSection EndGlobal diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj new file mode 100644 index 0000000..57407d4 --- /dev/null +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs similarity index 63% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs rename to src/Common/Common.Tests/Common/DatabaseContainer.cs index dcb4b42..5aefe67 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -1,25 +1,23 @@ -using DotNet.Testcontainers.Builders; using Testcontainers.SqlEdge; -namespace Modules.Warehouse.Tests.Common; +namespace Common.Tests.Common; /// -/// Wraper for SQL edge container +/// Wrapper for SQL edge container /// public class DatabaseContainer { // private static readonly int _exposedPort = Random.Shared.Next(10000, 60000); - private static readonly int _exposedPort = 20_000; - private static readonly int _internalPort = 1433; - + // private static readonly int _exposedPort = 20_000; + // private static readonly int _internalPort = 1433; private readonly SqlEdgeContainer _container = new SqlEdgeBuilder() .WithName("Modular-Monolith-IntegrationTests-DbContainer") .WithPassword("Password123") .WithAutoRemove(true) - .WithPortBinding(_internalPort) - // .WithExposedPort(_exposedPort) - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(_internalPort)) + // .WithPortBinding(_internalPort) + // // .WithExposedPort(_exposedPort) + // .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(_internalPort)) .Build(); public string? ConnectionString { get; private set; } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs b/src/Common/Common.Tests/Common/IntegrationTestBase.cs similarity index 78% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs rename to src/Common/Common.Tests/Common/IntegrationTestBase.cs index 680f14c..10a2ed3 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/IntegrationTestBase.cs +++ b/src/Common/Common.Tests/Common/IntegrationTestBase.cs @@ -1,38 +1,36 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Modules.Warehouse.Common.Persistence; +using Xunit; using Xunit.Abstractions; -namespace Modules.Warehouse.Tests.Common; +namespace Common.Tests.Common; /// /// Integration tests inherit from this to access helper classes /// -[Collection(TestingDatabaseFixtureCollection.Name)] -public abstract class IntegrationTestBase : IAsyncLifetime +public abstract class IntegrationTestBase : IAsyncLifetime where TDbContext : DbContext { private readonly IServiceScope _scope; - private readonly TestingDatabaseFixture _fixture; + private readonly TestingDatabaseFixture _fixture; protected IMediator Mediator { get; } protected IQueryable GetQueryable() where T : class => DbContext.Set().AsNoTracking(); - internal WarehouseDbContext DbContext { get; } + private TDbContext DbContext { get; } - protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper output) + protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper output) { _fixture = fixture; _fixture.SetOutput(output); _scope = _fixture.ScopeFactory.CreateScope(); Mediator = _scope.ServiceProvider.GetRequiredService(); - DbContext = _scope.ServiceProvider.GetRequiredService(); + DbContext = _scope.ServiceProvider.GetRequiredService(); } - protected async Task AddEntityAsync(T entity, CancellationToken cancellationToken = default) where T : class { await DbContext.Set().AddAsync(entity, cancellationToken); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/TestingDatabaseFixture.cs b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs similarity index 68% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Common/TestingDatabaseFixture.cs rename to src/Common/Common.Tests/Common/TestingDatabaseFixture.cs index 7484acd..4787ca1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/TestingDatabaseFixture.cs +++ b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs @@ -1,15 +1,16 @@ +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Modules.Warehouse.Common.Persistence; using Respawn; +using Xunit; using Xunit.Abstractions; -namespace Modules.Warehouse.Tests.Common; +namespace Common.Tests.Common; /// /// Initializes and resets the database before and after each test /// // ReSharper disable once ClassNeverInstantiated.Global -public class TestingDatabaseFixture : IAsyncLifetime +public class TestingDatabaseFixture : IAsyncLifetime where TDbContext : DbContext { private string ConnectionString => Factory.Database.ConnectionString!; @@ -17,7 +18,7 @@ public class TestingDatabaseFixture : IAsyncLifetime public IServiceScopeFactory ScopeFactory { get; private set; } = null!; - public WebApiTestFactory Factory { get; } = new(); + public WebApiTestFactory Factory { get; } = new(); public async Task InitializeAsync() { @@ -27,7 +28,7 @@ public async Task InitializeAsync() // Create and seed database using var scope = ScopeFactory.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.EnsureCreatedAsync(); // NOTE: If there are any tables you want to skip being reset, they can be configured here @@ -46,13 +47,3 @@ public async Task ResetState() public void SetOutput(ITestOutputHelper output) => Factory.Output = output; } - -[CollectionDefinition(Name)] -public class TestingDatabaseFixtureCollection : ICollectionFixture -{ - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. - - public const string Name = nameof(TestingDatabaseFixtureCollection); -} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WebApiTestFactory.cs b/src/Common/Common.Tests/Common/WebApiTestFactory.cs similarity index 80% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WebApiTestFactory.cs rename to src/Common/Common.Tests/Common/WebApiTestFactory.cs index bda2feb..e4f6841 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WebApiTestFactory.cs +++ b/src/Common/Common.Tests/Common/WebApiTestFactory.cs @@ -1,20 +1,20 @@ +using Common.Tests.Extensions; using Meziantou.Extensions.Logging.Xunit; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Tests.Common.Extensions; using WebApi; using Xunit.Abstractions; -namespace Modules.Warehouse.Tests.Common; +namespace Common.Tests.Common; /// /// Host builder (services, DI, and configuration) for integration tests /// -public class WebApiTestFactory : WebApplicationFactory +public class WebApiTestFactory : WebApplicationFactory where TDbContext : DbContext { public DatabaseContainer Database { get; } = new(); @@ -35,7 +35,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) // Override default DB registration to use out Test Container instead builder.ConfigureTestServices(services => { - services.ReplaceDbContext(Database); + services.ReplaceDbContext(Database); }); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs similarity index 95% rename from src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs rename to src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs index ec1965f..f986045 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/Extensions/ServiceCollectionExt.cs +++ b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs @@ -1,11 +1,12 @@ using Common.SharedKernel.Persistence.Interceptors; +using Common.Tests.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Modules.Warehouse.Common.Persistence.Interceptors; using System.Diagnostics; -namespace Modules.Warehouse.Tests.Common.Extensions; +namespace Common.Tests.Extensions; internal static class ServiceCollectionExt { diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs new file mode 100644 index 0000000..7ce3cd8 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs @@ -0,0 +1,23 @@ +using Common.Tests.Common; +using Modules.Warehouse.Common.Persistence; +using Xunit.Abstractions; + +namespace Modules.Warehouse.Tests.Common; + +public abstract class WarehouseDatabaseFixture : TestingDatabaseFixture; + +[Collection(TestingDatabaseFixtureCollection.Name)] +public abstract class WarehouseIntegrationTestBase( + WarehouseDatabaseFixture fixture, + ITestOutputHelper output) + : IntegrationTestBase(fixture, output); + +[CollectionDefinition(Name)] +public class TestingDatabaseFixtureCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + + public const string Name = nameof(TestingDatabaseFixtureCollection); +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj index 652cf4d..895d0e1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs index 2650692..2f939db 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs @@ -9,9 +9,11 @@ namespace Modules.Warehouse.Tests.Products; -public class ProductIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class ProductIntegrationTests(WarehouseDatabaseFixture fixture, ITestOutputHelper output) + : WarehouseIntegrationTestBase(fixture, output) { + private readonly ITestOutputHelper _output = output; + [Fact] public async Task CreateProduct_ValidRequest_ReturnsCreatedProduct() { @@ -53,6 +55,6 @@ public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, st // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var content = await response.Content.ReadAsStringAsync(); - output.WriteLine(content); + _output.WriteLine(content); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs index 70f74a4..f92fa60 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs @@ -9,8 +9,8 @@ namespace Modules.Warehouse.Tests.Storage.UseCases; -public class AllocateStorageCommandIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class AllocateStorageCommandIntegrationTests (WarehouseDatabaseFixture fixture, ITestOutputHelper output) + : WarehouseIntegrationTestBase(fixture, output) { [Fact] public async Task AllocateStorage_ValidRequest_ReturnsOk() diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs index c93b010..5f4cfc2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs @@ -10,9 +10,11 @@ namespace Modules.Warehouse.Tests.Storage.UseCases; -public class CreateAisleCommandIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class CreateAisleCommandIntegrationTests (WarehouseDatabaseFixture fixture, ITestOutputHelper output) + : WarehouseIntegrationTestBase(fixture, output) { + private readonly ITestOutputHelper _output = output; + [Fact] public async Task CreateAisle_ValidRequest_ReturnsCreatedAisle() { @@ -57,6 +59,6 @@ public async Task CreateAisle_WithInvalidRequest_Throws(string name, int numBays // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var content = await response.Content.ReadAsStringAsync(); - output.WriteLine(content); + _output.WriteLine(content); } } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs index ad24cb1..f3acc52 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs @@ -3,14 +3,13 @@ using Modules.Warehouse.Storage.Domain; using Modules.Warehouse.Storage.UseCases; using Modules.Warehouse.Tests.Common; -using System.Net; using System.Net.Http.Json; using Xunit.Abstractions; namespace Modules.Warehouse.Tests.Storage.UseCases; -public class GetItemLocationQueryIntegrationTests (TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class GetItemLocationQueryIntegrationTests (WarehouseDatabaseFixture fixture, ITestOutputHelper output) + : WarehouseIntegrationTestBase(fixture, output) { [Fact] public async Task Query_ValidRequest_ReturnsOk() diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index b75c406..0a01f25 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -4,7 +4,8 @@ namespace Modules.Warehouse.Common.Persistence; -internal class WarehouseDbContext : DbContext +// Needs to be public for tests +public class WarehouseDbContext : DbContext { internal DbSet Aisles => Set(); From d2bf85c1634cc80971c595f5d11341711967c965 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:02:27 +1000 Subject: [PATCH 49/87] =?UTF-8?q?=F0=9F=A7=AA=20Add=20test=20report=20to?= =?UTF-8?q?=20GitHub=20workflow=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Enhance CI workflow for .NET project Added manual trigger and updated permissions in the workflow. Upgraded action versions and configured release builds and test logging. Added test result reporting for improved visibility. * Change to debug build * 🧪 Update CI for .NET setup and improve test reporting Refactored GitHub Actions workflow for .NET by updating to actions/checkout v4 and actions/setup-dotnet v3. Improved test stage identification by renaming "Smoke Test Report" to "Test Report". --- .github/workflows/dotnet.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 0bfc8aa..2b47d44 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -8,6 +8,12 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + workflow_dispatch: + +permissions: + contents: read + actions: read + checks: write jobs: build: @@ -15,14 +21,26 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Setup .NET uses: actions/setup-dotnet@v3 with: dotnet-version: 8.0.x + - name: Restore dependencies run: dotnet restore + - name: Build - run: dotnet build --no-restore + run: dotnet build --no-restore -c Debug + - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test --no-build --verbosity normal -c Debug --logger "trx;LogFileName=test-results.trx" + + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() + with: + name: Tests Results + path: "**/test-results.trx" + reporter: dotnet-trx From 6d785180bcf65720bd0c47e595026963e95f4745 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:05:31 +1000 Subject: [PATCH 50/87] =?UTF-8?q?=F0=9F=90=9B=20Fix=20broken=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/WarehouseIntegrationTestBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs index 7ce3cd8..578579c 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs @@ -4,7 +4,8 @@ namespace Modules.Warehouse.Tests.Common; -public abstract class WarehouseDatabaseFixture : TestingDatabaseFixture; +// ReSharper disable once ClassNeverInstantiated.Global +public class WarehouseDatabaseFixture : TestingDatabaseFixture; [Collection(TestingDatabaseFixtureCollection.Name)] public abstract class WarehouseIntegrationTestBase( From b2f680ff69ee2ff5fcc6f70d80700c383ae4339f Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Fri, 30 Aug 2024 06:01:44 +1000 Subject: [PATCH 51/87] =?UTF-8?q?=E2=9C=A8=20Add=20create=20category=20com?= =?UTF-8?q?mand=20and=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `CreateCategoryCommand` with validation and handler, enabling category creation. Additionally, integrate endpoint mapping in `CatalogModule` and add comprehensive integration tests to verify functionality and error handling. #39 --- .../Categories/ProductIntegrationTests.cs | 96 +++++++++++++++++++ .../Common/WarehouseIntegrationTestBase.cs | 24 +++++ .../Modules.Catalog.Tests.csproj | 1 + .../Products/Modules.Catalog/CatalogModule.cs | 6 +- .../Categories/CreateProductCommand.cs | 65 +++++++++++++ .../Categories/Domain/CategoryErrors.cs | 10 ++ .../Common/Persistence/CatalogDbContext.cs | 2 +- .../Products/UseCases/CreateProductCommand.cs | 8 +- 8 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 src/Modules/Products/Modules.Catalog.Tests/Categories/ProductIntegrationTests.cs create mode 100644 src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs create mode 100644 src/Modules/Products/Modules.Catalog/Categories/CreateProductCommand.cs create mode 100644 src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs diff --git a/src/Modules/Products/Modules.Catalog.Tests/Categories/ProductIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Categories/ProductIntegrationTests.cs new file mode 100644 index 0000000..70f4648 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Tests/Categories/ProductIntegrationTests.cs @@ -0,0 +1,96 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Categories; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Tests.Common; +using System.Net; +using System.Net.Http.Json; +using Xunit.Abstractions; + +namespace Modules.Catalog.Tests.Categories; + +public class CategoryIntegrationTests(CatalogDatabaseFixture fixture, ITestOutputHelper output) + : CatalogIntegrationTestBase(fixture, output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public async Task CreateCategory_ValidRequest_ShouldReturnCreated() + { + // Arrange + var client = GetAnonymousClient(); + + var request = new CreateCategoryCommand.Request("Name"); + + // Act + var response = await client.PostAsJsonAsync("/api/categories", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + var categories = await GetQueryable().ToListAsync(); + categories.Should().HaveCount(1); + + var category = categories.First(); + category.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + category.CreatedBy.Should().NotBeNullOrWhiteSpace(); + category.Name.Should().Be(request.Name); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task CreateCategory_InvalidRequest_ReturnsBadRequest(string name) + { + // Arrange + var client = GetAnonymousClient(); + var request = new CreateCategoryCommand.Request(name); + + // Act + var response = await client.PostAsJsonAsync("/api/categories", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var categories = await GetQueryable().ToListAsync(); + categories.Should().HaveCount(0); + } + + [Fact] + public async Task CreateCategory_DuplicateRequest_ReturnsBadRequest() + { + // Arrange + var client = GetAnonymousClient(); + var request = new CreateCategoryCommand.Request("Name"); + + // Act + var response = await client.PostAsJsonAsync("/api/categories", request); + response.StatusCode.Should().Be(HttpStatusCode.Created); + var response2 = await client.PostAsJsonAsync("/api/categories", request); + + // Assert + response2.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + // + // [Theory] + // [InlineData(null, "12345678")] + // [InlineData("", "12345678")] + // [InlineData(" ", "12345678")] + // [InlineData("name", null)] + // [InlineData("name", "")] + // [InlineData("name", "123")] + // public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, string sku) + // { + // // Arrange + // var client = GetAnonymousClient(); + // var request = new CreateProductCommand.Request(name, sku); + // + // // Act + // var response = await client.PostAsJsonAsync("/api/products", request); + // + // // Assert + // response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + // var content = await response.Content.ReadAsStringAsync(); + // _output.WriteLine(content); + // } +} diff --git a/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs b/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs new file mode 100644 index 0000000..fa07b40 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs @@ -0,0 +1,24 @@ +using Common.Tests.Common; +using Modules.Catalog.Common.Persistence; +using Xunit.Abstractions; + +namespace Modules.Catalog.Tests.Common; + +// ReSharper disable once ClassNeverInstantiated.Global +public class CatalogDatabaseFixture : TestingDatabaseFixture; + +[Collection(TestingDatabaseFixtureCollection.Name)] +public abstract class CatalogIntegrationTestBase( + CatalogDatabaseFixture fixture, + ITestOutputHelper output) + : IntegrationTestBase(fixture, output); + +[CollectionDefinition(Name)] +public class TestingDatabaseFixtureCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + + public const string Name = nameof(TestingDatabaseFixtureCollection); +} diff --git a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj index e0d3a67..1ab3684 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj +++ b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 53050ce..49df158 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Modules.Catalog.Categories; using Modules.Catalog.Common.Persistence; namespace Modules.Catalog; @@ -24,9 +25,6 @@ public static void UseCatalog(this WebApplication app) // app.UseInfrastructureMiddleware(); // // TODO: Consider source generation or reflection for endpoint mapping - // CreateAisleCommand.Endpoint.MapEndpoint(app); - // CreateProductCommand.Endpoint.MapEndpoint(app); - // AllocateStorageCommand.Endpoint.MapEndpoint(app); - // GetItemLocationQuery.Endpoint.MapEndpoint(app); + CreateCategoryCommand.Endpoint.MapEndpoint(app); } } diff --git a/src/Modules/Products/Modules.Catalog/Categories/CreateProductCommand.cs b/src/Modules/Products/Modules.Catalog/Categories/CreateProductCommand.cs new file mode 100644 index 0000000..e40c72d --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Categories/CreateProductCommand.cs @@ -0,0 +1,65 @@ +using Common.SharedKernel; +using Common.SharedKernel.Api; +using ErrorOr; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Common.Persistence; + +namespace Modules.Catalog.Categories; + +public static class CreateCategoryCommand +{ + public record Request(string Name) : IRequest>; + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/categories", async (Request request, ISender sender) => + { + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Created(); + }) + .WithName("CreateCategory") + .WithTags("Catalog") + .ProducesPost() + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.Name).NotEmpty(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly CatalogDbContext _dbContext; + + public Handler(CatalogDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var exists = _dbContext.Categories.Any(c => c.Name == request.Name); + + if (exists) + return CategoryErrors.DuplicateName; + + var category = Category.Create(request.Name); + _dbContext.Categories.Add(category); + await _dbContext.SaveChangesAsync(cancellationToken); + + return Result.Success; + } + } +} diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs new file mode 100644 index 0000000..da34620 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs @@ -0,0 +1,10 @@ +using ErrorOr; + +namespace Modules.Catalog.Categories.Domain; + +public static class CategoryErrors +{ + public static readonly Error DuplicateName = Error.Validation( + "Category.DuplicateName", + "Can't create categories with duplicate names"); +} diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs index afd9054..0fd6965 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs @@ -4,7 +4,7 @@ namespace Modules.Catalog.Common.Persistence; -internal class CatalogDbContext : DbContext +public class CatalogDbContext : DbContext { internal DbSet Products => Set(); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs index 564530c..17ba227 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs @@ -42,11 +42,11 @@ public Validator() internal class Handler : IRequestHandler> { - private readonly WarehouseDbContext _dbDbContext; + private readonly WarehouseDbContext _dbContext; public Handler(WarehouseDbContext dbContext) { - _dbDbContext = dbContext; + _dbContext = dbContext; } public async Task> Handle(Request request, CancellationToken cancellationToken) @@ -54,8 +54,8 @@ public async Task> Handle(Request request, CancellationToken ca var sku = Sku.Create(request.Sku); var product = Product.Create(request.Name, sku); - _dbDbContext.Products.Add(product); - await _dbDbContext.SaveChangesAsync(cancellationToken); + _dbContext.Products.Add(product); + await _dbContext.SaveChangesAsync(cancellationToken); return Result.Success; } From 8ee7978931bd57f3208ce30a5a9b4c53092c5800 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Fri, 30 Aug 2024 06:50:04 +1000 Subject: [PATCH 52/87] =?UTF-8?q?=E2=9C=A8=20Add=20commands=20to=20manage?= =?UTF-8?q?=20product=20categories=20in=20the=20catalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented `AddProductCategoryCommand` and `RemoveProductCategoryCommand` to handle adding and removing categories from products. Created `ProductErrors` and updated various references to accommodate these changes. #37 #48 --- .../Products/ProductTests.cs | 2 +- .../Categories/Domain/CategoryErrors.cs | 4 + .../Common/Persistence/CatalogDbContext.cs | 2 +- .../Configuration/ProductConfiguration.cs | 2 +- .../Products/{ => Domain}/Product.cs | 2 +- .../Products/Domain/ProductErrors.cs | 10 +++ .../ProductStoredIntegrationEventHandler.cs | 1 + .../UseCases/AddProductCategoryCommand.cs | 83 +++++++++++++++++++ .../UseCases/RemoveProductCategoryCommand.cs | 83 +++++++++++++++++++ .../EventualConsistencyMiddleware.cs | 1 + .../CatalogDbContextInitialiser.cs | 4 +- 11 files changed, 188 insertions(+), 6 deletions(-) rename src/Modules/Products/Modules.Catalog/Products/{ => Domain}/Product.cs (97%) create mode 100644 src/Modules/Products/Modules.Catalog/Products/Domain/ProductErrors.cs create mode 100644 src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs create mode 100644 src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs diff --git a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductTests.cs index ffbf86e..c14f94d 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductTests.cs @@ -1,7 +1,7 @@ using Common.SharedKernel.Domain.Entities; using FluentAssertions; using Modules.Catalog.Categories.Domain; -using Modules.Catalog.Products; +using Modules.Catalog.Products.Domain; namespace Modules.Catalog.Tests.Products; diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs index da34620..b44c285 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/CategoryErrors.cs @@ -7,4 +7,8 @@ public static class CategoryErrors public static readonly Error DuplicateName = Error.Validation( "Category.DuplicateName", "Can't create categories with duplicate names"); + + public static readonly Error NotFound = Error.NotFound( + "Category.NotFound", + "Category not found"); } diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs index 0fd6965..9b6350f 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; using Modules.Catalog.Categories.Domain; -using Modules.Catalog.Products; +using Modules.Catalog.Products.Domain; namespace Modules.Catalog.Common.Persistence; diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs index 72dcd07..7730042 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/Configuration/ProductConfiguration.cs @@ -2,7 +2,7 @@ using Common.SharedKernel.Persistence.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Modules.Catalog.Products; +using Modules.Catalog.Products.Domain; namespace Modules.Catalog.Common.Persistence.Configuration; diff --git a/src/Modules/Products/Modules.Catalog/Products/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs similarity index 97% rename from src/Modules/Products/Modules.Catalog/Products/Product.cs rename to src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs index 5c79c69..40f4d91 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Product.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs @@ -2,7 +2,7 @@ using Common.SharedKernel.Domain.Entities; using Modules.Catalog.Categories.Domain; -namespace Modules.Catalog.Products; +namespace Modules.Catalog.Products.Domain; internal record ProductId(Guid Value) : IStronglyTypedId; diff --git a/src/Modules/Products/Modules.Catalog/Products/Domain/ProductErrors.cs b/src/Modules/Products/Modules.Catalog/Products/Domain/ProductErrors.cs new file mode 100644 index 0000000..1f1e05c --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/Domain/ProductErrors.cs @@ -0,0 +1,10 @@ +using ErrorOr; + +namespace Modules.Catalog.Products.Domain; + +public static class ProductErrors +{ + public static readonly Error NotFound = Error.NotFound( + "Product.NotFound", + "Product with the specified ID does not exist."); +} diff --git a/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs b/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs index e74e907..3483a76 100644 --- a/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs +++ b/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs @@ -1,6 +1,7 @@ using MediatR; using Microsoft.Extensions.Logging; using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.Domain; using Modules.Warehouse.Messages; namespace Modules.Catalog.Products.IntegrationEvents; diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs new file mode 100644 index 0000000..1393639 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs @@ -0,0 +1,83 @@ +using Common.SharedKernel; +using Common.SharedKernel.Api; +using ErrorOr; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.Domain; + +namespace Modules.Catalog.Products.UseCases; + +public static class AddProductCategoryCommand +{ + public record Request(Guid ProductId, Guid CategoryId) : IRequest>; + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/products/{productId:guid}/category/{categoryId:guid}", + async (Guid productId, Guid categoryId, ISender sender) => + { + var request = new Request(productId, categoryId); + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Created(); + }) + .WithName("AddProductCategory") + .WithTags("Catalog") + .ProducesPost() + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.ProductId) + .NotEmpty(); + + RuleFor(r => r.CategoryId) + .NotEmpty(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly CatalogDbContext _dbContext; + + public Handler(CatalogDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var productId = new ProductId(request.ProductId); + var product = + await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == productId, + cancellationToken: cancellationToken); + + if (product is null) + return ProductErrors.NotFound; + + var categoryId = new CategoryId(request.CategoryId); + var category = + await _dbContext.Categories.FirstOrDefaultAsync(c => c.Id == categoryId, + cancellationToken: cancellationToken); + + if (category is null) + return CategoryErrors.NotFound; + + product.AddCategory(category); + await _dbContext.SaveChangesAsync(cancellationToken); + + return Result.Success; + } + } +} diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs new file mode 100644 index 0000000..7ad9cc5 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs @@ -0,0 +1,83 @@ +using Common.SharedKernel; +using Common.SharedKernel.Api; +using ErrorOr; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.Domain; + +namespace Modules.Catalog.Products.UseCases; + +public static class RemoveProductCategoryCommand +{ + public record Request(Guid ProductId, Guid CategoryId) : IRequest>; + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapDelete("/api/products/{productId:guid}/category/{categoryId:guid}", + async (Guid productId, Guid categoryId, ISender sender) => + { + var request = new Request(productId, categoryId); + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.NoContent(); + }) + .WithName("RemoveProductCategory") + .WithTags("Catalog") + .ProducesDelete() + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.ProductId) + .NotEmpty(); + + RuleFor(r => r.CategoryId) + .NotEmpty(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly CatalogDbContext _dbContext; + + public Handler(CatalogDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var productId = new ProductId(request.ProductId); + var product = + await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == productId, + cancellationToken: cancellationToken); + + if (product is null) + return ProductErrors.NotFound; + + var categoryId = new CategoryId(request.CategoryId); + var category = + await _dbContext.Categories.FirstOrDefaultAsync(c => c.Id == categoryId, + cancellationToken: cancellationToken); + + if (category is null) + return CategoryErrors.NotFound; + + product.RemoveCategory(category); + await _dbContext.SaveChangesAsync(cancellationToken); + + return Result.Success; + } + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs index cd0f0c2..b64b10d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs @@ -17,6 +17,7 @@ public EventualConsistencyMiddleware(RequestDelegate next) } // TODO: See if we can make this middleware generic + // TODO: Possibly use IDbContextFactory to dynamically create the context public async Task InvokeAsync(HttpContext context, IPublisher publisher, WarehouseDbContext dbContext) { var transaction = await dbContext.Database.BeginTransactionAsync(); diff --git a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs index d25b149..7fbba6f 100644 --- a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs +++ b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs @@ -4,7 +4,7 @@ using Modules.Catalog.Categories.Domain; using Modules.Catalog.Common.Persistence; using Modules.Warehouse.Products.Domain; -using ProductId = Modules.Catalog.Products.ProductId; +using ProductId = Modules.Catalog.Products.Domain.ProductId; namespace Database.Initialisers; @@ -73,7 +73,7 @@ private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEn // However, to simplify test data seed, we'll manually pass products into the catalog foreach (var warehouseProduct in warehouseProducts) { - var catalogProduct = Modules.Catalog.Products.Product.Create( + var catalogProduct = Modules.Catalog.Products.Domain.Product.Create( warehouseProduct.Name, warehouseProduct.Sku.Value, new ProductId(warehouseProduct.Id.Value)); From 39d092311675bf13603c6039786c070b8a6d6a9e Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Fri, 30 Aug 2024 07:16:37 +1000 Subject: [PATCH 53/87] =?UTF-8?q?=F0=9F=A7=AA=20Implement=20product=20cate?= =?UTF-8?q?gory=20management=20with=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added endpoints for adding and removing product categories, utilizing specifications for querying products. Included integration tests for ensuring correct functionality and response handling for these actions. #37 #48 --- .../Common.Tests/Common/DatabaseContainer.cs | 20 +++---- .../Common.Tests/Common/WebApiTestFactory.cs | 2 +- ...onTests.cs => CategoryIntegrationTests.cs} | 23 -------- .../Common/WarehouseIntegrationTestBase.cs | 6 +- .../Products/ProductIntegrationTests.cs | 56 +++++++++++++++++++ .../Products/Modules.Catalog/CatalogModule.cs | 3 + .../Products/Domain/ProductByIdSpec.cs | 13 +++++ .../UseCases/AddProductCategoryCommand.cs | 9 +-- .../UseCases/RemoveProductCategoryCommand.cs | 9 +-- .../Common/WarehouseIntegrationTestBase.cs | 6 +- 10 files changed, 98 insertions(+), 49 deletions(-) rename src/Modules/Products/Modules.Catalog.Tests/Categories/{ProductIntegrationTests.cs => CategoryIntegrationTests.cs} (75%) create mode 100644 src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs create mode 100644 src/Modules/Products/Modules.Catalog/Products/Domain/ProductByIdSpec.cs diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index 5aefe67..40b837b 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -7,18 +7,16 @@ namespace Common.Tests.Common; /// public class DatabaseContainer { - // private static readonly int _exposedPort = Random.Shared.Next(10000, 60000); - // private static readonly int _exposedPort = 20_000; - // private static readonly int _internalPort = 1433; + private readonly SqlEdgeContainer _container; - private readonly SqlEdgeContainer _container = new SqlEdgeBuilder() - .WithName("Modular-Monolith-IntegrationTests-DbContainer") - .WithPassword("Password123") - .WithAutoRemove(true) - // .WithPortBinding(_internalPort) - // // .WithExposedPort(_exposedPort) - // .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(_internalPort)) - .Build(); + public DatabaseContainer(string name) + { + _container = new SqlEdgeBuilder() + .WithName($"Modular-Monolith-Tests-{name}") + .WithPassword("Password123") + .WithAutoRemove(true) + .Build(); + } public string? ConnectionString { get; private set; } diff --git a/src/Common/Common.Tests/Common/WebApiTestFactory.cs b/src/Common/Common.Tests/Common/WebApiTestFactory.cs index e4f6841..5faf380 100644 --- a/src/Common/Common.Tests/Common/WebApiTestFactory.cs +++ b/src/Common/Common.Tests/Common/WebApiTestFactory.cs @@ -16,7 +16,7 @@ namespace Common.Tests.Common; /// public class WebApiTestFactory : WebApplicationFactory where TDbContext : DbContext { - public DatabaseContainer Database { get; } = new(); + public DatabaseContainer Database { get; } = new(typeof(TDbContext).Name); public ITestOutputHelper? Output { private get; set; } diff --git a/src/Modules/Products/Modules.Catalog.Tests/Categories/ProductIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryIntegrationTests.cs similarity index 75% rename from src/Modules/Products/Modules.Catalog.Tests/Categories/ProductIntegrationTests.cs rename to src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryIntegrationTests.cs index 70f4648..0df7de2 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Categories/ProductIntegrationTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryIntegrationTests.cs @@ -70,27 +70,4 @@ public async Task CreateCategory_DuplicateRequest_ReturnsBadRequest() // Assert response2.StatusCode.Should().Be(HttpStatusCode.BadRequest); } - - // - // [Theory] - // [InlineData(null, "12345678")] - // [InlineData("", "12345678")] - // [InlineData(" ", "12345678")] - // [InlineData("name", null)] - // [InlineData("name", "")] - // [InlineData("name", "123")] - // public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, string sku) - // { - // // Arrange - // var client = GetAnonymousClient(); - // var request = new CreateProductCommand.Request(name, sku); - // - // // Act - // var response = await client.PostAsJsonAsync("/api/products", request); - // - // // Assert - // response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - // var content = await response.Content.ReadAsStringAsync(); - // _output.WriteLine(content); - // } } diff --git a/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs b/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs index fa07b40..016ffe7 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs @@ -7,18 +7,18 @@ namespace Modules.Catalog.Tests.Common; // ReSharper disable once ClassNeverInstantiated.Global public class CatalogDatabaseFixture : TestingDatabaseFixture; -[Collection(TestingDatabaseFixtureCollection.Name)] +[Collection(CatalogFixtureCollection.Name)] public abstract class CatalogIntegrationTestBase( CatalogDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output); [CollectionDefinition(Name)] -public class TestingDatabaseFixtureCollection : ICollectionFixture +public class CatalogFixtureCollection : ICollectionFixture { // This class has no code, and is never created. Its purpose is simply // to be the place to apply [CollectionDefinition] and all the // ICollectionFixture<> interfaces. - public const string Name = nameof(TestingDatabaseFixtureCollection); + public const string Name = nameof(CatalogFixtureCollection); } diff --git a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs new file mode 100644 index 0000000..3e8f0fc --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs @@ -0,0 +1,56 @@ +using Ardalis.Specification.EntityFrameworkCore; +using FluentAssertions; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Products.Domain; +using Modules.Catalog.Tests.Common; +using System.Net; +using Xunit.Abstractions; + +namespace Modules.Catalog.Tests.Products; + +public class ProductIntegrationTests(CatalogDatabaseFixture fixture, ITestOutputHelper output) + : CatalogIntegrationTestBase(fixture, output) +{ + [Fact] + public async Task AddProductCategory_ValidRequest_ShouldReturnNotContent() + { + // Arrange + var client = GetAnonymousClient(); + var category = Category.Create("category"); + await AddEntityAsync(category); + var product = Product.Create("product", "12345678"); + await AddEntityAsync(product); + + // Act + var response = await client.PostAsync($"/api/products/{product.Id.Value}/categories/{category.Id.Value}",null); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + var updatedProduct = GetQueryable().WithSpecification(new ProductByIdSpec(new ProductId(product.Id.Value))).FirstOrDefault(); + updatedProduct.Should().NotBeNull(); + updatedProduct!.Categories.Should().HaveCount(1); + updatedProduct.Categories[0].Should().BeEquivalentTo(category); + } + + [Fact] + public async Task RemoveProductCategory_ValidRequest_ShouldReturnNotContent() + { + // Arrange + var client = GetAnonymousClient(); + var category = Category.Create("category"); + await AddEntityAsync(category); + var product = Product.Create("product", "12345678"); + await AddEntityAsync(product); + product.AddCategory(category); + await SaveAsync(); + + // Act + var response = await client.DeleteAsync($"/api/products/{product.Id.Value}/categories/{category.Id.Value}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + var updatedProduct = GetQueryable().WithSpecification(new ProductByIdSpec(new ProductId(product.Id.Value))).FirstOrDefault(); + updatedProduct.Should().NotBeNull(); + updatedProduct!.Categories.Should().HaveCount(0); + } +} diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 49df158..52fed99 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Modules.Catalog.Categories; using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.UseCases; namespace Modules.Catalog; @@ -26,5 +27,7 @@ public static void UseCatalog(this WebApplication app) // // TODO: Consider source generation or reflection for endpoint mapping CreateCategoryCommand.Endpoint.MapEndpoint(app); + AddProductCategoryCommand.Endpoint.MapEndpoint(app); + RemoveProductCategoryCommand.Endpoint.MapEndpoint(app); } } diff --git a/src/Modules/Products/Modules.Catalog/Products/Domain/ProductByIdSpec.cs b/src/Modules/Products/Modules.Catalog/Products/Domain/ProductByIdSpec.cs new file mode 100644 index 0000000..b83f8c3 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/Domain/ProductByIdSpec.cs @@ -0,0 +1,13 @@ +using Ardalis.Specification; + +namespace Modules.Catalog.Products.Domain; + +internal class ProductByIdSpec : Specification +{ + public ProductByIdSpec(ProductId id) + { + Query + .Include(p => p.Categories) + .Where(p => p.Id == id); + } +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs index 1393639..2963939 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs @@ -1,3 +1,4 @@ +using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; using ErrorOr; @@ -21,7 +22,7 @@ public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/products/{productId:guid}/category/{categoryId:guid}", + app.MapPost("/api/products/{productId:guid}/categories/{categoryId:guid}", async (Guid productId, Guid categoryId, ISender sender) => { var request = new Request(productId, categoryId); @@ -59,9 +60,9 @@ public Handler(CatalogDbContext dbContext) public async Task> Handle(Request request, CancellationToken cancellationToken) { var productId = new ProductId(request.ProductId); - var product = - await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == productId, - cancellationToken: cancellationToken); + var product = await _dbContext.Products + .WithSpecification(new ProductByIdSpec(productId)) + .FirstOrDefaultAsync(cancellationToken); if (product is null) return ProductErrors.NotFound; diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs index 7ad9cc5..a344d0d 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs @@ -1,3 +1,4 @@ +using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; using ErrorOr; @@ -21,7 +22,7 @@ public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapDelete("/api/products/{productId:guid}/category/{categoryId:guid}", + app.MapDelete("/api/products/{productId:guid}/categories/{categoryId:guid}", async (Guid productId, Guid categoryId, ISender sender) => { var request = new Request(productId, categoryId); @@ -59,9 +60,9 @@ public Handler(CatalogDbContext dbContext) public async Task> Handle(Request request, CancellationToken cancellationToken) { var productId = new ProductId(request.ProductId); - var product = - await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == productId, - cancellationToken: cancellationToken); + var product = await _dbContext.Products + .WithSpecification(new ProductByIdSpec(productId)) + .FirstOrDefaultAsync(cancellationToken); if (product is null) return ProductErrors.NotFound; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs index 578579c..0e0fe69 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs @@ -7,18 +7,18 @@ namespace Modules.Warehouse.Tests.Common; // ReSharper disable once ClassNeverInstantiated.Global public class WarehouseDatabaseFixture : TestingDatabaseFixture; -[Collection(TestingDatabaseFixtureCollection.Name)] +[Collection(WarehouseFixtureCollection.Name)] public abstract class WarehouseIntegrationTestBase( WarehouseDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output); [CollectionDefinition(Name)] -public class TestingDatabaseFixtureCollection : ICollectionFixture +public class WarehouseFixtureCollection : ICollectionFixture { // This class has no code, and is never created. Its purpose is simply // to be the place to apply [CollectionDefinition] and all the // ICollectionFixture<> interfaces. - public const string Name = nameof(TestingDatabaseFixtureCollection); + public const string Name = nameof(WarehouseFixtureCollection); } From 869eded2345959e8e8384447251ea515a42bb614 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 3 Sep 2024 06:02:40 +1000 Subject: [PATCH 54/87] =?UTF-8?q?=E2=9C=A8=20Add=20GetProductQuery=20use?= =?UTF-8?q?=20case=20to=20CatalogModule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented the GetProductQuery use case including request, response, handler, and endpoint mapping. This new feature allows retrieving product details by product ID in the CatalogModule. #36 --- .../Modules.Orders/Orders/Order/Order.cs | 4 +- .../Products/Modules.Catalog/CatalogModule.cs | 1 + .../Products/UseCases/GetProductQuery.cs | 65 +++++++++++++++++++ .../Modules.Warehouse/BackOrders/BackOrder.cs | 2 + 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs index 3bc23ac..d0df741 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs @@ -28,9 +28,11 @@ internal class Order : AggregateRoot public Money AmountPaid { get; private set; } = null!; +#pragma warning disable CS0414 // Field is assigned but its value is never used private Payment.Payment _payment = null!; +#pragma warning restore CS0414 // Field is assigned but its value is never used - public OrderStatus Status { get; private set; } + public OrderStatus Status { get; private set; } = null!; public DateTimeOffset ShippingDate { get; private set; } diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 52fed99..3524fe1 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -29,5 +29,6 @@ public static void UseCatalog(this WebApplication app) CreateCategoryCommand.Endpoint.MapEndpoint(app); AddProductCategoryCommand.Endpoint.MapEndpoint(app); RemoveProductCategoryCommand.Endpoint.MapEndpoint(app); + GetProductQuery.Endpoint.MapEndpoint(app); } } diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs new file mode 100644 index 0000000..48700f6 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs @@ -0,0 +1,65 @@ +using Ardalis.Specification.EntityFrameworkCore; +using Common.SharedKernel; +using Common.SharedKernel.Api; +using ErrorOr; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.Domain; + +namespace Modules.Catalog.Products.UseCases; + +public static class GetProductQuery +{ + public record Request(Guid ProductId) : IRequest>; + + public record Response(string Name, Guid Id, string Sku, decimal Price, List Categories); + + public record CategoryDto(Guid Id, string Name); + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("/api/products/{productId:guid}", + async (Guid productId, ISender sender) => + { + var request = new Request(productId); + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.NoContent(); + }) + .WithName("GetProduct") + .WithTags("Catalog") + .ProducesGet() + .WithOpenApi(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly CatalogDbContext _dbContext; + + public Handler(CatalogDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var productId = new ProductId(request.ProductId); + var product = await _dbContext.Products + .WithSpecification(new ProductByIdSpec(productId)) + .Select(p => new Response(p.Name, p.Id.Value, p.Sku, p.Price.Amount, + p.Categories.Select(c => new CategoryDto(c.Id.Value, c.Name)).ToList())) + .FirstOrDefaultAsync(cancellationToken); + + if (product is null) + return ProductErrors.NotFound; + + return product; + } + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs index 6aa8a7f..c10f093 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs @@ -2,6 +2,8 @@ using Common.SharedKernel.Domain.Base; using Modules.Warehouse.Products.Domain; +#pragma warning disable CS0414 // Field is assigned but its value is never used + namespace Modules.Warehouse.BackOrders; internal record BackOrderId(Guid Value); From 3c8b3b0df54ed445b98ab8bd46583b31fe03287a Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 3 Sep 2024 06:13:09 +1000 Subject: [PATCH 55/87] =?UTF-8?q?=F0=9F=A7=AA=20Improve=20GetProductQuery?= =?UTF-8?q?=20response=20handling=20and=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated GetProductQuery to return Ok with the response value instead of NoContent. Added integration tests to verify correct behavior when a product exists and when it does not. #36 --- .../Products/ProductIntegrationTests.cs | 41 +++++++++++++++++++ .../Products/UseCases/GetProductQuery.cs | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs index 3e8f0fc..51365f1 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs @@ -2,8 +2,10 @@ using FluentAssertions; using Modules.Catalog.Categories.Domain; using Modules.Catalog.Products.Domain; +using Modules.Catalog.Products.UseCases; using Modules.Catalog.Tests.Common; using System.Net; +using System.Net.Http.Json; using Xunit.Abstractions; namespace Modules.Catalog.Tests.Products; @@ -53,4 +55,43 @@ public async Task RemoveProductCategory_ValidRequest_ShouldReturnNotContent() updatedProduct.Should().NotBeNull(); updatedProduct!.Categories.Should().HaveCount(0); } + + [Fact] + public async Task GetProductQuery_WhenProductExists_ShouldReturnOk() + { + // Arrange + var client = GetAnonymousClient(); + var category = Category.Create("category"); + await AddEntityAsync(category); + var product = Product.Create("product", "12345678"); + await AddEntityAsync(product); + product.AddCategory(category); + await SaveAsync(); + + // Act + var response = await client.GetAsync($"/api/products/{product.Id.Value}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var json = await response.Content.ReadFromJsonAsync(); + json.Should().NotBeNull(); + json!.Name.Should().Be(product.Name); + json.Sku.Should().Be(product.Sku); + json.Price.Should().Be(product.Price.Amount); + json.Categories.Should().HaveCount(1); + json.Categories[0].Name.Should().Be(category.Name); + } + + [Fact] + public async Task GetProductQuery_WhenProductDoesNotExist_ShouldReturnNotFound() + { + // Arrange + var client = GetAnonymousClient(); + + // Act + var response = await client.GetAsync($"/api/products/{Guid.NewGuid()}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } } diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs index 48700f6..e33a8c0 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs @@ -29,7 +29,7 @@ public static void MapEndpoint(IEndpointRouteBuilder app) { var request = new Request(productId); var response = await sender.Send(request); - return response.IsError ? response.Problem() : TypedResults.NoContent(); + return response.IsError ? response.Problem() : TypedResults.Ok(response.Value); }) .WithName("GetProduct") .WithTags("Catalog") From 5d2f882c56232ee2559319a0d4e49f55caf2dfd9 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 3 Sep 2024 06:23:40 +1000 Subject: [PATCH 56/87] =?UTF-8?q?=E2=9C=A8=20Add=20UpdateProductPriceComma?= =?UTF-8?q?nd=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented UpdateProductPriceCommand including request validation and handler logic to update a product price in the database. Added a corresponding API endpoint to handle update requests and integrated it with the mediator pattern. --- ModularMonolith.Catalog.slnf | 13 +++ .../UseCases/UpdateProductPriceCommand.cs | 79 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 ModularMonolith.Catalog.slnf create mode 100644 src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs diff --git a/ModularMonolith.Catalog.slnf b/ModularMonolith.Catalog.slnf new file mode 100644 index 0000000..8fecd2f --- /dev/null +++ b/ModularMonolith.Catalog.slnf @@ -0,0 +1,13 @@ +{ + "solution": { + "path": "ModularMonolith.sln", + "projects": [ + "src\\Common\\Common.SharedKernel\\Common.SharedKernel.csproj", + "src\\Common\\Common.Tests\\Common.Tests.csproj", + "src\\Modules\\Products\\Modules.Catalog.Tests\\Modules.Catalog.Tests.csproj", + "src\\Modules\\Products\\Modules.Catalog\\Modules.Catalog.csproj", + "src\\WebApi\\WebApi.csproj", + "tools\\Database\\Database.csproj" + ] + } +} diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs new file mode 100644 index 0000000..0e6e17a --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs @@ -0,0 +1,79 @@ +using Ardalis.Specification.EntityFrameworkCore; +using Common.SharedKernel; +using Common.SharedKernel.Api; +using Common.SharedKernel.Domain.Entities; +using ErrorOr; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.Domain; +using System.Text.Json.Serialization; + +namespace Modules.Catalog.Products.UseCases; + +public static class UpdateProductPriceCommand +{ + public record Request([property: FromRoute]Guid ProductId, decimal Price) : IRequest>; + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/products/{productId:guid}", + async (Request request, ISender sender) => + { + // var request = new Request(productId, categoryId); + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.NoContent(); + }) + .WithName("UpdateProductPrice") + .WithTags("Catalog") + .ProducesPost() + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.ProductId) + .NotEmpty(); + + RuleFor(r => r.Price) + .NotEmpty(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly CatalogDbContext _dbContext; + + public Handler(CatalogDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var productId = new ProductId(request.ProductId); + var product = await _dbContext.Products + .WithSpecification(new ProductByIdSpec(productId)) + .FirstOrDefaultAsync(cancellationToken); + + if (product is null) + return ProductErrors.NotFound; + + var money = Money.Create(request.Price); + product.UpdatePrice(money); + + return Result.Success; + } + } +} From b604dafeb8493d3f3b75fd0f457075f0af42b66b Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 3 Sep 2024 06:45:19 +1000 Subject: [PATCH 57/87] =?UTF-8?q?=E2=9C=A8=20Add=20update=20product=20pric?= =?UTF-8?q?e=20endpoint=20and=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored the `UpdateProductPriceCommand` to include a PUT endpoint for updating product prices. Added new integration tests to verify the functionality and ensure the product price is updated correctly in the database. #38 --- .../Common.SharedKernel.csproj | 2 +- .../EntitySaveChangesInterceptor.cs | 2 ++ .../Products/ProductIntegrationTests.cs | 29 +++++++++++++++++-- .../Products/Modules.Catalog/CatalogModule.cs | 1 + .../Modules.Catalog/Modules.Catalog.csproj | 6 ++-- .../UseCases/UpdateProductPriceCommand.cs | 15 ++++++---- .../Modules.Warehouse.csproj | 6 ++-- 7 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 9fdee9e..e7db165 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs b/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs index 2f1400a..bf78d2c 100644 --- a/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs +++ b/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs @@ -28,6 +28,8 @@ private void UpdateEntities(DbContext? context) if (context is null) return; + var entries = context.ChangeTracker.Entries(); + foreach (var entry in context.ChangeTracker.Entries()) { if (entry.State is EntityState.Added) diff --git a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs index 51365f1..5cdd249 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs @@ -24,11 +24,12 @@ public async Task AddProductCategory_ValidRequest_ShouldReturnNotContent() await AddEntityAsync(product); // Act - var response = await client.PostAsync($"/api/products/{product.Id.Value}/categories/{category.Id.Value}",null); + var response = await client.PostAsync($"/api/products/{product.Id.Value}/categories/{category.Id.Value}", null); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); - var updatedProduct = GetQueryable().WithSpecification(new ProductByIdSpec(new ProductId(product.Id.Value))).FirstOrDefault(); + var updatedProduct = GetQueryable() + .WithSpecification(new ProductByIdSpec(new ProductId(product.Id.Value))).FirstOrDefault(); updatedProduct.Should().NotBeNull(); updatedProduct!.Categories.Should().HaveCount(1); updatedProduct.Categories[0].Should().BeEquivalentTo(category); @@ -51,7 +52,8 @@ public async Task RemoveProductCategory_ValidRequest_ShouldReturnNotContent() // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); - var updatedProduct = GetQueryable().WithSpecification(new ProductByIdSpec(new ProductId(product.Id.Value))).FirstOrDefault(); + var updatedProduct = GetQueryable() + .WithSpecification(new ProductByIdSpec(new ProductId(product.Id.Value))).FirstOrDefault(); updatedProduct.Should().NotBeNull(); updatedProduct!.Categories.Should().HaveCount(0); } @@ -94,4 +96,25 @@ public async Task GetProductQuery_WhenProductDoesNotExist_ShouldReturnNotFound() // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + + [Fact] + public async Task UpdateProductPrice_ValidRequest_ShouldReturnNoContent() + { + // Arrange + var client = GetAnonymousClient(); + var product = Product.Create("product", "12345678"); + await AddEntityAsync(product); + await SaveAsync(); + var request = new UpdateProductPriceCommand.Request(10.0m); + + // Act + var response = await client.PutAsJsonAsync($"/api/products/{product.Id.Value}/price", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + var updatedProduct = GetQueryable() + .WithSpecification(new ProductByIdSpec(new ProductId(product.Id.Value))).FirstOrDefault(); + updatedProduct.Should().NotBeNull(); + updatedProduct!.Price.Amount.Should().Be(request.Price); + } } diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 3524fe1..57394e3 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -30,5 +30,6 @@ public static void UseCatalog(this WebApplication app) AddProductCategoryCommand.Endpoint.MapEndpoint(app); RemoveProductCategoryCommand.Endpoint.MapEndpoint(app); GetProductQuery.Endpoint.MapEndpoint(app); + UpdateProductPriceCommand.Endpoint.MapEndpoint(app); } } diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 1bd9ff1..bbd1df9 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -6,11 +6,11 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs index 0e6e17a..e94685c 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs @@ -7,10 +7,8 @@ using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; -using Modules.Catalog.Categories.Domain; using Modules.Catalog.Common.Persistence; using Modules.Catalog.Products.Domain; using System.Text.Json.Serialization; @@ -19,16 +17,20 @@ namespace Modules.Catalog.Products.UseCases; public static class UpdateProductPriceCommand { - public record Request([property: FromRoute]Guid ProductId, decimal Price) : IRequest>; + public record Request(decimal Price) : IRequest> + { + [JsonIgnore] + public Guid ProductId { get; set; } + } public static class Endpoint { public static void MapEndpoint(IEndpointRouteBuilder app) { - app.MapPost("/api/products/{productId:guid}", - async (Request request, ISender sender) => + app.MapPut("/api/products/{productId:guid}/price", + async (Guid productId, Request request, ISender sender) => { - // var request = new Request(productId, categoryId); + request.ProductId = productId; var response = await sender.Send(request); return response.IsError ? response.Problem() : TypedResults.NoContent(); }) @@ -72,6 +74,7 @@ public async Task> Handle(Request request, CancellationToken ca var money = Money.Create(request.Price); product.UpdatePrice(money); + await _dbContext.SaveChangesAsync(cancellationToken); return Result.Success; } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 268f719..f0694b1 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -6,11 +6,11 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From abb6423b1800d35322c69f3a2efc5c6c839cdbac Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 3 Sep 2024 06:48:38 +1000 Subject: [PATCH 58/87] =?UTF-8?q?=F0=9F=93=A6=20Update=20package=20depende?= =?UTF-8?q?ncies=20across=20several=20projects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgraded various package versions for better compatibility and performance. Changes include Ardalis.Specification, Microsoft.NET.Test.Sdk, xUnit, and other essential packages. Ensure each module and test project maintains consistency with the latest package versions. --- src/Common/Common.SharedKernel/Common.SharedKernel.csproj | 8 ++++---- src/Common/Common.Tests/Common.Tests.csproj | 8 ++++---- .../Modules.Customers.Tests.csproj | 8 ++++---- .../Modules.Orders.Tests/Modules.Orders.Tests.csproj | 8 ++++---- src/Modules/Orders/Modules.Orders/Modules.Orders.csproj | 4 ++-- .../Modules.Catalog.Tests/Modules.Catalog.Tests.csproj | 8 ++++---- .../Products/Modules.Catalog/Modules.Catalog.csproj | 8 ++++---- .../Modules.Warehouse.Tests.csproj | 8 ++++---- .../Warehouse/Modules.Warehouse/Modules.Warehouse.csproj | 8 ++++---- src/WebApi.Tests/WebApi.Tests.csproj | 8 ++++---- src/WebApi/WebApi.csproj | 4 ++-- tools/Database/Database.csproj | 2 +- 12 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index e7db165..573cc91 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -1,10 +1,10 @@  - - - - + + + + diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index 57407d4..4587b20 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -2,7 +2,7 @@ - + @@ -11,12 +11,12 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj index 1f97651..c924585 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj +++ b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj @@ -7,13 +7,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index a7189e3..c58afe5 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -7,13 +7,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index 4e8a635..430cb9d 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj index 1ab3684..22aa707 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj +++ b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj @@ -7,13 +7,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index bbd1df9..0784c6d 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -4,11 +4,11 @@ - - + + - - + + all diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj index 895d0e1..45aa801 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj @@ -7,7 +7,7 @@ - + @@ -16,12 +16,12 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index f0694b1..18a31d3 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -4,11 +4,11 @@ - - + + - - + + all diff --git a/src/WebApi.Tests/WebApi.Tests.csproj b/src/WebApi.Tests/WebApi.Tests.csproj index 120fbeb..e07e68b 100644 --- a/src/WebApi.Tests/WebApi.Tests.csproj +++ b/src/WebApi.Tests/WebApi.Tests.csproj @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index b676224..338d9d6 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/tools/Database/Database.csproj b/tools/Database/Database.csproj index ee9ef1e..cd03fd6 100644 --- a/tools/Database/Database.csproj +++ b/tools/Database/Database.csproj @@ -6,7 +6,7 @@ - + From 180117dd32a2272877d0145be9c524a22d513abc Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 12 Sep 2024 07:05:47 +1000 Subject: [PATCH 59/87] =?UTF-8?q?=E2=9C=A8=20Add=20search=20functionality?= =?UTF-8?q?=20to=20products?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a new `SearchProductsQuery` use case to enable searching for products by name and category. Implemented the corresponding endpoint and handler in the `CatalogModule`. #40 --- .../Products/Modules.Catalog/CatalogModule.cs | 1 + .../Products/UseCases/SearchProductsQuery.cs | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 57394e3..725d285 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -31,5 +31,6 @@ public static void UseCatalog(this WebApplication app) RemoveProductCategoryCommand.Endpoint.MapEndpoint(app); GetProductQuery.Endpoint.MapEndpoint(app); UpdateProductPriceCommand.Endpoint.MapEndpoint(app); + SearchProductsQuery.Endpoint.MapEndpoint(app); } } diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs new file mode 100644 index 0000000..e17aa43 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs @@ -0,0 +1,66 @@ +using Common.SharedKernel; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.Domain; + +namespace Modules.Catalog.Products.UseCases; + +public static class SearchProductsQuery +{ + public record Request(string? Name, Guid? CategoryId) : IRequest>; + + public record Response(string Name, Guid Id, string Sku, decimal Price); + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("/api/products", + async (string name, Guid categoryId, ISender sender) => + { + var request = new Request(name, categoryId); + var response = await sender.Send(request); + TypedResults.Ok(response); + }) + .WithName("SearchProducts") + .WithTags("Catalog") + .ProducesGet() + .WithOpenApi(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly CatalogDbContext _dbContext; + + public Handler(CatalogDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + IQueryable query = _dbContext.Products; + + if (!string.IsNullOrWhiteSpace(request.Name)) + query = query.Where(p => p.Name.Contains(request.Name)); + + if (request.CategoryId is not null) + { + var categoryId = new CategoryId(request.CategoryId.Value); + query = query.Where(p => p.Categories.Any(c => c.Id == categoryId)); + } + + var products = await query + .Select(p => new Response(p.Name, p.Id.Value, p.Sku, p.Price.Amount)) + .ToListAsync(cancellationToken); + + return products; + } + } +} From 33e6ee2151adf8e0d284e0588a38f88adab16441 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 12 Sep 2024 08:48:14 +1000 Subject: [PATCH 60/87] =?UTF-8?q?=E2=9C=A8=20Add=20global=20error=20handle?= =?UTF-8?q?r=20and=20update=20test=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a global error handler to improve error management across the application. Also, updated multiple dependencies to the latest versions and made minor adjustments to the database container initialization process for increased reliability. --- src/Common/Common.Tests/Common.Tests.csproj | 8 ++-- .../Common.Tests/Common/DatabaseContainer.cs | 34 ++++++++------ .../Common.Tests/Common/WebApiTestFactory.cs | 2 +- .../Extensions/ServiceCollectionExt.cs | 4 +- .../Modules.Customers.Tests.csproj | 2 +- .../Modules.Orders.Tests.csproj | 2 +- .../Modules.Catalog.Tests.csproj | 2 +- .../Modules.Warehouse.Tests.csproj | 9 +--- src/WebApi.Tests/WebApi.Tests.csproj | 2 +- src/WebApi/Extensions/GlobalErrors.cs | 45 +++++++++++++++++++ src/WebApi/Program.cs | 2 + 11 files changed, 79 insertions(+), 33 deletions(-) create mode 100644 src/WebApi/Extensions/GlobalErrors.cs diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index 4587b20..acc3eaf 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -1,12 +1,10 @@  - - + - - - + + diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index 40b837b..4e6ac85 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -7,24 +7,32 @@ namespace Common.Tests.Common; /// public class DatabaseContainer { - private readonly SqlEdgeContainer _container; - - public DatabaseContainer(string name) - { - _container = new SqlEdgeBuilder() - .WithName($"Modular-Monolith-Tests-{name}") - .WithPassword("Password123") - .WithAutoRemove(true) - .Build(); - } + private readonly SqlEdgeContainer _container = new SqlEdgeBuilder() + .WithName($"Modular-Monolith-Tests-{Guid.NewGuid()}") + .WithPassword("Password123") + // .WithWaitStrategy(Wait.ForUnixContainer()) + // .WithAutoRemove(true) + .Build(); public string? ConnectionString { get; private set; } public async Task InitializeAsync() { - await _container.StartAsync(); - ConnectionString = _container.GetConnectionString(); + try + { + await _container.StartAsync(); + ConnectionString = _container.GetConnectionString(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } } - public Task DisposeAsync() => _container.StopAsync() ?? Task.CompletedTask; + public async Task DisposeAsync() + { + await _container.StopAsync(); + await _container.DisposeAsync(); + } } diff --git a/src/Common/Common.Tests/Common/WebApiTestFactory.cs b/src/Common/Common.Tests/Common/WebApiTestFactory.cs index 5faf380..e4f6841 100644 --- a/src/Common/Common.Tests/Common/WebApiTestFactory.cs +++ b/src/Common/Common.Tests/Common/WebApiTestFactory.cs @@ -16,7 +16,7 @@ namespace Common.Tests.Common; /// public class WebApiTestFactory : WebApplicationFactory where TDbContext : DbContext { - public DatabaseContainer Database { get; } = new(typeof(TDbContext).Name); + public DatabaseContainer Database { get; } = new(); public ITestOutputHelper? Output { private get; set; } diff --git a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs index f986045..23673c3 100644 --- a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs +++ b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs @@ -22,8 +22,8 @@ internal static IServiceCollection ReplaceDbContext( .RemoveAll() .AddDbContext((_, options) => { - options.UseSqlServer(databaseContainer.ConnectionString, - b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); + options.UseSqlServer(databaseContainer.ConnectionString); + // b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); options.LogTo(m => Debug.WriteLine(m)); options.EnableSensitiveDataLogging(); diff --git a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj index c924585..6929395 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj +++ b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index c58afe5..0da595c 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj index 22aa707..8aee52d 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj +++ b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj index 45aa801..89e9066 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj @@ -7,14 +7,7 @@ - - - - - - - - + diff --git a/src/WebApi.Tests/WebApi.Tests.csproj b/src/WebApi.Tests/WebApi.Tests.csproj index e07e68b..010b620 100644 --- a/src/WebApi.Tests/WebApi.Tests.csproj +++ b/src/WebApi.Tests/WebApi.Tests.csproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/WebApi/Extensions/GlobalErrors.cs b/src/WebApi/Extensions/GlobalErrors.cs new file mode 100644 index 0000000..e27596f --- /dev/null +++ b/src/WebApi/Extensions/GlobalErrors.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace WebApi.Extensions; + +public static class GlobalErrorsExt +{ + public static void AddGlobalErrorHandler(this IServiceCollection services) + { + services.AddExceptionHandler(); + services.AddProblemDetails(); + } +} + +internal sealed class GlobalExceptionHandler : IExceptionHandler +{ + private readonly ILogger _logger; + + public GlobalExceptionHandler(ILogger logger) + { + _logger = logger; + } + + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + _logger.LogError( + exception, "Exception occurred: {Message}", exception.Message); + + var problemDetails = new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Title = "Server error" + }; + + httpContext.Response.StatusCode = problemDetails.Status.Value; + + await httpContext.Response + .WriteAsJsonAsync(problemDetails, cancellationToken); + + return true; + } +} diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index aa80165..7f2387c 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -8,6 +8,8 @@ { builder.Services.AddSwagger(); + builder.Services.AddGlobalErrorHandler(); + builder.Services.AddCommon(); builder.Services.AddMediatR(); From 07d495cb98b3414636f8edf3f0af2d0a0f624fc2 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 14 Sep 2024 06:55:02 +1000 Subject: [PATCH 61/87] =?UTF-8?q?=F0=9F=93=A6=20Upgraded=20to=20.NET=209?= =?UTF-8?q?=20RC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 34 +++++++++++++++++++ Directory.Build.props | 2 +- .../Common.SharedKernel.csproj | 6 ++-- src/Common/Common.Tests/Common.Tests.csproj | 3 +- .../Modules.Orders/Modules.Orders.csproj | 2 +- .../Modules.Catalog/Modules.Catalog.csproj | 12 +++---- .../Modules.Warehouse.csproj | 12 +++---- src/WebApi/WebApi.csproj | 2 +- tools/Database/Database.csproj | 2 +- 9 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9cc25dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +########################################################################################## +# +# CODE STYLE RULES +# +########################################################################################## + +# All files +[*] +indent_style = space + +# Xml files +[*.{xml,yml,json}] +indent_size = 2 +tab_width = 2 + +# C# Projects +[*.csproj] +indent_size = 4 +tab_width = 4 + +# C# files +[*.cs] +indent_size = 4 +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = false + +# File scoped namespaces +csharp_style_namespace_declarations = file_scoped:silent diff --git a/Directory.Build.props b/Directory.Build.props index 3785898..72e27d8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net9.0 enable enable diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 573cc91..18d11ee 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -3,9 +3,9 @@ - - - + + + diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index acc3eaf..8d9304b 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -1,12 +1,13 @@  + - + diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index 430cb9d..7028be7 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 0784c6d..b217bb1 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -6,15 +6,15 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 18a31d3..6402938 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -6,15 +6,15 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 338d9d6..b86c74a 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -5,7 +5,7 @@ - + diff --git a/tools/Database/Database.csproj b/tools/Database/Database.csproj index cd03fd6..15f335a 100644 --- a/tools/Database/Database.csproj +++ b/tools/Database/Database.csproj @@ -5,7 +5,7 @@ - + From 66c01c6a78c363c81f046a9226b615c780e7bc60 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 14 Sep 2024 07:33:12 +1000 Subject: [PATCH 62/87] =?UTF-8?q?=E2=9C=A8=20Upgrade=20.NET=20version=20in?= =?UTF-8?q?=20GitHub=20Actions=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the GitHub Actions workflow file to use .NET version 9.0.x instead of 8.0.x. This ensures compatibility with new features and improvements in the latest .NET release. --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2b47d44..a5abf60 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -26,7 +26,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore From 31361fdf95dc8baf9b671ca8ba7b1c57436faa3e Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 14 Sep 2024 08:04:39 +1000 Subject: [PATCH 63/87] =?UTF-8?q?=F0=9F=9A=A8=20fixed=20all=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 7 ++++++ Directory.Build.props | 4 ++++ ModularMonolith.sln | 6 +++++ .../Common.SharedKernel/Api/ErrorOrExt.cs | 6 ++--- .../Domain/Interfaces/IAggregateRoot.cs | 6 ++--- .../EntitySaveChangesInterceptor.cs | 4 ++-- .../Extensions/ServiceCollectionExt.cs | 4 ++-- .../Modules.Customers.Tests/CustomerTests.cs | 4 ++-- .../Modules.Customers/AssemblyInfo.cs | 2 +- .../Cart/CartItemTests.cs | 3 +-- .../Modules.Orders.Tests/Cart/CartTests.cs | 3 +-- .../Modules.Orders.Tests/LineItemTests.cs | 3 +-- .../Orders/Modules.Orders/AssemblyInfo.cs | 2 +- .../Orders/Modules.Orders/Carts/Cart.cs | 4 ++-- .../Modules.Orders/Orders/Order/Order.cs | 8 +++---- .../Modules.Orders/Orders/Payment/Payment.cs | 4 ++-- .../Orders/Modules.Orders/OrdersModule.cs | 9 ++++---- .../Categories/CategoryIntegrationTests.cs | 8 +++---- .../Products/Modules.Catalog/AssemblyInfo.cs | 4 ++-- .../Products/ProductIntegrationTests.cs | 6 ++--- .../Storage/Domain/AisleTests.cs | 8 +++---- .../Domain/StorageAllocationServiceTests.cs | 6 ++--- .../AllocateStorageCommandIntegrationTests.cs | 4 ++-- .../CreateAisleCommandIntegrationTests.cs | 8 +++---- .../GetItemLocationQueryIntegrationTests.cs | 4 ++-- .../Modules.Warehouse/BackOrders/BackOrder.cs | 19 +++++++--------- .../Modules.Warehouse.csproj | 22 ++++++++----------- .../Storage/UseCases/GetItemLocationQuery.cs | 3 +-- src/WebApi/Program.cs | 4 ++-- .../WarehouseDbContextInitialiser.cs | 4 ++-- 30 files changed, 88 insertions(+), 91 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9cc25dd..e35d5f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -32,3 +32,10 @@ insert_final_newline = false # File scoped namespaces csharp_style_namespace_declarations = file_scoped:silent + +csharp_prefer_braces = when_multiline + +# https://learn.microsoft.com/en-gb/dotnet/fundamentals/code-analysis/style-rules/ide0007-ide0008 +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true +csharp_style_var_elsewhere = true diff --git a/Directory.Build.props b/Directory.Build.props index 72e27d8..2edf161 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,6 +4,10 @@ enable enable + true + + CS1591,NU1902,NU1903,NU1904 + latest diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 14b9baf..5afc8c9 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -58,6 +58,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Tests", "src\WebApi. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "src\Common\Common.Tests\Common.Tests.csproj", "{51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".sln", ".sln", "{63C08527-F5AA-4234-BED9-A75281776B1F}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/Common/Common.SharedKernel/Api/ErrorOrExt.cs b/src/Common/Common.SharedKernel/Api/ErrorOrExt.cs index 802929e..b1fab22 100644 --- a/src/Common/Common.SharedKernel/Api/ErrorOrExt.cs +++ b/src/Common/Common.SharedKernel/Api/ErrorOrExt.cs @@ -37,12 +37,12 @@ private static ValidationProblem ValidationProblem(IErrorOr error) var errors = new Dictionary(); foreach (var e in error.Errors!) { - if (errors.Remove(e.Code, out string[]? value)) - errors.Add(e.Code, [..value, e.Description]); + if (errors.Remove(e.Code, out var value)) + errors.Add(e.Code, [.. value, e.Description]); else errors.Add(e.Code, [e.Description]); } return TypedResults.ValidationProblem(errors, title: "One or more validation errors occurred."); } -} +} \ No newline at end of file diff --git a/src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs index da4eba6..c96b4ee 100644 --- a/src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IAggregateRoot.cs @@ -1,6 +1,4 @@ -using Common.SharedKernel.Domain.Base; - -namespace Common.SharedKernel.Domain.Interfaces; +namespace Common.SharedKernel.Domain.Interfaces; public interface IAggregateRoot { @@ -13,4 +11,4 @@ public interface IAggregateRoot // void ClearDomainEvents(); IReadOnlyList PopDomainEvents(); -} +} \ No newline at end of file diff --git a/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs b/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs index bf78d2c..b44e561 100644 --- a/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs +++ b/src/Common/Common.SharedKernel/Persistence/Interceptors/EntitySaveChangesInterceptor.cs @@ -28,7 +28,7 @@ private void UpdateEntities(DbContext? context) if (context is null) return; - var entries = context.ChangeTracker.Entries(); + // var entries = context.ChangeTracker.Entries(); foreach (var entry in context.ChangeTracker.Entries()) { @@ -52,4 +52,4 @@ public static bool HasChangedOwnedEntities(this EntityEntry entry) => r.TargetEntry != null && r.TargetEntry.Metadata.IsOwned() && r.TargetEntry.State is EntityState.Added or EntityState.Modified); -} +} \ No newline at end of file diff --git a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs index 23673c3..52fac95 100644 --- a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs +++ b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs @@ -23,7 +23,7 @@ internal static IServiceCollection ReplaceDbContext( .AddDbContext((_, options) => { options.UseSqlServer(databaseContainer.ConnectionString); - // b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); + // b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); options.LogTo(m => Debug.WriteLine(m)); options.EnableSensitiveDataLogging(); @@ -38,4 +38,4 @@ internal static IServiceCollection ReplaceDbContext( return services; } -} +} \ No newline at end of file diff --git a/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs b/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs index d33d53e..b12eebc 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs +++ b/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs @@ -57,7 +57,7 @@ public void UpdateAddress_ShouldUpdateAddress() { // Arrange var customer = Customer.Create("test@example.com", "John", "Doe"); - var address = new Address("123 Main St", null,"City", "State", "12345", "US"); + var address = new Address("123 Main St", null, "City", "State", "12345", "US"); // Act customer.UpdateAddress(address); @@ -65,4 +65,4 @@ public void UpdateAddress_ShouldUpdateAddress() // Assert customer.Address.Should().Be(address); } -} +} \ No newline at end of file diff --git a/src/Modules/Customers/Modules.Customers/AssemblyInfo.cs b/src/Modules/Customers/Modules.Customers/AssemblyInfo.cs index e5ee732..ec4b0b7 100644 --- a/src/Modules/Customers/Modules.Customers/AssemblyInfo.cs +++ b/src/Modules/Customers/Modules.Customers/AssemblyInfo.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("Modules.Customers.Tests")] +[assembly: InternalsVisibleTo("Modules.Customers.Tests")] \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs index fecbe61..215b631 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs @@ -1,5 +1,4 @@ using Common.SharedKernel.Domain.Entities; -using FluentAssertions; using Modules.Orders.Carts; using Modules.Orders.Orders; @@ -104,4 +103,4 @@ public void DecreaseQuantity_TooMany_ShouldThrow() // Assert act.Should().Throw(); } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs index 6dfbf7b..eaa5355 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs @@ -1,5 +1,4 @@ using Common.SharedKernel.Domain.Entities; -using FluentAssertions; using Modules.Orders.Orders; namespace Modules.Orders.Tests.Cart; @@ -91,4 +90,4 @@ public void UpdateTotal_ShouldCalculateTotalPriceCorrectly() // Assert cart.TotalPrice.Amount.Should().Be(50); } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs index 1579bbc..0ee773d 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs @@ -1,5 +1,4 @@ using Common.SharedKernel.Domain.Entities; -using FluentAssertions; using Modules.Orders.Orders; using Modules.Orders.Orders.LineItem; using Modules.Orders.Orders.Order; @@ -157,4 +156,4 @@ public void RemoveQuantity_TooMany_ShouldThrow() // Assert act.Should().Throw(); } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/AssemblyInfo.cs b/src/Modules/Orders/Modules.Orders/AssemblyInfo.cs index 2208f78..2c4fa35 100644 --- a/src/Modules/Orders/Modules.Orders/AssemblyInfo.cs +++ b/src/Modules/Orders/Modules.Orders/AssemblyInfo.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("Modules.Orders.Tests")] +[assembly: InternalsVisibleTo("Modules.Orders.Tests")] \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs index f3a8727..af6dcf2 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs @@ -8,7 +8,7 @@ internal record CartId(Guid Value); internal class Cart : AggregateRoot { - private List _items = []; + private readonly List _items = []; public IReadOnlyList Items => _items.AsReadOnly(); @@ -64,4 +64,4 @@ private void UpdateTotal() var total = _items.Sum(i => i.LinePrice.Amount); TotalPrice = new Money(currency, total); } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs index d0df741..53086bf 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs @@ -28,9 +28,7 @@ internal class Order : AggregateRoot public Money AmountPaid { get; private set; } = null!; -#pragma warning disable CS0414 // Field is assigned but its value is never used - private Payment.Payment _payment = null!; -#pragma warning restore CS0414 // Field is assigned but its value is never used + // private readonly Payment.Payment _payment = null!; public OrderStatus Status { get; private set; } = null!; @@ -47,7 +45,7 @@ internal class Order : AggregateRoot /// /// Shipping total. Excludes tax. /// - public Money ShippingTotal {get; private set; } = null!; + public Money ShippingTotal { get; private set; } = null!; /// /// Tax of the order. Calculated on the OrderSubTotal and ShippingTotal. @@ -182,4 +180,4 @@ private void UpdateOrderTotal() OrderSubTotal = new Money(currency, amount); TaxTotal = new Money(currency, OrderSubTotal.Amount * TaxRate); } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs index 36bf70e..1864b83 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs @@ -15,7 +15,7 @@ private Payment() { } - public static Payment Create (Money amount, PaymentType paymentType) + public static Payment Create(Money amount, PaymentType paymentType) { ArgumentNullException.ThrowIfNull(amount); ArgumentNullException.ThrowIfNull(paymentType); @@ -29,4 +29,4 @@ public static Payment Create (Money amount, PaymentType paymentType) return payment; } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/OrdersModule.cs b/src/Modules/Orders/Modules.Orders/OrdersModule.cs index 8a224e4..54b6782 100644 --- a/src/Modules/Orders/Modules.Orders/OrdersModule.cs +++ b/src/Modules/Orders/Modules.Orders/OrdersModule.cs @@ -1,14 +1,13 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; namespace Modules.Orders; public static class OrdersModule { - public static void AddOrders(this IServiceCollection services) - { - } + // public static void AddOrders(this IServiceCollection services) + // { + // } public static void UseOrders(this WebApplication app) { @@ -28,4 +27,4 @@ public static void UseOrders(this WebApplication app) } } -record OrderDto(string Name, string Description); +public record OrderDto(string Name, string Description); \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryIntegrationTests.cs index 0df7de2..6d14090 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryIntegrationTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Categories/CategoryIntegrationTests.cs @@ -12,8 +12,6 @@ namespace Modules.Catalog.Tests.Categories; public class CategoryIntegrationTests(CatalogDatabaseFixture fixture, ITestOutputHelper output) : CatalogIntegrationTestBase(fixture, output) { - private readonly ITestOutputHelper _output = output; - [Fact] public async Task CreateCategory_ValidRequest_ShouldReturnCreated() { @@ -40,11 +38,11 @@ public async Task CreateCategory_ValidRequest_ShouldReturnCreated() [InlineData(null)] [InlineData("")] [InlineData(" ")] - public async Task CreateCategory_InvalidRequest_ReturnsBadRequest(string name) + public async Task CreateCategory_InvalidRequest_ReturnsBadRequest(string? name) { // Arrange var client = GetAnonymousClient(); - var request = new CreateCategoryCommand.Request(name); + var request = new CreateCategoryCommand.Request(name!); // Act var response = await client.PostAsJsonAsync("/api/categories", request); @@ -70,4 +68,4 @@ public async Task CreateCategory_DuplicateRequest_ReturnsBadRequest() // Assert response2.StatusCode.Should().Be(HttpStatusCode.BadRequest); } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs index 8391513..0cc3af2 100644 --- a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs +++ b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs @@ -1,4 +1,4 @@ using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("Modules.Catalog.Tests")] -[assembly: InternalsVisibleTo("Database")] +[assembly: InternalsVisibleTo("Modules.Catalog.Tests")] +[assembly: InternalsVisibleTo("Database")] \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs index 2f939db..02bf650 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs @@ -43,11 +43,11 @@ public async Task CreateProduct_ValidRequest_ReturnsCreatedProduct() [InlineData("name", null)] [InlineData("name", "")] [InlineData("name", "123")] - public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, string sku) + public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string? name, string? sku) { // Arrange var client = GetAnonymousClient(); - var request = new CreateProductCommand.Request(name, sku); + var request = new CreateProductCommand.Request(name!, sku!); // Act var response = await client.PostAsJsonAsync("/api/products", request); @@ -57,4 +57,4 @@ public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string name, st var content = await response.Content.ReadAsStringAsync(); _output.WriteLine(content); } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs index af6c066..fb06201 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs @@ -27,9 +27,9 @@ public void LookingUpProduct_ReturnsAisleBayAndShelf() StorageAllocationService.AllocateStorage(new List { aisle }, productA); StorageAllocationService.AllocateStorage(new List { aisle }, productB); - string aisleName = string.Empty; - string bayName = string.Empty; - string shelfName = string.Empty; + var aisleName = string.Empty; + var bayName = string.Empty; + var shelfName = string.Empty; // NOTE: If look ups like there were to cause performacne problems, two-way relationships could be added between // Different storage locations (e.g. Shelf->Bay->Aisle) to make lookups more efficient. @@ -123,4 +123,4 @@ public void AssignProduct_WithNoAvailableStorage_ReturnsError() // Assert result.IsError.Should().BeTrue(); } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs index 391842d..4c685d7 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs @@ -51,7 +51,6 @@ public void AllocateStorage_WhenMaxStorageUsed_ShouldHaveNoAvailableStorage() var numBays = 2; var numShelves = 3; var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var sut = new StorageAllocationService(); var productId = new ProductId(Guid.NewGuid()); // Act @@ -73,11 +72,10 @@ public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() var numBays = 2; var numShelves = 3; var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var sut = new StorageAllocationService(); var productId = new ProductId(Guid.NewGuid()); // Act - for(var i = 0; i < numBays * numShelves; i++) + for (var i = 0; i < numBays * numShelves; i++) { StorageAllocationService.AllocateStorage([aisle], productId); } @@ -86,4 +84,4 @@ public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() var result = StorageAllocationService.AllocateStorage([aisle], productId); result.IsError.Should().BeTrue(); } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs index f92fa60..1ae3545 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs @@ -9,7 +9,7 @@ namespace Modules.Warehouse.Tests.Storage.UseCases; -public class AllocateStorageCommandIntegrationTests (WarehouseDatabaseFixture fixture, ITestOutputHelper output) +public class AllocateStorageCommandIntegrationTests(WarehouseDatabaseFixture fixture, ITestOutputHelper output) : WarehouseIntegrationTestBase(fixture, output) { [Fact] @@ -29,4 +29,4 @@ public async Task AllocateStorage_ValidRequest_ReturnsOk() // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs index 5f4cfc2..670453a 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs @@ -10,7 +10,7 @@ namespace Modules.Warehouse.Tests.Storage.UseCases; -public class CreateAisleCommandIntegrationTests (WarehouseDatabaseFixture fixture, ITestOutputHelper output) +public class CreateAisleCommandIntegrationTests(WarehouseDatabaseFixture fixture, ITestOutputHelper output) : WarehouseIntegrationTestBase(fixture, output) { private readonly ITestOutputHelper _output = output; @@ -47,11 +47,11 @@ public async Task CreateAisle_ValidRequest_ReturnsCreatedAisle() [InlineData("", 1, 1)] [InlineData(" ", 1, 1)] [InlineData(null, 1, 1)] - public async Task CreateAisle_WithInvalidRequest_Throws(string name, int numBays, int numShelves) + public async Task CreateAisle_WithInvalidRequest_Throws(string? name, int numBays, int numShelves) { // Arrange var client = GetAnonymousClient(); - var request = new CreateAisleCommand.Request(name, numBays, numShelves); + var request = new CreateAisleCommand.Request(name!, numBays, numShelves); // Act var response = await client.PostAsJsonAsync("/api/aisles", request); @@ -61,4 +61,4 @@ public async Task CreateAisle_WithInvalidRequest_Throws(string name, int numBays var content = await response.Content.ReadAsStringAsync(); _output.WriteLine(content); } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs index f3acc52..fac2838 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs @@ -8,7 +8,7 @@ namespace Modules.Warehouse.Tests.Storage.UseCases; -public class GetItemLocationQueryIntegrationTests (WarehouseDatabaseFixture fixture, ITestOutputHelper output) +public class GetItemLocationQueryIntegrationTests(WarehouseDatabaseFixture fixture, ITestOutputHelper output) : WarehouseIntegrationTestBase(fixture, output) { [Fact] @@ -35,4 +35,4 @@ public async Task Query_ValidRequest_ReturnsOk() response.BayName.Should().Be("Bay 1"); response.ShelfName.Should().Be("Shelf 1"); } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs index c10f093..f193a99 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs @@ -1,8 +1,5 @@ using Ardalis.SmartEnum; using Common.SharedKernel.Domain.Base; -using Modules.Warehouse.Products.Domain; - -#pragma warning disable CS0414 // Field is assigned but its value is never used namespace Modules.Warehouse.BackOrders; @@ -10,11 +7,11 @@ internal record BackOrderId(Guid Value); internal class BackOrder : AggregateRoot { - private ProductId _productId = null!; + // private ProductId _productId = null!; - private int _quantityOrdered; + // private int _quantityOrdered; - private int _quantityReceived; + // private int _quantityReceived; public BackOrderStatus Status { get; private set; } = null!; @@ -22,14 +19,14 @@ private BackOrder() { } - public static BackOrder Create(ProductId productId, int quantityOrdered) + public static BackOrder Create(/*ProductId productId, int quantityOrdered*/) { var backOrder = new BackOrder { Id = new BackOrderId(Guid.NewGuid()), - _productId = productId, - _quantityOrdered = quantityOrdered, - _quantityReceived = 0, + // _productId = productId, + // _quantityOrdered = quantityOrdered, + // _quantityReceived = 0, Status = BackOrderStatus.Pending }; @@ -45,4 +42,4 @@ internal class BackOrderStatus : SmartEnum private BackOrderStatus(int id, string name) : base(name, id) { } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 6402938..524c307 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -1,29 +1,25 @@  - - - - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + - + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs index 39f30ec..5dca1d2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs @@ -3,7 +3,6 @@ using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Common.Persistence; @@ -75,4 +74,4 @@ public async Task> Handle(Request request, CancellationToken c return Error.NotFound(description: "Product not found"); } } -} +} \ No newline at end of file diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 7f2387c..28850c4 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -14,7 +14,7 @@ builder.Services.AddMediatR(); - builder.Services.AddOrders(); + // builder.Services.AddOrders(); builder.Services.AddWarehouse(builder.Configuration); builder.Services.AddCatalog(builder.Configuration); } @@ -35,4 +35,4 @@ app.UseCatalog(); app.Run(); -} +} \ No newline at end of file diff --git a/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs b/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs index b202966..4205674 100644 --- a/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs +++ b/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs @@ -53,7 +53,7 @@ private async Task SeedAisles() if (await _dbContext.Aisles.AnyAsync()) return; - for (int i = 1; i <= NumAisles; i++) + for (var i = 1; i <= NumAisles; i++) { var aisle = Aisle.Create($"Aisle {i}", NumBays, NumShelves); _dbContext.Aisles.Add(aisle); @@ -81,4 +81,4 @@ private async Task> SeedProductsAsync() return products; } -} +} \ No newline at end of file From 0b3a27d91f303c19f06e1d303b0a688b4a8c0f55 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 14 Sep 2024 08:13:59 +1000 Subject: [PATCH 64/87] =?UTF-8?q?=E2=9C=A8=20Upgrade=20setup-dotnet=20acti?= =?UTF-8?q?on=20to=20version=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the GitHub Actions workflow to use the latest version of the setup-dotnet action. This ensures compatibility with the latest .NET 9.0.x features and improvements. --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index a5abf60..dc3f1c4 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 9.0.x From 69d703986253854ce28c05b32ed0a92bf2970490 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:41:46 +1000 Subject: [PATCH 65/87] =?UTF-8?q?=F0=9F=A7=AA=20fix=20test=20containers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Common/Common.Tests/Common/DatabaseContainer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index 4e6ac85..df78613 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -1,3 +1,4 @@ +using DotNet.Testcontainers.Builders; using Testcontainers.SqlEdge; namespace Common.Tests.Common; @@ -10,8 +11,8 @@ public class DatabaseContainer private readonly SqlEdgeContainer _container = new SqlEdgeBuilder() .WithName($"Modular-Monolith-Tests-{Guid.NewGuid()}") .WithPassword("Password123") - // .WithWaitStrategy(Wait.ForUnixContainer()) - // .WithAutoRemove(true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(1433)) + .WithAutoRemove(true) .Build(); public string? ConnectionString { get; private set; } @@ -35,4 +36,4 @@ public async Task DisposeAsync() await _container.StopAsync(); await _container.DisposeAsync(); } -} +} \ No newline at end of file From 35fbe7b27d8ca21826484322e423672c273f95b7 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:54:53 +1000 Subject: [PATCH 66/87] =?UTF-8?q?=F0=9F=A7=AA=20Another=20crack=20at=20fix?= =?UTF-8?q?ing=20the=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Common/Common.Tests/Common.Tests.csproj | 2 ++ .../Common.Tests/Common/DatabaseContainer.cs | 22 ++++++------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index 8d9304b..5c986eb 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -5,6 +5,8 @@ + + diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index df78613..d5b879b 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -1,5 +1,4 @@ -using DotNet.Testcontainers.Builders; -using Testcontainers.SqlEdge; +using Testcontainers.MsSql; namespace Common.Tests.Common; @@ -8,10 +7,11 @@ namespace Common.Tests.Common; /// public class DatabaseContainer { - private readonly SqlEdgeContainer _container = new SqlEdgeBuilder() - .WithName($"Modular-Monolith-Tests-{Guid.NewGuid()}") + private readonly MsSqlContainer _container = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") + .WithName($"BizCover-IntegrationTests-{Guid.NewGuid()}") .WithPassword("Password123") - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(1433)) + .WithPortBinding(1433, true) .WithAutoRemove(true) .Build(); @@ -19,16 +19,8 @@ public class DatabaseContainer public async Task InitializeAsync() { - try - { - await _container.StartAsync(); - ConnectionString = _container.GetConnectionString(); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } + await _container.StartAsync(); + ConnectionString = _container.GetConnectionString(); } public async Task DisposeAsync() From 70643ba183f06e7dc21100598348ed309d1f24f9 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 21 Sep 2024 08:10:18 +1000 Subject: [PATCH 67/87] Temporarily remove tests #53 --- .github/workflows/dotnet.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index dc3f1c4..446ad53 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -34,13 +34,13 @@ jobs: - name: Build run: dotnet build --no-restore -c Debug - - name: Test - run: dotnet test --no-build --verbosity normal -c Debug --logger "trx;LogFileName=test-results.trx" - - - name: Test Report - uses: dorny/test-reporter@v1 - if: success() || failure() - with: - name: Tests Results - path: "**/test-results.trx" - reporter: dotnet-trx +# - name: Test +# run: dotnet test --no-build --verbosity normal -c Debug --logger "trx;LogFileName=test-results.trx" +# +# - name: Test Report +# uses: dorny/test-reporter@v1 +# if: success() || failure() +# with: +# name: Tests Results +# path: "**/test-results.trx" +# reporter: dotnet-trx From 382af55faa3eec8ae4294557a42a4ad05d3ec1ac Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:29:47 +1000 Subject: [PATCH 68/87] =?UTF-8?q?=F0=9F=A7=AA=20added=20retry=20to=20test?= =?UTF-8?q?=20container=20creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common.Tests/Common/DatabaseContainer.cs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index d5b879b..bb6a2c2 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -15,14 +15,44 @@ public class DatabaseContainer .WithAutoRemove(true) .Build(); + private const int MaxRetries = 5; + public string? ConnectionString { get; private set; } public async Task InitializeAsync() { - await _container.StartAsync(); + await StartWithRetry(); ConnectionString = _container.GetConnectionString(); } + private async Task StartWithRetry() + { + // NOTE: For some reason the container sometimes fails to start up. Add in a retry to protect against this + var notReady = true; + var numTries = 0; + + while (notReady) + { + if (numTries >= MaxRetries) + { + Console.WriteLine("Max tries reached, giving up"); + break; + } + + try + { + await _container.StartAsync(); + notReady = false; + } + catch (Exception ex) + { + numTries++; + await Task.Delay(2000); + Console.WriteLine($"container failed to start: {ex.Message}"); + } + } + } + public async Task DisposeAsync() { await _container.StopAsync(); From 4108a178cb9a34732dad2a33137690188e456c3f Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:30:19 +1000 Subject: [PATCH 69/87] Switched to using Guid.CreateVersion7() #55 --- src/Common/Common.SharedKernel/Domain/Uuid.cs | 6 +++ .../Modules.Customers/Customers/Customer.cs | 8 ++-- .../Modules.Customers/GlobalUsings.cs | 4 ++ .../Cart/CartItemTests.cs | 13 +++---- .../Modules.Orders.Tests/Cart/CartTests.cs | 17 ++++----- .../Modules.Orders.Tests/GlobalUsings.cs | 4 +- .../Modules.Orders.Tests/LineItemTests.cs | 37 +++++++++---------- .../Orders/LineItemTests.cs | 27 +++++++------- .../Modules.Orders.Tests/Orders/OrderTests.cs | 23 ++++++------ .../Orders/PaymentTests.cs | 1 - .../Orders/Modules.Orders/Carts/Cart.cs | 4 +- .../Orders/Modules.Orders/Carts/CartItem.cs | 6 +-- .../Orders/Modules.Orders/GlobalUsings.cs | 5 +++ .../Orders/LineItem/LineItem.cs | 8 ++-- .../Modules.Orders/Orders/Order/Order.cs | 6 +-- .../Modules.Orders/Orders/Payment/Payment.cs | 5 +-- .../Products/ProductIntegrationTests.cs | 5 ++- .../Categories/Domain/Category.cs | 8 ++-- .../Products/Modules.Catalog/GlobalUsings.cs | 5 +++ .../Products/Domain/Product.cs | 6 +-- .../UseCases/UpdateProductPriceCommand.cs | 3 +- .../Modules.Warehouse.Tests/GlobalUsings.cs | 2 + .../Products/ProductIntegrationTests.cs | 1 - .../Products/ProductTests.cs | 1 - .../Storage/Domain/AisleTests.cs | 9 ++--- .../Domain/StorageAllocationServiceTests.cs | 9 ++--- .../AllocateStorageCommandIntegrationTests.cs | 1 - .../CreateAisleCommandIntegrationTests.cs | 1 - .../GetItemLocationQueryIntegrationTests.cs | 1 - .../Modules.Warehouse/BackOrders/BackOrder.cs | 3 +- .../Modules.Warehouse/GlobalUsings.cs | 2 + .../Products/Domain/Product.cs | 7 ++-- .../Modules.Warehouse/Products/Domain/Sku.cs | 4 +- .../Modules.Warehouse/Storage/Domain/Aisle.cs | 5 +-- .../Modules.Warehouse/Storage/Domain/Bay.cs | 6 +-- .../Modules.Warehouse/Storage/Domain/Shelf.cs | 5 +-- 36 files changed, 123 insertions(+), 135 deletions(-) create mode 100644 src/Common/Common.SharedKernel/Domain/Uuid.cs create mode 100644 src/Modules/Customers/Modules.Customers/GlobalUsings.cs create mode 100644 src/Modules/Orders/Modules.Orders/GlobalUsings.cs create mode 100644 src/Modules/Products/Modules.Catalog/GlobalUsings.cs diff --git a/src/Common/Common.SharedKernel/Domain/Uuid.cs b/src/Common/Common.SharedKernel/Domain/Uuid.cs new file mode 100644 index 0000000..a1f0eaf --- /dev/null +++ b/src/Common/Common.SharedKernel/Domain/Uuid.cs @@ -0,0 +1,6 @@ +namespace Common.SharedKernel.Domain; + +public static class Uuid +{ + public static Guid Create() => Guid.CreateVersion7(); +} \ No newline at end of file diff --git a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs index 88c4027..24d4000 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Customer.cs @@ -1,6 +1,4 @@ -using Common.SharedKernel.Domain.Base; - -namespace Modules.Customers.Customers; +namespace Modules.Customers.Customers; /* Invariants: * - Must have a unique email address (handled by application) @@ -20,7 +18,7 @@ private Customer() { } internal static Customer Create(string email, string firstName, string lastName) { - var customer = new Customer { Id = new CustomerId(Guid.NewGuid()) }; + var customer = new Customer { Id = new CustomerId(Uuid.Create()) }; customer.UpdateEmail(email); customer.UpdateName(firstName, lastName); customer.AddDomainEvent(CustomerCreatedEvent.Create(customer)); @@ -47,4 +45,4 @@ public void UpdateAddress(Address address) ArgumentNullException.ThrowIfNull(address); Address = address; } -} +} \ No newline at end of file diff --git a/src/Modules/Customers/Modules.Customers/GlobalUsings.cs b/src/Modules/Customers/Modules.Customers/GlobalUsings.cs new file mode 100644 index 0000000..b33b085 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/GlobalUsings.cs @@ -0,0 +1,4 @@ +// Global using directives + +global using Common.SharedKernel.Domain; +global using Common.SharedKernel.Domain.Base; \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs index 215b631..99e0bbd 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Carts; using Modules.Orders.Orders; @@ -10,7 +9,7 @@ public class CartItemTests public void Create_ValidParameters_ShouldCreateCartItem() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var quantity = 2; var unitPrice = Money.Create(100m); @@ -28,7 +27,7 @@ public void Create_ValidParameters_ShouldCreateCartItem() public void Create_NegativeQuantity_ShouldThrow() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var quantity = -1; var unitPrice = Money.Create(100m); @@ -43,7 +42,7 @@ public void Create_NegativeQuantity_ShouldThrow() public void Create_NegativeUnitPrice_ShouldThrow() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var quantity = 2; var unitPrice = Money.Create(-100m); @@ -58,7 +57,7 @@ public void Create_NegativeUnitPrice_ShouldThrow() public void IncreaseQuantity_ShouldIncreaseQuantity() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var quantity = 2; var unitPrice = Money.Create(100m); var cartItem = CartItem.Create(productId, quantity, unitPrice); @@ -75,7 +74,7 @@ public void IncreaseQuantity_ShouldIncreaseQuantity() public void DecreaseQuantity_ShouldDecreaseQuantity() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var quantity = 5; var unitPrice = Money.Create(100m); var cartItem = CartItem.Create(productId, quantity, unitPrice); @@ -92,7 +91,7 @@ public void DecreaseQuantity_ShouldDecreaseQuantity() public void DecreaseQuantity_TooMany_ShouldThrow() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var quantity = 2; var unitPrice = Money.Create(100m); var cartItem = CartItem.Create(productId, quantity, unitPrice); diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs index eaa5355..7a60264 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Orders; namespace Modules.Orders.Tests.Cart; @@ -9,7 +8,7 @@ public class CartTests public void AddItem_ShouldIncreaseQuantity_WhenItemAlreadyExists() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId, 1, unitPrice); @@ -26,8 +25,8 @@ public void AddItem_ShouldIncreaseQuantity_WhenItemAlreadyExists() public void AddItem_ShouldAddNewItem_WhenItemDoesNotExist() { // Arrange - var productId1 = new ProductId(Guid.NewGuid()); - var productId2 = new ProductId(Guid.NewGuid()); + var productId1 = new ProductId(Uuid.Create()); + var productId2 = new ProductId(Uuid.Create()); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId1, 1, unitPrice); @@ -45,7 +44,7 @@ public void AddItem_ShouldAddNewItem_WhenItemDoesNotExist() public void RemoveItem_ShouldRemoveItem_WhenItemExists() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId, 1, unitPrice); @@ -61,8 +60,8 @@ public void RemoveItem_ShouldRemoveItem_WhenItemExists() public void RemoveItem_ShouldDoNothing_WhenItemDoesNotExist() { // Arrange - var productId1 = new ProductId(Guid.NewGuid()); - var productId2 = new ProductId(Guid.NewGuid()); + var productId1 = new ProductId(Uuid.Create()); + var productId2 = new ProductId(Uuid.Create()); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId1, 1, unitPrice); @@ -78,8 +77,8 @@ public void RemoveItem_ShouldDoNothing_WhenItemDoesNotExist() public void UpdateTotal_ShouldCalculateTotalPriceCorrectly() { // Arrange - var productId1 = new ProductId(Guid.NewGuid()); - var productId2 = new ProductId(Guid.NewGuid()); + var productId1 = new ProductId(Uuid.Create()); + var productId2 = new ProductId(Uuid.Create()); var unitPrice1 = new Money(Currency.Default, 10); var unitPrice2 = new Money(Currency.Default, 20); var cart = Carts.Cart.Create(productId1, 1, unitPrice1); diff --git a/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs b/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs index 91743bb..d22e914 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs @@ -1,2 +1,4 @@ +global using Common.SharedKernel.Domain; +global using Common.SharedKernel.Domain.Entities; global using Xunit; -global using FluentAssertions; +global using FluentAssertions; \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs index 0ee773d..ac42e4b 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Orders; using Modules.Orders.Orders.LineItem; using Modules.Orders.Orders.Order; @@ -11,8 +10,8 @@ public class LineItemTests public void Create_ValidParameters_ShouldCreateLineItem() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 2; @@ -30,8 +29,8 @@ public void Create_ValidParameters_ShouldCreateLineItem() public void Create_NegativePrice_ShouldThrow() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(-100m); var quantity = 2; @@ -46,8 +45,8 @@ public void Create_NegativePrice_ShouldThrow() public void Create_ZeroQuantity_ShouldThrow() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 0; @@ -62,8 +61,8 @@ public void Create_ZeroQuantity_ShouldThrow() public void Total_ShouldReturnCorrectAmount() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 2; @@ -78,8 +77,8 @@ public void Total_ShouldReturnCorrectAmount() public void Tax_ShouldReturnCorrectAmount() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 2; @@ -94,8 +93,8 @@ public void Tax_ShouldReturnCorrectAmount() public void TotalIncludingTax_ShouldReturnCorrectAmount() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 2; @@ -110,8 +109,8 @@ public void TotalIncludingTax_ShouldReturnCorrectAmount() public void AddQuantity_ShouldIncreaseQuantity() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -127,8 +126,8 @@ public void AddQuantity_ShouldIncreaseQuantity() public void RemoveQuantity_ShouldDecreaseQuantity() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 5; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -144,8 +143,8 @@ public void RemoveQuantity_ShouldDecreaseQuantity() public void RemoveQuantity_TooMany_ShouldThrow() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = Money.Create(100m); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs index c2b7d2d..17c60af 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Orders; using Modules.Orders.Orders.LineItem; using Modules.Orders.Orders.Order; @@ -11,8 +10,8 @@ public class LineItemTests public void Create_ShouldInitializeLineItemWithCorrectValues() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.Default, 100); var quantity = 2; @@ -31,8 +30,8 @@ public void Create_ShouldInitializeLineItemWithCorrectValues() public void Create_ShouldThrowException_WhenPriceIsNegativeOrZero() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.Default, 0); var quantity = 2; Action act = () => LineItem.Create(orderId, productId, price, quantity); @@ -45,8 +44,8 @@ public void Create_ShouldThrowException_WhenPriceIsNegativeOrZero() public void Create_ShouldThrowException_WhenQuantityIsNegativeOrZero() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.Default, 100); var quantity = 0; Action act = () => LineItem.Create(orderId, productId, price, quantity); @@ -59,8 +58,8 @@ public void Create_ShouldThrowException_WhenQuantityIsNegativeOrZero() public void AddQuantity_ShouldIncreaseQuantity() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.Default, 100); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -77,8 +76,8 @@ public void AddQuantity_ShouldIncreaseQuantity() public void RemoveQuantity_ShouldDecreaseQuantity() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.Default, 100); var quantity = 5; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -95,8 +94,8 @@ public void RemoveQuantity_ShouldDecreaseQuantity() public void RemoveQuantity_ShouldThrowException_WhenRemovingMoreThanAvailable() { // Arrange - var orderId = new OrderId(Guid.NewGuid()); - var productId = new ProductId(Guid.NewGuid()); + var orderId = new OrderId(Uuid.Create()); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.Default, 100); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -106,4 +105,4 @@ public void RemoveQuantity_ShouldThrowException_WhenRemovingMoreThanAvailable() // Act & Assert act.Should().Throw(); } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs index 2bc2dc6..08f1240 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Orders; using Modules.Orders.Orders.Order; @@ -10,8 +9,8 @@ public class OrderTests public void AddLineItem_ShouldAddNewItem_WhenProductDoesNotExist() { // Arrange - var order = Order.Create(new CustomerId(Guid.NewGuid())); - var productId = new ProductId(Guid.NewGuid()); + var order = Order.Create(new CustomerId(Uuid.Create())); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.USD, 100); var quantity = 1; @@ -28,8 +27,8 @@ public void AddLineItem_ShouldAddNewItem_WhenProductDoesNotExist() public void AddLineItem_ShouldIncreaseQuantity_WhenProductExists() { // Arrange - var order = Order.Create(new CustomerId(Guid.NewGuid())); - var productId = new ProductId(Guid.NewGuid()); + var order = Order.Create(new CustomerId(Uuid.Create())); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); @@ -47,8 +46,8 @@ public void AddLineItem_ShouldIncreaseQuantity_WhenProductExists() public void RemoveLineItem_ShouldRemoveItem_WhenProductExists() { // Arrange - var order = Order.Create(new CustomerId(Guid.NewGuid())); - var productId = new ProductId(Guid.NewGuid()); + var order = Order.Create(new CustomerId(Uuid.Create())); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); @@ -65,8 +64,8 @@ public void RemoveLineItem_ShouldRemoveItem_WhenProductExists() public void AddPayment_ShouldUpdateAmountPaid_WhenPaymentIsValid() { // Arrange - var order = Order.Create(new CustomerId(Guid.NewGuid())); - var productId = new ProductId(Guid.NewGuid()); + var order = Order.Create(new CustomerId(Uuid.Create())); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); @@ -85,8 +84,8 @@ public void AddPayment_ShouldUpdateAmountPaid_WhenPaymentIsValid() public void ShipOrder_ShouldUpdateStatusAndShippingDate_WhenOrderIsReadyForShipping() { // Arrange - var order = Order.Create(new CustomerId(Guid.NewGuid())); - var productId = new ProductId(Guid.NewGuid()); + var order = Order.Create(new CustomerId(Uuid.Create())); + var productId = new ProductId(Uuid.Create()); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); @@ -103,4 +102,4 @@ public void ShipOrder_ShouldUpdateStatusAndShippingDate_WhenOrderIsReadyForShipp order.ShippingDate.Should().BeCloseTo(timeProvider.GetUtcNow(), TimeSpan.FromSeconds(1)); } } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs index 1fe619c..7c91074 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/PaymentTests.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Orders.Payment; namespace Modules.Orders.Tests.Orders; diff --git a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs index af6dcf2..5e37c0a 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs @@ -1,5 +1,3 @@ -using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Orders; namespace Modules.Orders.Carts; @@ -18,7 +16,7 @@ public static Cart Create(ProductId productId, int quantity, Money unitPrice) { var cart = new Cart { - Id = new CartId(Guid.NewGuid()) + Id = new CartId(Uuid.Create()) }; cart.AddItem(productId, quantity, unitPrice); diff --git a/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs b/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs index cd59ae0..4e9ea2b 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs @@ -1,5 +1,3 @@ -using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Entities; using Modules.Orders.Orders; namespace Modules.Orders.Carts; @@ -24,7 +22,7 @@ public static CartItem Create(ProductId productId, int quantity, Money unitPrice var cartItem = new CartItem { - Id = new CartItemId(Guid.NewGuid()), + Id = new CartItemId(Uuid.Create()), ProductId = productId, Quantity = quantity, UnitPrice = unitPrice, @@ -60,4 +58,4 @@ private CartItem() { } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/GlobalUsings.cs b/src/Modules/Orders/Modules.Orders/GlobalUsings.cs new file mode 100644 index 0000000..3205267 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Global using directives + +global using Common.SharedKernel.Domain; +global using Common.SharedKernel.Domain.Base; +global using Common.SharedKernel.Domain.Entities; \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs index d52f7a1..f62cfbf 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs @@ -1,6 +1,4 @@ -using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Entities; -using Modules.Orders.Orders.Order; +using Modules.Orders.Orders.Order; using Throw; namespace Modules.Orders.Orders.LineItem; @@ -35,7 +33,7 @@ internal static LineItem Create(OrderId orderId, ProductId productId, Money pric var lineItem = new LineItem() { - Id = new LineItemId(Guid.NewGuid()), + Id = new LineItemId(Uuid.Create()), OrderId = orderId, ProductId = productId, Price = price, @@ -52,4 +50,4 @@ internal void RemoveQuantity(int quantity) quantity.Throw("Can't remove all units. Remove the entire item instead").IfTrue(Quantity - quantity <= 0); Quantity -= quantity; } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs index 53086bf..9d82241 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs @@ -1,6 +1,4 @@ -using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Entities; -using Common.SharedKernel.Domain.Exceptions; +using Common.SharedKernel.Domain.Exceptions; using ErrorOr; using Modules.Orders.Orders.LineItem; using Success = ErrorOr.Success; @@ -67,7 +65,7 @@ public static Order Create(CustomerId customerId) { var order = new Order { - Id = new OrderId(Guid.NewGuid()), + Id = new OrderId(Uuid.Create()), CustomerId = customerId, AmountPaid = Money.Zero, OrderSubTotal = Money.Zero, diff --git a/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs index 1864b83..40fb6ef 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Payment/Payment.cs @@ -1,6 +1,3 @@ -using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Entities; - namespace Modules.Orders.Orders.Payment; internal record PaymentId(Guid Value); @@ -22,7 +19,7 @@ public static Payment Create(Money amount, PaymentType paymentType) var payment = new Payment { - Id = new PaymentId(Guid.NewGuid()), + Id = new PaymentId(Uuid.Create()), Amount = amount, PaymentType = paymentType }; diff --git a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs index 5cdd249..c0f0563 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs @@ -1,4 +1,5 @@ using Ardalis.Specification.EntityFrameworkCore; +using Common.SharedKernel.Domain; using FluentAssertions; using Modules.Catalog.Categories.Domain; using Modules.Catalog.Products.Domain; @@ -91,7 +92,7 @@ public async Task GetProductQuery_WhenProductDoesNotExist_ShouldReturnNotFound() var client = GetAnonymousClient(); // Act - var response = await client.GetAsync($"/api/products/{Guid.NewGuid()}"); + var response = await client.GetAsync($"/api/products/{Uuid.Create()}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); @@ -117,4 +118,4 @@ public async Task UpdateProductPrice_ValidRequest_ShouldReturnNoContent() updatedProduct.Should().NotBeNull(); updatedProduct!.Price.Amount.Should().Be(request.Price); } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs index 67e5860..f1f136b 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs @@ -1,6 +1,4 @@ -using Common.SharedKernel.Domain.Base; - -namespace Modules.Catalog.Categories.Domain; +namespace Modules.Catalog.Categories.Domain; internal record CategoryId(Guid Value) : IStronglyTypedId; @@ -18,7 +16,7 @@ public static Category Create(string name) { var category = new Category { - Id = new CategoryId(Guid.NewGuid()), + Id = new CategoryId(Uuid.Create()), }; category.UpdateName(name); @@ -33,4 +31,4 @@ private void UpdateName(string name) Name = name; } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/GlobalUsings.cs b/src/Modules/Products/Modules.Catalog/GlobalUsings.cs new file mode 100644 index 0000000..3205267 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Global using directives + +global using Common.SharedKernel.Domain; +global using Common.SharedKernel.Domain.Base; +global using Common.SharedKernel.Domain.Entities; \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs index 40f4d91..34dced7 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs @@ -1,5 +1,3 @@ -using Common.SharedKernel.Domain.Base; -using Common.SharedKernel.Domain.Entities; using Modules.Catalog.Categories.Domain; namespace Modules.Catalog.Products.Domain; @@ -31,7 +29,7 @@ public static Product Create(string name, string sku, ProductId? id = null) { Name = name, Sku = sku, - Id = id ?? new ProductId(Guid.NewGuid()) + Id = id ?? new ProductId(Uuid.Create()) }; return product; @@ -58,4 +56,4 @@ public void RemoveCategory(Category category) _categories.Remove(category); } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs index e94685c..24c955a 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs @@ -1,7 +1,6 @@ using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; -using Common.SharedKernel.Domain.Entities; using ErrorOr; using FluentValidation; using MediatR; @@ -79,4 +78,4 @@ public async Task> Handle(Request request, CancellationToken ca return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs index 8c927eb..5f12dfc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs @@ -1 +1,3 @@ +global using Common.SharedKernel.Domain; +global using FluentAssertions; global using Xunit; \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs index 02bf650..715e736 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Products.UseCases; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs index 93a95a7..3a607fd 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Modules.Warehouse.Products.Domain; namespace Modules.Warehouse.Tests.Products; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs index fb06201..e5247cc 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; using Xunit.Abstractions; @@ -18,8 +17,8 @@ public AisleTests(ITestOutputHelper output) public void LookingUpProduct_ReturnsAisleBayAndShelf() { // Exploratory - var productA = new ProductId(Guid.NewGuid()); - var productB = new ProductId(Guid.NewGuid()); + var productA = new ProductId(Uuid.Create()); + var productB = new ProductId(Uuid.Create()); var aisle = Aisle.Create("Aisle 1", 2, 3); StorageAllocationService.AllocateStorage(new List { aisle }, productA); @@ -95,7 +94,7 @@ public void Create_WithBaysAndShelves_CreatesCorrectNumberOfShelvesPerBay() public void AssignProduct_WithAvailableStorage_AssignsProductToShelf() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var sut = Aisle.Create("Aisle 1", 1, 1); // Act @@ -113,7 +112,7 @@ public void AssignProduct_WithAvailableStorage_AssignsProductToShelf() public void AssignProduct_WithNoAvailableStorage_ReturnsError() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var sut = Aisle.Create("Aisle 1", 1, 1); sut.AssignProduct(productId); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs index 4c685d7..290d40b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; @@ -10,7 +9,7 @@ public class StorageAllocationServiceTests public void AllocateStorage_ShouldAssignProductToFirstEmptyShelf() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var aisles = new List { Aisle.Create("name", 2, 2) @@ -28,7 +27,7 @@ public void AllocateStorage_ShouldAssignProductToFirstEmptyShelf() public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelfIsAvailable() { // Arrange - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); var aisles = new List { Aisle.Create("name", 1, 1) @@ -51,7 +50,7 @@ public void AllocateStorage_WhenMaxStorageUsed_ShouldHaveNoAvailableStorage() var numBays = 2; var numShelves = 3; var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); // Act for (var i = 0; i < numBays * numShelves; i++) @@ -72,7 +71,7 @@ public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() var numBays = 2; var numShelves = 3; var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var productId = new ProductId(Guid.NewGuid()); + var productId = new ProductId(Uuid.Create()); // Act for (var i = 0; i < numBays * numShelves; i++) diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs index 1ae3545..8e741ee 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/AllocateStorageCommandIntegrationTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; using Modules.Warehouse.Storage.UseCases; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs index 670453a..511cbad 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/CreateAisleCommandIntegrationTests.cs @@ -1,5 +1,4 @@ using Ardalis.Specification.EntityFrameworkCore; -using FluentAssertions; using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Storage.Domain; using Modules.Warehouse.Storage.UseCases; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs index fac2838..e3707c9 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/UseCases/GetItemLocationQueryIntegrationTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; using Modules.Warehouse.Storage.UseCases; diff --git a/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs index f193a99..32286bf 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/BackOrders/BackOrder.cs @@ -1,5 +1,4 @@ using Ardalis.SmartEnum; -using Common.SharedKernel.Domain.Base; namespace Modules.Warehouse.BackOrders; @@ -23,7 +22,7 @@ public static BackOrder Create(/*ProductId productId, int quantityOrdered*/) { var backOrder = new BackOrder { - Id = new BackOrderId(Guid.NewGuid()), + Id = new BackOrderId(Uuid.Create()), // _productId = productId, // _quantityOrdered = quantityOrdered, // _quantityReceived = 0, diff --git a/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs b/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs index f20e5a0..6a9bc45 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs @@ -1,3 +1,5 @@ global using FluentValidation; global using MediatR; global using Ardalis.Specification.EntityFrameworkCore; +global using Common.SharedKernel.Domain; +global using Common.SharedKernel.Domain.Base; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 596dc6f..9e1c112 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -1,5 +1,4 @@ -using Common.SharedKernel.Domain.Base; -using ErrorOr; +using ErrorOr; using Throw; namespace Modules.Warehouse.Products.Domain; @@ -28,7 +27,7 @@ public static Product Create(string name, Sku sku) var product = new Product { - Id = new ProductId(Guid.NewGuid()), + Id = new ProductId(Uuid.Create()), StockOnHand = 0 }; @@ -79,4 +78,4 @@ public void AddStock(int quantity) // { // // } -// } +// } \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs index 45110c0..d50b999 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Sku.cs @@ -1,6 +1,4 @@ -using Common.SharedKernel.Domain.Base; - -namespace Modules.Warehouse.Products.Domain; +namespace Modules.Warehouse.Products.Domain; internal record Sku : ValueObject { diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs index d2b8873..d759693 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Base; using ErrorOr; using Modules.Warehouse.Products.Domain; @@ -26,7 +25,7 @@ public static Aisle Create(string name, int numBays, int numShelves) var aisle = new Aisle { - Id = new AisleId(Guid.NewGuid()), + Id = new AisleId(Uuid.Create()), Name = name }; @@ -56,4 +55,4 @@ public ErrorOr AssignProduct(ProductId productId) return shelf; } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs index d37dfda..6f171c5 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs @@ -1,5 +1,3 @@ -using Common.SharedKernel.Domain.Base; - namespace Modules.Warehouse.Storage.Domain; public record BayId(Guid Value) : IStronglyTypedId; @@ -23,7 +21,7 @@ public static Bay Create(string name, int numShelves) var bay = new Bay { - Id = new BayId(Guid.NewGuid()), + Id = new BayId(Uuid.Create()), Name = name }; @@ -35,4 +33,4 @@ public static Bay Create(string name, int numShelves) return bay; } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs index 7e79d38..71e5ac7 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs @@ -1,4 +1,3 @@ -using Common.SharedKernel.Domain.Base; using Modules.Warehouse.Products.Domain; namespace Modules.Warehouse.Storage.Domain; @@ -19,7 +18,7 @@ public static Shelf Create(string name) return new Shelf { - Id = new ShelfId(Guid.NewGuid()), + Id = new ShelfId(Uuid.Create()), Name = name }; } @@ -29,4 +28,4 @@ public void AssignProduct(ProductId productId) ArgumentNullException.ThrowIfNull(productId); ProductId = productId; } -} +} \ No newline at end of file From bd7c097ad8fd56d768b8c46e7dbfbfdb68f90970 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:45:39 +1000 Subject: [PATCH 70/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improved=20usage=20o?= =?UTF-8?q?f=20StrongTyped=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common.SharedKernel/Domain/Base/Entity.cs | 7 +------ .../Domain/Interfaces/IStronglyTypedId.cs | 6 ++++++ .../Extensions/PropertyBuilderExtensions.cs | 2 +- .../Extensions/StronglyTypedIdConverter.cs | 2 +- .../Cart/CartItemTests.cs | 14 ++++++------- .../Modules.Orders.Tests/Cart/CartTests.cs | 18 ++++++++--------- .../Modules.Orders.Tests/LineItemTests.cs | 20 +++++++++---------- .../Orders/LineItemTests.cs | 14 ++++++------- .../Modules.Orders.Tests/Orders/OrderTests.cs | 12 +++++------ .../Orders/Modules.Orders/Carts/Cart.cs | 2 +- .../Orders/Modules.Orders/Carts/CartItem.cs | 2 +- .../Orders/Modules.Orders/Common/ProductId.cs | 9 +++++++-- .../Orders/LineItem/LineItem.cs | 3 ++- .../Modules.Orders/Orders/Order/Order.cs | 1 + .../Categories/Domain/Category.cs | 13 +++++++++--- .../Products/Domain/Product.cs | 10 ++++++++-- .../Storage/Domain/AisleTests.cs | 8 ++++---- .../Domain/StorageAllocationServiceTests.cs | 8 ++++---- .../Products/Domain/Product.cs | 12 ++++++++--- .../Modules.Warehouse/Storage/Domain/Aisle.cs | 10 ++++++++-- .../Modules.Warehouse/Storage/Domain/Bay.cs | 11 ++++++++-- .../Modules.Warehouse/Storage/Domain/Shelf.cs | 10 ++++++++-- 22 files changed, 120 insertions(+), 74 deletions(-) create mode 100644 src/Common/Common.SharedKernel/Domain/Interfaces/IStronglyTypedId.cs diff --git a/src/Common/Common.SharedKernel/Domain/Base/Entity.cs b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs index 7d236e5..cffd49d 100644 --- a/src/Common/Common.SharedKernel/Domain/Base/Entity.cs +++ b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs @@ -21,9 +21,4 @@ public void Updated(DateTimeOffset dateTime, string? user) UpdatedAt = dateTime; UpdatedBy = user; } -} - -public interface IStronglyTypedId -{ - T Value { get; } -} +} \ No newline at end of file diff --git a/src/Common/Common.SharedKernel/Domain/Interfaces/IStronglyTypedId.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IStronglyTypedId.cs new file mode 100644 index 0000000..a0ac5a4 --- /dev/null +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IStronglyTypedId.cs @@ -0,0 +1,6 @@ +namespace Common.SharedKernel.Domain.Interfaces; + +public interface IStronglyTypedId +{ + T Value { get; } +} \ No newline at end of file diff --git a/src/Common/Common.SharedKernel/Persistence/Extensions/PropertyBuilderExtensions.cs b/src/Common/Common.SharedKernel/Persistence/Extensions/PropertyBuilderExtensions.cs index 4eb1895..0046e30 100644 --- a/src/Common/Common.SharedKernel/Persistence/Extensions/PropertyBuilderExtensions.cs +++ b/src/Common/Common.SharedKernel/Persistence/Extensions/PropertyBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Common.SharedKernel.Persistence.Extensions; diff --git a/src/Common/Common.SharedKernel/Persistence/Extensions/StronglyTypedIdConverter.cs b/src/Common/Common.SharedKernel/Persistence/Extensions/StronglyTypedIdConverter.cs index 0536da1..6a1bf14 100644 --- a/src/Common/Common.SharedKernel/Persistence/Extensions/StronglyTypedIdConverter.cs +++ b/src/Common/Common.SharedKernel/Persistence/Extensions/StronglyTypedIdConverter.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Interfaces; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Common.SharedKernel.Persistence.Extensions; diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs index 99e0bbd..137a2c1 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs @@ -1,5 +1,5 @@ using Modules.Orders.Carts; -using Modules.Orders.Orders; +using Modules.Orders.Common; namespace Modules.Orders.Tests.Cart; @@ -9,7 +9,7 @@ public class CartItemTests public void Create_ValidParameters_ShouldCreateCartItem() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var quantity = 2; var unitPrice = Money.Create(100m); @@ -27,7 +27,7 @@ public void Create_ValidParameters_ShouldCreateCartItem() public void Create_NegativeQuantity_ShouldThrow() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var quantity = -1; var unitPrice = Money.Create(100m); @@ -42,7 +42,7 @@ public void Create_NegativeQuantity_ShouldThrow() public void Create_NegativeUnitPrice_ShouldThrow() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var quantity = 2; var unitPrice = Money.Create(-100m); @@ -57,7 +57,7 @@ public void Create_NegativeUnitPrice_ShouldThrow() public void IncreaseQuantity_ShouldIncreaseQuantity() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var quantity = 2; var unitPrice = Money.Create(100m); var cartItem = CartItem.Create(productId, quantity, unitPrice); @@ -74,7 +74,7 @@ public void IncreaseQuantity_ShouldIncreaseQuantity() public void DecreaseQuantity_ShouldDecreaseQuantity() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var quantity = 5; var unitPrice = Money.Create(100m); var cartItem = CartItem.Create(productId, quantity, unitPrice); @@ -91,7 +91,7 @@ public void DecreaseQuantity_ShouldDecreaseQuantity() public void DecreaseQuantity_TooMany_ShouldThrow() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var quantity = 2; var unitPrice = Money.Create(100m); var cartItem = CartItem.Create(productId, quantity, unitPrice); diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs index 7a60264..62013ac 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs @@ -1,4 +1,4 @@ -using Modules.Orders.Orders; +using Modules.Orders.Common; namespace Modules.Orders.Tests.Cart; @@ -8,7 +8,7 @@ public class CartTests public void AddItem_ShouldIncreaseQuantity_WhenItemAlreadyExists() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId, 1, unitPrice); @@ -25,8 +25,8 @@ public void AddItem_ShouldIncreaseQuantity_WhenItemAlreadyExists() public void AddItem_ShouldAddNewItem_WhenItemDoesNotExist() { // Arrange - var productId1 = new ProductId(Uuid.Create()); - var productId2 = new ProductId(Uuid.Create()); + var productId1 = new ProductId(); + var productId2 = new ProductId(); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId1, 1, unitPrice); @@ -44,7 +44,7 @@ public void AddItem_ShouldAddNewItem_WhenItemDoesNotExist() public void RemoveItem_ShouldRemoveItem_WhenItemExists() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId, 1, unitPrice); @@ -60,8 +60,8 @@ public void RemoveItem_ShouldRemoveItem_WhenItemExists() public void RemoveItem_ShouldDoNothing_WhenItemDoesNotExist() { // Arrange - var productId1 = new ProductId(Uuid.Create()); - var productId2 = new ProductId(Uuid.Create()); + var productId1 = new ProductId(); + var productId2 = new ProductId(); var unitPrice = new Money(Currency.Default, 10); var cart = Carts.Cart.Create(productId1, 1, unitPrice); @@ -77,8 +77,8 @@ public void RemoveItem_ShouldDoNothing_WhenItemDoesNotExist() public void UpdateTotal_ShouldCalculateTotalPriceCorrectly() { // Arrange - var productId1 = new ProductId(Uuid.Create()); - var productId2 = new ProductId(Uuid.Create()); + var productId1 = new ProductId(); + var productId2 = new ProductId(); var unitPrice1 = new Money(Currency.Default, 10); var unitPrice2 = new Money(Currency.Default, 20); var cart = Carts.Cart.Create(productId1, 1, unitPrice1); diff --git a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs index ac42e4b..212258c 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs @@ -1,4 +1,4 @@ -using Modules.Orders.Orders; +using Modules.Orders.Common; using Modules.Orders.Orders.LineItem; using Modules.Orders.Orders.Order; @@ -11,7 +11,7 @@ public void Create_ValidParameters_ShouldCreateLineItem() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 2; @@ -30,7 +30,7 @@ public void Create_NegativePrice_ShouldThrow() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(-100m); var quantity = 2; @@ -46,7 +46,7 @@ public void Create_ZeroQuantity_ShouldThrow() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 0; @@ -62,7 +62,7 @@ public void Total_ShouldReturnCorrectAmount() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 2; @@ -78,7 +78,7 @@ public void Tax_ShouldReturnCorrectAmount() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 2; @@ -94,7 +94,7 @@ public void TotalIncludingTax_ShouldReturnCorrectAmount() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 2; @@ -110,7 +110,7 @@ public void AddQuantity_ShouldIncreaseQuantity() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -127,7 +127,7 @@ public void RemoveQuantity_ShouldDecreaseQuantity() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 5; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -144,7 +144,7 @@ public void RemoveQuantity_TooMany_ShouldThrow() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = Money.Create(100m); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs index 17c60af..839d925 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs @@ -1,4 +1,4 @@ -using Modules.Orders.Orders; +using Modules.Orders.Common; using Modules.Orders.Orders.LineItem; using Modules.Orders.Orders.Order; @@ -11,7 +11,7 @@ public void Create_ShouldInitializeLineItemWithCorrectValues() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.Default, 100); var quantity = 2; @@ -31,7 +31,7 @@ public void Create_ShouldThrowException_WhenPriceIsNegativeOrZero() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.Default, 0); var quantity = 2; Action act = () => LineItem.Create(orderId, productId, price, quantity); @@ -45,7 +45,7 @@ public void Create_ShouldThrowException_WhenQuantityIsNegativeOrZero() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.Default, 100); var quantity = 0; Action act = () => LineItem.Create(orderId, productId, price, quantity); @@ -59,7 +59,7 @@ public void AddQuantity_ShouldIncreaseQuantity() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.Default, 100); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -77,7 +77,7 @@ public void RemoveQuantity_ShouldDecreaseQuantity() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.Default, 100); var quantity = 5; var lineItem = LineItem.Create(orderId, productId, price, quantity); @@ -95,7 +95,7 @@ public void RemoveQuantity_ShouldThrowException_WhenRemovingMoreThanAvailable() { // Arrange var orderId = new OrderId(Uuid.Create()); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.Default, 100); var quantity = 2; var lineItem = LineItem.Create(orderId, productId, price, quantity); diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs index 08f1240..e7d0376 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs @@ -1,4 +1,4 @@ -using Modules.Orders.Orders; +using Modules.Orders.Common; using Modules.Orders.Orders.Order; namespace Modules.Orders.Tests.Orders @@ -10,7 +10,7 @@ public void AddLineItem_ShouldAddNewItem_WhenProductDoesNotExist() { // Arrange var order = Order.Create(new CustomerId(Uuid.Create())); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.USD, 100); var quantity = 1; @@ -28,7 +28,7 @@ public void AddLineItem_ShouldIncreaseQuantity_WhenProductExists() { // Arrange var order = Order.Create(new CustomerId(Uuid.Create())); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); @@ -47,7 +47,7 @@ public void RemoveLineItem_ShouldRemoveItem_WhenProductExists() { // Arrange var order = Order.Create(new CustomerId(Uuid.Create())); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); @@ -65,7 +65,7 @@ public void AddPayment_ShouldUpdateAmountPaid_WhenPaymentIsValid() { // Arrange var order = Order.Create(new CustomerId(Uuid.Create())); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); @@ -85,7 +85,7 @@ public void ShipOrder_ShouldUpdateStatusAndShippingDate_WhenOrderIsReadyForShipp { // Arrange var order = Order.Create(new CustomerId(Uuid.Create())); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var price = new Money(Currency.USD, 100); var quantity = 1; order.AddLineItem(productId, price, quantity); diff --git a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs index 5e37c0a..dc7509b 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/Cart.cs @@ -1,4 +1,4 @@ -using Modules.Orders.Orders; +using Modules.Orders.Common; namespace Modules.Orders.Carts; diff --git a/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs b/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs index 4e9ea2b..0aa727c 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs @@ -1,4 +1,4 @@ -using Modules.Orders.Orders; +using Modules.Orders.Common; namespace Modules.Orders.Carts; diff --git a/src/Modules/Orders/Modules.Orders/Common/ProductId.cs b/src/Modules/Orders/Modules.Orders/Common/ProductId.cs index 40bb0a8..a8466b2 100644 --- a/src/Modules/Orders/Modules.Orders/Common/ProductId.cs +++ b/src/Modules/Orders/Modules.Orders/Common/ProductId.cs @@ -1,3 +1,8 @@ -namespace Modules.Orders.Orders; +namespace Modules.Orders.Common; -internal record ProductId(Guid Value); \ No newline at end of file +internal record ProductId(Guid Value) +{ + internal ProductId() : this(Uuid.Create()) + { + } +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs index f62cfbf..cc84ee9 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs @@ -1,4 +1,5 @@ -using Modules.Orders.Orders.Order; +using Modules.Orders.Common; +using Modules.Orders.Orders.Order; using Throw; namespace Modules.Orders.Orders.LineItem; diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs index 9d82241..16b5721 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs @@ -1,5 +1,6 @@ using Common.SharedKernel.Domain.Exceptions; using ErrorOr; +using Modules.Orders.Common; using Modules.Orders.Orders.LineItem; using Success = ErrorOr.Success; diff --git a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs index f1f136b..389eabf 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/Domain/Category.cs @@ -1,6 +1,13 @@ -namespace Modules.Catalog.Categories.Domain; +using Common.SharedKernel.Domain.Interfaces; -internal record CategoryId(Guid Value) : IStronglyTypedId; +namespace Modules.Catalog.Categories.Domain; + +internal record CategoryId(Guid Value) : IStronglyTypedId +{ + internal CategoryId() : this(Uuid.Create()) + { + } +} internal class Category : AggregateRoot { @@ -16,7 +23,7 @@ public static Category Create(string name) { var category = new Category { - Id = new CategoryId(Uuid.Create()), + Id = new CategoryId(), }; category.UpdateName(name); diff --git a/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs index 34dced7..98d71d5 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs @@ -1,8 +1,14 @@ +using Common.SharedKernel.Domain.Interfaces; using Modules.Catalog.Categories.Domain; namespace Modules.Catalog.Products.Domain; -internal record ProductId(Guid Value) : IStronglyTypedId; +internal record ProductId(Guid Value) : IStronglyTypedId +{ + internal ProductId() : this(Uuid.Create()) + { + } +} internal class Product : AggregateRoot { @@ -29,7 +35,7 @@ public static Product Create(string name, string sku, ProductId? id = null) { Name = name, Sku = sku, - Id = id ?? new ProductId(Uuid.Create()) + Id = id ?? new ProductId() }; return product; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs index e5247cc..2366abd 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs @@ -17,8 +17,8 @@ public AisleTests(ITestOutputHelper output) public void LookingUpProduct_ReturnsAisleBayAndShelf() { // Exploratory - var productA = new ProductId(Uuid.Create()); - var productB = new ProductId(Uuid.Create()); + var productA = new ProductId(); + var productB = new ProductId(); var aisle = Aisle.Create("Aisle 1", 2, 3); StorageAllocationService.AllocateStorage(new List { aisle }, productA); @@ -94,7 +94,7 @@ public void Create_WithBaysAndShelves_CreatesCorrectNumberOfShelvesPerBay() public void AssignProduct_WithAvailableStorage_AssignsProductToShelf() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var sut = Aisle.Create("Aisle 1", 1, 1); // Act @@ -112,7 +112,7 @@ public void AssignProduct_WithAvailableStorage_AssignsProductToShelf() public void AssignProduct_WithNoAvailableStorage_ReturnsError() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var sut = Aisle.Create("Aisle 1", 1, 1); sut.AssignProduct(productId); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs index 290d40b..cc425f0 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/StorageAllocationServiceTests.cs @@ -9,7 +9,7 @@ public class StorageAllocationServiceTests public void AllocateStorage_ShouldAssignProductToFirstEmptyShelf() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var aisles = new List { Aisle.Create("name", 2, 2) @@ -27,7 +27,7 @@ public void AllocateStorage_ShouldAssignProductToFirstEmptyShelf() public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelfIsAvailable() { // Arrange - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); var aisles = new List { Aisle.Create("name", 1, 1) @@ -50,7 +50,7 @@ public void AllocateStorage_WhenMaxStorageUsed_ShouldHaveNoAvailableStorage() var numBays = 2; var numShelves = 3; var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); // Act for (var i = 0; i < numBays * numShelves; i++) @@ -71,7 +71,7 @@ public void AllocateStorage_ShouldThrowException_WhenNoEmptyShelf() var numBays = 2; var numShelves = 3; var aisle = Aisle.Create("Aisle 1", numBays, numShelves); - var productId = new ProductId(Uuid.Create()); + var productId = new ProductId(); // Act for (var i = 0; i < numBays * numShelves; i++) diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 9e1c112..2a6d290 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -1,9 +1,15 @@ -using ErrorOr; +using Common.SharedKernel.Domain.Interfaces; +using ErrorOr; using Throw; namespace Modules.Warehouse.Products.Domain; -internal record ProductId(Guid Value) : IStronglyTypedId; +internal record ProductId(Guid Value) : IStronglyTypedId +{ + internal ProductId() : this(Uuid.Create()) + { + } +} internal class Product : AggregateRoot { @@ -27,7 +33,7 @@ public static Product Create(string name, Sku sku) var product = new Product { - Id = new ProductId(Uuid.Create()), + Id = new ProductId(), StockOnHand = 0 }; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs index d759693..7962790 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Aisle.cs @@ -1,9 +1,15 @@ +using Common.SharedKernel.Domain.Interfaces; using ErrorOr; using Modules.Warehouse.Products.Domain; namespace Modules.Warehouse.Storage.Domain; -internal record AisleId(Guid Value) : IStronglyTypedId; +internal record AisleId(Guid Value) : IStronglyTypedId +{ + internal AisleId() : this(Uuid.Create()) + { + } +} internal class Aisle : AggregateRoot { @@ -25,7 +31,7 @@ public static Aisle Create(string name, int numBays, int numShelves) var aisle = new Aisle { - Id = new AisleId(Uuid.Create()), + Id = new AisleId(), Name = name }; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs index 6f171c5..c78c67e 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Bay.cs @@ -1,6 +1,13 @@ +using Common.SharedKernel.Domain.Interfaces; + namespace Modules.Warehouse.Storage.Domain; -public record BayId(Guid Value) : IStronglyTypedId; +internal record BayId(Guid Value) : IStronglyTypedId +{ + internal BayId() : this(Uuid.Create()) + { + } +} internal class Bay : Entity { @@ -21,7 +28,7 @@ public static Bay Create(string name, int numShelves) var bay = new Bay { - Id = new BayId(Uuid.Create()), + Id = new BayId(), Name = name }; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs index 71e5ac7..b48ae9d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/Domain/Shelf.cs @@ -1,8 +1,14 @@ +using Common.SharedKernel.Domain.Interfaces; using Modules.Warehouse.Products.Domain; namespace Modules.Warehouse.Storage.Domain; -internal record ShelfId(Guid Value) : IStronglyTypedId; +internal record ShelfId(Guid Value) : IStronglyTypedId +{ + internal ShelfId() : this(Uuid.Create()) + { + } +} internal class Shelf : Entity { @@ -18,7 +24,7 @@ public static Shelf Create(string name) return new Shelf { - Id = new ShelfId(Uuid.Create()), + Id = new ShelfId(), Name = name }; } From f33542354d7ac30de57b245064974754882dfa60 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:52:07 +1000 Subject: [PATCH 71/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improved=20Test=20Co?= =?UTF-8?q?ntainer=20retry=20by=20introducing=20Polly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Common/Common.Tests/Common.Tests.csproj | 1 + .../Common.Tests/Common/DatabaseContainer.cs | 28 ++++--------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index 5c986eb..5817c0d 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -3,6 +3,7 @@ + diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index bb6a2c2..0ccb5bf 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -1,3 +1,4 @@ +using Polly; using Testcontainers.MsSql; namespace Common.Tests.Common; @@ -28,29 +29,10 @@ public async Task InitializeAsync() private async Task StartWithRetry() { // NOTE: For some reason the container sometimes fails to start up. Add in a retry to protect against this - var notReady = true; - var numTries = 0; - - while (notReady) - { - if (numTries >= MaxRetries) - { - Console.WriteLine("Max tries reached, giving up"); - break; - } - - try - { - await _container.StartAsync(); - notReady = false; - } - catch (Exception ex) - { - numTries++; - await Task.Delay(2000); - Console.WriteLine($"container failed to start: {ex.Message}"); - } - } + var policy = Policy.Handle() + .WaitAndRetry(MaxRetries, _ => TimeSpan.FromMilliseconds(2000)); + + await policy.Execute(async () => { await _container.StartAsync(); }); } public async Task DisposeAsync() From 38d44be79e9873bffe91ace5e17b1ba5cdc29700 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:59:20 +1000 Subject: [PATCH 72/87] =?UTF-8?q?=E2=9C=A8=20Add=20EntityFramework.Excepti?= =?UTF-8?q?ons=20to=20produce=20easy=20to=20read=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Common/Common.Tests/Common/DatabaseContainer.cs | 6 +++--- .../Common/Persistence/CatalogDbContext.cs | 13 +++++++++++-- .../Products/Modules.Catalog/Modules.Catalog.csproj | 1 + .../Common/Persistence/WarehouseDbContext.cs | 13 +++++++++++-- .../Modules.Warehouse/Modules.Warehouse.csproj | 1 + 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index 0ccb5bf..8cf820b 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -4,9 +4,9 @@ namespace Common.Tests.Common; /// -/// Wrapper for SQL edge container +/// Wrapper for MS SQL container /// -public class DatabaseContainer +public class DatabaseContainer : IAsyncDisposable { private readonly MsSqlContainer _container = new MsSqlBuilder() .WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") @@ -35,7 +35,7 @@ private async Task StartWithRetry() await policy.Execute(async () => { await _container.StartAsync(); }); } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { await _container.StopAsync(); await _container.DisposeAsync(); diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs index 9b6350f..f738cd7 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using EntityFramework.Exceptions.SqlServer; +using Microsoft.EntityFrameworkCore; using Modules.Catalog.Categories.Domain; using Modules.Catalog.Products.Domain; @@ -21,4 +22,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly); base.OnModelCreating(modelBuilder); } -} + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // Produces easy to read exceptions + optionsBuilder.UseExceptionProcessor(); + + base.OnConfiguring(optionsBuilder); + } +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index b217bb1..28aaf6d 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -3,6 +3,7 @@ + diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index 0a01f25..6c3f097 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using EntityFramework.Exceptions.SqlServer; +using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; @@ -22,4 +23,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); base.OnModelCreating(modelBuilder); } -} + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // Produces easy to read exceptions + optionsBuilder.UseExceptionProcessor(); + + base.OnConfiguring(optionsBuilder); + } +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 524c307..36b67ae 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -3,6 +3,7 @@ + From 27830e9bafb26eee004b1cd7028e545293b74e74 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:24:20 +1000 Subject: [PATCH 73/87] =?UTF-8?q?=F0=9F=A7=AA=20Refactor=20tests=20and=20a?= =?UTF-8?q?dd=20migrations=20(#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Start using EF migrations * Update test setup to use migrations and add retry to test container start. * Add catalog back to Database project * Fix async code in test container retry * Add tests back in --- .editorconfig | 2 +- .github/workflows/dotnet.yml | 20 +- ModularMonolith.sln | 2 + docker-compose.yml | 2 +- .../Common.Tests/Common/DatabaseContainer.cs | 25 ++- .../Common/IntegrationTestBase.cs | 6 +- .../Common/TestingDatabaseFixture.cs | 18 +- .../Common.Tests/Common/WebApiTestFactory.cs | 10 +- .../Extensions/ServiceCollectionExt.cs | 4 +- src/Modules/Products/AddCatalogMigration.ps1 | 5 + .../Common/WarehouseIntegrationTestBase.cs | 4 +- .../20240925223513_Initial.Designer.cs | 133 ++++++++++++ .../Migrations/20240925223513_Initial.cs | 104 ++++++++++ .../CatalogDbContextModelSnapshot.cs | 130 ++++++++++++ .../Warehouse/AddWarehouseMigration.ps1 | 5 + .../Common/WarehouseIntegrationTestBase.cs | 4 +- .../Modules.Warehouse.Tests/GlobalUsings.cs | 1 - .../20240925224635_Initial.Designer.cs | 190 ++++++++++++++++++ .../Migrations/20240925224635_Initial.cs | 149 ++++++++++++++ .../WarehouseDbContextModelSnapshot.cs | 187 +++++++++++++++++ src/WebApi.Tests/WebApi.Tests.csproj | 4 - src/WebApi/WebApi.csproj | 4 + tools/Database/Database.csproj | 6 + .../CatalogDbContextInitialiser.cs | 6 +- .../WarehouseDbContextInitialiser.cs | 4 +- tools/Database/Program.cs | 6 + 26 files changed, 983 insertions(+), 48 deletions(-) create mode 100644 src/Modules/Products/AddCatalogMigration.ps1 create mode 100644 src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.Designer.cs create mode 100644 src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.cs create mode 100644 src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/CatalogDbContextModelSnapshot.cs create mode 100644 src/Modules/Warehouse/AddWarehouseMigration.ps1 create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.Designer.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.cs create mode 100644 src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/WarehouseDbContextModelSnapshot.cs diff --git a/.editorconfig b/.editorconfig index e35d5f5..a586d89 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ root = true ########################################################################################## # All files -[*] +[*]ls' indent_style = space # Xml files diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 446ad53..dc3f1c4 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -34,13 +34,13 @@ jobs: - name: Build run: dotnet build --no-restore -c Debug -# - name: Test -# run: dotnet test --no-build --verbosity normal -c Debug --logger "trx;LogFileName=test-results.trx" -# -# - name: Test Report -# uses: dorny/test-reporter@v1 -# if: success() || failure() -# with: -# name: Tests Results -# path: "**/test-results.trx" -# reporter: dotnet-trx + - name: Test + run: dotnet test --no-build --verbosity normal -c Debug --logger "trx;LogFileName=test-results.trx" + + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() + with: + name: Tests Results + path: "**/test-results.trx" + reporter: dotnet-trx diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 5afc8c9..4868cb3 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -62,6 +62,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".sln", ".sln", "{63C08527-F ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props + src\Modules\Products\AddCatalogMigration.ps1 = src\Modules\Products\AddCatalogMigration.ps1 + src\Modules\Warehouse\AddWarehouseMigration.ps1 = src\Modules\Warehouse\AddWarehouseMigration.ps1 EndProjectSection EndProject Global diff --git a/docker-compose.yml b/docker-compose.yml index c8bcf6d..9a25199 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - 1800:1433 restart: unless-stopped healthcheck: - test: [ "CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P yourStrong(!)Password -Q 'SELECT 1' || exit 1" ] + test: [ "CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password123 -Q 'SELECT 1' || exit 1" ] interval: 10s retries: 10 start_period: 10s diff --git a/src/Common/Common.Tests/Common/DatabaseContainer.cs b/src/Common/Common.Tests/Common/DatabaseContainer.cs index 8cf820b..22b0a2e 100644 --- a/src/Common/Common.Tests/Common/DatabaseContainer.cs +++ b/src/Common/Common.Tests/Common/DatabaseContainer.cs @@ -29,15 +29,30 @@ public async Task InitializeAsync() private async Task StartWithRetry() { // NOTE: For some reason the container sometimes fails to start up. Add in a retry to protect against this - var policy = Policy.Handle() - .WaitAndRetry(MaxRetries, _ => TimeSpan.FromMilliseconds(2000)); - - await policy.Execute(async () => { await _container.StartAsync(); }); + var policy = Policy.Handle() + .WaitAndRetryAsync(MaxRetries, _ => TimeSpan.FromSeconds(5)); + + await policy.ExecuteAsync(async () => { await _container.StartAsync(); }); + + // var ready = false; + // while (!ready) + // { + // try + // { + // await _container.StartAsync(); + // ready = true; + // } + // catch (Exception) + // { + // await Task.Delay(2000); + // } + // } } public async ValueTask DisposeAsync() { await _container.StopAsync(); await _container.DisposeAsync(); + GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/src/Common/Common.Tests/Common/IntegrationTestBase.cs b/src/Common/Common.Tests/Common/IntegrationTestBase.cs index 10a2ed3..85a4cd7 100644 --- a/src/Common/Common.Tests/Common/IntegrationTestBase.cs +++ b/src/Common/Common.Tests/Common/IntegrationTestBase.cs @@ -13,7 +13,7 @@ public abstract class IntegrationTestBase : IAsyncLifetime where TDb { private readonly IServiceScope _scope; - private readonly TestingDatabaseFixture _fixture; + private readonly TestingDatabaseFixture _fixture; protected IMediator Mediator { get; } @@ -21,7 +21,7 @@ public abstract class IntegrationTestBase : IAsyncLifetime where TDb private TDbContext DbContext { get; } - protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper output) + protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper output) { _fixture = fixture; _fixture.SetOutput(output); @@ -63,4 +63,4 @@ public Task DisposeAsync() _scope.Dispose(); return Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs index 4787ca1..be23789 100644 --- a/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs +++ b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Modules.Catalog.Common.Persistence; +using Modules.Warehouse.Common.Persistence; using Respawn; using Xunit; using Xunit.Abstractions; @@ -10,7 +12,7 @@ namespace Common.Tests.Common; /// Initializes and resets the database before and after each test /// // ReSharper disable once ClassNeverInstantiated.Global -public class TestingDatabaseFixture : IAsyncLifetime where TDbContext : DbContext +public class TestingDatabaseFixture : IAsyncLifetime { private string ConnectionString => Factory.Database.ConnectionString!; @@ -18,7 +20,7 @@ public class TestingDatabaseFixture : IAsyncLifetime where TDbContex public IServiceScopeFactory ScopeFactory { get; private set; } = null!; - public WebApiTestFactory Factory { get; } = new(); + public WebApiTestFactory Factory { get; } = new(); public async Task InitializeAsync() { @@ -26,10 +28,14 @@ public async Task InitializeAsync() await Factory.Database.InitializeAsync(); ScopeFactory = Factory.Services.GetRequiredService(); - // Create and seed database + // Create and seed databases using var scope = ScopeFactory.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - await dbContext.Database.EnsureCreatedAsync(); + + var catalogDb = scope.ServiceProvider.GetRequiredService(); + await catalogDb.Database.MigrateAsync(); + + var warehouseDb = scope.ServiceProvider.GetRequiredService(); + await warehouseDb.Database.MigrateAsync(); // NOTE: If there are any tables you want to skip being reset, they can be configured here _checkpoint = await Respawner.CreateAsync(ConnectionString); @@ -46,4 +52,4 @@ public async Task ResetState() } public void SetOutput(ITestOutputHelper output) => Factory.Output = output; -} +} \ No newline at end of file diff --git a/src/Common/Common.Tests/Common/WebApiTestFactory.cs b/src/Common/Common.Tests/Common/WebApiTestFactory.cs index e4f6841..3fafb6b 100644 --- a/src/Common/Common.Tests/Common/WebApiTestFactory.cs +++ b/src/Common/Common.Tests/Common/WebApiTestFactory.cs @@ -3,9 +3,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Modules.Catalog.Common.Persistence; +using Modules.Warehouse.Common.Persistence; using WebApi; using Xunit.Abstractions; @@ -14,7 +15,7 @@ namespace Common.Tests.Common; /// /// Host builder (services, DI, and configuration) for integration tests /// -public class WebApiTestFactory : WebApplicationFactory where TDbContext : DbContext +public class WebApiTestFactory : WebApplicationFactory { public DatabaseContainer Database { get; } = new(); @@ -35,7 +36,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) // Override default DB registration to use out Test Container instead builder.ConfigureTestServices(services => { - services.ReplaceDbContext(Database); + services.ReplaceDbContext(Database); + services.ReplaceDbContext(Database); }); } -} +} \ No newline at end of file diff --git a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs index 52fac95..2b7f6bd 100644 --- a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs +++ b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs @@ -22,8 +22,8 @@ internal static IServiceCollection ReplaceDbContext( .RemoveAll() .AddDbContext((_, options) => { - options.UseSqlServer(databaseContainer.ConnectionString); - // b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); + options.UseSqlServer(databaseContainer.ConnectionString, + b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); options.LogTo(m => Debug.WriteLine(m)); options.EnableSensitiveDataLogging(); diff --git a/src/Modules/Products/AddCatalogMigration.ps1 b/src/Modules/Products/AddCatalogMigration.ps1 new file mode 100644 index 0000000..e2416fc --- /dev/null +++ b/src/Modules/Products/AddCatalogMigration.ps1 @@ -0,0 +1,5 @@ +dotnet ef migrations add Initial ` + --project ./Modules.Catalog ` + --startup-project ../../WebApi ` + --output-dir ./Common/Persistence/Migrations ` + --context CatalogDbContext diff --git a/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs b/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs index 016ffe7..3ca7922 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Common/WarehouseIntegrationTestBase.cs @@ -5,7 +5,7 @@ namespace Modules.Catalog.Tests.Common; // ReSharper disable once ClassNeverInstantiated.Global -public class CatalogDatabaseFixture : TestingDatabaseFixture; +public class CatalogDatabaseFixture : TestingDatabaseFixture; [Collection(CatalogFixtureCollection.Name)] public abstract class CatalogIntegrationTestBase( @@ -21,4 +21,4 @@ public class CatalogFixtureCollection : ICollectionFixture interfaces. public const string Name = nameof(CatalogFixtureCollection); -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.Designer.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.Designer.cs new file mode 100644 index 0000000..f0fd675 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.Designer.cs @@ -0,0 +1,133 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Catalog.Common.Persistence; + +#nullable disable + +namespace Modules.Catalog.Common.Persistence.Migrations +{ + [DbContext(typeof(CatalogDbContext))] + [Migration("20240925223513_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("catalog") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProduct", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProductId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("CategoryProduct", "catalog"); + }); + + modelBuilder.Entity("Modules.Catalog.Categories.Domain.Category", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories", "catalog"); + }); + + modelBuilder.Entity("Modules.Catalog.Products.Domain.Product", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.ComplexProperty>("Price", "Modules.Catalog.Products.Domain.Product.Price#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.HasKey("Id"); + + b.ToTable("Products", "catalog"); + }); + + modelBuilder.Entity("CategoryProduct", b => + { + b.HasOne("Modules.Catalog.Categories.Domain.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Modules.Catalog.Products.Domain.Product", null) + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.cs new file mode 100644 index 0000000..2d699cc --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/20240925223513_Initial.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Modules.Catalog.Common.Persistence.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "catalog"); + + migrationBuilder.CreateTable( + name: "Categories", + schema: "catalog", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + schema: "catalog", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Sku = table.Column(type: "nvarchar(max)", nullable: false), + Price_Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + Price_Currency = table.Column(type: "nvarchar(3)", maxLength: 3, nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CategoryProduct", + schema: "catalog", + columns: table => new + { + CategoriesId = table.Column(type: "uniqueidentifier", nullable: false), + ProductId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CategoryProduct", x => new { x.CategoriesId, x.ProductId }); + table.ForeignKey( + name: "FK_CategoryProduct_Categories_CategoriesId", + column: x => x.CategoriesId, + principalSchema: "catalog", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CategoryProduct_Products_ProductId", + column: x => x.ProductId, + principalSchema: "catalog", + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CategoryProduct_ProductId", + schema: "catalog", + table: "CategoryProduct", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CategoryProduct", + schema: "catalog"); + + migrationBuilder.DropTable( + name: "Categories", + schema: "catalog"); + + migrationBuilder.DropTable( + name: "Products", + schema: "catalog"); + } + } +} diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/CatalogDbContextModelSnapshot.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/CatalogDbContextModelSnapshot.cs new file mode 100644 index 0000000..22f6df9 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/Migrations/CatalogDbContextModelSnapshot.cs @@ -0,0 +1,130 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Catalog.Common.Persistence; + +#nullable disable + +namespace Modules.Catalog.Common.Persistence.Migrations +{ + [DbContext(typeof(CatalogDbContext))] + partial class CatalogDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("catalog") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProduct", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProductId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("CategoryProduct", "catalog"); + }); + + modelBuilder.Entity("Modules.Catalog.Categories.Domain.Category", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories", "catalog"); + }); + + modelBuilder.Entity("Modules.Catalog.Products.Domain.Product", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.ComplexProperty>("Price", "Modules.Catalog.Products.Domain.Product.Price#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.HasKey("Id"); + + b.ToTable("Products", "catalog"); + }); + + modelBuilder.Entity("CategoryProduct", b => + { + b.HasOne("Modules.Catalog.Categories.Domain.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Modules.Catalog.Products.Domain.Product", null) + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Warehouse/AddWarehouseMigration.ps1 b/src/Modules/Warehouse/AddWarehouseMigration.ps1 new file mode 100644 index 0000000..9e0e18f --- /dev/null +++ b/src/Modules/Warehouse/AddWarehouseMigration.ps1 @@ -0,0 +1,5 @@ +dotnet ef migrations add Initial ` + --project ./Modules.Warehouse ` + --startup-project ../../WebApi ` + --output-dir ./Common/Persistence/Migrations ` + --context WarehouseDbContext diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs index 0e0fe69..1039841 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Common/WarehouseIntegrationTestBase.cs @@ -5,7 +5,7 @@ namespace Modules.Warehouse.Tests.Common; // ReSharper disable once ClassNeverInstantiated.Global -public class WarehouseDatabaseFixture : TestingDatabaseFixture; +public class WarehouseDatabaseFixture : TestingDatabaseFixture; [Collection(WarehouseFixtureCollection.Name)] public abstract class WarehouseIntegrationTestBase( @@ -21,4 +21,4 @@ public class WarehouseFixtureCollection : ICollectionFixture interfaces. public const string Name = nameof(WarehouseFixtureCollection); -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs index 5f12dfc..a4c5550 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs @@ -1,3 +1,2 @@ -global using Common.SharedKernel.Domain; global using FluentAssertions; global using Xunit; \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.Designer.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.Designer.cs new file mode 100644 index 0000000..9752d71 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.Designer.cs @@ -0,0 +1,190 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Warehouse.Common.Persistence; + +#nullable disable + +namespace Modules.Warehouse.Common.Persistence.Migrations +{ + [DbContext(typeof(WarehouseDbContext))] + [Migration("20240925224635_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("warehouse") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Modules.Warehouse.Products.Domain.Product", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StockOnHand") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Products", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Aisle", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Aisles", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Bay", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AisleId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AisleId"); + + b.ToTable("Bay", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Shelf", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("BayId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BayId"); + + b.HasIndex("ProductId"); + + b.ToTable("Shelf", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Bay", b => + { + b.HasOne("Modules.Warehouse.Storage.Domain.Aisle", null) + .WithMany("Bays") + .HasForeignKey("AisleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Shelf", b => + { + b.HasOne("Modules.Warehouse.Storage.Domain.Bay", null) + .WithMany("Shelves") + .HasForeignKey("BayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Modules.Warehouse.Products.Domain.Product", null) + .WithMany() + .HasForeignKey("ProductId"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Aisle", b => + { + b.Navigation("Bays"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Bay", b => + { + b.Navigation("Shelves"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.cs new file mode 100644 index 0000000..8966e95 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/20240925224635_Initial.cs @@ -0,0 +1,149 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Modules.Warehouse.Common.Persistence.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "warehouse"); + + migrationBuilder.CreateTable( + name: "Aisles", + schema: "warehouse", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Aisles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + schema: "warehouse", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Sku = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + StockOnHand = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Bay", + schema: "warehouse", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + AisleId = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Bay", x => x.Id); + table.ForeignKey( + name: "FK_Bay_Aisles_AisleId", + column: x => x.AisleId, + principalSchema: "warehouse", + principalTable: "Aisles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Shelf", + schema: "warehouse", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + ProductId = table.Column(type: "uniqueidentifier", nullable: true), + BayId = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Shelf", x => x.Id); + table.ForeignKey( + name: "FK_Shelf_Bay_BayId", + column: x => x.BayId, + principalSchema: "warehouse", + principalTable: "Bay", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Shelf_Products_ProductId", + column: x => x.ProductId, + principalSchema: "warehouse", + principalTable: "Products", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Bay_AisleId", + schema: "warehouse", + table: "Bay", + column: "AisleId"); + + migrationBuilder.CreateIndex( + name: "IX_Shelf_BayId", + schema: "warehouse", + table: "Shelf", + column: "BayId"); + + migrationBuilder.CreateIndex( + name: "IX_Shelf_ProductId", + schema: "warehouse", + table: "Shelf", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Shelf", + schema: "warehouse"); + + migrationBuilder.DropTable( + name: "Bay", + schema: "warehouse"); + + migrationBuilder.DropTable( + name: "Products", + schema: "warehouse"); + + migrationBuilder.DropTable( + name: "Aisles", + schema: "warehouse"); + } + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/WarehouseDbContextModelSnapshot.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/WarehouseDbContextModelSnapshot.cs new file mode 100644 index 0000000..82dcb0d --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/Migrations/WarehouseDbContextModelSnapshot.cs @@ -0,0 +1,187 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Warehouse.Common.Persistence; + +#nullable disable + +namespace Modules.Warehouse.Common.Persistence.Migrations +{ + [DbContext(typeof(WarehouseDbContext))] + partial class WarehouseDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("warehouse") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Modules.Warehouse.Products.Domain.Product", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StockOnHand") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Products", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Aisle", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Aisles", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Bay", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AisleId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AisleId"); + + b.ToTable("Bay", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Shelf", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("BayId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BayId"); + + b.HasIndex("ProductId"); + + b.ToTable("Shelf", "warehouse"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Bay", b => + { + b.HasOne("Modules.Warehouse.Storage.Domain.Aisle", null) + .WithMany("Bays") + .HasForeignKey("AisleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Shelf", b => + { + b.HasOne("Modules.Warehouse.Storage.Domain.Bay", null) + .WithMany("Shelves") + .HasForeignKey("BayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Modules.Warehouse.Products.Domain.Product", null) + .WithMany() + .HasForeignKey("ProductId"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Aisle", b => + { + b.Navigation("Bays"); + }); + + modelBuilder.Entity("Modules.Warehouse.Storage.Domain.Bay", b => + { + b.Navigation("Shelves"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/WebApi.Tests/WebApi.Tests.csproj b/src/WebApi.Tests/WebApi.Tests.csproj index 010b620..4704fd0 100644 --- a/src/WebApi.Tests/WebApi.Tests.csproj +++ b/src/WebApi.Tests/WebApi.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - false true diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index b86c74a..8fdbe42 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -6,6 +6,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tools/Database/Database.csproj b/tools/Database/Database.csproj index 15f335a..dfed01c 100644 --- a/tools/Database/Database.csproj +++ b/tools/Database/Database.csproj @@ -14,4 +14,10 @@ + + + Always + + + diff --git a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs index 7fbba6f..da7921e 100644 --- a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs +++ b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs @@ -28,9 +28,7 @@ internal async Task InitializeAsync() { if (_dbContext.Database.IsSqlServer()) { - // TODO: Move to migrations - await _dbContext.Database.EnsureDeletedAsync(); - await _dbContext.Database.EnsureCreatedAsync(); + await _dbContext.Database.MigrateAsync(); } } catch (Exception e) @@ -86,4 +84,4 @@ private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEn await _dbContext.SaveChangesAsync(); } -} +} \ No newline at end of file diff --git a/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs b/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs index 4205674..e2136f0 100644 --- a/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs +++ b/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs @@ -30,9 +30,7 @@ internal async Task InitializeAsync() { if (_dbContext.Database.IsSqlServer()) { - // TODO: Move to migrations - await _dbContext.Database.EnsureDeletedAsync(); - await _dbContext.Database.EnsureCreatedAsync(); + await _dbContext.Database.MigrateAsync(); } } catch (Exception e) diff --git a/tools/Database/Program.cs b/tools/Database/Program.cs index eb164da..35182ef 100644 --- a/tools/Database/Program.cs +++ b/tools/Database/Program.cs @@ -14,6 +14,8 @@ { services.AddSingleton(TimeProvider.System); + var conn = context.Configuration.GetConnectionString("Warehouse"); + services.AddDbContext(options => { options.UseSqlServer(context.Configuration.GetConnectionString("Warehouse"), opt => @@ -39,6 +41,10 @@ // Initialise and seed database using var scope = app.Services.CreateScope(); + +Console.WriteLine("Waiting for SQL Server..."); +await Task.Delay(5000); + var warehouse = scope.ServiceProvider.GetRequiredService(); await warehouse.InitializeAsync(); var warehouseProducts = await warehouse.SeedAsync(); From 922bf05283a6c27b8300d93ef6cfe544a3d4702f Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:18:33 +1000 Subject: [PATCH 74/87] =?UTF-8?q?=E2=9C=A8=20Added=20register=20customer?= =?UTF-8?q?=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules.Customers.Tests/AddressTests.cs | 2 +- .../Modules.Customers.Tests/CustomerTests.cs | 2 +- .../Configuration/CustomerConfiguration.cs | 19 ++++++ .../Common/Persistence/CustomersDbContext.cs | 32 ++++++++++ .../Persistence/DepdendencyInjection.cs | 39 ++++++++++++ .../Modules.Customers/Customers/CustomerId.cs | 3 - .../Customers/{ => Domain}/Address.cs | 2 +- .../Customers/{ => Domain}/Customer.cs | 4 +- .../{ => Domain}/CustomerCreatedEvent.cs | 2 +- .../Customers/Domain/CustomerId.cs | 10 +++ .../UseCases/CreateProductCommand.cs | 62 +++++++++++++++++++ .../Modules.Customers/CustomersModule.cs | 30 +++++++++ .../Modules.Customers.csproj | 6 ++ src/WebApi/Program.cs | 6 +- src/WebApi/WebApi.csproj | 1 + 15 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs create mode 100644 src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs create mode 100644 src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs delete mode 100644 src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs rename src/Modules/Customers/Modules.Customers/Customers/{ => Domain}/Address.cs (94%) rename src/Modules/Customers/Modules.Customers/Customers/{ => Domain}/Customer.cs (96%) rename src/Modules/Customers/Modules.Customers/Customers/{ => Domain}/CustomerCreatedEvent.cs (87%) create mode 100644 src/Modules/Customers/Modules.Customers/Customers/Domain/CustomerId.cs create mode 100644 src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs create mode 100644 src/Modules/Customers/Modules.Customers/CustomersModule.cs diff --git a/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs b/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs index bac17ad..3772d30 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs +++ b/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs @@ -1,4 +1,4 @@ -using Modules.Customers.Customers; +using Modules.Customers.Customers.Domain; namespace Modules.Customers.Tests; diff --git a/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs b/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs index b12eebc..9672e88 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs +++ b/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs @@ -1,4 +1,4 @@ -using Modules.Customers.Customers; +using Modules.Customers.Customers.Domain; namespace Modules.Customers.Tests; diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs new file mode 100644 index 0000000..8011e4e --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs @@ -0,0 +1,19 @@ +using Common.SharedKernel.Persistence.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Customers.Customers.Domain; + +namespace Modules.Customers.Common.Persistence.Configuration; + +internal class CustomerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(m => m.Id); + + builder + .Property(m => m.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + } +} diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs new file mode 100644 index 0000000..12b5526 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs @@ -0,0 +1,32 @@ +using EntityFramework.Exceptions.SqlServer; +using Microsoft.EntityFrameworkCore; +using Modules.Customers.Customers.Domain; + +namespace Modules.Customers.Common.Persistence; + +// Needs to be public for tests +public class CustomersDbContext : DbContext +{ + internal DbSet Customers => Set(); + + + // Needs to be public for the Database project + public CustomersDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("customer"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(CustomersDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // Produces easy to read exceptions + optionsBuilder.UseExceptionProcessor(); + + base.OnConfiguring(optionsBuilder); + } +} diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs new file mode 100644 index 0000000..d999409 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs @@ -0,0 +1,39 @@ +using Common.SharedKernel.Persistence.Interceptors; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Persistence.Interceptors; + +namespace Modules.Customers.Common.Persistence; + +internal static class DepdendencyInjection +{ + internal static void AddPersistence(this IServiceCollection services, IConfiguration config) + { + var connectionString = config.GetConnectionString("Customers"); + services.AddDbContext(options => + { + options.UseSqlServer(connectionString, builder => + { + builder.MigrationsAssembly(typeof(CustomersModule).Assembly.FullName); + builder.EnableRetryOnFailure(3); + }); + + var serviceProvider = services.BuildServiceProvider(); + + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + }); + + services.AddScoped(); + services.AddScoped(); + // services.AddScoped(); + } + + public static IApplicationBuilder UseInfrastructureMiddleware(this IApplicationBuilder app) + { + return app; + } +} diff --git a/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs b/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs deleted file mode 100644 index 287a622..0000000 --- a/src/Modules/Customers/Modules.Customers/Customers/CustomerId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Modules.Customers.Customers; - -internal record CustomerId(Guid Value); diff --git a/src/Modules/Customers/Modules.Customers/Customers/Address.cs b/src/Modules/Customers/Modules.Customers/Customers/Domain/Address.cs similarity index 94% rename from src/Modules/Customers/Modules.Customers/Customers/Address.cs rename to src/Modules/Customers/Modules.Customers/Customers/Domain/Address.cs index 693cf58..0f69a3d 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Address.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Domain/Address.cs @@ -1,4 +1,4 @@ -namespace Modules.Customers.Customers; +namespace Modules.Customers.Customers.Domain; internal record Address { diff --git a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs b/src/Modules/Customers/Modules.Customers/Customers/Domain/Customer.cs similarity index 96% rename from src/Modules/Customers/Modules.Customers/Customers/Customer.cs rename to src/Modules/Customers/Modules.Customers/Customers/Domain/Customer.cs index 24d4000..4b3523f 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Customer.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Domain/Customer.cs @@ -1,4 +1,4 @@ -namespace Modules.Customers.Customers; +namespace Modules.Customers.Customers.Domain; /* Invariants: * - Must have a unique email address (handled by application) @@ -45,4 +45,4 @@ public void UpdateAddress(Address address) ArgumentNullException.ThrowIfNull(address); Address = address; } -} \ No newline at end of file +} diff --git a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs b/src/Modules/Customers/Modules.Customers/Customers/Domain/CustomerCreatedEvent.cs similarity index 87% rename from src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs rename to src/Modules/Customers/Modules.Customers/Customers/Domain/CustomerCreatedEvent.cs index eb49653..2252afb 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Domain/CustomerCreatedEvent.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Interfaces; -namespace Modules.Customers.Customers; +namespace Modules.Customers.Customers.Domain; internal record CustomerCreatedEvent(CustomerId Id, string FirstName, string LastName) : IDomainEvent { diff --git a/src/Modules/Customers/Modules.Customers/Customers/Domain/CustomerId.cs b/src/Modules/Customers/Modules.Customers/Customers/Domain/CustomerId.cs new file mode 100644 index 0000000..b22edbd --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Customers/Domain/CustomerId.cs @@ -0,0 +1,10 @@ +using Common.SharedKernel.Domain.Interfaces; + +namespace Modules.Customers.Customers.Domain; + +internal record CustomerId(Guid Value) : IStronglyTypedId +{ + internal CustomerId() : this(Uuid.Create()) + { + } +} diff --git a/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs b/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs new file mode 100644 index 0000000..646cd17 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs @@ -0,0 +1,62 @@ +using Common.SharedKernel; +using Common.SharedKernel.Api; +using ErrorOr; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Modules.Customers.Common.Persistence; +using Modules.Customers.Customers.Domain; + +namespace Modules.Customers.Customers.UseCases; + +public static class RegisterCustomerCommand +{ + public record Request(string FirstName, string LastName, string Email) : IRequest>; + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/customers", async (Request request, ISender sender) => + { + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Created(); + }) + .WithName("Create Customer") + .WithTags("Customers") + .ProducesPost() + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.FirstName).NotEmpty(); + RuleFor(r => r.LastName).NotEmpty(); + RuleFor(r => r.Email).NotEmpty(); + } + } + + internal class Handler : IRequestHandler> + { + private readonly CustomersDbContext _dbContext; + + public Handler(CustomersDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var customer = Customer.Create(request.Email, request.FirstName, request.LastName); + _dbContext.Customers.Add(customer); + await _dbContext.SaveChangesAsync(cancellationToken); + + return Result.Success; + } + } +} diff --git a/src/Modules/Customers/Modules.Customers/CustomersModule.cs b/src/Modules/Customers/Modules.Customers/CustomersModule.cs new file mode 100644 index 0000000..8176975 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/CustomersModule.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Modules.Customers.Common.Persistence; +using Modules.Customers.Customers.UseCases; + +namespace Modules.Customers; + +public static class CustomersModule +{ + public static void AddCustomers(this IServiceCollection services, IConfiguration configuration) + { + var applicationAssembly = typeof(CustomersModule).Assembly; + + services.AddHttpContextAccessor(); + + services.AddValidatorsFromAssembly(applicationAssembly); + + services.AddPersistence(configuration); + } + + public static void UseCustomers(this WebApplication app) + { + app.UseInfrastructureMiddleware(); + + // TODO: Consider source generation or reflection for endpoint mapping + RegisterCustomerCommand.Endpoint.MapEndpoint(app); + } +} diff --git a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj index ba34f7a..95a9659 100644 --- a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj +++ b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj @@ -5,7 +5,13 @@ + + + + + + diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 28850c4..ae45a99 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,5 +1,6 @@ using Common.SharedKernel; using Modules.Catalog; +using Modules.Customers; using Modules.Orders; using Modules.Warehouse; using WebApi.Extensions; @@ -14,9 +15,9 @@ builder.Services.AddMediatR(); - // builder.Services.AddOrders(); builder.Services.AddWarehouse(builder.Configuration); builder.Services.AddCatalog(builder.Configuration); + builder.Services.AddCustomers(builder.Configuration); } var app = builder.Build(); @@ -33,6 +34,7 @@ app.UseOrders(); app.UseWarehouse(); app.UseCatalog(); + app.UseCustomers(); app.Run(); -} \ No newline at end of file +} diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 8fdbe42..eae5944 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -14,6 +14,7 @@ + From fff621a0a06a71b6cc68288c971f7d867f7c5051 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:34:11 -0400 Subject: [PATCH 75/87] =?UTF-8?q?=F0=9F=A7=AA=20Added=20register=20custome?= =?UTF-8?q?r=20tests=20and=20custom=20HTTP=20assertions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpResponseMessageAssertions.cs | 41 +++++++++ src/Common/Common.Tests/Common.Tests.csproj | 1 + .../Common/TestingDatabaseFixture.cs | 6 +- .../Common.Tests/Common/WebApiTestFactory.cs | 4 +- src/Modules/Customers/AddMigration.ps1 | 5 ++ .../Common/CustomersIntegrationTestBase.cs | 24 ++++++ .../{ => Customers}/AddressTests.cs | 0 .../{ => Customers}/CustomerTests.cs | 0 .../Customers/CustomersIntegrationTests.cs | 65 +++++++++++++++ .../Modules.Customers.Tests.csproj | 1 + .../Configuration/CustomerConfiguration.cs | 3 + .../20241002110547_Initial.Designer.cs | 83 +++++++++++++++++++ .../Migrations/20241002110547_Initial.cs | 45 ++++++++++ .../CustomersDbContextModelSnapshot.cs | 80 ++++++++++++++++++ .../Customers/Domain/Address.cs | 15 ++-- .../UseCases/CreateProductCommand.cs | 4 +- ...dCatalogMigration.ps1 => AddMigration.ps1} | 0 ...arehouseMigration.ps1 => AddMigration.ps1} | 0 src/WebApi/Extensions/MediatRExtensions.cs | 2 + 19 files changed, 371 insertions(+), 8 deletions(-) create mode 100644 src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs create mode 100644 src/Modules/Customers/AddMigration.ps1 create mode 100644 src/Modules/Customers/Modules.Customers.Tests/Common/CustomersIntegrationTestBase.cs rename src/Modules/Customers/Modules.Customers.Tests/{ => Customers}/AddressTests.cs (100%) rename src/Modules/Customers/Modules.Customers.Tests/{ => Customers}/CustomerTests.cs (100%) create mode 100644 src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs create mode 100644 src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.Designer.cs create mode 100644 src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.cs create mode 100644 src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/CustomersDbContextModelSnapshot.cs rename src/Modules/Products/{AddCatalogMigration.ps1 => AddMigration.ps1} (100%) rename src/Modules/Warehouse/{AddWarehouseMigration.ps1 => AddMigration.ps1} (100%) diff --git a/src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs b/src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs new file mode 100644 index 0000000..2c5a885 --- /dev/null +++ b/src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Primitives; +using System.Net; + +namespace Common.Tests.Assertions; + +public class HttpResponseMessageAssertions : ReferenceTypeAssertions +{ + public HttpResponseMessageAssertions(HttpResponseMessage instance) : base(instance) { } + + protected override string Identifier => "HttpResponseMessage"; + + // TODO: Update other tests to use this extension + public AndConstraint BeSuccessWithStatusCode(HttpStatusCode statusCode, string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject) + .ForCondition(s => s.StatusCode == statusCode) + .FailWith(GetFailureMessage(statusCode)); + return new AndConstraint(this); + } + + private string GetFailureMessage(HttpStatusCode statusCode) + { + var body = Subject.Content.ReadAsStringAsync().Result; + body = EscapeCurlyBraces(body); + return $"Expected status code '{statusCode}' but got '{Subject.StatusCode}', reason: {Environment.NewLine}{body}"; + } + + private static string EscapeCurlyBraces(string input) + { + return input.Replace("{", "{{").Replace("}", "}}"); + } +} + +public static class HttpContentExtensions +{ + public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) => new(instance); +} diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index 5817c0d..7d82264 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs index be23789..66c829a 100644 --- a/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs +++ b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Modules.Catalog.Common.Persistence; +using Modules.Customers.Common.Persistence; using Modules.Warehouse.Common.Persistence; using Respawn; using Xunit; @@ -37,6 +38,9 @@ public async Task InitializeAsync() var warehouseDb = scope.ServiceProvider.GetRequiredService(); await warehouseDb.Database.MigrateAsync(); + var customersDb = scope.ServiceProvider.GetRequiredService(); + await customersDb.Database.MigrateAsync(); + // NOTE: If there are any tables you want to skip being reset, they can be configured here _checkpoint = await Respawner.CreateAsync(ConnectionString); } @@ -52,4 +56,4 @@ public async Task ResetState() } public void SetOutput(ITestOutputHelper output) => Factory.Output = output; -} \ No newline at end of file +} diff --git a/src/Common/Common.Tests/Common/WebApiTestFactory.cs b/src/Common/Common.Tests/Common/WebApiTestFactory.cs index 3fafb6b..d1e6bfd 100644 --- a/src/Common/Common.Tests/Common/WebApiTestFactory.cs +++ b/src/Common/Common.Tests/Common/WebApiTestFactory.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Modules.Catalog.Common.Persistence; +using Modules.Customers.Common.Persistence; using Modules.Warehouse.Common.Persistence; using WebApi; using Xunit.Abstractions; @@ -38,6 +39,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) { services.ReplaceDbContext(Database); services.ReplaceDbContext(Database); + services.ReplaceDbContext(Database); }); } -} \ No newline at end of file +} diff --git a/src/Modules/Customers/AddMigration.ps1 b/src/Modules/Customers/AddMigration.ps1 new file mode 100644 index 0000000..411de12 --- /dev/null +++ b/src/Modules/Customers/AddMigration.ps1 @@ -0,0 +1,5 @@ +dotnet ef migrations add Initial ` + --project ./Modules.Customers ` + --startup-project ../../WebApi ` + --output-dir ./Common/Persistence/Migrations ` + --context CustomersDbContext diff --git a/src/Modules/Customers/Modules.Customers.Tests/Common/CustomersIntegrationTestBase.cs b/src/Modules/Customers/Modules.Customers.Tests/Common/CustomersIntegrationTestBase.cs new file mode 100644 index 0000000..4fabb67 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers.Tests/Common/CustomersIntegrationTestBase.cs @@ -0,0 +1,24 @@ +using Common.Tests.Common; +using Modules.Customers.Common.Persistence; +using Xunit.Abstractions; + +namespace Modules.Customers.Tests.Common; + +// ReSharper disable once ClassNeverInstantiated.Global +public class CustomersDatabaseFixture : TestingDatabaseFixture; + +[Collection(CustomersFixtureCollection.Name)] +public abstract class CustomersIntegrationTestBase( + CustomersDatabaseFixture fixture, + ITestOutputHelper output) + : IntegrationTestBase(fixture, output); + +[CollectionDefinition(Name)] +public class CustomersFixtureCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + + public const string Name = nameof(CustomersFixtureCollection); +} diff --git a/src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs b/src/Modules/Customers/Modules.Customers.Tests/Customers/AddressTests.cs similarity index 100% rename from src/Modules/Customers/Modules.Customers.Tests/AddressTests.cs rename to src/Modules/Customers/Modules.Customers.Tests/Customers/AddressTests.cs diff --git a/src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs b/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomerTests.cs similarity index 100% rename from src/Modules/Customers/Modules.Customers.Tests/CustomerTests.cs rename to src/Modules/Customers/Modules.Customers.Tests/Customers/CustomerTests.cs diff --git a/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs b/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs new file mode 100644 index 0000000..309f1f8 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs @@ -0,0 +1,65 @@ +using Common.Tests.Assertions; +using Microsoft.EntityFrameworkCore; +using Modules.Customers.Customers.Domain; +using Modules.Customers.Customers.UseCases; +using Modules.Customers.Tests.Common; +using System.Net; +using System.Net.Http.Json; +using Xunit.Abstractions; + +namespace Modules.Customers.Tests.Customers; + +public class CustomersIntegrationTests(CustomersDatabaseFixture fixture, ITestOutputHelper output) + : CustomersIntegrationTestBase(fixture, output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public async Task RegisterCustomer_ValidRequest_ReturnsSuccess() + { + // Arrange + var client = GetAnonymousClient(); + var request = new RegisterCustomerCommand.Request("first", "last", "email@foo.com"); + + // Act + var response = await client.PostAsJsonAsync("/api/customers", request); + + // Assert + HttpContentExtensions.Should(response).BeSuccessWithStatusCode(HttpStatusCode.Created); + var customers = await GetQueryable().ToListAsync(); + customers.Should().HaveCount(1); + + var customer = customers.First(); + customer.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + customer.CreatedBy.Should().NotBeNullOrWhiteSpace(); + customer.FirstName.Should().Be(request.FirstName); + customer.LastName.Should().Be(request.LastName); + customer.Email.Should().Be(request.Email); + } + + [Theory] + [InlineData(null, "last", "email@foo.com")] + [InlineData("", "last", "email@foo.com")] + [InlineData(" ", "last", "email@foo.com")] + [InlineData("first", null, "email@foo.com")] + [InlineData("first", "", "email@foo.com")] + [InlineData("first", " ", "email@foo.com")] + [InlineData("first", "last", null)] + [InlineData("first", "last", "")] + [InlineData("first", "last", " ")] + [InlineData("first", "last", "email")] + public async Task RegisterCustomer_InvalidRequest_ReturnsBadRequest(string? firstName, string? lastName, string? email) + { + // Arrange + var client = GetAnonymousClient(); + var request = new RegisterCustomerCommand.Request(firstName!, lastName!, email!); + + // Act + var response = await client.PostAsJsonAsync("/api/customers", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var content = await response.Content.ReadAsStringAsync(); + _output.WriteLine(content); + } +} diff --git a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj index 6929395..a1fb2e3 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj +++ b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs index 8011e4e..8d9376f 100644 --- a/src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/Configuration/CustomerConfiguration.cs @@ -15,5 +15,8 @@ public void Configure(EntityTypeBuilder builder) .Property(m => m.Id) .HasStronglyTypedId() .ValueGeneratedNever(); + + // Using Owned as ComplexTypes don't support nullable records + builder.OwnsOne(m => m.Address); } } diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.Designer.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.Designer.cs new file mode 100644 index 0000000..d19d224 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.Designer.cs @@ -0,0 +1,83 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Customers.Common.Persistence; + +#nullable disable + +namespace Modules.Customers.Common.Persistence.Migrations +{ + [DbContext(typeof(CustomersDbContext))] + [Migration("20241002110547_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("customer") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Modules.Customers.Customers.Domain.Customer", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers", "customer"); + }); + + modelBuilder.Entity("Modules.Customers.Customers.Domain.Customer", b => + { + b.OwnsOne("Modules.Customers.Customers.Domain.Address", "Address", b1 => + { + b1.Property("CustomerId") + .HasColumnType("uniqueidentifier"); + + b1.HasKey("CustomerId"); + + b1.ToTable("Customers", "customer"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("Address"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.cs new file mode 100644 index 0000000..4d8b462 --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/20241002110547_Initial.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Modules.Customers.Common.Persistence.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "customer"); + + migrationBuilder.CreateTable( + name: "Customers", + schema: "customer", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Customers", + schema: "customer"); + } + } +} diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/CustomersDbContextModelSnapshot.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/CustomersDbContextModelSnapshot.cs new file mode 100644 index 0000000..6811f8c --- /dev/null +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/Migrations/CustomersDbContextModelSnapshot.cs @@ -0,0 +1,80 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Customers.Common.Persistence; + +#nullable disable + +namespace Modules.Customers.Common.Persistence.Migrations +{ + [DbContext(typeof(CustomersDbContext))] + partial class CustomersDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("customer") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Modules.Customers.Customers.Domain.Customer", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers", "customer"); + }); + + modelBuilder.Entity("Modules.Customers.Customers.Domain.Customer", b => + { + b.OwnsOne("Modules.Customers.Customers.Domain.Address", "Address", b1 => + { + b1.Property("CustomerId") + .HasColumnType("uniqueidentifier"); + + b1.HasKey("CustomerId"); + + b1.ToTable("Customers", "customer"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("Address"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Customers/Modules.Customers/Customers/Domain/Address.cs b/src/Modules/Customers/Modules.Customers/Customers/Domain/Address.cs index 0f69a3d..733add1 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/Domain/Address.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/Domain/Address.cs @@ -2,12 +2,17 @@ internal record Address { - internal string Line1 { get; } + internal string Line1 { get; } = null!; internal string? Line2 { get; } - internal string City { get; } - public string State { get; } - public string ZipCode { get; } - public string Country { get; } + internal string City { get; } = null!; + public string State { get; } = null!; + public string ZipCode { get; } = null!; + public string Country { get; } = null!; + + // Needed for EF + private Address() + { + } internal Address(string line1, string? line2, string city, string state, string zipCode, string country) { diff --git a/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs b/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs index 646cd17..b5d2950 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs @@ -37,7 +37,9 @@ public Validator() { RuleFor(r => r.FirstName).NotEmpty(); RuleFor(r => r.LastName).NotEmpty(); - RuleFor(r => r.Email).NotEmpty(); + RuleFor(r => r.Email) + .NotEmpty() + .EmailAddress(); } } diff --git a/src/Modules/Products/AddCatalogMigration.ps1 b/src/Modules/Products/AddMigration.ps1 similarity index 100% rename from src/Modules/Products/AddCatalogMigration.ps1 rename to src/Modules/Products/AddMigration.ps1 diff --git a/src/Modules/Warehouse/AddWarehouseMigration.ps1 b/src/Modules/Warehouse/AddMigration.ps1 similarity index 100% rename from src/Modules/Warehouse/AddWarehouseMigration.ps1 rename to src/Modules/Warehouse/AddMigration.ps1 diff --git a/src/WebApi/Extensions/MediatRExtensions.cs b/src/WebApi/Extensions/MediatRExtensions.cs index ba78fa2..8d29baf 100644 --- a/src/WebApi/Extensions/MediatRExtensions.cs +++ b/src/WebApi/Extensions/MediatRExtensions.cs @@ -1,5 +1,6 @@ using Common.SharedKernel.Behaviours; using Modules.Catalog; +using Modules.Customers; using Modules.Warehouse; using System.Reflection; @@ -11,6 +12,7 @@ public static class MediatRExtensions [ typeof(WarehouseModule).Assembly, typeof(CatalogModule).Assembly, + typeof(CustomersModule).Assembly, ]; public static void AddMediatR(this IServiceCollection services) From 4a734ad634ef956f230839b8dc0c0828a1cc51ab Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 8 Oct 2024 03:47:07 +1000 Subject: [PATCH 76/87] =?UTF-8?q?=E2=9C=A8=20Add=20item=20to=20cart=20(#60?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Started adding feature to orders module * Fix build * Added create cart integration test * Got test working * Added test for adding a product to an existing cart. * Added validator tests --- ModularMonolith.sln | 7 + .../Domain/Entities/Money.cs | 4 + .../Domain/Ids/ProductId.cs | 10 ++ .../Domain/{ => Ids}/Uuid.cs | 0 .../HttpResponseMessageAssertions.cs | 2 +- .../Common/TestingDatabaseFixture.cs | 4 + .../Common.Tests/Common/WebApiTestFactory.cs | 2 + .../Customers/CustomersIntegrationTests.cs | 2 +- src/Modules/Orders/AddMigration.ps1 | 5 + .../Carts/CartIntegrationTests.cs | 103 +++++++++++++ .../{Cart => Carts}/CartItemTests.cs | 7 +- .../{Cart => Carts}/CartTests.cs | 14 +- .../Common/OrdersIntegrationTestBase.cs | 45 ++++++ .../Modules.Orders.Tests/GlobalUsings.cs | 1 + .../Modules.Orders.Tests/LineItemTests.cs | 1 - .../Modules.Orders.Tests.csproj | 1 + .../Orders/LineItemTests.cs | 1 - .../Modules.Orders.Tests/Orders/OrderTests.cs | 1 - .../Carts/AddProductToCartCommand.cs | 100 +++++++++++++ .../Modules.Orders/Carts/{ => Domain}/Cart.cs | 16 +- .../Carts/Domain/CartByIdSpec.cs | 13 ++ .../Carts/{ => Domain}/CartItem.cs | 18 ++- .../Carts/Domain/ProductErrors.cs | 10 ++ .../Configuration/CartConfiguration.cs | 26 ++++ .../Configuration/CartItemConfiguration.cs | 27 ++++ .../Persistence/DepdendencyInjection.cs | 41 +++++ .../20241007162003_Initial.Designer.cs | 141 ++++++++++++++++++ .../Migrations/20241007162003_Initial.cs | 83 +++++++++++ .../OrdersDbContextModelSnapshot.cs | 138 +++++++++++++++++ .../Common/Persistence/OrdersDbContext.cs | 30 ++++ .../Orders/Modules.Orders/Common/ProductId.cs | 8 - .../Modules.Orders/Modules.Orders.csproj | 9 ++ .../Orders/LineItem/LineItem.cs | 4 +- .../Modules.Orders/Orders/Order/Order.cs | 4 +- .../Orders/Modules.Orders/OrdersModule.cs | 20 ++- .../GetProductQuery.cs | 13 ++ .../Modules.Catalog.Messages.csproj | 6 + .../Modules.Catalog.Tests/GlobalUsings.cs | 1 + .../Products/ProductIntegrationTests.cs | 3 +- .../Products/Modules.Catalog/AssemblyInfo.cs | 3 +- .../Products/Modules.Catalog/CatalogModule.cs | 1 + ...uctCommand.cs => CreateCategoryCommand.cs} | 0 .../Products/Modules.Catalog/GlobalUsings.cs | 3 +- .../Modules.Catalog/Modules.Catalog.csproj | 1 + .../Products/Domain/Product.cs | 17 +-- .../GetProductQuery.cs | 16 +- .../ProductStoredIntegrationEvent.cs} | 11 +- .../Modules.Warehouse.Tests/GlobalUsings.cs | 1 + .../Storage/Domain/AisleTests.cs | 3 +- .../Modules.Warehouse/GlobalUsings.cs | 1 + .../Products/Domain/Product.cs | 17 +-- src/WebApi/Extensions/MediatRExtensions.cs | 2 + src/WebApi/Program.cs | 1 + .../CatalogDbContextInitialiser.cs | 5 +- 54 files changed, 920 insertions(+), 83 deletions(-) create mode 100644 src/Common/Common.SharedKernel/Domain/Ids/ProductId.cs rename src/Common/Common.SharedKernel/Domain/{ => Ids}/Uuid.cs (100%) create mode 100644 src/Modules/Orders/AddMigration.ps1 create mode 100644 src/Modules/Orders/Modules.Orders.Tests/Carts/CartIntegrationTests.cs rename src/Modules/Orders/Modules.Orders.Tests/{Cart => Carts}/CartItemTests.cs (96%) rename src/Modules/Orders/Modules.Orders.Tests/{Cart => Carts}/CartTests.cs (85%) create mode 100644 src/Modules/Orders/Modules.Orders.Tests/Common/OrdersIntegrationTestBase.cs create mode 100644 src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs rename src/Modules/Orders/Modules.Orders/Carts/{ => Domain}/Cart.cs (83%) create mode 100644 src/Modules/Orders/Modules.Orders/Carts/Domain/CartByIdSpec.cs rename src/Modules/Orders/Modules.Orders/Carts/{ => Domain}/CartItem.cs (82%) create mode 100644 src/Modules/Orders/Modules.Orders/Carts/Domain/ProductErrors.cs create mode 100644 src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartConfiguration.cs create mode 100644 src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartItemConfiguration.cs create mode 100644 src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs create mode 100644 src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.Designer.cs create mode 100644 src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.cs create mode 100644 src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/OrdersDbContextModelSnapshot.cs create mode 100644 src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs delete mode 100644 src/Modules/Orders/Modules.Orders/Common/ProductId.cs create mode 100644 src/Modules/Products/Modules.Catalog.Messages/GetProductQuery.cs create mode 100644 src/Modules/Products/Modules.Catalog.Messages/Modules.Catalog.Messages.csproj rename src/Modules/Products/Modules.Catalog/Categories/{CreateProductCommand.cs => CreateCategoryCommand.cs} (100%) rename src/Modules/Products/Modules.Catalog/Products/{UseCases => Integrations}/GetProductQuery.cs (77%) rename src/Modules/Products/Modules.Catalog/Products/{IntegrationEvents/ProductStoredIntegrationEventHandler.cs => Integrations/ProductStoredIntegrationEvent.cs} (60%) diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 4868cb3..9fd6c69 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -66,6 +66,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".sln", ".sln", "{63C08527-F src\Modules\Warehouse\AddWarehouseMigration.ps1 = src\Modules\Warehouse\AddWarehouseMigration.ps1 EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catalog.Messages", "src\Modules\Products\Modules.Catalog.Messages\Modules.Catalog.Messages.csproj", "{75591E8A-4FE8-4179-9332-50A28D7A0073}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +133,10 @@ Global {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Release|Any CPU.Build.0 = Release|Any CPU + {75591E8A-4FE8-4179-9332-50A28D7A0073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75591E8A-4FE8-4179-9332-50A28D7A0073}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75591E8A-4FE8-4179-9332-50A28D7A0073}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75591E8A-4FE8-4179-9332-50A28D7A0073}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {916135AD-7D7F-4472-BDAB-C5F2BA5F8C67} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} @@ -153,5 +159,6 @@ Global {A105835C-C285-4AA5-AADF-CCA4BCC933B1} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {13094B62-3DBC-4C6F-9ECC-D2DC433C319F} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9} = {3E4B904F-1D6C-437B-8208-C6D17F995528} + {75591E8A-4FE8-4179-9332-50A28D7A0073} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} EndGlobalSection EndGlobal diff --git a/src/Common/Common.SharedKernel/Domain/Entities/Money.cs b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs index 58e49f6..7c007e6 100644 --- a/src/Common/Common.SharedKernel/Domain/Entities/Money.cs +++ b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs @@ -6,6 +6,10 @@ public record Money(Currency Currency, decimal Amount) { public static Money Create(decimal amount) => new(Currency.Default, amount); + public Money(decimal amount) : this(Currency.Default, amount) + { + } + public static Money Default => new(Currency.Default, 0); public static Money Zero => Default; diff --git a/src/Common/Common.SharedKernel/Domain/Ids/ProductId.cs b/src/Common/Common.SharedKernel/Domain/Ids/ProductId.cs new file mode 100644 index 0000000..5581eec --- /dev/null +++ b/src/Common/Common.SharedKernel/Domain/Ids/ProductId.cs @@ -0,0 +1,10 @@ +using Common.SharedKernel.Domain.Interfaces; + +namespace Common.SharedKernel.Domain.Ids; + +public record ProductId(Guid Value) : IStronglyTypedId +{ + public ProductId() : this(Uuid.Create()) + { + } +} diff --git a/src/Common/Common.SharedKernel/Domain/Uuid.cs b/src/Common/Common.SharedKernel/Domain/Ids/Uuid.cs similarity index 100% rename from src/Common/Common.SharedKernel/Domain/Uuid.cs rename to src/Common/Common.SharedKernel/Domain/Ids/Uuid.cs diff --git a/src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs b/src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs index 2c5a885..31d985f 100644 --- a/src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs +++ b/src/Common/Common.Tests/Assertions/HttpResponseMessageAssertions.cs @@ -12,7 +12,7 @@ public HttpResponseMessageAssertions(HttpResponseMessage instance) : base(instan protected override string Identifier => "HttpResponseMessage"; // TODO: Update other tests to use this extension - public AndConstraint BeSuccessWithStatusCode(HttpStatusCode statusCode, string because = "", params object[] becauseArgs) + public AndConstraint BeStatusCode(HttpStatusCode statusCode, string because = "", params object[] becauseArgs) { Execute.Assertion .BecauseOf(because, becauseArgs) diff --git a/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs index 66c829a..25c39dc 100644 --- a/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs +++ b/src/Common/Common.Tests/Common/TestingDatabaseFixture.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Modules.Catalog.Common.Persistence; using Modules.Customers.Common.Persistence; +using Modules.Orders.Common.Persistence; using Modules.Warehouse.Common.Persistence; using Respawn; using Xunit; @@ -41,6 +42,9 @@ public async Task InitializeAsync() var customersDb = scope.ServiceProvider.GetRequiredService(); await customersDb.Database.MigrateAsync(); + var ordersDb = scope.ServiceProvider.GetRequiredService(); + await ordersDb.Database.MigrateAsync(); + // NOTE: If there are any tables you want to skip being reset, they can be configured here _checkpoint = await Respawner.CreateAsync(ConnectionString); } diff --git a/src/Common/Common.Tests/Common/WebApiTestFactory.cs b/src/Common/Common.Tests/Common/WebApiTestFactory.cs index d1e6bfd..dca2eb5 100644 --- a/src/Common/Common.Tests/Common/WebApiTestFactory.cs +++ b/src/Common/Common.Tests/Common/WebApiTestFactory.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Modules.Catalog.Common.Persistence; using Modules.Customers.Common.Persistence; +using Modules.Orders.Common.Persistence; using Modules.Warehouse.Common.Persistence; using WebApi; using Xunit.Abstractions; @@ -40,6 +41,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.ReplaceDbContext(Database); services.ReplaceDbContext(Database); services.ReplaceDbContext(Database); + services.ReplaceDbContext(Database); }); } } diff --git a/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs b/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs index 309f1f8..fd04cec 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs +++ b/src/Modules/Customers/Modules.Customers.Tests/Customers/CustomersIntegrationTests.cs @@ -25,7 +25,7 @@ public async Task RegisterCustomer_ValidRequest_ReturnsSuccess() var response = await client.PostAsJsonAsync("/api/customers", request); // Assert - HttpContentExtensions.Should(response).BeSuccessWithStatusCode(HttpStatusCode.Created); + HttpContentExtensions.Should(response).BeStatusCode(HttpStatusCode.Created); var customers = await GetQueryable().ToListAsync(); customers.Should().HaveCount(1); diff --git a/src/Modules/Orders/AddMigration.ps1 b/src/Modules/Orders/AddMigration.ps1 new file mode 100644 index 0000000..88ce494 --- /dev/null +++ b/src/Modules/Orders/AddMigration.ps1 @@ -0,0 +1,5 @@ +dotnet ef migrations add Initial ` + --project ./Modules.Orders ` + --startup-project ../../WebApi ` + --output-dir ./Common/Persistence/Migrations ` + --context OrdersDbContext diff --git a/src/Modules/Orders/Modules.Orders.Tests/Carts/CartIntegrationTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Carts/CartIntegrationTests.cs new file mode 100644 index 0000000..1041637 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders.Tests/Carts/CartIntegrationTests.cs @@ -0,0 +1,103 @@ +using Common.Tests.Assertions; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Products.Domain; +using Modules.Orders.Carts; +using Modules.Orders.Carts.Domain; +using Modules.Orders.Tests.Common; +using System.Net; +using System.Net.Http.Json; +using Xunit.Abstractions; + +namespace Modules.Orders.Tests.Carts; + +public class CartIntegrationTests(OrdersDatabaseFixture fixture, ITestOutputHelper output) + : OrdersIntegrationTestBase(fixture, output) +{ + [Fact] + public async Task CreateCart_WithValidRequest_ReturnsCart() + { + // Arrange + var product = Product.Create("name", "12345678"); + product.UpdatePrice(new Money(100)); + fixture.CatalogDbContext.Products.Add(product); + await fixture.CatalogDbContext.SaveChangesAsync(); + var client = GetAnonymousClient(); + var quantity = 1; + var request = new AddProductToCartCommand.Request(null, product.Id.Value, quantity); + + // Act + var response = await client.PostAsJsonAsync("/api/carts", request); + + // Assert + HttpContentExtensions.Should(response).BeStatusCode(HttpStatusCode.OK); + var carts = await GetQueryable().Include(c => c.Items).ToListAsync(); + carts.Should().HaveCount(1); + + var cart = carts.First(); + cart.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + cart.CreatedBy.Should().NotBeNullOrWhiteSpace(); + cart.Id.Should().NotBeNull(); + cart.Items.Should().HaveCount(1); + + var item = cart.Items.First(); + item.Should().NotBeNull(); + item.ProductId.Should().Be(product.Id); + item.Quantity.Should().Be(quantity); + item.LinePrice.Amount.Should().Be(100); + item.UnitPrice.Amount.Should().Be(100); + } + + [Fact] + public async Task AddProduct_WithExistingCart_ReturnsCart() + { + // Arrange + var product = Product.Create("name", "12345678"); + product.UpdatePrice(new Money(100)); + fixture.CatalogDbContext.Products.Add(product); + await fixture.CatalogDbContext.SaveChangesAsync(); + var client = GetAnonymousClient(); + var quantity = 1; + var request1 = new AddProductToCartCommand.Request(null, product.Id.Value, quantity); + var response1 = await client.PostAsJsonAsync("/api/carts", request1); + var content = await response1.Content.ReadFromJsonAsync(); + var request2 = new AddProductToCartCommand.Request(content!.CartId, product.Id.Value, quantity); + + // Act + var response2 = await client.PostAsJsonAsync("/api/carts", request2); + + // Assert + HttpContentExtensions.Should(response2).BeStatusCode(HttpStatusCode.OK); + var carts = await GetQueryable().Include(c => c.Items).ToListAsync(); + carts.Should().HaveCount(1); + + var cart = carts.First(); + cart.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + cart.CreatedBy.Should().NotBeNullOrWhiteSpace(); + cart.Id.Should().NotBeNull(); + cart.Items.Should().HaveCount(1); + + var item = cart.Items.First(); + item.Should().NotBeNull(); + item.ProductId.Should().Be(product.Id); + item.Quantity.Should().Be(quantity + quantity); + item.LinePrice.Amount.Should().Be(200); + item.UnitPrice.Amount.Should().Be(100); + } + + [Theory] + [InlineData("00000000-0000-0000-0000-000000000000", 1)] + [InlineData("73060DE4-5AD8-4574-B857-5D5C5F44203F", 0)] + [InlineData("73060DE4-5AD8-4574-B857-5D5C5F44203F", -1)] + public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string productId, int quantity) + { + // Arrange + var client = GetAnonymousClient(); + var request = new AddProductToCartCommand.Request(null, Guid.Parse(productId), quantity); + + // Act + var response = await client.PostAsJsonAsync("/api/carts", request); + + // Assert + HttpContentExtensions.Should(response).BeStatusCode(HttpStatusCode.BadRequest); + } +} diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Carts/CartItemTests.cs similarity index 96% rename from src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs rename to src/Modules/Orders/Modules.Orders.Tests/Carts/CartItemTests.cs index 137a2c1..f904b5f 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Carts/CartItemTests.cs @@ -1,7 +1,6 @@ -using Modules.Orders.Carts; -using Modules.Orders.Common; +using Modules.Orders.Carts.Domain; -namespace Modules.Orders.Tests.Cart; +namespace Modules.Orders.Tests.Carts; public class CartItemTests { @@ -102,4 +101,4 @@ public void DecreaseQuantity_TooMany_ShouldThrow() // Assert act.Should().Throw(); } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Carts/CartTests.cs similarity index 85% rename from src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs rename to src/Modules/Orders/Modules.Orders.Tests/Carts/CartTests.cs index 62013ac..2fc9de4 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Cart/CartTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Carts/CartTests.cs @@ -1,6 +1,6 @@ -using Modules.Orders.Common; +using Modules.Orders.Carts.Domain; -namespace Modules.Orders.Tests.Cart; +namespace Modules.Orders.Tests.Carts; public class CartTests { @@ -10,7 +10,7 @@ public void AddItem_ShouldIncreaseQuantity_WhenItemAlreadyExists() // Arrange var productId = new ProductId(); var unitPrice = new Money(Currency.Default, 10); - var cart = Carts.Cart.Create(productId, 1, unitPrice); + var cart = Cart.Create(productId, 1, unitPrice); // Act cart.AddItem(productId, 2, unitPrice); @@ -28,7 +28,7 @@ public void AddItem_ShouldAddNewItem_WhenItemDoesNotExist() var productId1 = new ProductId(); var productId2 = new ProductId(); var unitPrice = new Money(Currency.Default, 10); - var cart = Carts.Cart.Create(productId1, 1, unitPrice); + var cart = Cart.Create(productId1, 1, unitPrice); // Act cart.AddItem(productId2, 2, unitPrice); @@ -46,7 +46,7 @@ public void RemoveItem_ShouldRemoveItem_WhenItemExists() // Arrange var productId = new ProductId(); var unitPrice = new Money(Currency.Default, 10); - var cart = Carts.Cart.Create(productId, 1, unitPrice); + var cart = Cart.Create(productId, 1, unitPrice); // Act cart.RemoveItem(productId); @@ -63,7 +63,7 @@ public void RemoveItem_ShouldDoNothing_WhenItemDoesNotExist() var productId1 = new ProductId(); var productId2 = new ProductId(); var unitPrice = new Money(Currency.Default, 10); - var cart = Carts.Cart.Create(productId1, 1, unitPrice); + var cart = Cart.Create(productId1, 1, unitPrice); // Act cart.RemoveItem(productId2); @@ -81,7 +81,7 @@ public void UpdateTotal_ShouldCalculateTotalPriceCorrectly() var productId2 = new ProductId(); var unitPrice1 = new Money(Currency.Default, 10); var unitPrice2 = new Money(Currency.Default, 20); - var cart = Carts.Cart.Create(productId1, 1, unitPrice1); + var cart = Cart.Create(productId1, 1, unitPrice1); // Act cart.AddItem(productId2, 2, unitPrice2); diff --git a/src/Modules/Orders/Modules.Orders.Tests/Common/OrdersIntegrationTestBase.cs b/src/Modules/Orders/Modules.Orders.Tests/Common/OrdersIntegrationTestBase.cs new file mode 100644 index 0000000..c0e57bd --- /dev/null +++ b/src/Modules/Orders/Modules.Orders.Tests/Common/OrdersIntegrationTestBase.cs @@ -0,0 +1,45 @@ +using Common.Tests.Common; +using Microsoft.Extensions.DependencyInjection; +using Modules.Catalog.Common.Persistence; +using Modules.Orders.Common.Persistence; +using Xunit.Abstractions; + +namespace Modules.Orders.Tests.Common; + +// ReSharper disable once ClassNeverInstantiated.Global +public class OrdersDatabaseFixture : TestingDatabaseFixture, IAsyncLifetime +{ + private IServiceScope _scope; + + public CatalogDbContext CatalogDbContext { get; private set; } + + public new async Task InitializeAsync() + { + await base.InitializeAsync(); + _scope = ScopeFactory.CreateScope(); + CatalogDbContext = _scope.ServiceProvider.GetRequiredService(); + } + + public new async Task DisposeAsync() + { + await base.DisposeAsync(); + await CatalogDbContext.DisposeAsync(); + _scope.Dispose(); + } +} + +[Collection(OrdersFixtureCollection.Name)] +public abstract class OrdersIntegrationTestBase( + OrdersDatabaseFixture fixture, + ITestOutputHelper output) + : IntegrationTestBase(fixture, output); + +[CollectionDefinition(Name)] +public class OrdersFixtureCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + + public const string Name = nameof(OrdersFixtureCollection); +} diff --git a/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs b/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs index d22e914..f7d30a4 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/GlobalUsings.cs @@ -1,4 +1,5 @@ global using Common.SharedKernel.Domain; global using Common.SharedKernel.Domain.Entities; +global using Common.SharedKernel.Domain.Ids; global using Xunit; global using FluentAssertions; \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs index 212258c..413994e 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/LineItemTests.cs @@ -1,4 +1,3 @@ -using Modules.Orders.Common; using Modules.Orders.Orders.LineItem; using Modules.Orders.Orders.Order; diff --git a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index 0da595c..3918d38 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs index 839d925..9b72cca 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/LineItemTests.cs @@ -1,4 +1,3 @@ -using Modules.Orders.Common; using Modules.Orders.Orders.LineItem; using Modules.Orders.Orders.Order; diff --git a/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs index e7d0376..e057015 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs +++ b/src/Modules/Orders/Modules.Orders.Tests/Orders/OrderTests.cs @@ -1,4 +1,3 @@ -using Modules.Orders.Common; using Modules.Orders.Orders.Order; namespace Modules.Orders.Tests.Orders diff --git a/src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs b/src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs new file mode 100644 index 0000000..4b47894 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs @@ -0,0 +1,100 @@ +using Ardalis.Specification.EntityFrameworkCore; +using Common.SharedKernel; +using Common.SharedKernel.Api; +using Common.SharedKernel.Domain.Ids; +using ErrorOr; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Messages; +using Modules.Orders.Carts.Domain; +using Modules.Orders.Common.Persistence; + +namespace Modules.Orders.Carts; + +public static class AddProductToCartCommand +{ + public record Request(Guid? CartId, Guid ProductId, int Quantity) : IRequest>; + + public record Response(Guid CartId); + + public static class Endpoint + { + public static void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("/api/carts", + async (Request request, ISender sender) => + { + var response = await sender.Send(request); + return response.IsError ? response.Problem() : TypedResults.Ok(response.Value); + }) + .WithName("AddProductToCart") + .WithTags("Orders") + .ProducesPost() + .WithOpenApi(); + } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.ProductId) + .NotEmpty(); + + RuleFor(r => r.Quantity) + .NotEmpty() + .GreaterThan(0); + } + } + + internal class Handler : IRequestHandler> + { + private readonly OrdersDbContext _dbContext; + private readonly IMediator _mediator; + + public Handler(OrdersDbContext dbContext, IMediator mediator) + { + _dbContext = dbContext; + _mediator = mediator; + } + + public async Task> Handle(Request request, CancellationToken cancellationToken) + { + var query = new GetProductQuery.Request(request.ProductId); + var product = await _mediator.Send(query, cancellationToken); + if (product.IsError) + return product.Errors; + + var productId = new ProductId(product.Value.Id); + var price = new Money(product.Value.Price); + var quantity = request.Quantity; + Cart? cart; + + if (request.CartId is null) + { + cart = Cart.Create(productId, quantity, price); + _dbContext.Carts.Add(cart); + } + else + { + var cartId = new CartId(request.CartId.Value); + cart = await _dbContext.Carts + .WithSpecification(new CartByIdSpec(cartId)) + .FirstOrDefaultAsync(cancellationToken); + + if (cart is null) + return CartErrors.NotFound; + + cart.AddItem(productId, quantity, price); + } + + await _dbContext.SaveChangesAsync(cancellationToken); + + return new Response(cart.Id.Value); + } + } +} diff --git a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs b/src/Modules/Orders/Modules.Orders/Carts/Domain/Cart.cs similarity index 83% rename from src/Modules/Orders/Modules.Orders/Carts/Cart.cs rename to src/Modules/Orders/Modules.Orders/Carts/Domain/Cart.cs index dc7509b..85ae1b0 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/Cart.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/Domain/Cart.cs @@ -1,8 +1,14 @@ -using Modules.Orders.Common; +using Common.SharedKernel.Domain.Ids; +using Common.SharedKernel.Domain.Interfaces; -namespace Modules.Orders.Carts; +namespace Modules.Orders.Carts.Domain; -internal record CartId(Guid Value); +internal record CartId(Guid Value) : IStronglyTypedId +{ + public CartId() : this(Uuid.Create()) + { + } +} internal class Cart : AggregateRoot { @@ -16,7 +22,7 @@ public static Cart Create(ProductId productId, int quantity, Money unitPrice) { var cart = new Cart { - Id = new CartId(Uuid.Create()) + Id = new CartId() }; cart.AddItem(productId, quantity, unitPrice); @@ -62,4 +68,4 @@ private void UpdateTotal() var total = _items.Sum(i => i.LinePrice.Amount); TotalPrice = new Money(currency, total); } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Modules.Orders/Carts/Domain/CartByIdSpec.cs b/src/Modules/Orders/Modules.Orders/Carts/Domain/CartByIdSpec.cs new file mode 100644 index 0000000..c942e93 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Carts/Domain/CartByIdSpec.cs @@ -0,0 +1,13 @@ +using Ardalis.Specification; + +namespace Modules.Orders.Carts.Domain; + +internal class CartByIdSpec : Specification +{ + public CartByIdSpec(CartId id) : base() + { + Query + .Where(i => i.Id == id) + .Include(i => i.Items); + } +} diff --git a/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs b/src/Modules/Orders/Modules.Orders/Carts/Domain/CartItem.cs similarity index 82% rename from src/Modules/Orders/Modules.Orders/Carts/CartItem.cs rename to src/Modules/Orders/Modules.Orders/Carts/Domain/CartItem.cs index 0aa727c..7e59b15 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/CartItem.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/Domain/CartItem.cs @@ -1,8 +1,14 @@ -using Modules.Orders.Common; +using Common.SharedKernel.Domain.Ids; +using Common.SharedKernel.Domain.Interfaces; -namespace Modules.Orders.Carts; +namespace Modules.Orders.Carts.Domain; -internal record CartItemId(Guid Value); +internal record CartItemId(Guid Value) : IStronglyTypedId +{ + public CartItemId() : this(Uuid.Create()) + { + } +} internal class CartItem : Entity { @@ -22,7 +28,7 @@ public static CartItem Create(ProductId productId, int quantity, Money unitPrice var cartItem = new CartItem { - Id = new CartItemId(Uuid.Create()), + Id = new CartItemId(), ProductId = productId, Quantity = quantity, UnitPrice = unitPrice, @@ -54,8 +60,8 @@ public void DecreaseQuantity(int quantity) private void UpdateLinePrice() => LinePrice = UnitPrice with { Amount = UnitPrice.Amount * Quantity }; + // Needed for EF private CartItem() { } - -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Modules.Orders/Carts/Domain/ProductErrors.cs b/src/Modules/Orders/Modules.Orders/Carts/Domain/ProductErrors.cs new file mode 100644 index 0000000..a11d226 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Carts/Domain/ProductErrors.cs @@ -0,0 +1,10 @@ +using ErrorOr; + +namespace Modules.Orders.Carts.Domain; + +public static class CartErrors +{ + public static readonly Error NotFound = Error.Validation( + "Cart.NotFound", + "Cannot find the cart specified"); +} diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartConfiguration.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartConfiguration.cs new file mode 100644 index 0000000..a8d5016 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartConfiguration.cs @@ -0,0 +1,26 @@ +using Common.SharedKernel.Persistence; +using Common.SharedKernel.Persistence.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Orders.Carts.Domain; + +namespace Modules.Orders.Common.Persistence.Configuration; + +internal class CartConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(p => p.Id); + + builder.Property(p => p.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.ComplexProperty(m => m.TotalPrice, MoneyConfiguration.BuildAction); + + builder.HasMany(p => p.Items); + + // TODO: Try to get this working. Perhaps try owned entity? + // builder.ComplexProperty(p => p.Items); + } +} diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartItemConfiguration.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartItemConfiguration.cs new file mode 100644 index 0000000..907e93d --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/Configuration/CartItemConfiguration.cs @@ -0,0 +1,27 @@ +using Common.SharedKernel.Domain.Ids; +using Common.SharedKernel.Persistence; +using Common.SharedKernel.Persistence.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Orders.Carts.Domain; + +namespace Modules.Orders.Common.Persistence.Configuration; + +internal class CartItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(p => p.Id); + + builder.Property(p => p.Id) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.Property(p => p.ProductId) + .HasStronglyTypedId() + .ValueGeneratedNever(); + + builder.ComplexProperty(m => m.UnitPrice, MoneyConfiguration.BuildAction); + builder.ComplexProperty(m => m.LinePrice, MoneyConfiguration.BuildAction); + } +} diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs new file mode 100644 index 0000000..c504326 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs @@ -0,0 +1,41 @@ +using Common.SharedKernel.Persistence.Interceptors; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Common.Persistence.Interceptors; + +namespace Modules.Orders.Common.Persistence; + +internal static class DependencyInjection +{ + internal static void AddPersistence(this IServiceCollection services, IConfiguration config) + { + var connectionString = config.GetConnectionString("Catalog"); + services.AddDbContext(options => + { + options.UseSqlServer(connectionString, builder => + { + builder.MigrationsAssembly(typeof(OrdersModule).Assembly.FullName); + // builder.EnableRetryOnFailure(); + }); + + var serviceProvider = services.BuildServiceProvider(); + + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + }); + + services.AddScoped(); + services.AddScoped(); + // services.AddScoped(); + } + + public static IApplicationBuilder UseInfrastructureMiddleware(this IApplicationBuilder app) + { + // TODO: Will need to add this when any events are fired + // app.UseMiddleware(); + return app; + } +} diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.Designer.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.Designer.cs new file mode 100644 index 0000000..921bccf --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.Designer.cs @@ -0,0 +1,141 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Orders.Common.Persistence; + +#nullable disable + +namespace Modules.Orders.Common.Persistence.Migrations +{ + [DbContext(typeof(OrdersDbContext))] + [Migration("20241007162003_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("catalog") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.Cart", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.ComplexProperty>("TotalPrice", "Modules.Orders.Carts.Domain.Cart.TotalPrice#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.HasKey("Id"); + + b.ToTable("Carts", "catalog"); + }); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.CartItem", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CartId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.ComplexProperty>("LinePrice", "Modules.Orders.Carts.Domain.CartItem.LinePrice#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.ComplexProperty>("UnitPrice", "Modules.Orders.Carts.Domain.CartItem.UnitPrice#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItem", "catalog"); + }); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.CartItem", b => + { + b.HasOne("Modules.Orders.Carts.Domain.Cart", null) + .WithMany("Items") + .HasForeignKey("CartId"); + }); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.Cart", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.cs new file mode 100644 index 0000000..2cc9982 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/20241007162003_Initial.cs @@ -0,0 +1,83 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Modules.Orders.Common.Persistence.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "catalog"); + + migrationBuilder.CreateTable( + name: "Carts", + schema: "catalog", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TotalPrice_Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + TotalPrice_Currency = table.Column(type: "nvarchar(3)", maxLength: 3, nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Carts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CartItem", + schema: "catalog", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ProductId = table.Column(type: "uniqueidentifier", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + CartId = table.Column(type: "uniqueidentifier", nullable: true), + LinePrice_Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + LinePrice_Currency = table.Column(type: "nvarchar(3)", maxLength: 3, nullable: false), + UnitPrice_Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + UnitPrice_Currency = table.Column(type: "nvarchar(3)", maxLength: 3, nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CartItem", x => x.Id); + table.ForeignKey( + name: "FK_CartItem_Carts_CartId", + column: x => x.CartId, + principalSchema: "catalog", + principalTable: "Carts", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_CartItem_CartId", + schema: "catalog", + table: "CartItem", + column: "CartId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CartItem", + schema: "catalog"); + + migrationBuilder.DropTable( + name: "Carts", + schema: "catalog"); + } + } +} diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/OrdersDbContextModelSnapshot.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/OrdersDbContextModelSnapshot.cs new file mode 100644 index 0000000..b757bc1 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/Migrations/OrdersDbContextModelSnapshot.cs @@ -0,0 +1,138 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Modules.Orders.Common.Persistence; + +#nullable disable + +namespace Modules.Orders.Common.Persistence.Migrations +{ + [DbContext(typeof(OrdersDbContext))] + partial class OrdersDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("catalog") + .HasAnnotation("ProductVersion", "9.0.0-rc.1.24451.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.Cart", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.ComplexProperty>("TotalPrice", "Modules.Orders.Carts.Domain.Cart.TotalPrice#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.HasKey("Id"); + + b.ToTable("Carts", "catalog"); + }); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.CartItem", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CartId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.ComplexProperty>("LinePrice", "Modules.Orders.Carts.Domain.CartItem.LinePrice#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.ComplexProperty>("UnitPrice", "Modules.Orders.Carts.Domain.CartItem.UnitPrice#Money", b1 => + { + b1.IsRequired(); + + b1.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + }); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItem", "catalog"); + }); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.CartItem", b => + { + b.HasOne("Modules.Orders.Carts.Domain.Cart", null) + .WithMany("Items") + .HasForeignKey("CartId"); + }); + + modelBuilder.Entity("Modules.Orders.Carts.Domain.Cart", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs new file mode 100644 index 0000000..57be3a3 --- /dev/null +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs @@ -0,0 +1,30 @@ +using EntityFramework.Exceptions.SqlServer; +using Microsoft.EntityFrameworkCore; +using Modules.Orders.Carts.Domain; + +namespace Modules.Orders.Common.Persistence; + +public class OrdersDbContext : DbContext +{ + internal DbSet Carts => Set(); + + // Needs to be public for the Database project + public OrdersDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("catalog"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrdersDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // Produces easy to read exceptions + optionsBuilder.UseExceptionProcessor(); + + base.OnConfiguring(optionsBuilder); + } +} diff --git a/src/Modules/Orders/Modules.Orders/Common/ProductId.cs b/src/Modules/Orders/Modules.Orders/Common/ProductId.cs deleted file mode 100644 index a8466b2..0000000 --- a/src/Modules/Orders/Modules.Orders/Common/ProductId.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Modules.Orders.Common; - -internal record ProductId(Guid Value) -{ - internal ProductId() : this(Uuid.Create()) - { - } -} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index 7028be7..607c4e4 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -2,15 +2,24 @@ + + + + + + + + + diff --git a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs index cc84ee9..0fb2680 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/LineItem/LineItem.cs @@ -1,4 +1,4 @@ -using Modules.Orders.Common; +using Common.SharedKernel.Domain.Ids; using Modules.Orders.Orders.Order; using Throw; @@ -51,4 +51,4 @@ internal void RemoveQuantity(int quantity) quantity.Throw("Can't remove all units. Remove the entire item instead").IfTrue(Quantity - quantity <= 0); Quantity -= quantity; } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs index 16b5721..76ca877 100644 --- a/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs +++ b/src/Modules/Orders/Modules.Orders/Orders/Order/Order.cs @@ -1,6 +1,6 @@ using Common.SharedKernel.Domain.Exceptions; +using Common.SharedKernel.Domain.Ids; using ErrorOr; -using Modules.Orders.Common; using Modules.Orders.Orders.LineItem; using Success = ErrorOr.Success; @@ -179,4 +179,4 @@ private void UpdateOrderTotal() OrderSubTotal = new Money(currency, amount); TaxTotal = new Money(currency, OrderSubTotal.Amount * TaxRate); } -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Modules.Orders/OrdersModule.cs b/src/Modules/Orders/Modules.Orders/OrdersModule.cs index 54b6782..4097470 100644 --- a/src/Modules/Orders/Modules.Orders/OrdersModule.cs +++ b/src/Modules/Orders/Modules.Orders/OrdersModule.cs @@ -1,14 +1,22 @@ -using Microsoft.AspNetCore.Builder; +using FluentValidation; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Modules.Orders.Carts; +using Modules.Orders.Common.Persistence; namespace Modules.Orders; public static class OrdersModule { - // public static void AddOrders(this IServiceCollection services) - // { - // } + public static void AddOrders(this IServiceCollection services, IConfiguration configuration) + { + services.AddPersistence(configuration); + services.AddValidatorsFromAssembly(typeof(OrdersModule).Assembly); + } + // TODO: Refactor to REPR pattern public static void UseOrders(this WebApplication app) { app.MapGet("/api/orders", () => @@ -24,7 +32,9 @@ public static void UseOrders(this WebApplication app) .WithName("GetOrders") .WithTags("Orders") .WithOpenApi(); + + AddProductToCartCommand.Endpoint.MapEndpoint(app); } } -public record OrderDto(string Name, string Description); \ No newline at end of file +public record OrderDto(string Name, string Description); diff --git a/src/Modules/Products/Modules.Catalog.Messages/GetProductQuery.cs b/src/Modules/Products/Modules.Catalog.Messages/GetProductQuery.cs new file mode 100644 index 0000000..b961645 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Messages/GetProductQuery.cs @@ -0,0 +1,13 @@ +using ErrorOr; +using MediatR; + +namespace Modules.Catalog.Messages; + +public static class GetProductQuery +{ + public record Request(Guid ProductId) : IRequest>; + + public record Response(string Name, Guid Id, string Sku, decimal Price, List Categories); + + public record CategoryDto(Guid Id, string Name); +} diff --git a/src/Modules/Products/Modules.Catalog.Messages/Modules.Catalog.Messages.csproj b/src/Modules/Products/Modules.Catalog.Messages/Modules.Catalog.Messages.csproj new file mode 100644 index 0000000..900a609 --- /dev/null +++ b/src/Modules/Products/Modules.Catalog.Messages/Modules.Catalog.Messages.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs b/src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs index 8c927eb..1d0108c 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/GlobalUsings.cs @@ -1 +1,2 @@ +global using Common.SharedKernel.Domain.Ids; global using Xunit; \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs index c0f0563..a3628f5 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Products/Modules.Catalog.Tests/Products/ProductIntegrationTests.cs @@ -2,6 +2,7 @@ using Common.SharedKernel.Domain; using FluentAssertions; using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Messages; using Modules.Catalog.Products.Domain; using Modules.Catalog.Products.UseCases; using Modules.Catalog.Tests.Common; @@ -118,4 +119,4 @@ public async Task UpdateProductPrice_ValidRequest_ShouldReturnNoContent() updatedProduct.Should().NotBeNull(); updatedProduct!.Price.Amount.Should().Be(request.Price); } -} \ No newline at end of file +} diff --git a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs index 0cc3af2..982a9b0 100644 --- a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs +++ b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Modules.Catalog.Tests")] -[assembly: InternalsVisibleTo("Database")] \ No newline at end of file +[assembly: InternalsVisibleTo("Modules.Orders.Tests")] +[assembly: InternalsVisibleTo("Database")] diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 725d285..dd7e6a3 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Modules.Catalog.Categories; using Modules.Catalog.Common.Persistence; +using Modules.Catalog.Products.Integrations; using Modules.Catalog.Products.UseCases; namespace Modules.Catalog; diff --git a/src/Modules/Products/Modules.Catalog/Categories/CreateProductCommand.cs b/src/Modules/Products/Modules.Catalog/Categories/CreateCategoryCommand.cs similarity index 100% rename from src/Modules/Products/Modules.Catalog/Categories/CreateProductCommand.cs rename to src/Modules/Products/Modules.Catalog/Categories/CreateCategoryCommand.cs diff --git a/src/Modules/Products/Modules.Catalog/GlobalUsings.cs b/src/Modules/Products/Modules.Catalog/GlobalUsings.cs index 3205267..b70d7d7 100644 --- a/src/Modules/Products/Modules.Catalog/GlobalUsings.cs +++ b/src/Modules/Products/Modules.Catalog/GlobalUsings.cs @@ -2,4 +2,5 @@ global using Common.SharedKernel.Domain; global using Common.SharedKernel.Domain.Base; -global using Common.SharedKernel.Domain.Entities; \ No newline at end of file +global using Common.SharedKernel.Domain.Entities; +global using Common.SharedKernel.Domain.Ids; \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 28aaf6d..4f69fc3 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs index 98d71d5..d5f38a5 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Domain/Product.cs @@ -1,14 +1,13 @@ -using Common.SharedKernel.Domain.Interfaces; using Modules.Catalog.Categories.Domain; namespace Modules.Catalog.Products.Domain; - -internal record ProductId(Guid Value) : IStronglyTypedId -{ - internal ProductId() : this(Uuid.Create()) - { - } -} +// +// internal record ProductId(Guid Value) : IStronglyTypedId +// { +// internal ProductId() : this(Uuid.Create()) +// { +// } +// } internal class Product : AggregateRoot { @@ -62,4 +61,4 @@ public void RemoveCategory(Category category) _categories.Remove(category); } -} \ No newline at end of file +} diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs b/src/Modules/Products/Modules.Catalog/Products/Integrations/GetProductQuery.cs similarity index 77% rename from src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs rename to src/Modules/Products/Modules.Catalog/Products/Integrations/GetProductQuery.cs index e33a8c0..b6c625b 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/GetProductQuery.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Integrations/GetProductQuery.cs @@ -9,16 +9,18 @@ using Microsoft.EntityFrameworkCore; using Modules.Catalog.Common.Persistence; using Modules.Catalog.Products.Domain; +using Request = Modules.Catalog.Messages.GetProductQuery.Request; +using Response = Modules.Catalog.Messages.GetProductQuery.Response; -namespace Modules.Catalog.Products.UseCases; +namespace Modules.Catalog.Products.Integrations; public static class GetProductQuery { - public record Request(Guid ProductId) : IRequest>; - - public record Response(string Name, Guid Id, string Sku, decimal Price, List Categories); - - public record CategoryDto(Guid Id, string Name); + // public record Request(Guid ProductId) : IRequest>; + // + // public record Response(string Name, Guid Id, string Sku, decimal Price, List Categories); + // + // public record CategoryDto(Guid Id, string Name); public static class Endpoint { @@ -53,7 +55,7 @@ public async Task> Handle(Request request, CancellationToken c var product = await _dbContext.Products .WithSpecification(new ProductByIdSpec(productId)) .Select(p => new Response(p.Name, p.Id.Value, p.Sku, p.Price.Amount, - p.Categories.Select(c => new CategoryDto(c.Id.Value, c.Name)).ToList())) + p.Categories.Select(c => new Messages.GetProductQuery.CategoryDto(c.Id.Value, c.Name)).ToList())) .FirstOrDefaultAsync(cancellationToken); if (product is null) diff --git a/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs b/src/Modules/Products/Modules.Catalog/Products/Integrations/ProductStoredIntegrationEvent.cs similarity index 60% rename from src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs rename to src/Modules/Products/Modules.Catalog/Products/Integrations/ProductStoredIntegrationEvent.cs index 3483a76..8fc375d 100644 --- a/src/Modules/Products/Modules.Catalog/Products/IntegrationEvents/ProductStoredIntegrationEventHandler.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Integrations/ProductStoredIntegrationEvent.cs @@ -2,22 +2,21 @@ using Microsoft.Extensions.Logging; using Modules.Catalog.Common.Persistence; using Modules.Catalog.Products.Domain; -using Modules.Warehouse.Messages; -namespace Modules.Catalog.Products.IntegrationEvents; +namespace Modules.Catalog.Products.Integrations; -internal class ProductStoredIntegrationEventHandler : INotificationHandler +internal class ProductStoredIntegrationEvent : INotificationHandler { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly CatalogDbContext _dbContext; - public ProductStoredIntegrationEventHandler(ILogger logger, CatalogDbContext dbContext) + public ProductStoredIntegrationEvent(ILogger logger, CatalogDbContext dbContext) { _logger = logger; _dbContext = dbContext; } - public async Task Handle(ProductStoredIntegrationEvent notification, CancellationToken cancellationToken) + public async Task Handle(Warehouse.Messages.ProductStoredIntegrationEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("Product stored integration event received"); diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs index a4c5550..b46ec7b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/GlobalUsings.cs @@ -1,2 +1,3 @@ +global using Common.SharedKernel.Domain.Ids; global using FluentAssertions; global using Xunit; \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs index 2366abd..21d26a0 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Storage/Domain/AisleTests.cs @@ -1,4 +1,3 @@ -using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; using Xunit.Abstractions; @@ -122,4 +121,4 @@ public void AssignProduct_WithNoAvailableStorage_ReturnsError() // Assert result.IsError.Should().BeTrue(); } -} \ No newline at end of file +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs b/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs index 6a9bc45..f4de4cd 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/GlobalUsings.cs @@ -3,3 +3,4 @@ global using Ardalis.Specification.EntityFrameworkCore; global using Common.SharedKernel.Domain; global using Common.SharedKernel.Domain.Base; +global using Common.SharedKernel.Domain.Ids; diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs index 2a6d290..db46712 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/Domain/Product.cs @@ -1,15 +1,14 @@ -using Common.SharedKernel.Domain.Interfaces; -using ErrorOr; +using ErrorOr; using Throw; namespace Modules.Warehouse.Products.Domain; -internal record ProductId(Guid Value) : IStronglyTypedId -{ - internal ProductId() : this(Uuid.Create()) - { - } -} +// internal record ProductId(Guid Value) : IStronglyTypedId +// { +// internal ProductId() : this(Uuid.Create()) +// { +// } +// } internal class Product : AggregateRoot { @@ -84,4 +83,4 @@ public void AddStock(int quantity) // { // // } -// } \ No newline at end of file +// } diff --git a/src/WebApi/Extensions/MediatRExtensions.cs b/src/WebApi/Extensions/MediatRExtensions.cs index 8d29baf..18efc28 100644 --- a/src/WebApi/Extensions/MediatRExtensions.cs +++ b/src/WebApi/Extensions/MediatRExtensions.cs @@ -1,6 +1,7 @@ using Common.SharedKernel.Behaviours; using Modules.Catalog; using Modules.Customers; +using Modules.Orders; using Modules.Warehouse; using System.Reflection; @@ -13,6 +14,7 @@ public static class MediatRExtensions typeof(WarehouseModule).Assembly, typeof(CatalogModule).Assembly, typeof(CustomersModule).Assembly, + typeof(OrdersModule).Assembly, ]; public static void AddMediatR(this IServiceCollection services) diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index ae45a99..31068c3 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -18,6 +18,7 @@ builder.Services.AddWarehouse(builder.Configuration); builder.Services.AddCatalog(builder.Configuration); builder.Services.AddCustomers(builder.Configuration); + builder.Services.AddOrders(builder.Configuration); } var app = builder.Build(); diff --git a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs index da7921e..c698c27 100644 --- a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs +++ b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs @@ -4,7 +4,6 @@ using Modules.Catalog.Categories.Domain; using Modules.Catalog.Common.Persistence; using Modules.Warehouse.Products.Domain; -using ProductId = Modules.Catalog.Products.Domain.ProductId; namespace Database.Initialisers; @@ -74,7 +73,7 @@ private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEn var catalogProduct = Modules.Catalog.Products.Domain.Product.Create( warehouseProduct.Name, warehouseProduct.Sku.Value, - new ProductId(warehouseProduct.Id.Value)); + warehouseProduct.Id); var productCategory = categoryFaker.Generate(); catalogProduct.AddCategory(productCategory); @@ -84,4 +83,4 @@ private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEn await _dbContext.SaveChangesAsync(); } -} \ No newline at end of file +} From 2ff5e8d029f1c6894c2e50d90bb35c2693b9bc60 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:06:05 +1000 Subject: [PATCH 77/87] =?UTF-8?q?=E2=9C=A8=2016=20Replace=20Docker=20Compo?= =?UTF-8?q?se=20with=20Aspire=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add in aspire projects * Add API project reference * Add in SQL server Aspire integration * Added migration project for creating, migrating and seeding SQL data * added catalog initializer to MigrationService * Removed unneeded code * Ensure seeding only happens in dev environment * Tidy up * Got DB Seeding for warehouse to work * Removed docker compose and up script. * Add aspire client package to catalog and warehouse modules * Params in product search is now optional * Temporarily remove DbContext.OnConfiguring() call * Got API working with Aspire * Fix DbContext pooling problem * Remove Database project * Add DbContextPooling for tests * Refactor Migration initialization code * Fixed DbContext Pooling issue with Customers and Orders * Convert customers and orders to use Aspire * Fixed up tests * Add aspire workload to GitHub action * Fix aspire command --- .github/workflows/dotnet.yml | 3 + ModularMonolith.sln | 34 ++++-- docker-compose.yml | 22 ---- src/AppHost/AppHost.csproj | 19 +++ src/AppHost/Program.cs | 39 ++++++ src/AppHost/Properties/launchSettings.json | 29 +++++ src/AppHost/appsettings.Development.json | 8 ++ src/AppHost/appsettings.json | 9 ++ .../Common.ServiceDefaults.csproj | 22 ++++ .../Common.ServiceDefaults/Extensions.cs | 112 ++++++++++++++++++ .../Extensions/ServiceCollectionExt.cs | 7 +- .../Common/Persistence/CustomersDbContext.cs | 11 +- .../Persistence/DepdendencyInjection.cs | 31 ++--- .../Modules.Customers/CustomersModule.cs | 10 +- .../Modules.Customers.csproj | 18 ++- .../Persistence/DepdendencyInjection.cs | 31 ++--- .../Common/Persistence/OrdersDbContext.cs | 11 +- .../Modules.Orders/Modules.Orders.csproj | 15 ++- .../Orders/Modules.Orders/OrdersModule.cs | 9 +- .../Products/Modules.Catalog/AssemblyInfo.cs | 1 + .../Products/Modules.Catalog/CatalogModule.cs | 10 +- .../Common/Persistence/CatalogDbContext.cs | 13 +- .../Persistence/DepdendencyInjection.cs | 32 +++-- .../Modules.Catalog/Modules.Catalog.csproj | 12 +- .../Products/UseCases/SearchProductsQuery.cs | 4 +- .../Products/ProductIntegrationTests.cs | 7 +- .../Modules.Warehouse/AssemblyInfo.cs | 1 + .../EventualConsistencyMiddleware.cs | 18 +-- .../Persistence/DepdendencyInjection.cs | 32 ++--- .../Common/Persistence/WarehouseDbContext.cs | 13 +- .../Modules.Warehouse.csproj | 11 +- .../Modules.Warehouse/WarehouseModule.cs | 10 +- src/WebApi/Program.cs | 14 ++- src/WebApi/WebApi.csproj | 6 + src/WebApi/WebApi.http | 2 +- tools/Database/Database.csproj | 23 ---- .../CatalogDbContextInitialiser.cs | 2 +- .../WarehouseDbContextInitialiser.cs | 82 ------------- tools/Database/Program.cs | 54 --------- tools/Database/appsettings.json | 12 -- .../CatalogDbContextInitializer.cs | 71 +++++++++++ .../CustomersDbContextInitializer.cs | 15 +++ .../Initializers/DbContextInitializerBase.cs | 45 +++++++ .../OrdersDbContextInitializer.cs | 15 +++ .../WarehouseDbContextInitializer.cs | 70 +++++++++++ .../MigrationService/MigrationService.csproj | 23 ++++ tools/MigrationService/Program.cs | 31 +++++ .../Properties/launchSettings.json | 12 ++ tools/MigrationService/Worker.cs | 63 ++++++++++ .../appsettings.Development.json | 8 ++ tools/MigrationService/appsettings.json | 8 ++ up.ps1 | 10 -- 52 files changed, 779 insertions(+), 391 deletions(-) delete mode 100644 docker-compose.yml create mode 100644 src/AppHost/AppHost.csproj create mode 100644 src/AppHost/Program.cs create mode 100644 src/AppHost/Properties/launchSettings.json create mode 100644 src/AppHost/appsettings.Development.json create mode 100644 src/AppHost/appsettings.json create mode 100644 src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj create mode 100644 src/Common/Common.ServiceDefaults/Extensions.cs delete mode 100644 tools/Database/Database.csproj delete mode 100644 tools/Database/Initialisers/WarehouseDbContextInitialiser.cs delete mode 100644 tools/Database/Program.cs delete mode 100644 tools/Database/appsettings.json create mode 100644 tools/MigrationService/Initializers/CatalogDbContextInitializer.cs create mode 100644 tools/MigrationService/Initializers/CustomersDbContextInitializer.cs create mode 100644 tools/MigrationService/Initializers/DbContextInitializerBase.cs create mode 100644 tools/MigrationService/Initializers/OrdersDbContextInitializer.cs create mode 100644 tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs create mode 100644 tools/MigrationService/MigrationService.csproj create mode 100644 tools/MigrationService/Program.cs create mode 100644 tools/MigrationService/Properties/launchSettings.json create mode 100644 tools/MigrationService/Worker.cs create mode 100644 tools/MigrationService/appsettings.Development.json create mode 100644 tools/MigrationService/appsettings.json delete mode 100644 up.ps1 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index dc3f1c4..f895d6d 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -28,6 +28,9 @@ jobs: with: dotnet-version: 9.0.x + - name: Install Aspire + run: dotnet workload install aspire --include-previews + - name: Restore dependencies run: dotnet restore diff --git a/ModularMonolith.sln b/ModularMonolith.sln index 9fd6c69..2e7f232 100644 --- a/ModularMonolith.sln +++ b/ModularMonolith.sln @@ -44,14 +44,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Customers.Tests", " EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7915FF68-4EC9-497B-BD33-1A3DA7D6B457}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".infra", ".infra", "{758DF8D4-BEBB-4AD8-9B9E-84F613420CB2}" - ProjectSection(SolutionItems) = preProject - up.ps1 = up.ps1 - docker-compose.yml = docker-compose.yml - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "tools\Database\Database.csproj", "{9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Warehouse.Messages", "src\Modules\Warehouse\Modules.Warehouse.Messages\Modules.Warehouse.Messages.csproj", "{A105835C-C285-4AA5-AADF-CCA4BCC933B1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Tests", "src\WebApi.Tests\WebApi.Tests.csproj", "{13094B62-3DBC-4C6F-9ECC-D2DC433C319F}" @@ -66,6 +58,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".sln", ".sln", "{63C08527-F src\Modules\Warehouse\AddWarehouseMigration.ps1 = src\Modules\Warehouse\AddWarehouseMigration.ps1 EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.ServiceDefaults", "src\Common\Common.ServiceDefaults\Common.ServiceDefaults.csproj", "{E2A265D4-CEFE-4A59-BCC5-12FB20963F9D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "src\AppHost\AppHost.csproj", "{2FE29E82-698B-4E82-9068-7ABBC33B51C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrationService", "tools\MigrationService\MigrationService.csproj", "{08342A95-6F66-4FF2-9D7D-1073D6EB2CD7}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Catalog.Messages", "src\Modules\Products\Modules.Catalog.Messages\Modules.Catalog.Messages.csproj", "{75591E8A-4FE8-4179-9332-50A28D7A0073}" EndProject Global @@ -117,10 +115,6 @@ Global {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Debug|Any CPU.Build.0 = Debug|Any CPU {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Release|Any CPU.ActiveCfg = Release|Any CPU {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D}.Release|Any CPU.Build.0 = Release|Any CPU - {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB}.Release|Any CPU.Build.0 = Release|Any CPU {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A105835C-C285-4AA5-AADF-CCA4BCC933B1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -133,6 +127,18 @@ Global {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9}.Release|Any CPU.Build.0 = Release|Any CPU + {E2A265D4-CEFE-4A59-BCC5-12FB20963F9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2A265D4-CEFE-4A59-BCC5-12FB20963F9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2A265D4-CEFE-4A59-BCC5-12FB20963F9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2A265D4-CEFE-4A59-BCC5-12FB20963F9D}.Release|Any CPU.Build.0 = Release|Any CPU + {2FE29E82-698B-4E82-9068-7ABBC33B51C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FE29E82-698B-4E82-9068-7ABBC33B51C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FE29E82-698B-4E82-9068-7ABBC33B51C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FE29E82-698B-4E82-9068-7ABBC33B51C3}.Release|Any CPU.Build.0 = Release|Any CPU + {08342A95-6F66-4FF2-9D7D-1073D6EB2CD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08342A95-6F66-4FF2-9D7D-1073D6EB2CD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08342A95-6F66-4FF2-9D7D-1073D6EB2CD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08342A95-6F66-4FF2-9D7D-1073D6EB2CD7}.Release|Any CPU.Build.0 = Release|Any CPU {75591E8A-4FE8-4179-9332-50A28D7A0073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75591E8A-4FE8-4179-9332-50A28D7A0073}.Debug|Any CPU.Build.0 = Debug|Any CPU {75591E8A-4FE8-4179-9332-50A28D7A0073}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -155,10 +161,12 @@ Global {C9C4959A-0DB6-4C6C-9811-A42D7A5E3CE0} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {11925734-961D-4761-B209-BF601E59EB95} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} {50B76FBE-8FF0-4EA8-A6D0-5DE1AC4B598D} = {41494B34-2A0F-4AF6-96DA-C25AEBAA424C} - {9C4C7E4C-48AD-4E9C-93BB-B7F6F15A35EB} = {7915FF68-4EC9-497B-BD33-1A3DA7D6B457} {A105835C-C285-4AA5-AADF-CCA4BCC933B1} = {D4C452DB-CB41-4B65-8A1A-FCD6E7811EE8} {13094B62-3DBC-4C6F-9ECC-D2DC433C319F} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} {51EA2161-32E7-4B5D-AAFF-E3F8D9D4E3A9} = {3E4B904F-1D6C-437B-8208-C6D17F995528} + {E2A265D4-CEFE-4A59-BCC5-12FB20963F9D} = {3E4B904F-1D6C-437B-8208-C6D17F995528} + {2FE29E82-698B-4E82-9068-7ABBC33B51C3} = {382656EC-4C92-485C-8BC5-349D1A5C05C7} + {08342A95-6F66-4FF2-9D7D-1073D6EB2CD7} = {7915FF68-4EC9-497B-BD33-1A3DA7D6B457} {75591E8A-4FE8-4179-9332-50A28D7A0073} = {1E1A153A-D69A-4EC5-BD21-DE4249E8FA4F} EndGlobalSection EndGlobal diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 9a25199..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -# docker run --cap-add SYS_PTRACE -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 --name azuresqledge -d mcr.microsoft.com/azure-sql-edge - -name: modular-monolith -services: - db: - environment: - ACCEPT_EULA: "Y" - SA_PASSWORD: "Password123" - container_name: modular-monolith-db - platform: linux/amd64 - # NOTE: can't use azure-sql-edge as it doesn't support .NET CLR. - image: mcr.microsoft.com/mssql/server - ports: - # {{exposed}}:{{internal}} - you'll need to contain the exposed ports if you have more than one DB server running at a time - - 1800:1433 - restart: unless-stopped - healthcheck: - test: [ "CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password123 -Q 'SELECT 1' || exit 1" ] - interval: 10s - retries: 10 - start_period: 10s - timeout: 3s diff --git a/src/AppHost/AppHost.csproj b/src/AppHost/AppHost.csproj new file mode 100644 index 0000000..245c78d --- /dev/null +++ b/src/AppHost/AppHost.csproj @@ -0,0 +1,19 @@ + + + + Exe + true + 25ad7f3b-101b-4d4c-aa10-629ae07db0e5 + + + + + + + + + + + + + diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs new file mode 100644 index 0000000..39c5e92 --- /dev/null +++ b/src/AppHost/Program.cs @@ -0,0 +1,39 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(); + +// TODO: Figure out how to keep these running after the AppHost shuts down +// TODO: Perhaps we can store the SQL Server in a variable to add multiple DB's to it? +var warehouseDb = builder + .AddSqlServer("warehouse-sql") + .AddDatabase("warehouse"); + +var catalogDb = builder + .AddSqlServer("catalog-sql") + .AddDatabase("catalog"); + +var customersDb = builder + .AddSqlServer("customers-sql") + .AddDatabase("customers"); + +var ordersDb = builder + .AddSqlServer("orders-sql") + .AddDatabase("orders"); + +builder.AddProject("migrations") + .WithReference(warehouseDb) + .WithReference(catalogDb) + .WithReference(customersDb) + .WithReference(ordersDb); + +builder + .AddProject("api") + .WithExternalHttpEndpoints() + .WithReference(warehouseDb) + .WithReference(catalogDb) + .WithReference(customersDb) + .WithReference(ordersDb); + +builder + .Build() + .Run(); diff --git a/src/AppHost/Properties/launchSettings.json b/src/AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..3b4235c --- /dev/null +++ b/src/AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17074;http://localhost:15235", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21257", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22208" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15235", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19245", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20247" + } + } + } +} diff --git a/src/AppHost/appsettings.Development.json b/src/AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/AppHost/appsettings.json b/src/AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/src/AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj b/src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj new file mode 100644 index 0000000..c64fc85 --- /dev/null +++ b/src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/Common/Common.ServiceDefaults/Extensions.cs b/src/Common/Common.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..6bb5343 --- /dev/null +++ b/src/Common/Common.ServiceDefaults/Extensions.cs @@ -0,0 +1,112 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs index 2b7f6bd..d37dbd7 100644 --- a/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs +++ b/src/Common/Common.Tests/Extensions/ServiceCollectionExt.cs @@ -1,5 +1,6 @@ using Common.SharedKernel.Persistence.Interceptors; using Common.Tests.Common; +using EntityFramework.Exceptions.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -20,7 +21,7 @@ internal static IServiceCollection ReplaceDbContext( services .RemoveAll>() .RemoveAll() - .AddDbContext((_, options) => + .AddDbContextPool((_, options) => { options.UseSqlServer(databaseContainer.ConnectionString, b => b.MigrationsAssembly(typeof(T).Assembly.FullName)); @@ -34,8 +35,10 @@ internal static IServiceCollection ReplaceDbContext( serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService() ); + + options.UseExceptionProcessor(); }); return services; } -} \ No newline at end of file +} diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs index 12b5526..44274fc 100644 --- a/src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/CustomersDbContext.cs @@ -1,5 +1,4 @@ -using EntityFramework.Exceptions.SqlServer; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Modules.Customers.Customers.Domain; namespace Modules.Customers.Common.Persistence; @@ -21,12 +20,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(CustomersDbContext).Assembly); base.OnModelCreating(modelBuilder); } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - // Produces easy to read exceptions - optionsBuilder.UseExceptionProcessor(); - - base.OnConfiguring(optionsBuilder); - } } diff --git a/src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs index d999409..e1506dd 100644 --- a/src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Customers/Modules.Customers/Common/Persistence/DepdendencyInjection.cs @@ -1,34 +1,29 @@ using Common.SharedKernel.Persistence.Interceptors; +using EntityFramework.Exceptions.SqlServer; using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Warehouse.Common.Persistence.Interceptors; namespace Modules.Customers.Common.Persistence; internal static class DepdendencyInjection { - internal static void AddPersistence(this IServiceCollection services, IConfiguration config) + internal static void AddPersistence(this IHostApplicationBuilder builder) { - var connectionString = config.GetConnectionString("Customers"); - services.AddDbContext(options => - { - options.UseSqlServer(connectionString, builder => + builder.AddSqlServerDbContext("warehouse", + null, + options => { - builder.MigrationsAssembly(typeof(CustomersModule).Assembly.FullName); - builder.EnableRetryOnFailure(3); + var serviceProvider = builder.Services.BuildServiceProvider(); + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + options.UseExceptionProcessor(); }); - var serviceProvider = services.BuildServiceProvider(); - - options.AddInterceptors( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService()); - }); - - services.AddScoped(); - services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // services.AddScoped(); } diff --git a/src/Modules/Customers/Modules.Customers/CustomersModule.cs b/src/Modules/Customers/Modules.Customers/CustomersModule.cs index 8176975..daada56 100644 --- a/src/Modules/Customers/Modules.Customers/CustomersModule.cs +++ b/src/Modules/Customers/Modules.Customers/CustomersModule.cs @@ -1,7 +1,7 @@ using FluentValidation; using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Customers.Common.Persistence; using Modules.Customers.Customers.UseCases; @@ -9,15 +9,15 @@ namespace Modules.Customers; public static class CustomersModule { - public static void AddCustomers(this IServiceCollection services, IConfiguration configuration) + public static void AddCustomers(this IHostApplicationBuilder builder) { var applicationAssembly = typeof(CustomersModule).Assembly; - services.AddHttpContextAccessor(); + builder.Services.AddHttpContextAccessor(); - services.AddValidatorsFromAssembly(applicationAssembly); + builder.Services.AddValidatorsFromAssembly(applicationAssembly); - services.AddPersistence(configuration); + builder.AddPersistence(); } public static void UseCustomers(this WebApplication app) diff --git a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj index 95a9659..5dc1121 100644 --- a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj +++ b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj @@ -5,9 +5,21 @@ - - - + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs index c504326..918026a 100644 --- a/src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/DepdendencyInjection.cs @@ -1,34 +1,29 @@ using Common.SharedKernel.Persistence.Interceptors; +using EntityFramework.Exceptions.SqlServer; using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Warehouse.Common.Persistence.Interceptors; namespace Modules.Orders.Common.Persistence; internal static class DependencyInjection { - internal static void AddPersistence(this IServiceCollection services, IConfiguration config) + internal static void AddPersistence(this IHostApplicationBuilder builder) { - var connectionString = config.GetConnectionString("Catalog"); - services.AddDbContext(options => - { - options.UseSqlServer(connectionString, builder => + builder.AddSqlServerDbContext("warehouse", + null, + options => { - builder.MigrationsAssembly(typeof(OrdersModule).Assembly.FullName); - // builder.EnableRetryOnFailure(); + var serviceProvider = builder.Services.BuildServiceProvider(); + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + options.UseExceptionProcessor(); }); - var serviceProvider = services.BuildServiceProvider(); - - options.AddInterceptors( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService()); - }); - - services.AddScoped(); - services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // services.AddScoped(); } diff --git a/src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs b/src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs index 57be3a3..0a1eb6c 100644 --- a/src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs +++ b/src/Modules/Orders/Modules.Orders/Common/Persistence/OrdersDbContext.cs @@ -1,5 +1,4 @@ -using EntityFramework.Exceptions.SqlServer; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Modules.Orders.Carts.Domain; namespace Modules.Orders.Common.Persistence; @@ -19,12 +18,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrdersDbContext).Assembly); base.OnModelCreating(modelBuilder); } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - // Produces easy to read exceptions - optionsBuilder.UseExceptionProcessor(); - - base.OnConfiguring(optionsBuilder); - } } diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index 607c4e4..c763645 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -5,18 +5,23 @@ - - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Modules/Orders/Modules.Orders/OrdersModule.cs b/src/Modules/Orders/Modules.Orders/OrdersModule.cs index 4097470..8eb8059 100644 --- a/src/Modules/Orders/Modules.Orders/OrdersModule.cs +++ b/src/Modules/Orders/Modules.Orders/OrdersModule.cs @@ -1,8 +1,7 @@ using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Orders.Carts; using Modules.Orders.Common.Persistence; @@ -10,10 +9,10 @@ namespace Modules.Orders; public static class OrdersModule { - public static void AddOrders(this IServiceCollection services, IConfiguration configuration) + public static void AddOrders(this IHostApplicationBuilder builder) { - services.AddPersistence(configuration); - services.AddValidatorsFromAssembly(typeof(OrdersModule).Assembly); + builder.AddPersistence(); + builder.Services.AddValidatorsFromAssembly(typeof(OrdersModule).Assembly); } // TODO: Refactor to REPR pattern diff --git a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs index 982a9b0..df09665 100644 --- a/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs +++ b/src/Modules/Products/Modules.Catalog/AssemblyInfo.cs @@ -3,3 +3,4 @@ [assembly: InternalsVisibleTo("Modules.Catalog.Tests")] [assembly: InternalsVisibleTo("Modules.Orders.Tests")] [assembly: InternalsVisibleTo("Database")] +[assembly: InternalsVisibleTo("MigrationService")] diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index dd7e6a3..3af3dbf 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -1,7 +1,7 @@ using FluentValidation; using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Catalog.Categories; using Modules.Catalog.Common.Persistence; using Modules.Catalog.Products.Integrations; @@ -11,15 +11,15 @@ namespace Modules.Catalog; public static class CatalogModule { - public static void AddCatalog(this IServiceCollection services, IConfiguration configuration) + public static void AddCatalog(this IHostApplicationBuilder builder) { var applicationAssembly = typeof(CatalogModule).Assembly; - services.AddHttpContextAccessor(); + builder.Services.AddHttpContextAccessor(); - services.AddValidatorsFromAssembly(applicationAssembly); + builder.Services.AddValidatorsFromAssembly(applicationAssembly); - services.AddPersistence(configuration); + builder.AddPersistence(); } public static void UseCatalog(this WebApplication app) diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs index f738cd7..9b6350f 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/CatalogDbContext.cs @@ -1,5 +1,4 @@ -using EntityFramework.Exceptions.SqlServer; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Modules.Catalog.Categories.Domain; using Modules.Catalog.Products.Domain; @@ -22,12 +21,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly); base.OnModelCreating(modelBuilder); } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - // Produces easy to read exceptions - optionsBuilder.UseExceptionProcessor(); - - base.OnConfiguring(optionsBuilder); - } -} \ No newline at end of file +} diff --git a/src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs index bd0c6f8..e46d2ab 100644 --- a/src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Products/Modules.Catalog/Common/Persistence/DepdendencyInjection.cs @@ -1,34 +1,30 @@ using Common.SharedKernel.Persistence.Interceptors; +using EntityFramework.Exceptions.SqlServer; using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Warehouse.Common.Persistence.Interceptors; namespace Modules.Catalog.Common.Persistence; internal static class DependencyInjection { - internal static void AddPersistence(this IServiceCollection services, IConfiguration config) + internal static void AddPersistence(this IHostApplicationBuilder builder) { - var connectionString = config.GetConnectionString("Catalog"); - services.AddDbContext(options => - { - options.UseSqlServer(connectionString, builder => + builder.AddSqlServerDbContext("catalog", + null, + options => { - builder.MigrationsAssembly(typeof(CatalogModule).Assembly.FullName); - // builder.EnableRetryOnFailure(); - }); - - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = builder.Services.BuildServiceProvider(); + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); - options.AddInterceptors( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService()); - }); + options.UseExceptionProcessor(); + }); - services.AddScoped(); - services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // services.AddScoped(); } diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 4f69fc3..0a5d705 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -2,15 +2,15 @@ - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs index e17aa43..ad53fd3 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs @@ -21,11 +21,11 @@ public static class Endpoint public static void MapEndpoint(IEndpointRouteBuilder app) { app.MapGet("/api/products", - async (string name, Guid categoryId, ISender sender) => + async (string? name, Guid? categoryId, ISender sender) => { var request = new Request(name, categoryId); var response = await sender.Send(request); - TypedResults.Ok(response); + return TypedResults.Ok(response); }) .WithName("SearchProducts") .WithTags("Catalog") diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs index 715e736..fa0f8b2 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Products/ProductIntegrationTests.cs @@ -1,3 +1,4 @@ +using Common.Tests.Assertions; using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Products.UseCases; @@ -24,7 +25,7 @@ public async Task CreateProduct_ValidRequest_ReturnsCreatedProduct() var response = await client.PostAsJsonAsync("/api/products", request); // Assert - response.StatusCode.Should().Be(HttpStatusCode.Created); + HttpContentExtensions.Should(response).BeStatusCode(HttpStatusCode.Created); var products = await GetQueryable().ToListAsync(); products.Should().HaveCount(1); @@ -52,8 +53,8 @@ public async Task CreateProduct_InvalidRequest_ReturnsBadRequest(string? name, s var response = await client.PostAsJsonAsync("/api/products", request); // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + HttpContentExtensions.Should(response).BeStatusCode(HttpStatusCode.BadRequest); var content = await response.Content.ReadAsStringAsync(); _output.WriteLine(content); } -} \ No newline at end of file +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs b/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs index 6025fb5..faeac09 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/AssemblyInfo.cs @@ -2,3 +2,4 @@ [assembly: InternalsVisibleTo("Modules.Warehouse.Tests")] [assembly: InternalsVisibleTo("Database")] +[assembly: InternalsVisibleTo("MigrationService")] diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs index b64b10d..3b9f6e5 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Middleware/EventualConsistencyMiddleware.cs @@ -1,5 +1,6 @@ using Common.SharedKernel.Domain.Interfaces; using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Common.Persistence; namespace Modules.Warehouse.Common.Middleware; @@ -20,20 +21,23 @@ public EventualConsistencyMiddleware(RequestDelegate next) // TODO: Possibly use IDbContextFactory to dynamically create the context public async Task InvokeAsync(HttpContext context, IPublisher publisher, WarehouseDbContext dbContext) { - var transaction = await dbContext.Database.BeginTransactionAsync(); + // var transaction = await dbContext.Database.BeginTransactionAsync(); context.Response.OnCompleted(async () => { try { if (context.Items.TryGetValue(DomainEventsKey, out var value) && value is Queue domainEvents) { - while (domainEvents.TryDequeue(out var nextEvent)) + var strategy = dbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => { - await publisher.Publish(nextEvent); - } + while (domainEvents.TryDequeue(out var nextEvent)) + { + await publisher.Publish(nextEvent); + } + }); } - - await transaction.CommitAsync(); + // await transaction.CommitAsync(); } catch (EventualConsistencyException) { @@ -41,7 +45,7 @@ public async Task InvokeAsync(HttpContext context, IPublisher publisher, Warehou } finally { - await transaction.DisposeAsync(); + // await transaction.DisposeAsync(); } }); diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs index 09ce416..61fbe88 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/DepdendencyInjection.cs @@ -1,8 +1,8 @@ using Common.SharedKernel.Persistence.Interceptors; +using EntityFramework.Exceptions.SqlServer; using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Warehouse.Common.Middleware; using Modules.Warehouse.Common.Persistence.Interceptors; @@ -10,27 +10,21 @@ namespace Modules.Warehouse.Common.Persistence; internal static class DepdendencyInjection { - internal static void AddPersistence(this IServiceCollection services, IConfiguration config) + internal static void AddPersistence(this IHostApplicationBuilder builder) { - var connectionString = config.GetConnectionString("Warehouse"); - services.AddDbContext(options => - { - options.UseSqlServer(connectionString, builder => + builder.AddSqlServerDbContext("warehouse", + null, + options => { - builder.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); - // builder.EnableRetryOnFailure(); + var serviceProvider = builder.Services.BuildServiceProvider(); + options.AddInterceptors( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + options.UseExceptionProcessor(); }); - var serviceProvider = services.BuildServiceProvider(); - - options.AddInterceptors( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService()); - }); - - - services.AddScoped(); - services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // services.AddScoped(); } diff --git a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs index 6c3f097..0a01f25 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Common/Persistence/WarehouseDbContext.cs @@ -1,5 +1,4 @@ -using EntityFramework.Exceptions.SqlServer; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; @@ -23,12 +22,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); base.OnModelCreating(modelBuilder); } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - // Produces easy to read exceptions - optionsBuilder.UseExceptionProcessor(); - - base.OnConfiguring(optionsBuilder); - } -} \ No newline at end of file +} diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 36b67ae..25d3d47 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -2,11 +2,14 @@ - - + + + + + all @@ -19,8 +22,4 @@ - - - - diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index dfc6411..d33201b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Modules.Warehouse.Common.Persistence; using Modules.Warehouse.Products.UseCases; using Modules.Warehouse.Storage.UseCases; @@ -9,15 +9,15 @@ namespace Modules.Warehouse; public static class WarehouseModule { - public static void AddWarehouse(this IServiceCollection services, IConfiguration configuration) + public static void AddWarehouse(this IHostApplicationBuilder builder) { var applicationAssembly = typeof(WarehouseModule).Assembly; - services.AddHttpContextAccessor(); + builder.Services.AddHttpContextAccessor(); - services.AddValidatorsFromAssembly(applicationAssembly); + builder.Services.AddValidatorsFromAssembly(applicationAssembly); - services.AddPersistence(configuration); + builder.AddPersistence(); } public static void UseWarehouse(this WebApplication app) diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 31068c3..98b09f1 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -7,6 +7,9 @@ var builder = WebApplication.CreateBuilder(args); { + // Add service defaults & Aspire components. + builder.AddServiceDefaults(); + builder.Services.AddSwagger(); builder.Services.AddGlobalErrorHandler(); @@ -15,10 +18,11 @@ builder.Services.AddMediatR(); - builder.Services.AddWarehouse(builder.Configuration); - builder.Services.AddCatalog(builder.Configuration); - builder.Services.AddCustomers(builder.Configuration); - builder.Services.AddOrders(builder.Configuration); + // builder.Services.AddOrders(); + builder.AddWarehouse(); + builder.AddCatalog(); + builder.AddCustomers(); + builder.AddOrders(); } var app = builder.Build(); @@ -37,5 +41,7 @@ app.UseCatalog(); app.UseCustomers(); + app.MapDefaultEndpoints(); + app.Run(); } diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index eae5944..9494d94 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -5,6 +5,10 @@ + + + + all @@ -14,9 +18,11 @@ + + diff --git a/src/WebApi/WebApi.http b/src/WebApi/WebApi.http index adaaa53..96319d8 100644 --- a/src/WebApi/WebApi.http +++ b/src/WebApi/WebApi.http @@ -1,4 +1,4 @@ -//@Web_HostAddress = http://localhost:5059 +#@Web_HostAddress = http://localhost:5059 @Web_HostAddress = https://localhost:7061 GET {{Web_HostAddress}}/api/orders diff --git a/tools/Database/Database.csproj b/tools/Database/Database.csproj deleted file mode 100644 index dfed01c..0000000 --- a/tools/Database/Database.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - Exe - - - - - - - - - - - - - - - Always - - - - diff --git a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs index c698c27..853bb40 100644 --- a/tools/Database/Initialisers/CatalogDbContextInitialiser.cs +++ b/tools/Database/Initialisers/CatalogDbContextInitialiser.cs @@ -73,7 +73,7 @@ private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEn var catalogProduct = Modules.Catalog.Products.Domain.Product.Create( warehouseProduct.Name, warehouseProduct.Sku.Value, - warehouseProduct.Id); + new ProductId(warehouseProduct.Id.Value)); var productCategory = categoryFaker.Generate(); catalogProduct.AddCategory(productCategory); diff --git a/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs b/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs deleted file mode 100644 index e2136f0..0000000 --- a/tools/Database/Initialisers/WarehouseDbContextInitialiser.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Bogus; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Products.Domain; -using Modules.Warehouse.Storage.Domain; - -namespace Database.Initialisers; - -internal class WarehouseDbContextInitialiser -{ - private readonly ILogger _logger; - private readonly WarehouseDbContext _dbContext; - - private const int NumProducts = 20; - private const int NumAisles = 10; - private const int NumShelves = 5; - private const int NumBays = 20; - - // public constructor needed for DI - public WarehouseDbContextInitialiser(ILogger logger, WarehouseDbContext dbContext) - { - _logger = logger; - _dbContext = dbContext; - } - - internal async Task InitializeAsync() - { - try - { - if (_dbContext.Database.IsSqlServer()) - { - await _dbContext.Database.MigrateAsync(); - } - } - catch (Exception e) - { - _logger.LogError(e, "An error occurred while migrating or initializing the database"); - throw; - } - } - - internal async Task> SeedAsync() - { - await SeedAisles(); - return await SeedProductsAsync(); - } - - private async Task SeedAisles() - { - if (await _dbContext.Aisles.AnyAsync()) - return; - - for (var i = 1; i <= NumAisles; i++) - { - var aisle = Aisle.Create($"Aisle {i}", NumBays, NumShelves); - _dbContext.Aisles.Add(aisle); - } - - await _dbContext.SaveChangesAsync(); - } - - private async Task> SeedProductsAsync() - { - if (await _dbContext.Products.AnyAsync()) - return []; - - // TODO: Consider how to handle integration events that get raised and handled - - var skuFaker = new Faker() - .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); - - var faker = new Faker() - .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), skuFaker.Generate())); - - var products = faker.Generate(NumProducts); - _dbContext.Products.AddRange(products); - await _dbContext.SaveChangesAsync(); - - return products; - } -} \ No newline at end of file diff --git a/tools/Database/Program.cs b/tools/Database/Program.cs deleted file mode 100644 index 35182ef..0000000 --- a/tools/Database/Program.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Database.Initialisers; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Modules.Catalog; -using Modules.Catalog.Common.Persistence; -using Modules.Warehouse; -using Modules.Warehouse.Common.Persistence; - -var builder = Host.CreateDefaultBuilder(args); - -builder.ConfigureServices((context, services) => -{ - services.AddSingleton(TimeProvider.System); - - var conn = context.Configuration.GetConnectionString("Warehouse"); - - services.AddDbContext(options => - { - options.UseSqlServer(context.Configuration.GetConnectionString("Warehouse"), opt => - { - opt.MigrationsAssembly(typeof(WarehouseModule).Assembly.FullName); - }); - }); - - services.AddDbContext(options => - { - options.UseSqlServer(context.Configuration.GetConnectionString("Catalog"), opt => - { - opt.MigrationsAssembly(typeof(CatalogModule).Assembly.FullName); - }); - }); - - services.AddScoped(); - services.AddScoped(); -}); - -var app = builder.Build(); -app.Start(); - -// Initialise and seed database -using var scope = app.Services.CreateScope(); - -Console.WriteLine("Waiting for SQL Server..."); -await Task.Delay(5000); - -var warehouse = scope.ServiceProvider.GetRequiredService(); -await warehouse.InitializeAsync(); -var warehouseProducts = await warehouse.SeedAsync(); - -var catalog = scope.ServiceProvider.GetRequiredService(); -await catalog.InitializeAsync(); -await catalog.SeedAsync(warehouseProducts); diff --git a/tools/Database/appsettings.json b/tools/Database/appsettings.json deleted file mode 100644 index a07a4ba..0000000 --- a/tools/Database/appsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "ConnectionStrings": { - "Warehouse": "Server=localhost,1800;Initial Catalog=Warehouse;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;", - "Catalog": "Server=localhost,1800;Initial Catalog=Catalog;Persist Security Info=False;User ID=sa;Password=Password123;MultipleActiveResultSets=True;TrustServerCertificate=True;Connection Timeout=30;" - } -} diff --git a/tools/MigrationService/Initializers/CatalogDbContextInitializer.cs b/tools/MigrationService/Initializers/CatalogDbContextInitializer.cs new file mode 100644 index 0000000..83d94f9 --- /dev/null +++ b/tools/MigrationService/Initializers/CatalogDbContextInitializer.cs @@ -0,0 +1,71 @@ +using Bogus; +using Microsoft.EntityFrameworkCore; +using Modules.Catalog.Categories.Domain; +using Modules.Catalog.Common.Persistence; +using Modules.Warehouse.Products.Domain; + +namespace MigrationService.Initializers; + +internal class CatalogDbContextInitializer : DbContextInitializerBase +{ + private const int NumCategories = 10; + + public CatalogDbContextInitializer(CatalogDbContext dbContext) :base(dbContext) + { + } + + public async Task SeedDataAsync(IReadOnlyList products, CancellationToken cancellationToken) + { + var strategy = DbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + // Seed the database + await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken); + var categories = await SeedCategories(); + await SeedProductsAsync(products, categories); + await DbContext.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + }); + } + + private async Task> SeedCategories() + { + if (await DbContext.Categories.AnyAsync()) + return []; + + var categoryFaker = new Faker() + .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1).First()!)); + + var categories = categoryFaker.Generate(NumCategories); + DbContext.Categories.AddRange(categories); + await DbContext.SaveChangesAsync(); + + return categories; + } + + private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEnumerable categories) + { + if (await DbContext.Products.AnyAsync()) + return; + + var categoryFaker = new Faker() + .CustomInstantiator(f => f.PickRandom(categories)); + + // Usually integration events would propagate products to the catalog + // However, to simplify test data seed, we'll manually pass products into the catalog + foreach (var warehouseProduct in warehouseProducts) + { + var catalogProduct = Modules.Catalog.Products.Domain.Product.Create( + warehouseProduct.Name, + warehouseProduct.Sku.Value, + warehouseProduct.Id); + + var productCategory = categoryFaker.Generate(); + catalogProduct.AddCategory(productCategory); + + DbContext.Products.Add(catalogProduct); + } + + await DbContext.SaveChangesAsync(); + } +} diff --git a/tools/MigrationService/Initializers/CustomersDbContextInitializer.cs b/tools/MigrationService/Initializers/CustomersDbContextInitializer.cs new file mode 100644 index 0000000..9a24bc7 --- /dev/null +++ b/tools/MigrationService/Initializers/CustomersDbContextInitializer.cs @@ -0,0 +1,15 @@ +using Modules.Customers.Common.Persistence; + +namespace MigrationService.Initializers; + +internal class CustomersDbContextInitializer:DbContextInitializerBase +{ + public CustomersDbContextInitializer(CustomersDbContext dbContext) :base(dbContext) + { + } + + public Task SeedDataAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/tools/MigrationService/Initializers/DbContextInitializerBase.cs b/tools/MigrationService/Initializers/DbContextInitializerBase.cs new file mode 100644 index 0000000..e1f5d80 --- /dev/null +++ b/tools/MigrationService/Initializers/DbContextInitializerBase.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace MigrationService.Initializers; + +internal abstract class DbContextInitializerBase where T : DbContext +{ + protected readonly T DbContext; + + // public constructor needed for DI + internal DbContextInitializerBase(T dbContext) + { + DbContext = dbContext; + } + + internal async Task EnsureDatabaseAsync(CancellationToken cancellationToken) + { + var dbCreator = DbContext.GetService(); + + var strategy = DbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + // Create the database if it does not exist. + // Do this first so there is then a database to start a transaction against. + if (!await dbCreator.ExistsAsync(cancellationToken)) + { + await dbCreator.CreateAsync(cancellationToken); + } + }); + } + + internal async Task RunMigrationAsync(CancellationToken cancellationToken) + { + var strategy = DbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + // Run migration in a transaction to avoid partial migration if it fails. + await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken); + await DbContext.Database.MigrateAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + }); + } + +} \ No newline at end of file diff --git a/tools/MigrationService/Initializers/OrdersDbContextInitializer.cs b/tools/MigrationService/Initializers/OrdersDbContextInitializer.cs new file mode 100644 index 0000000..ff86dcc --- /dev/null +++ b/tools/MigrationService/Initializers/OrdersDbContextInitializer.cs @@ -0,0 +1,15 @@ +using Modules.Orders.Common.Persistence; + +namespace MigrationService.Initializers; + +internal class OrdersDbContextInitializer:DbContextInitializerBase +{ + public OrdersDbContextInitializer(OrdersDbContext dbContext) :base(dbContext) + { + } + + public Task SeedDataAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs b/tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs new file mode 100644 index 0000000..ad1f7b7 --- /dev/null +++ b/tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs @@ -0,0 +1,70 @@ +using Bogus; +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Common.Persistence; +using Modules.Warehouse.Products.Domain; +using Modules.Warehouse.Storage.Domain; + +namespace MigrationService.Initializers; + +internal class WarehouseDbContextInitializer: DbContextInitializerBase +{ + private const int NumProducts = 20; + private const int NumAisles = 10; + private const int NumShelves = 5; + private const int NumBays = 20; + + public WarehouseDbContextInitializer(WarehouseDbContext dbContext) :base(dbContext) + { + } + + public async Task> SeedDataAsync(CancellationToken cancellationToken) + { + var strategy = DbContext.Database.CreateExecutionStrategy(); + IReadOnlyList products = []; + await strategy.ExecuteAsync(async () => + { + // Seed the database + await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken); + await SeedAisles(); + products = await SeedProductsAsync(); + await DbContext.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + }); + + return products; + } + + private async Task SeedAisles() + { + if (await DbContext.Aisles.AnyAsync()) + return; + + for (var i = 1; i <= NumAisles; i++) + { + var aisle = Aisle.Create($"Aisle {i}", NumBays, NumShelves); + DbContext.Aisles.Add(aisle); + } + + await DbContext.SaveChangesAsync(); + } + + private async Task> SeedProductsAsync() + { + if (await DbContext.Products.AnyAsync()) + return []; + + // TODO: Consider how to handle integration events that get raised and handled + + var skuFaker = new Faker() + .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); + + var faker = new Faker() + .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), skuFaker.Generate())); + + var products = faker.Generate(NumProducts); + DbContext.Products.AddRange(products); + await DbContext.SaveChangesAsync(); + + return products; + } +} diff --git a/tools/MigrationService/MigrationService.csproj b/tools/MigrationService/MigrationService.csproj new file mode 100644 index 0000000..9e9f8c9 --- /dev/null +++ b/tools/MigrationService/MigrationService.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + dotnet-MigrationService-4e08dc3d-e9e8-4f5f-80dd-1c91f47f77fe + + + + + + + + + + + + + + + + diff --git a/tools/MigrationService/Program.cs b/tools/MigrationService/Program.cs new file mode 100644 index 0000000..5c87ea1 --- /dev/null +++ b/tools/MigrationService/Program.cs @@ -0,0 +1,31 @@ +using MigrationService; +using MigrationService.Initializers; +using Modules.Catalog.Common.Persistence; +using Modules.Customers.Common.Persistence; +using Modules.Orders.Common.Persistence; +using Modules.Warehouse.Common.Persistence; + +var builder = Host.CreateApplicationBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddHostedService(); + +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName)); + +builder.Services.AddScoped(); +builder.AddSqlServerDbContext("warehouse"); + +builder.Services.AddScoped(); +builder.AddSqlServerDbContext("catalog"); + +builder.Services.AddScoped(); +builder.AddSqlServerDbContext("customers"); + +builder.Services.AddScoped(); +builder.AddSqlServerDbContext("orders"); + +var host = builder.Build(); + +host.Run(); diff --git a/tools/MigrationService/Properties/launchSettings.json b/tools/MigrationService/Properties/launchSettings.json new file mode 100644 index 0000000..7f5b75d --- /dev/null +++ b/tools/MigrationService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "MigrationService": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/tools/MigrationService/Worker.cs b/tools/MigrationService/Worker.cs new file mode 100644 index 0000000..5849110 --- /dev/null +++ b/tools/MigrationService/Worker.cs @@ -0,0 +1,63 @@ +using MigrationService.Initializers; +using OpenTelemetry.Trace; +using System.Diagnostics; + +namespace MigrationService; + +public class Worker( + IServiceProvider serviceProvider, + IHostApplicationLifetime hostApplicationLifetime, + ILogger logger) : BackgroundService +{ + public const string ActivitySourceName = "Migrations"; + private static readonly ActivitySource _activitySource = new(ActivitySourceName); + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + using var activity = _activitySource.StartActivity("Migrating database", ActivityKind.Client); + + try + { + logger.LogInformation("Waiting for SQL Server to be ready"); + await Task.Delay(30_000, cancellationToken); + + var sw = Stopwatch.StartNew(); + using var scope = serviceProvider.CreateScope(); + var environment = scope.ServiceProvider.GetRequiredService(); + + var warehouseInitializer = scope.ServiceProvider.GetRequiredService(); + await warehouseInitializer.EnsureDatabaseAsync(cancellationToken); + await warehouseInitializer.RunMigrationAsync(cancellationToken); + + var catalogInitializer = scope.ServiceProvider.GetRequiredService(); + await catalogInitializer.EnsureDatabaseAsync(cancellationToken); + await catalogInitializer.RunMigrationAsync(cancellationToken); + + var customersInitializer = scope.ServiceProvider.GetRequiredService(); + await customersInitializer.EnsureDatabaseAsync(cancellationToken); + await customersInitializer.RunMigrationAsync(cancellationToken); + + var ordersInitializer = scope.ServiceProvider.GetRequiredService(); + await ordersInitializer.EnsureDatabaseAsync(cancellationToken); + await ordersInitializer.RunMigrationAsync(cancellationToken); + + if (environment.IsDevelopment()) + { + var products = await warehouseInitializer.SeedDataAsync(cancellationToken); + await catalogInitializer.SeedDataAsync(products, cancellationToken); + await customersInitializer.SeedDataAsync(cancellationToken); + await ordersInitializer.SeedDataAsync(cancellationToken); + } + + sw.Stop(); + logger.LogInformation($"DB creation and seeding took {sw.Elapsed} "); + } + catch (Exception ex) + { + activity?.RecordException(ex); + throw; + } + + hostApplicationLifetime.StopApplication(); + } +} diff --git a/tools/MigrationService/appsettings.Development.json b/tools/MigrationService/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/tools/MigrationService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/tools/MigrationService/appsettings.json b/tools/MigrationService/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/tools/MigrationService/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/up.ps1 b/up.ps1 deleted file mode 100644 index 5981474..0000000 --- a/up.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -Write-Host "🚢 Starting Docker Compose" -ForegroundColor Green -docker compose up -d - -$upScriptPath = $Script:MyInvocation.MyCommand.Path | Split-Path - -Write-Host "🚀 Creating and Seeding Database" -ForegroundColor Green -Set-Location ./tools/Database/ -dotnet run - -Set-Location $upScriptPath From ef74e3ee6227d3582aae50bb805d2c35cba9b007 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:07:29 -0400 Subject: [PATCH 78/87] =?UTF-8?q?=F0=9F=93=A6=20Upgrade=20packages=20to=20?= =?UTF-8?q?.NET=209=20RC=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common.ServiceDefaults/Common.ServiceDefaults.csproj | 2 +- src/Common/Common.SharedKernel/Common.SharedKernel.csproj | 6 +++--- src/Common/Common.Tests/Common.Tests.csproj | 2 +- .../Customers/Modules.Customers/Modules.Customers.csproj | 6 +++--- src/Modules/Orders/Modules.Orders/Modules.Orders.csproj | 6 +++--- src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj | 2 +- .../Warehouse/Modules.Warehouse/Modules.Warehouse.csproj | 4 ++-- src/WebApi/WebApi.csproj | 4 ++-- tools/MigrationService/MigrationService.csproj | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj b/src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj index c64fc85..8a7d3e7 100644 --- a/src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj +++ b/src/Common/Common.ServiceDefaults/Common.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index 18d11ee..ee08a03 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -3,9 +3,9 @@ - - - + + + diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index 7d82264..a327689 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj index 5dc1121..131003d 100644 --- a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj +++ b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj @@ -8,15 +8,15 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index c763645..958287f 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -9,15 +9,15 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index 0a5d705..b8b93ed 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index 25d3d47..f5de3b9 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 9494d94..c8c0c9e 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tools/MigrationService/MigrationService.csproj b/tools/MigrationService/MigrationService.csproj index 9e9f8c9..3cea5ab 100644 --- a/tools/MigrationService/MigrationService.csproj +++ b/tools/MigrationService/MigrationService.csproj @@ -10,7 +10,7 @@ - + From 5c6073c76c2d1c729562a0107ebc919da6a55276 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:26:48 -0400 Subject: [PATCH 79/87] =?UTF-8?q?=F0=9F=93=A6=20Upgrade=20other=20nuget=20?= =?UTF-8?q?packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Common/Common.SharedKernel/Common.SharedKernel.csproj | 4 ++-- src/Common/Common.Tests/Common.Tests.csproj | 8 ++++---- .../Modules.Customers.Tests.csproj | 4 ++-- .../Customers/Modules.Customers/Modules.Customers.csproj | 2 +- .../Modules.Orders.Tests/Modules.Orders.Tests.csproj | 4 ++-- src/Modules/Orders/Modules.Orders/Modules.Orders.csproj | 2 +- .../Modules.Catalog.Tests/Modules.Catalog.Tests.csproj | 4 ++-- .../Products/Modules.Catalog/Modules.Catalog.csproj | 6 +++--- .../Modules.Warehouse.Tests.csproj | 4 ++-- .../Warehouse/Modules.Warehouse/Modules.Warehouse.csproj | 6 +++--- src/WebApi.Tests/WebApi.Tests.csproj | 2 +- src/WebApi/WebApi.csproj | 2 +- tools/MigrationService/MigrationService.csproj | 2 +- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj index ee08a03..cfc277c 100644 --- a/src/Common/Common.SharedKernel/Common.SharedKernel.csproj +++ b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj @@ -1,8 +1,8 @@  - - + + diff --git a/src/Common/Common.Tests/Common.Tests.csproj b/src/Common/Common.Tests/Common.Tests.csproj index a327689..317604c 100644 --- a/src/Common/Common.Tests/Common.Tests.csproj +++ b/src/Common/Common.Tests/Common.Tests.csproj @@ -1,10 +1,10 @@  - - + + - + @@ -14,7 +14,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj index a1fb2e3..0d10703 100644 --- a/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj +++ b/src/Modules/Customers/Modules.Customers.Tests/Modules.Customers.Tests.csproj @@ -6,9 +6,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj index 131003d..388ec67 100644 --- a/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj +++ b/src/Modules/Customers/Modules.Customers/Modules.Customers.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj index 3918d38..0a6c564 100644 --- a/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj +++ b/src/Modules/Orders/Modules.Orders.Tests/Modules.Orders.Tests.csproj @@ -6,9 +6,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj index 958287f..50f0189 100644 --- a/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj +++ b/src/Modules/Orders/Modules.Orders/Modules.Orders.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj index 8aee52d..50ee1e6 100644 --- a/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj +++ b/src/Modules/Products/Modules.Catalog.Tests/Modules.Catalog.Tests.csproj @@ -6,9 +6,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj index b8b93ed..d43868b 100644 --- a/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj +++ b/src/Modules/Products/Modules.Catalog/Modules.Catalog.csproj @@ -1,10 +1,10 @@  - + - - + + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj index 89e9066..17c15a4 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Tests/Modules.Warehouse.Tests.csproj @@ -6,10 +6,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj index f5de3b9..aae6ded 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse/Modules.Warehouse.csproj @@ -1,10 +1,10 @@  - + - - + + diff --git a/src/WebApi.Tests/WebApi.Tests.csproj b/src/WebApi.Tests/WebApi.Tests.csproj index 4704fd0..3e35c8a 100644 --- a/src/WebApi.Tests/WebApi.Tests.csproj +++ b/src/WebApi.Tests/WebApi.Tests.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index c8c0c9e..ecd3058 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tools/MigrationService/MigrationService.csproj b/tools/MigrationService/MigrationService.csproj index 3cea5ab..851a3b6 100644 --- a/tools/MigrationService/MigrationService.csproj +++ b/tools/MigrationService/MigrationService.csproj @@ -9,7 +9,7 @@ - + From 52a04e2479861187a4eb2b8de9dfcb9c395d0945 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 9 Oct 2024 23:05:50 -0400 Subject: [PATCH 80/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20to=20use?= =?UTF-8?q?=20SLNX=20solution=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ModularMonolith.slnx | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 ModularMonolith.slnx diff --git a/ModularMonolith.slnx b/ModularMonolith.slnx new file mode 100644 index 0000000..2b1bc7a --- /dev/null +++ b/ModularMonolith.slnx @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 931bb634094c5fbbfbc7f54f8d195270f4b2d098 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:54:15 -0400 Subject: [PATCH 81/87] =?UTF-8?q?=F0=9F=90=9B=20Fix=20transaction=20migrat?= =?UTF-8?q?ion=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 2 +- .../MigrationService/Initializers/DbContextInitializerBase.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index a586d89..e35d5f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ root = true ########################################################################################## # All files -[*]ls' +[*] indent_style = space # Xml files diff --git a/tools/MigrationService/Initializers/DbContextInitializerBase.cs b/tools/MigrationService/Initializers/DbContextInitializerBase.cs index e1f5d80..e31e276 100644 --- a/tools/MigrationService/Initializers/DbContextInitializerBase.cs +++ b/tools/MigrationService/Initializers/DbContextInitializerBase.cs @@ -36,9 +36,9 @@ internal async Task RunMigrationAsync(CancellationToken cancellationToken) await strategy.ExecuteAsync(async () => { // Run migration in a transaction to avoid partial migration if it fails. - await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken); + // await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken); await DbContext.Database.MigrateAsync(cancellationToken); - await transaction.CommitAsync(cancellationToken); + // await transaction.CommitAsync(cancellationToken); }); } From 90fd720adf03ce3a18e9640fed29283021dd86fd Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:05:19 -0400 Subject: [PATCH 82/87] =?UTF-8?q?=F0=9F=A7=B1=20Run=20all=20DBs=20in=20a?= =?UTF-8?q?=20single=20Server=20in=20AppHost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AppHost/Program.cs | 18 +++++++----------- tools/MigrationService/Worker.cs | 4 ++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs index 39c5e92..e301680 100644 --- a/src/AppHost/Program.cs +++ b/src/AppHost/Program.cs @@ -2,22 +2,18 @@ var builder = DistributedApplication.CreateBuilder(); -// TODO: Figure out how to keep these running after the AppHost shuts down -// TODO: Perhaps we can store the SQL Server in a variable to add multiple DB's to it? -var warehouseDb = builder - .AddSqlServer("warehouse-sql") +var sqlServer = builder.AddSqlServer("sql-server"); + +var warehouseDb = sqlServer .AddDatabase("warehouse"); -var catalogDb = builder - .AddSqlServer("catalog-sql") +var catalogDb = sqlServer .AddDatabase("catalog"); -var customersDb = builder - .AddSqlServer("customers-sql") +var customersDb = sqlServer .AddDatabase("customers"); -var ordersDb = builder - .AddSqlServer("orders-sql") +var ordersDb = sqlServer .AddDatabase("orders"); builder.AddProject("migrations") @@ -36,4 +32,4 @@ builder .Build() - .Run(); + .Run(); \ No newline at end of file diff --git a/tools/MigrationService/Worker.cs b/tools/MigrationService/Worker.cs index 5849110..60a56c2 100644 --- a/tools/MigrationService/Worker.cs +++ b/tools/MigrationService/Worker.cs @@ -19,7 +19,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) try { logger.LogInformation("Waiting for SQL Server to be ready"); - await Task.Delay(30_000, cancellationToken); + await Task.Delay(10_000, cancellationToken); var sw = Stopwatch.StartNew(); using var scope = serviceProvider.CreateScope(); @@ -60,4 +60,4 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) hostApplicationLifetime.StopApplication(); } -} +} \ No newline at end of file From e88cd448edea5c7afa41e5e060ecf8d91cf805af Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:39:23 -0400 Subject: [PATCH 83/87] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Small=20tidy=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ModularMonolith.slnx | 2 +- src/AppHost/Program.cs | 16 ++--- .../CatalogDbContextInitializer.cs | 72 +++++++++---------- .../CustomersDbContextInitializer.cs | 6 +- .../Initializers/DbContextInitializerBase.cs | 1 - .../OrdersDbContextInitializer.cs | 6 +- .../WarehouseDbContextInitializer.cs | 6 +- 7 files changed, 50 insertions(+), 59 deletions(-) diff --git a/ModularMonolith.slnx b/ModularMonolith.slnx index 2b1bc7a..51f1b04 100644 --- a/ModularMonolith.slnx +++ b/ModularMonolith.slnx @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs index e301680..1142d09 100644 --- a/src/AppHost/Program.cs +++ b/src/AppHost/Program.cs @@ -3,18 +3,10 @@ var builder = DistributedApplication.CreateBuilder(); var sqlServer = builder.AddSqlServer("sql-server"); - -var warehouseDb = sqlServer - .AddDatabase("warehouse"); - -var catalogDb = sqlServer - .AddDatabase("catalog"); - -var customersDb = sqlServer - .AddDatabase("customers"); - -var ordersDb = sqlServer - .AddDatabase("orders"); +var warehouseDb = sqlServer.AddDatabase("warehouse"); +var catalogDb = sqlServer.AddDatabase("catalog"); +var customersDb = sqlServer.AddDatabase("customers"); +var ordersDb = sqlServer.AddDatabase("orders"); builder.AddProject("migrations") .WithReference(warehouseDb) diff --git a/tools/MigrationService/Initializers/CatalogDbContextInitializer.cs b/tools/MigrationService/Initializers/CatalogDbContextInitializer.cs index 83d94f9..409e862 100644 --- a/tools/MigrationService/Initializers/CatalogDbContextInitializer.cs +++ b/tools/MigrationService/Initializers/CatalogDbContextInitializer.cs @@ -8,13 +8,13 @@ namespace MigrationService.Initializers; internal class CatalogDbContextInitializer : DbContextInitializerBase { - private const int NumCategories = 10; + private const int NumCategories = 10; - public CatalogDbContextInitializer(CatalogDbContext dbContext) :base(dbContext) - { - } + public CatalogDbContextInitializer(CatalogDbContext dbContext) : base(dbContext) + { + } - public async Task SeedDataAsync(IReadOnlyList products, CancellationToken cancellationToken) + public async Task SeedDataAsync(IReadOnlyList products, CancellationToken cancellationToken) { var strategy = DbContext.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => @@ -29,43 +29,43 @@ await strategy.ExecuteAsync(async () => } private async Task> SeedCategories() - { - if (await DbContext.Categories.AnyAsync()) - return []; + { + if (await DbContext.Categories.AnyAsync()) + return []; - var categoryFaker = new Faker() - .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1).First()!)); + var categoryFaker = new Faker() + .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1).First()!)); - var categories = categoryFaker.Generate(NumCategories); - DbContext.Categories.AddRange(categories); - await DbContext.SaveChangesAsync(); + var categories = categoryFaker.Generate(NumCategories); + DbContext.Categories.AddRange(categories); + await DbContext.SaveChangesAsync(); - return categories; - } + return categories; + } - private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEnumerable categories) - { - if (await DbContext.Products.AnyAsync()) - return; + private async Task SeedProductsAsync(IEnumerable warehouseProducts, IEnumerable categories) + { + if (await DbContext.Products.AnyAsync()) + return; - var categoryFaker = new Faker() - .CustomInstantiator(f => f.PickRandom(categories)); + var categoryFaker = new Faker() + .CustomInstantiator(f => f.PickRandom(categories)); - // Usually integration events would propagate products to the catalog - // However, to simplify test data seed, we'll manually pass products into the catalog - foreach (var warehouseProduct in warehouseProducts) - { - var catalogProduct = Modules.Catalog.Products.Domain.Product.Create( - warehouseProduct.Name, - warehouseProduct.Sku.Value, - warehouseProduct.Id); + // Usually integration events would propagate products to the catalog + // However, to simplify test data seed, we'll manually pass products into the catalog + foreach (var warehouseProduct in warehouseProducts) + { + var catalogProduct = Modules.Catalog.Products.Domain.Product.Create( + warehouseProduct.Name, + warehouseProduct.Sku.Value, + warehouseProduct.Id); - var productCategory = categoryFaker.Generate(); - catalogProduct.AddCategory(productCategory); + var productCategory = categoryFaker.Generate(); + catalogProduct.AddCategory(productCategory); - DbContext.Products.Add(catalogProduct); - } + DbContext.Products.Add(catalogProduct); + } - await DbContext.SaveChangesAsync(); - } -} + await DbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/tools/MigrationService/Initializers/CustomersDbContextInitializer.cs b/tools/MigrationService/Initializers/CustomersDbContextInitializer.cs index 9a24bc7..a4166e8 100644 --- a/tools/MigrationService/Initializers/CustomersDbContextInitializer.cs +++ b/tools/MigrationService/Initializers/CustomersDbContextInitializer.cs @@ -2,9 +2,9 @@ namespace MigrationService.Initializers; -internal class CustomersDbContextInitializer:DbContextInitializerBase +internal class CustomersDbContextInitializer : DbContextInitializerBase { - public CustomersDbContextInitializer(CustomersDbContext dbContext) :base(dbContext) + public CustomersDbContextInitializer(CustomersDbContext dbContext) : base(dbContext) { } @@ -12,4 +12,4 @@ public Task SeedDataAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/tools/MigrationService/Initializers/DbContextInitializerBase.cs b/tools/MigrationService/Initializers/DbContextInitializerBase.cs index e31e276..d0045e6 100644 --- a/tools/MigrationService/Initializers/DbContextInitializerBase.cs +++ b/tools/MigrationService/Initializers/DbContextInitializerBase.cs @@ -41,5 +41,4 @@ await strategy.ExecuteAsync(async () => // await transaction.CommitAsync(cancellationToken); }); } - } \ No newline at end of file diff --git a/tools/MigrationService/Initializers/OrdersDbContextInitializer.cs b/tools/MigrationService/Initializers/OrdersDbContextInitializer.cs index ff86dcc..d605258 100644 --- a/tools/MigrationService/Initializers/OrdersDbContextInitializer.cs +++ b/tools/MigrationService/Initializers/OrdersDbContextInitializer.cs @@ -2,9 +2,9 @@ namespace MigrationService.Initializers; -internal class OrdersDbContextInitializer:DbContextInitializerBase +internal class OrdersDbContextInitializer : DbContextInitializerBase { - public OrdersDbContextInitializer(OrdersDbContext dbContext) :base(dbContext) + public OrdersDbContextInitializer(OrdersDbContext dbContext) : base(dbContext) { } @@ -12,4 +12,4 @@ public Task SeedDataAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs b/tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs index ad1f7b7..3b1b831 100644 --- a/tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs +++ b/tools/MigrationService/Initializers/WarehouseDbContextInitializer.cs @@ -6,14 +6,14 @@ namespace MigrationService.Initializers; -internal class WarehouseDbContextInitializer: DbContextInitializerBase +internal class WarehouseDbContextInitializer : DbContextInitializerBase { private const int NumProducts = 20; private const int NumAisles = 10; private const int NumShelves = 5; private const int NumBays = 20; - public WarehouseDbContextInitializer(WarehouseDbContext dbContext) :base(dbContext) + public WarehouseDbContextInitializer(WarehouseDbContext dbContext) : base(dbContext) { } @@ -67,4 +67,4 @@ private async Task> SeedProductsAsync() return products; } -} +} \ No newline at end of file From da6e4af348cd7513dc206dadbd3c82e6d25058c0 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:02:16 -0400 Subject: [PATCH 84/87] Convert warehouse module to use endpoint discovery --- .../Discovery/EndpointDiscovery.cs | 32 +++++++++++++++++++ .../Discovery/IEndpoint.cs | 8 +++++ .../{ => Discovery}/IModule.cs | 4 +-- .../Products/UseCases/CreateProductCommand.cs | 7 ++-- .../UseCases/AllocateStorageCommand.cs | 7 ++-- .../Storage/UseCases/CreateAisleCommand.cs | 7 ++-- .../Storage/UseCases/GetItemLocationQuery.cs | 6 ++-- .../Modules.Warehouse/WarehouseModule.cs | 29 ++++------------- src/WebApi/Host/ModuleDiscovery.cs | 4 +-- src/WebApi/Program.cs | 3 +- 10 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 src/Common/Common.SharedKernel/Discovery/IEndpoint.cs rename src/Common/Common.SharedKernel/{ => Discovery}/IModule.cs (82%) diff --git a/src/Common/Common.SharedKernel/Discovery/EndpointDiscovery.cs b/src/Common/Common.SharedKernel/Discovery/EndpointDiscovery.cs index e69de29..14a2995 100644 --- a/src/Common/Common.SharedKernel/Discovery/EndpointDiscovery.cs +++ b/src/Common/Common.SharedKernel/Discovery/EndpointDiscovery.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Routing; +using System.Reflection; + +namespace Common.SharedKernel.Discovery; + +public static class EndpointDiscovery +{ + private static readonly Type _endpointType = typeof(IEndpoint); + + public static void DiscoverEndpoints(this IEndpointRouteBuilder builder, params Assembly[] assemblies) + { + if (assemblies.Length == 0) + throw new ArgumentException("At least one assembly must be provided.", nameof(assemblies)); + + var moduleTypes = GetModuleTypes(assemblies); + + foreach (var type in moduleTypes) + { + var obj = Activator.CreateInstance(type); + var method = GetMapEndpointMethod(type); + method?.Invoke(obj, [builder]); + } + } + + private static IEnumerable GetModuleTypes(params Assembly[] assemblies) => + assemblies.SelectMany(x => x.GetTypes()) + .Where(x => _endpointType.IsAssignableFrom(x) && + x is { IsInterface: false, IsAbstract: false }); + + private static MethodInfo? GetMapEndpointMethod(Type type) => + type.GetMethod(nameof(IEndpoint.MapEndpoint)); +} \ No newline at end of file diff --git a/src/Common/Common.SharedKernel/Discovery/IEndpoint.cs b/src/Common/Common.SharedKernel/Discovery/IEndpoint.cs new file mode 100644 index 0000000..a50046a --- /dev/null +++ b/src/Common/Common.SharedKernel/Discovery/IEndpoint.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Routing; + +namespace Common.SharedKernel.Discovery; + +public interface IEndpoint +{ + void MapEndpoint(IEndpointRouteBuilder app); +} \ No newline at end of file diff --git a/src/Common/Common.SharedKernel/IModule.cs b/src/Common/Common.SharedKernel/Discovery/IModule.cs similarity index 82% rename from src/Common/Common.SharedKernel/IModule.cs rename to src/Common/Common.SharedKernel/Discovery/IModule.cs index 9dd9f81..d3c98c3 100644 --- a/src/Common/Common.SharedKernel/IModule.cs +++ b/src/Common/Common.SharedKernel/Discovery/IModule.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace Common.SharedKernel; +namespace Common.SharedKernel.Discovery; public interface IModule { void AddServices(IServiceCollection services, IConfiguration configuration); -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs index 17ba227..28a8dd8 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Products/UseCases/CreateProductCommand.cs @@ -1,5 +1,6 @@ using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -13,9 +14,9 @@ public static class CreateProductCommand { public record Request(string Name, string Sku) : IRequest>; - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("/api/products", async (Request request, ISender sender) => { @@ -60,4 +61,4 @@ public async Task> Handle(Request request, CancellationToken ca return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs index 2d32130..cae185c 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/AllocateStorageCommand.cs @@ -1,4 +1,5 @@ using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -14,9 +15,9 @@ public static class AllocateStorageCommand { public record Request(Guid ProductId) : IRequest>; - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("/api/aisles/allocate-storage", async (Request request, ISender sender) => { @@ -67,4 +68,4 @@ public async Task> Handle(Request request, CancellationToken ca return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs index af186bc..8b32c04 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/CreateAisleCommand.cs @@ -1,4 +1,5 @@ using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -12,9 +13,9 @@ public static class CreateAisleCommand { public record Request(string Name, int NumBays, int NumShelves) : IRequest>; - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("/api/aisles", async (Request request, ISender sender) => { @@ -54,4 +55,4 @@ public async Task> Handle(Request request, CancellationToken ca return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs index 5dca1d2..c329f64 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/Storage/UseCases/GetItemLocationQuery.cs @@ -1,12 +1,12 @@ using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Products.Domain; using Modules.Warehouse.Storage.Domain; namespace Modules.Warehouse.Storage.UseCases; @@ -17,9 +17,9 @@ public record Request(Guid ProductId) : IRequest>; public record Response(string AisleName, string BayName, string ShelfName); - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapGet("/api/aisles/products/{productId:guid}", async (Guid productId, ISender sender) => { diff --git a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs index 02ac3ad..632b6af 100644 --- a/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse/WarehouseModule.cs @@ -1,22 +1,21 @@ -using Common.SharedKernel; +using Common.SharedKernel.Discovery; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Modules.Warehouse.Common.Persistence; -using Modules.Warehouse.Products.UseCases; -using Modules.Warehouse.Storage.UseCases; +using System.Reflection; namespace Modules.Warehouse; public static class WarehouseModule { + private static readonly Assembly _module = typeof(WarehouseModule).Assembly; + public static void AddWarehouse(this IHostApplicationBuilder builder) { - var applicationAssembly = typeof(WarehouseModule).Assembly; - builder.Services.AddHttpContextAccessor(); - builder.Services.AddValidatorsFromAssembly(applicationAssembly); + builder.Services.AddValidatorsFromAssembly(_module); builder.AddPersistence(); } @@ -24,20 +23,6 @@ public static void AddWarehouse(this IHostApplicationBuilder builder) public static void UseWarehouse(this WebApplication app) { app.UseInfrastructureMiddleware(); - - // TODO: Consider source generation or reflection for endpoint mapping - CreateAisleCommand.Endpoint.MapEndpoint(app); - CreateProductCommand.Endpoint.MapEndpoint(app); - AllocateStorageCommand.Endpoint.MapEndpoint(app); - GetItemLocationQuery.Endpoint.MapEndpoint(app); - } -} - -public class WarehouseModule2 : IModule -{ - public void AddServices(IServiceCollection services, IConfiguration configuration) - { - services.AddWarehouse(configuration); + app.DiscoverEndpoints(_module); } - -} +} \ No newline at end of file diff --git a/src/WebApi/Host/ModuleDiscovery.cs b/src/WebApi/Host/ModuleDiscovery.cs index c08fff3..483609d 100644 --- a/src/WebApi/Host/ModuleDiscovery.cs +++ b/src/WebApi/Host/ModuleDiscovery.cs @@ -1,4 +1,4 @@ -using Common.SharedKernel; +using Common.SharedKernel.Discovery; using System.Reflection; namespace Web.Host; @@ -31,4 +31,4 @@ private static IEnumerable GetModuleTypes(params Assembly[] assemblies) => private static MethodInfo? GetAddServicesMethod(Type type) => type.GetMethod(nameof(IModule.AddServices), BindingFlags.Static | BindingFlags.Public); -} +} \ No newline at end of file diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index f82fe6c..ccd0c5f 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -5,7 +5,6 @@ using Modules.Warehouse; using WebApi.Extensions; -var appAssembly = Assembly.GetExecutingAssembly(); var builder = WebApplication.CreateBuilder(args); { // Add service defaults & Aspire components. @@ -45,4 +44,4 @@ app.MapDefaultEndpoints(); app.Run(); -} +} \ No newline at end of file From 6e01aaf9a6aaad6d4404e8ac77effb49bdef1d15 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:06:26 -0400 Subject: [PATCH 85/87] Auto discover orders endpoints --- .../Modules.Orders/Carts/AddProductToCartCommand.cs | 7 ++++--- src/Modules/Orders/Modules.Orders/OrdersModule.cs | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs b/src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs index 4b47894..260d98b 100644 --- a/src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs +++ b/src/Modules/Orders/Modules.Orders/Carts/AddProductToCartCommand.cs @@ -1,6 +1,7 @@ using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using Common.SharedKernel.Domain.Ids; using ErrorOr; using FluentValidation; @@ -21,9 +22,9 @@ public record Request(Guid? CartId, Guid ProductId, int Quantity) : IRequest @@ -97,4 +98,4 @@ public async Task> Handle(Request request, CancellationToken c return new Response(cart.Id.Value); } } -} +} \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders/OrdersModule.cs b/src/Modules/Orders/Modules.Orders/OrdersModule.cs index 8eb8059..407e6f6 100644 --- a/src/Modules/Orders/Modules.Orders/OrdersModule.cs +++ b/src/Modules/Orders/Modules.Orders/OrdersModule.cs @@ -1,8 +1,8 @@ -using FluentValidation; +using Common.SharedKernel.Discovery; +using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; -using Modules.Orders.Carts; using Modules.Orders.Common.Persistence; namespace Modules.Orders; @@ -32,8 +32,8 @@ public static void UseOrders(this WebApplication app) .WithTags("Orders") .WithOpenApi(); - AddProductToCartCommand.Endpoint.MapEndpoint(app); + app.DiscoverEndpoints(typeof(OrdersModule).Assembly); } } -public record OrderDto(string Name, string Description); +public record OrderDto(string Name, string Description); \ No newline at end of file From 6779d7d89997f44c4e6e1d262f7dfad16d442858 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:09:11 -0400 Subject: [PATCH 86/87] Auto discover customer endpoints --- .../Customers/UseCases/CreateProductCommand.cs | 7 ++++--- .../Modules.Customers/CustomersModule.cs | 16 ++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs b/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs index b5d2950..b4d5d9d 100644 --- a/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs +++ b/src/Modules/Customers/Modules.Customers/Customers/UseCases/CreateProductCommand.cs @@ -1,5 +1,6 @@ using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using FluentValidation; using MediatR; @@ -15,9 +16,9 @@ public static class RegisterCustomerCommand { public record Request(string FirstName, string LastName, string Email) : IRequest>; - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("/api/customers", async (Request request, ISender sender) => { @@ -61,4 +62,4 @@ public async Task> Handle(Request request, CancellationToken ca return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Customers/Modules.Customers/CustomersModule.cs b/src/Modules/Customers/Modules.Customers/CustomersModule.cs index daada56..319ce8c 100644 --- a/src/Modules/Customers/Modules.Customers/CustomersModule.cs +++ b/src/Modules/Customers/Modules.Customers/CustomersModule.cs @@ -1,21 +1,22 @@ -using FluentValidation; +using Common.SharedKernel.Discovery; +using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Modules.Customers.Common.Persistence; -using Modules.Customers.Customers.UseCases; +using System.Reflection; namespace Modules.Customers; public static class CustomersModule { + private static readonly Assembly _module = typeof(CustomersModule).Assembly; + public static void AddCustomers(this IHostApplicationBuilder builder) { - var applicationAssembly = typeof(CustomersModule).Assembly; - builder.Services.AddHttpContextAccessor(); - builder.Services.AddValidatorsFromAssembly(applicationAssembly); + builder.Services.AddValidatorsFromAssembly(_module); builder.AddPersistence(); } @@ -24,7 +25,6 @@ public static void UseCustomers(this WebApplication app) { app.UseInfrastructureMiddleware(); - // TODO: Consider source generation or reflection for endpoint mapping - RegisterCustomerCommand.Endpoint.MapEndpoint(app); + app.DiscoverEndpoints(_module); } -} +} \ No newline at end of file From 6863f99d3ca0af744580e18fb90470a76cb9d1f4 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:12:30 -0400 Subject: [PATCH 87/87] Auto discover catalog module endpoints --- .../Products/Modules.Catalog/CatalogModule.cs | 28 ++++++++++--------- .../Categories/CreateCategoryCommand.cs | 7 +++-- .../Products/Integrations/GetProductQuery.cs | 7 +++-- .../UseCases/AddProductCategoryCommand.cs | 7 +++-- .../UseCases/RemoveProductCategoryCommand.cs | 7 +++-- .../Products/UseCases/SearchProductsQuery.cs | 7 +++-- .../UseCases/UpdateProductPriceCommand.cs | 5 ++-- 7 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/Modules/Products/Modules.Catalog/CatalogModule.cs b/src/Modules/Products/Modules.Catalog/CatalogModule.cs index 3af3dbf..72cfeab 100644 --- a/src/Modules/Products/Modules.Catalog/CatalogModule.cs +++ b/src/Modules/Products/Modules.Catalog/CatalogModule.cs @@ -1,23 +1,23 @@ -using FluentValidation; +using Common.SharedKernel.Discovery; +using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Modules.Catalog.Categories; using Modules.Catalog.Common.Persistence; -using Modules.Catalog.Products.Integrations; -using Modules.Catalog.Products.UseCases; +using System.Reflection; namespace Modules.Catalog; public static class CatalogModule { + private static readonly Assembly _module = typeof(CatalogModule).Assembly; + public static void AddCatalog(this IHostApplicationBuilder builder) { - var applicationAssembly = typeof(CatalogModule).Assembly; builder.Services.AddHttpContextAccessor(); - builder.Services.AddValidatorsFromAssembly(applicationAssembly); + builder.Services.AddValidatorsFromAssembly(_module); builder.AddPersistence(); } @@ -26,12 +26,14 @@ public static void UseCatalog(this WebApplication app) { // app.UseInfrastructureMiddleware(); + app.DiscoverEndpoints(_module); + // // TODO: Consider source generation or reflection for endpoint mapping - CreateCategoryCommand.Endpoint.MapEndpoint(app); - AddProductCategoryCommand.Endpoint.MapEndpoint(app); - RemoveProductCategoryCommand.Endpoint.MapEndpoint(app); - GetProductQuery.Endpoint.MapEndpoint(app); - UpdateProductPriceCommand.Endpoint.MapEndpoint(app); - SearchProductsQuery.Endpoint.MapEndpoint(app); + // CreateCategoryCommand.Endpoint.MapEndpoint(app); + // AddProductCategoryCommand.Endpoint.MapEndpoint(app); + // RemoveProductCategoryCommand.Endpoint.MapEndpoint(app); + // GetProductQuery.Endpoint.MapEndpoint(app); + // UpdateProductPriceCommand.Endpoint.MapEndpoint(app); + // SearchProductsQuery.Endpoint.MapEndpoint(app); } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Categories/CreateCategoryCommand.cs b/src/Modules/Products/Modules.Catalog/Categories/CreateCategoryCommand.cs index e40c72d..953a5bc 100644 --- a/src/Modules/Products/Modules.Catalog/Categories/CreateCategoryCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Categories/CreateCategoryCommand.cs @@ -1,5 +1,6 @@ using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using FluentValidation; using MediatR; @@ -15,9 +16,9 @@ public static class CreateCategoryCommand { public record Request(string Name) : IRequest>; - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("/api/categories", async (Request request, ISender sender) => { @@ -62,4 +63,4 @@ public async Task> Handle(Request request, CancellationToken ca return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/Integrations/GetProductQuery.cs b/src/Modules/Products/Modules.Catalog/Products/Integrations/GetProductQuery.cs index b6c625b..f882838 100644 --- a/src/Modules/Products/Modules.Catalog/Products/Integrations/GetProductQuery.cs +++ b/src/Modules/Products/Modules.Catalog/Products/Integrations/GetProductQuery.cs @@ -1,6 +1,7 @@ using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using MediatR; using Microsoft.AspNetCore.Builder; @@ -22,9 +23,9 @@ public static class GetProductQuery // // public record CategoryDto(Guid Id, string Name); - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapGet("/api/products/{productId:guid}", async (Guid productId, ISender sender) => @@ -64,4 +65,4 @@ public async Task> Handle(Request request, CancellationToken c return product; } } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs index 2963939..b14cbdd 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/AddProductCategoryCommand.cs @@ -1,6 +1,7 @@ using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using FluentValidation; using MediatR; @@ -18,9 +19,9 @@ public static class AddProductCategoryCommand { public record Request(Guid ProductId, Guid CategoryId) : IRequest>; - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("/api/products/{productId:guid}/categories/{categoryId:guid}", async (Guid productId, Guid categoryId, ISender sender) => @@ -81,4 +82,4 @@ await _dbContext.Categories.FirstOrDefaultAsync(c => c.Id == categoryId, return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs index a344d0d..28d3140 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/RemoveProductCategoryCommand.cs @@ -1,6 +1,7 @@ using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using FluentValidation; using MediatR; @@ -18,9 +19,9 @@ public static class RemoveProductCategoryCommand { public record Request(Guid ProductId, Guid CategoryId) : IRequest>; - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapDelete("/api/products/{productId:guid}/categories/{categoryId:guid}", async (Guid productId, Guid categoryId, ISender sender) => @@ -81,4 +82,4 @@ await _dbContext.Categories.FirstOrDefaultAsync(c => c.Id == categoryId, return Result.Success; } } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs index ad53fd3..e17c988 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/SearchProductsQuery.cs @@ -1,4 +1,5 @@ using Common.SharedKernel; +using Common.SharedKernel.Discovery; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -16,9 +17,9 @@ public record Request(string? Name, Guid? CategoryId) : IRequest @@ -63,4 +64,4 @@ public async Task> Handle(Request request, CancellationT return products; } } -} +} \ No newline at end of file diff --git a/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs index 24c955a..0f7e335 100644 --- a/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs +++ b/src/Modules/Products/Modules.Catalog/Products/UseCases/UpdateProductPriceCommand.cs @@ -1,6 +1,7 @@ using Ardalis.Specification.EntityFrameworkCore; using Common.SharedKernel; using Common.SharedKernel.Api; +using Common.SharedKernel.Discovery; using ErrorOr; using FluentValidation; using MediatR; @@ -22,9 +23,9 @@ public record Request(decimal Price) : IRequest> public Guid ProductId { get; set; } } - public static class Endpoint + public class Endpoint : IEndpoint { - public static void MapEndpoint(IEndpointRouteBuilder app) + public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPut("/api/products/{productId:guid}/price", async (Guid productId, Request request, ISender sender) =>