From 9de89218265f5cce0053fd7c704c9dade8f70045 Mon Sep 17 00:00:00 2001 From: Vladimir Konovalenko Date: Tue, 21 Dec 2021 20:05:34 +0300 Subject: [PATCH] Initial commit. --- .gitignore | 42 + README.md | 157 +- ReadmeFiles/.gitkeep | 0 ReadmeFiles/Microservice Diagram.png | Bin 0 -> 39388 bytes ReadmeFiles/Saga Workflow Diagram.png | Bin 0 -> 249718 bytes ReadmeFiles/saga_workflow.png | Bin 0 -> 110226 bytes build.sh | 14 + configs/grafana/dashboards.yml | 10 + configs/grafana/dashboards/masstransit.json | 1653 +++++++++++++++++ configs/grafana/datasources.yml | 44 + configs/prometheus.yml | 27 + docker-compose.yml | 123 ++ scripts/buildservice.sh | 5 + scripts/wait-for-it.sh | 182 ++ specs/saga_workflow.def | 21 + src/ApiService/ApiService.csproj | 27 + .../Configurations/RabbitMqConfiguration.cs | 9 + .../Consumers/FeedbackRequestedConsumer.cs | 14 + .../FeedbackRequestedConsumerDefinition.cs | 22 + .../GetArchivedOrderResponseConsumer.cs | 25 + ...ArchivedOrderResponseConsumerDefinition.cs | 22 + .../NewOrderConfirmationRequestedConsumer.cs | 23 + ...ConfirmationRequestedConsumerDefinition.cs | 22 + .../Consumers/OrderRejectedConsumer.cs | 23 + .../OrderRejectedConsumerDefinition.cs | 22 + .../Controllers/OrdersController.cs | 258 +++ src/ApiService/Dockerfile | 8 + .../Implementations/RoutingConfiguration.cs | 11 + .../Interfaces/IRoutingConfiguration.cs | 8 + src/ApiService/Program.cs | 20 + src/ApiService/Properties/launchSettings.json | 31 + src/ApiService/Startup.cs | 113 ++ src/ApiService/appsettings.Development.json | 21 + src/ApiService/appsettings.json | 21 + src/CartService/CartService.csproj | 32 + .../Configurations/EndpointsConfiguration.cs | 13 + .../Configurations/RabbitMqConfiguration.cs | 16 + .../Consumers/AddCartPositionConsumer.cs | 68 + .../AddCartPositionConsumerDefinition.cs | 27 + src/CartService/Consumers/GetCartConsumer.cs | 77 + .../Consumers/GetCartConsumerDefinition.cs | 27 + .../Consumers/RemoveCartPositionConsumer.cs | 53 + .../RemoveCartPositionConsumerDefinition.cs | 27 + .../Configurations/CartConfiguration.cs | 24 + .../CartPositionConfiguration.cs | 26 + .../Configurations/GoodConfiguration.cs | 23 + src/CartService/Database/Models/Cart.cs | 20 + .../Database/Models/CartPosition.cs | 34 + src/CartService/Database/Models/Good.cs | 26 + src/CartService/Database/NpgSqlContext.cs | 35 + .../Repositories/CartPositionRepository.cs | 65 + .../Database/Repositories/CartRepository.cs | 51 + .../Database/Repositories/GoodRepository.cs | 51 + .../Interfaces/ICartPositionRepository.cs | 25 + .../Interfaces/ICartRepository.cs | 18 + .../Interfaces/IGoodRepository.cs | 20 + src/CartService/Dockerfile | 6 + .../20211028164439_Initial.Designer.cs | 112 ++ .../Migrations/20211028164439_Initial.cs | 101 + .../Migrations/NpgSqlContextModelSnapshot.cs | 110 ++ src/CartService/Program.cs | 88 + .../Properties/launchSettings.json | 11 + src/CartService/appsettings.Development.json | 23 + src/CartService/appsettings.json | 23 + .../ApiService.Contracts.csproj | 15 + .../ManagerApi/ConfirmOrder.cs | 11 + .../ManagerApi/GetArchivedOrder.cs | 9 + .../ManagerApi/GetArchivedOrderResponse.cs | 28 + .../NewOrderConfirmationRequested.cs | 16 + .../ManagerApi/RejectOrder.cs | 13 + .../MonitoringApi/GetAllOrdersState.cs | 7 + .../GetAllOrdersStateResponse.cs | 10 + .../MonitoringApi/GetOrderState.cs | 9 + .../MonitoringApi/GetOrderStateResponse.cs | 10 + .../UserApi/AbortOrder.cs | 9 + .../UserApi/FeedbackReceived.cs | 13 + .../UserApi/FeedbackRequested.cs | 9 + .../UserApi/OrderAborted.cs | 9 + .../UserApi/OrderRejected.cs | 10 + .../UserApi/OrderSubmitted.cs | 12 + .../UserApi/SendFeedback.cs | 13 + .../CartService.Contracts/AddCartPosition.cs | 13 + .../CartService.Contracts.csproj | 15 + .../CartService.Contracts/GetCart.cs | 9 + .../CartService.Contracts/GetCartResponse.cs | 15 + .../RemoveCartPosition.cs | 13 + .../Contracts.Shared/CartPosition.cs | 11 + .../Contracts.Shared/Contracts.Shared.csproj | 7 + .../DeliveryOrder.cs | 13 + .../DeliveryService.Contracts.csproj | 16 + .../OrderDelivered.cs | 9 + .../FeedbackService.Contracts/AddFeedback.cs | 13 + .../FeedbackAdded.cs | 9 + .../FeedbackService.Contracts.csproj | 11 + .../GetOrderFeedback.cs | 9 + .../GetOrderFeedbackResponse.cs | 12 + .../HistoryService.Contracts/ArchiveOrder.cs | 25 + .../GetOrderFromArchive.cs | 9 + .../GetOrderFromArchiveResponse.cs | 19 + .../HistoryService.Contracts.csproj | 15 + .../HistoryService.Contracts/OrderAdded.cs | 9 + .../ErrorReservingMoney.cs | 11 + .../PaymentService.Contracts/MoneyReserved.cs | 10 + .../MoneyUnreserved.cs | 10 + .../PaymentService.Contracts.csproj | 11 + .../PaymentService.Contracts/ReserveMoney.cs | 11 + .../UnreserveMoney.cs | 11 + .../Configurations/EndpointsConfiguration.cs | 8 + .../Configurations/RabbitMqConfiguration.cs | 10 + .../Consumers/DeliveryOrderConsumer.cs | 31 + .../DeliveryOrderConsumerDefinition.cs | 27 + src/DeliveryService/DeliveryService.csproj | 37 + src/DeliveryService/Dockerfile | 6 + src/DeliveryService/Program.cs | 56 + .../Properties/launchSettings.json | 11 + .../appsettings.Development.json | 19 + src/DeliveryService/appsettings.json | 19 + .../Configurations/EndpointsConfiguration.cs | 13 + .../Configurations/RabbitMqConfiguration.cs | 16 + .../Consumers/AddFeedbackConsumer.cs | 45 + .../AddFeedbackConsumerDefinition.cs | 27 + .../Consumers/GetOrderFeedbackConsumer.cs | 33 + .../GetOrderFeedbackConsumerDefinition.cs | 27 + .../Database/Models/Feedback.cs | 24 + src/FeedbackService/Database/NpgSqlContext.cs | 26 + .../Repositories/FeedbackRepository.cs | 50 + .../Interfaces/IFeedbackRepository.cs | 19 + src/FeedbackService/Dockerfile | 6 + src/FeedbackService/FeedbackService.csproj | 30 + .../20211031093642_Initial.Designer.cs | 43 + .../Migrations/20211031093642_Initial.cs | 30 + .../Migrations/NpgSqlContextModelSnapshot.cs | 41 + src/FeedbackService/Program.cs | 82 + .../Properties/launchSettings.json | 11 + .../appsettings.Development.json | 25 + src/FeedbackService/appsettings.json | 25 + .../Configurations/EndpointsConfiguration.cs | 13 + .../Configurations/RabbitMqConfiguration.cs | 16 + .../Consumers/ArchivedOrderConsumer.cs | 44 + .../ArchivedOrderConsumerDefinition.cs | 22 + .../Consumers/GetOrderFromArchiveConsumer.cs | 38 + .../GetOrderFromArchiveConsumerDefinition.cs | 27 + .../Database/Models/ArchivedOrder.cs | 34 + src/HistoryService/Database/NpgSqlContext.cs | 21 + .../Repositories/ArchivedOrderRepository.cs | 42 + .../Interfaces/IArchivedOrderRepository.cs | 19 + src/HistoryService/Dockerfile | 6 + src/HistoryService/HistoryService.csproj | 31 + .../20211031111914_Initial.Designer.cs | 55 + .../Migrations/20211031111914_Initial.cs | 34 + ...211102100836_RemovedTotalPrice.Designer.cs | 52 + .../20211102100836_RemovedTotalPrice.cs | 24 + ...256_ChangedArchievedToArchived.Designer.cs | 52 + ...211111144256_ChangedArchievedToArchived.cs | 52 + .../Migrations/NpgSqlContextModelSnapshot.cs | 50 + src/HistoryService/Program.cs | 84 + .../Properties/launchSettings.json | 11 + .../appsettings.Development.json | 25 + src/HistoryService/appsettings.json | 25 + src/MassTransitAdvancedExample.sln | 130 ++ .../Configurations/EndpointsConfiguration.cs | 14 + .../Configurations/RabbitMqConfiguration.cs | 10 + .../Consumers/GetAllOrdersStateConsumer.cs | 31 + .../GetAllOrdersStateConsumerDefinition.cs | 22 + .../Consumers/GetArchivedOrderConsumer.cs | 64 + .../GetArchivedOrderConsumerDefinition.cs | 22 + .../Consumers/GetOrderStateConsumer.cs | 33 + .../GetOrderStateConsumerDefinition.cs | 22 + .../CartPositionConfiguration.cs | 23 + .../Database/Configurations/OrderStateMap.cs | 25 + .../FromDtoCartPositionToDbConverter.cs | 37 + .../Database/Models/CartPosition.cs | 33 + .../Database/StateMachinesDbContext.cs | 36 + src/OrderOrchestratorService/Dockerfile | 6 + .../FeedbackReceivingTimeoutExpired.cs | 9 + .../20211105110239_Initial.Designer.cs | 109 ++ .../Migrations/20211105110239_Initial.cs | 77 + ..._FeedbackReceivingTimeoutToken.Designer.cs | 112 ++ ...608_Added_FeedbackReceivingTimeoutToken.cs | 24 + .../StateMachinesDbContextModelSnapshot.cs | 110 ++ .../OrderOrchestratorService.csproj | 34 + src/OrderOrchestratorService/Program.cs | 106 ++ .../Properties/launchSettings.json | 11 + .../ArchivedOrderState.cs | 35 + .../ArchivedOrderStateMachine.cs | 189 ++ .../ArchivedOrderStateMachineDefinition.cs | 19 + .../OrderStateMachine/OrderState.cs | 33 + .../OrderStateMachine/OrderStateMachine.cs | 346 ++++ .../OrderStateMachineDefinition.cs | 22 + .../appsettings.Development.json | 32 + src/OrderOrchestratorService/appsettings.json | 32 + .../Configurations/EndpointsConfiguration.cs | 7 + .../Configurations/RabbitMqConfiguration.cs | 10 + .../Consumers/ReserveMoneyConsumer.cs | 47 + .../ReserveMoneyConsumerDefinition.cs | 23 + .../Consumers/UnreserveMoneyConsumer.cs | 34 + src/PaymentService/Dockerfile | 6 + src/PaymentService/PaymentService.csproj | 34 + src/PaymentService/Program.cs | 58 + .../Properties/launchSettings.json | 11 + .../appsettings.Development.json | 18 + src/PaymentService/appsettings.json | 18 + .../ConsumerStubs/ArchiveOrderConsumer.cs | 21 + .../ConsumerStubs/GetCartConsumer.cs | 27 + .../ConsumerStubs/GetOrderFeedbackConsumer.cs | 19 + .../GetOrderFromArchiveConsumer.cs | 23 + .../ConsumerStubs/ReserveMoneyConsumer.cs | 35 + .../GetArchivedOrderConsumerTests.cs | 73 + .../OrderOrchestratorService.Tests.csproj | 33 + .../StateMachines/OrderStateMachineTests.cs | 247 +++ .../StateMachines/StateMachineTestFixture.cs | 149 ++ .../PaymentService.Tests.csproj | 29 + .../ReserveMoneyConsumerTests.cs | 103 + 213 files changed, 9134 insertions(+), 2 deletions(-) create mode 100644 ReadmeFiles/.gitkeep create mode 100644 ReadmeFiles/Microservice Diagram.png create mode 100644 ReadmeFiles/Saga Workflow Diagram.png create mode 100644 ReadmeFiles/saga_workflow.png create mode 100644 build.sh create mode 100644 configs/grafana/dashboards.yml create mode 100644 configs/grafana/dashboards/masstransit.json create mode 100644 configs/grafana/datasources.yml create mode 100644 configs/prometheus.yml create mode 100644 docker-compose.yml create mode 100644 scripts/buildservice.sh create mode 100644 scripts/wait-for-it.sh create mode 100644 specs/saga_workflow.def create mode 100644 src/ApiService/ApiService.csproj create mode 100644 src/ApiService/Configurations/RabbitMqConfiguration.cs create mode 100644 src/ApiService/Consumers/FeedbackRequestedConsumer.cs create mode 100644 src/ApiService/Consumers/FeedbackRequestedConsumerDefinition.cs create mode 100644 src/ApiService/Consumers/GetArchivedOrderResponseConsumer.cs create mode 100644 src/ApiService/Consumers/GetArchivedOrderResponseConsumerDefinition.cs create mode 100644 src/ApiService/Consumers/NewOrderConfirmationRequestedConsumer.cs create mode 100644 src/ApiService/Consumers/NewOrderConfirmationRequestedConsumerDefinition.cs create mode 100644 src/ApiService/Consumers/OrderRejectedConsumer.cs create mode 100644 src/ApiService/Consumers/OrderRejectedConsumerDefinition.cs create mode 100644 src/ApiService/Controllers/OrdersController.cs create mode 100644 src/ApiService/Dockerfile create mode 100644 src/ApiService/Models/Implementations/RoutingConfiguration.cs create mode 100644 src/ApiService/Models/Interfaces/IRoutingConfiguration.cs create mode 100644 src/ApiService/Program.cs create mode 100644 src/ApiService/Properties/launchSettings.json create mode 100644 src/ApiService/Startup.cs create mode 100644 src/ApiService/appsettings.Development.json create mode 100644 src/ApiService/appsettings.json create mode 100644 src/CartService/CartService.csproj create mode 100644 src/CartService/Configurations/EndpointsConfiguration.cs create mode 100644 src/CartService/Configurations/RabbitMqConfiguration.cs create mode 100644 src/CartService/Consumers/AddCartPositionConsumer.cs create mode 100644 src/CartService/Consumers/AddCartPositionConsumerDefinition.cs create mode 100644 src/CartService/Consumers/GetCartConsumer.cs create mode 100644 src/CartService/Consumers/GetCartConsumerDefinition.cs create mode 100644 src/CartService/Consumers/RemoveCartPositionConsumer.cs create mode 100644 src/CartService/Consumers/RemoveCartPositionConsumerDefinition.cs create mode 100644 src/CartService/Database/Configurations/CartConfiguration.cs create mode 100644 src/CartService/Database/Configurations/CartPositionConfiguration.cs create mode 100644 src/CartService/Database/Configurations/GoodConfiguration.cs create mode 100644 src/CartService/Database/Models/Cart.cs create mode 100644 src/CartService/Database/Models/CartPosition.cs create mode 100644 src/CartService/Database/Models/Good.cs create mode 100644 src/CartService/Database/NpgSqlContext.cs create mode 100644 src/CartService/Database/Repositories/CartPositionRepository.cs create mode 100644 src/CartService/Database/Repositories/CartRepository.cs create mode 100644 src/CartService/Database/Repositories/GoodRepository.cs create mode 100644 src/CartService/Database/Repositories/Interfaces/ICartPositionRepository.cs create mode 100644 src/CartService/Database/Repositories/Interfaces/ICartRepository.cs create mode 100644 src/CartService/Database/Repositories/Interfaces/IGoodRepository.cs create mode 100644 src/CartService/Dockerfile create mode 100644 src/CartService/Migrations/20211028164439_Initial.Designer.cs create mode 100644 src/CartService/Migrations/20211028164439_Initial.cs create mode 100644 src/CartService/Migrations/NpgSqlContextModelSnapshot.cs create mode 100644 src/CartService/Program.cs create mode 100644 src/CartService/Properties/launchSettings.json create mode 100644 src/CartService/appsettings.Development.json create mode 100644 src/CartService/appsettings.json create mode 100644 src/Contracts/ApiService.Contracts/ApiService.Contracts.csproj create mode 100644 src/Contracts/ApiService.Contracts/ManagerApi/ConfirmOrder.cs create mode 100644 src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrder.cs create mode 100644 src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrderResponse.cs create mode 100644 src/Contracts/ApiService.Contracts/ManagerApi/NewOrderConfirmationRequested.cs create mode 100644 src/Contracts/ApiService.Contracts/ManagerApi/RejectOrder.cs create mode 100644 src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersState.cs create mode 100644 src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersStateResponse.cs create mode 100644 src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderState.cs create mode 100644 src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderStateResponse.cs create mode 100644 src/Contracts/ApiService.Contracts/UserApi/AbortOrder.cs create mode 100644 src/Contracts/ApiService.Contracts/UserApi/FeedbackReceived.cs create mode 100644 src/Contracts/ApiService.Contracts/UserApi/FeedbackRequested.cs create mode 100644 src/Contracts/ApiService.Contracts/UserApi/OrderAborted.cs create mode 100644 src/Contracts/ApiService.Contracts/UserApi/OrderRejected.cs create mode 100644 src/Contracts/ApiService.Contracts/UserApi/OrderSubmitted.cs create mode 100644 src/Contracts/ApiService.Contracts/UserApi/SendFeedback.cs create mode 100644 src/Contracts/CartService.Contracts/AddCartPosition.cs create mode 100644 src/Contracts/CartService.Contracts/CartService.Contracts.csproj create mode 100644 src/Contracts/CartService.Contracts/GetCart.cs create mode 100644 src/Contracts/CartService.Contracts/GetCartResponse.cs create mode 100644 src/Contracts/CartService.Contracts/RemoveCartPosition.cs create mode 100644 src/Contracts/Contracts.Shared/CartPosition.cs create mode 100644 src/Contracts/Contracts.Shared/Contracts.Shared.csproj create mode 100644 src/Contracts/DeliveryService.Contracts/DeliveryOrder.cs create mode 100644 src/Contracts/DeliveryService.Contracts/DeliveryService.Contracts.csproj create mode 100644 src/Contracts/DeliveryService.Contracts/OrderDelivered.cs create mode 100644 src/Contracts/FeedbackService.Contracts/AddFeedback.cs create mode 100644 src/Contracts/FeedbackService.Contracts/FeedbackAdded.cs create mode 100644 src/Contracts/FeedbackService.Contracts/FeedbackService.Contracts.csproj create mode 100644 src/Contracts/FeedbackService.Contracts/GetOrderFeedback.cs create mode 100644 src/Contracts/FeedbackService.Contracts/GetOrderFeedbackResponse.cs create mode 100644 src/Contracts/HistoryService.Contracts/ArchiveOrder.cs create mode 100644 src/Contracts/HistoryService.Contracts/GetOrderFromArchive.cs create mode 100644 src/Contracts/HistoryService.Contracts/GetOrderFromArchiveResponse.cs create mode 100644 src/Contracts/HistoryService.Contracts/HistoryService.Contracts.csproj create mode 100644 src/Contracts/HistoryService.Contracts/OrderAdded.cs create mode 100644 src/Contracts/PaymentService.Contracts/ErrorReservingMoney.cs create mode 100644 src/Contracts/PaymentService.Contracts/MoneyReserved.cs create mode 100644 src/Contracts/PaymentService.Contracts/MoneyUnreserved.cs create mode 100644 src/Contracts/PaymentService.Contracts/PaymentService.Contracts.csproj create mode 100644 src/Contracts/PaymentService.Contracts/ReserveMoney.cs create mode 100644 src/Contracts/PaymentService.Contracts/UnreserveMoney.cs create mode 100644 src/DeliveryService/Configurations/EndpointsConfiguration.cs create mode 100644 src/DeliveryService/Configurations/RabbitMqConfiguration.cs create mode 100644 src/DeliveryService/Consumers/DeliveryOrderConsumer.cs create mode 100644 src/DeliveryService/Consumers/DeliveryOrderConsumerDefinition.cs create mode 100644 src/DeliveryService/DeliveryService.csproj create mode 100644 src/DeliveryService/Dockerfile create mode 100644 src/DeliveryService/Program.cs create mode 100644 src/DeliveryService/Properties/launchSettings.json create mode 100644 src/DeliveryService/appsettings.Development.json create mode 100644 src/DeliveryService/appsettings.json create mode 100644 src/FeedbackService/Configurations/EndpointsConfiguration.cs create mode 100644 src/FeedbackService/Configurations/RabbitMqConfiguration.cs create mode 100644 src/FeedbackService/Consumers/AddFeedbackConsumer.cs create mode 100644 src/FeedbackService/Consumers/AddFeedbackConsumerDefinition.cs create mode 100644 src/FeedbackService/Consumers/GetOrderFeedbackConsumer.cs create mode 100644 src/FeedbackService/Consumers/GetOrderFeedbackConsumerDefinition.cs create mode 100644 src/FeedbackService/Database/Models/Feedback.cs create mode 100644 src/FeedbackService/Database/NpgSqlContext.cs create mode 100644 src/FeedbackService/Database/Repositories/FeedbackRepository.cs create mode 100644 src/FeedbackService/Database/Repositories/Interfaces/IFeedbackRepository.cs create mode 100644 src/FeedbackService/Dockerfile create mode 100644 src/FeedbackService/FeedbackService.csproj create mode 100644 src/FeedbackService/Migrations/20211031093642_Initial.Designer.cs create mode 100644 src/FeedbackService/Migrations/20211031093642_Initial.cs create mode 100644 src/FeedbackService/Migrations/NpgSqlContextModelSnapshot.cs create mode 100644 src/FeedbackService/Program.cs create mode 100644 src/FeedbackService/Properties/launchSettings.json create mode 100644 src/FeedbackService/appsettings.Development.json create mode 100644 src/FeedbackService/appsettings.json create mode 100644 src/HistoryService/Configurations/EndpointsConfiguration.cs create mode 100644 src/HistoryService/Configurations/RabbitMqConfiguration.cs create mode 100644 src/HistoryService/Consumers/ArchivedOrderConsumer.cs create mode 100644 src/HistoryService/Consumers/ArchivedOrderConsumerDefinition.cs create mode 100644 src/HistoryService/Consumers/GetOrderFromArchiveConsumer.cs create mode 100644 src/HistoryService/Consumers/GetOrderFromArchiveConsumerDefinition.cs create mode 100644 src/HistoryService/Database/Models/ArchivedOrder.cs create mode 100644 src/HistoryService/Database/NpgSqlContext.cs create mode 100644 src/HistoryService/Database/Repositories/ArchivedOrderRepository.cs create mode 100644 src/HistoryService/Database/Repositories/Interfaces/IArchivedOrderRepository.cs create mode 100644 src/HistoryService/Dockerfile create mode 100644 src/HistoryService/HistoryService.csproj create mode 100644 src/HistoryService/Migrations/20211031111914_Initial.Designer.cs create mode 100644 src/HistoryService/Migrations/20211031111914_Initial.cs create mode 100644 src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.Designer.cs create mode 100644 src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.cs create mode 100644 src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.Designer.cs create mode 100644 src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.cs create mode 100644 src/HistoryService/Migrations/NpgSqlContextModelSnapshot.cs create mode 100644 src/HistoryService/Program.cs create mode 100644 src/HistoryService/Properties/launchSettings.json create mode 100644 src/HistoryService/appsettings.Development.json create mode 100644 src/HistoryService/appsettings.json create mode 100644 src/MassTransitAdvancedExample.sln create mode 100644 src/OrderOrchestratorService/Configurations/EndpointsConfiguration.cs create mode 100644 src/OrderOrchestratorService/Configurations/RabbitMqConfiguration.cs create mode 100644 src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumer.cs create mode 100644 src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumerDefinition.cs create mode 100644 src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumer.cs create mode 100644 src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumerDefinition.cs create mode 100644 src/OrderOrchestratorService/Consumers/GetOrderStateConsumer.cs create mode 100644 src/OrderOrchestratorService/Consumers/GetOrderStateConsumerDefinition.cs create mode 100644 src/OrderOrchestratorService/Database/Configurations/CartPositionConfiguration.cs create mode 100644 src/OrderOrchestratorService/Database/Configurations/OrderStateMap.cs create mode 100644 src/OrderOrchestratorService/Database/Converters/FromDtoCartPositionToDbConverter.cs create mode 100644 src/OrderOrchestratorService/Database/Models/CartPosition.cs create mode 100644 src/OrderOrchestratorService/Database/StateMachinesDbContext.cs create mode 100644 src/OrderOrchestratorService/Dockerfile create mode 100644 src/OrderOrchestratorService/InternalContracts/FeedbackReceivingTimeoutExpired.cs create mode 100644 src/OrderOrchestratorService/Migrations/20211105110239_Initial.Designer.cs create mode 100644 src/OrderOrchestratorService/Migrations/20211105110239_Initial.cs create mode 100644 src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.Designer.cs create mode 100644 src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.cs create mode 100644 src/OrderOrchestratorService/Migrations/StateMachinesDbContextModelSnapshot.cs create mode 100644 src/OrderOrchestratorService/OrderOrchestratorService.csproj create mode 100644 src/OrderOrchestratorService/Program.cs create mode 100644 src/OrderOrchestratorService/Properties/launchSettings.json create mode 100644 src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderState.cs create mode 100644 src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachine.cs create mode 100644 src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachineDefinition.cs create mode 100644 src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderState.cs create mode 100644 src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachine.cs create mode 100644 src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachineDefinition.cs create mode 100644 src/OrderOrchestratorService/appsettings.Development.json create mode 100644 src/OrderOrchestratorService/appsettings.json create mode 100644 src/PaymentService/Configurations/EndpointsConfiguration.cs create mode 100644 src/PaymentService/Configurations/RabbitMqConfiguration.cs create mode 100644 src/PaymentService/Consumers/ReserveMoneyConsumer.cs create mode 100644 src/PaymentService/Consumers/ReserveMoneyConsumerDefinition.cs create mode 100644 src/PaymentService/Consumers/UnreserveMoneyConsumer.cs create mode 100644 src/PaymentService/Dockerfile create mode 100644 src/PaymentService/PaymentService.csproj create mode 100644 src/PaymentService/Program.cs create mode 100644 src/PaymentService/Properties/launchSettings.json create mode 100644 src/PaymentService/appsettings.Development.json create mode 100644 src/PaymentService/appsettings.json create mode 100644 src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ArchiveOrderConsumer.cs create mode 100644 src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetCartConsumer.cs create mode 100644 src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFeedbackConsumer.cs create mode 100644 src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFromArchiveConsumer.cs create mode 100644 src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ReserveMoneyConsumer.cs create mode 100644 src/Tests/OrderOrchestratorService.Tests/GetArchivedOrderConsumerTests.cs create mode 100644 src/Tests/OrderOrchestratorService.Tests/OrderOrchestratorService.Tests.csproj create mode 100644 src/Tests/OrderOrchestratorService.Tests/StateMachines/OrderStateMachineTests.cs create mode 100644 src/Tests/OrderOrchestratorService.Tests/StateMachines/StateMachineTestFixture.cs create mode 100644 src/Tests/PaymentService.Tests/PaymentService.Tests.csproj create mode 100644 src/Tests/PaymentService.Tests/ReserveMoneyConsumerTests.cs diff --git a/.gitignore b/.gitignore index dfcfd56..62ab883 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ mono_crash.* [Rr]eleases/ x64/ x86/ +[Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ @@ -61,6 +62,9 @@ project.lock.json project.fragment.lock.json artifacts/ +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + # StyleCop StyleCopReport.xml @@ -86,6 +90,7 @@ StyleCopReport.xml *.tmp_proj *_wpftmp.csproj *.log +*.tlog *.vspscc *.vssscc .builds @@ -137,6 +142,11 @@ _TeamCity* .axoCover/* !.axoCover/settings.json +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + # Visual Studio code coverage results *.coverage *.coveragexml @@ -196,6 +206,9 @@ PublishScripts/ *.nuget.props *.nuget.targets +# Nuget personal access tokens and Credentials +# nuget.config + # Microsoft Azure Build Output csx/ *.build.csdef @@ -348,3 +361,32 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml + +# Project data +data/ +appsettings/ \ No newline at end of file diff --git a/README.md b/README.md index a1caa0b..480c857 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,155 @@ -# masstransit-demo -Demo of using MassTransit for distibuted applications creation. +# MassTransit Advanced Example + +## **Техническое описание** + +Данный проект представляет собой демо-версию микросервисной архитектуры с оркестрацией и ипользованием service bus. В качестве реализации service bus был взят фреймворк MassTransit. + +## *Суть проекта* + +Приложение представляет собой сервер для обработки клиентских заказов. Обработка заказаов состоит из следующих действий: ++ Подтверждение заказа ++ Резервация денег на карте клиента ++ Подтверждение/отклонение заказа менеджером ++ Доставка заказа ++ Получение обратной связи от клиента ++ Архивация заказа(записать всю сущность в базу данных) + + +## *Допущения* + ++ После подтверждения заказа пользователь не может сделать новый заказ или изменить текущую корзину, пока предыдущий не пройдет все стадии(его сага должна завершиться) + +## **Схема микросервисов** + +![Microservice Diagram](/ReadmeFiles/Microservice Diagram.png) + + +## **Миросервисы** + +### API service + +#### *Бизнес логика* ++ API для пользователя и менеджера ++ Консьюмеры, которые обрабатывают события о заказе (в данном проекте эти события просто выводятся на консоль, но в реальном проекте скорее всего будет отправляться уведомления по веб-сокетам) + +#### *Consumers* ++ NewOrderConfirmationRequestedConsumer: обрабатывает сообщения о том, что кто-то из менеджеров должен подтвердить заказ ++ OrderRejectedConsumer: обрабатывает сообщения о том, что заказ был отклонен менеджером ++ FeedbackRequestedConsumer: обрабатывает сообщения о том, что был запрос на обратную связь от клиента ++ GetArchievedOrderResponseConsumer: обрабатывает сообщения о том, что запрос на получение архированного заказа был обработан и пришло сообщение с данными о заказе + +### Cart service + +#### *Бизнес логика* ++ Добавления/удаления товаров из корзины (данные хранятся в бд) ++ Обработка запроса на получение актуальной корзины + +#### *Особенности* ++ Если в базе уже содержится добавляемый товар, то цена товара берется из базы. Если подобного товара в базе нет, то цена товара рандомится(чобы не услонять API) + +#### *Consumers* ++ AddCartPositionConsumer: добавляет товар к заказу ++ RemoveCartPositionConsumer: убирает товар из заказа ++ GetCartConsumer: возвращает запрашиваемую корзину + +### Delivery service + +#### *Бизнес логика* + ++ Имитация пролонгированной во времени операции - доставки заказа + +#### *Consumers* + ++ DeliveryOrderConsumer: имитирует доставку заказа + +### Feedback service + +#### *Бизнес логика* ++ Добавление отзывов пользователей (данные хранятся в бд) ++ Отправка сохранённых отзывов по запросу + +#### *Consumers* ++ AddFeedbackConsumer: добавляет отзыв пользователя ++ GetOrderFeedbackConsumer: возвращает запрашиваемый отзыв по конкретному заказу + +### Payment service + +#### *Бизнес логика* ++ Резервирует указанную сумму ++ Отменяет резервацию указанной суммы + +#### *Особенности* ++ Данный сервис максимально "тупой", он прост логирует отправляемые ему команды и все + +#### *Consumers* ++ ReserveMoneyConsumer: резервирует необходимую сумму ++ UnreserveMoneyConsumer: отменяет резервацию необходимой суммы + +### History service + +#### *Бизнес логика* + ++ Сохранение финализированных саг (данные хранятся в бд) ++ Отправка сохранённых отзывов по запросу + +#### *Consumers* + ++ ArchivedOrderConsumer: сохраняет сагу ++ GetOrderFromArchiveConsumer: возвращает информацию о завершённой саге + +## **Оркестратор** + +### *Техническое описание* +В данном сервисе в основном содержатся саги (паттерн диспетчер процессов), который работают на машинах состояний (используется библиотека Automatonymous). + +### *Саги* + +**OrderStateMachine** + +Данная сага описывает жизненный цикл заказа. Данная сага хранится персистентно с использованием **EntityFrameworkCore**. + +#### Реагирует на события: ++ OrderSubmitted ++ OrderConfirmed ++ OrderRejected ++ OrderDelivered ++ ReceivedFeedback ++ OrderAborted + +#### Workflow schema + +![Saga workflow](/ReadmeFiles/saga_workflow.png) + +[SagaWorkflow](ReadmeFiles/Saga Workflow Diagram.png) + +**ArchievedOrderStateMachine** + +Агрегирующая сага. Данная сага обращается к трем микросервисам(CartService, HistoryService, FeedbackService) для получения заархивированного заказа. Вполне стандартный кейс для использования стейт машины. Данные собираются паралелльно с помощью композит ивентов. + +Подводные камни данной саги: + ++ Не умеет работать в паре с IRequestClient, из-за чего необходимо сохранять RequestId и ResponseAddress в инстансе + + +### Консьюмеры + ++ GetOrderStateConsumer: получить состояние заказа по Id ++ GetAllOrdersStateConsumer: получить состояние всех заказов ++ GetArchivedOrderConsumer: получить заархивированный заказ (альтернатива ArchievedOrderStateMachine). + +## Инструкция по деплою +1. Запусть `build.sh` в корневой директории проекта. (Данный скрипт собирает докер образы всех сервисов и создаёт файлы конфигов) +2. `docker-compose up` + +## Эндпоинты ++ Swagger UI: `127.0.0.1:80/swagger` ++ RabbitMQ: `127.0.0.1:15672` (guest/guest) ++ Prometeus: `127.0.0.1:9090` ++ Grafana: `127.0.0.1:3000` (admin/admin) + +## Подводные камни + ++ NRT плохо работает внутри стейт машины из-за ее декларативности, читаемость падает. Мы отключили фичу в файле со стейт машиной и инстансом ++ В персистентных (а лучше во всех) сагах нужно использовать outbox, чтобы сага сначала транзитилась в нужный стейт, а потом уже отправляла сообщения ++ При проверке одноного инстанса в юнит тестах на разные стейты с помощью метода Exists оба исхода могут оказатся положительными, так как метод awaitable ++ Инциализаторы в стейт машинах не могут прокидывать хедеры через двойное подчеркивание \ No newline at end of file diff --git a/ReadmeFiles/.gitkeep b/ReadmeFiles/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ReadmeFiles/Microservice Diagram.png b/ReadmeFiles/Microservice Diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5a35164968a784a903aac2ffbc29d23490417afd GIT binary patch literal 39388 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

&$U`jf^IjOLR}%mJ zaT7oKWk>#o|97YOkLCR<1q+1j{kv@`5dk$ABa&?(&|g!E{If#N+Y~=||SXW%+ zGxA;ey#XVom2WXTr&Xl5vkvlw`Pa-MNz$^7^fmvoV{+^V>cG07wtby3M@BJoMDJ&^ z-knL2jz0iB^BCqNMs^JhT^sr7W)kv_>dNYi*2C+GwSDR%@6L!ogHOl&@k0_ebw9PUcj#BJv%oBDR%VCDOO%sv<9`F^b@o$g%+u~y8(%{P9u(-sV#_(G=LY%h zoz;Yi`9GwHJ$^ z4s^2I_k3^eVt9F9&g5JzEc1wzu>|z8F@^h&fcw;s&Fw!hS9C-rfWS0RM5(!zeTCZ3 z_0I`CA=`DQl?B%Nhz>38@{=6tlfE9ijCY3lw|Ud&w>&myTHcPO z9N~L8$7Wel5+Cb#BC*2YbNwZ3TREl2>H=Tum9n)|T`j_C)) z4>KlENtQ4%S!OhhqV7TGnqj|zaOKkEu+d|47J;?yXlhQdZ zNoa(2&BFzU_2(x|LxFnW^NN5dNCSGv)ANx1(J#STx5kdmBn6t<_Xmc0*O9&*NuwM* zUAhwl^qDfxcyXNcsA&ly!&NM`d9MDP{rXvsyH;6|0UXgX~zF*A-_d?G(%vN zJL(e;Gmy02R8!AiN&8o&_jkl+ZC%|dWLlGDrdrk;N09FE}owT=e=2nrR>r84g z7sNeCK#2-w@p74S#nC|YgsUO;FA9f&(REo+wa>-pxq#zG5~=U@m?pX`VVXHLc)IL4 zXFP;1`W>?)onN9!9Rn@>6YwTdGmY?w!Ac5>DH%brbR!Y=CsM^Cs{86b4|0@aXyu3Y-#bj&)4sIqi=p)Yl;ubojf zeQZY8?9$g4G#Hs*Yeg~i-WwWbq@_1rxKY?Um}$zFooq|9AG=wjzj2Z>5HSlGe(3Uf?k#X zESLk63>h{J8p<(^6g(IgZ_sM~S;IG;K?~wdmC4V>0U#9SvPyQ7hf5qbe zj;1grKEnG43-DLj_^W^Y)h+)IQn$}>g%%}DiwiCx(C-qaMFoox=(q%Z_EaSV`d)(k zj01Zrk6@tz!k#VFKv3$OU`t}-D+C(ZoFoB-d$zMf_*)5d`agXwudfTxM3+IHhX!7m zfbS{^;u@DH+6R=Rqze7Vnllgj^}PGSdE2wdy`^8$um61}eyq9AcXhhEeYKA`pyc-D z-rKFuccs^UxTGkde-wKbg6Em9Dpr3q&-9ecg>8(aM^*s$uAO-0`lIc|^r~Jt1%m&5cyscR4-%>#g@AbV8UZMytva)PA2oCM`T+4i-LiPN49tJ0+3~(A6q`Scj z;h#T1_pO3(_)v){ZW&OS!pq6IQJo0tPA;jK@$F%Q0>^#mz*c|i#?n|*&%iDLKAwFU zJrLe^0C|s!YOt<~sMy{fQ%TQx5Hi`Nvi^a|K4N>Jf$01FwNoeP#$I^SZLxu4squ*6 z0mN|ZcY8_b8wi|_j`=kL94XU&3?HJUm#6!tR6%i6VcD1mkc0F}U`*ELC46_fpQ5t6 z9?mXiBc#oZjrn|UWOmnU)O~ApX0c)}+=w@Lm}O(Lt7>-|c+V9==PK4B!lpX&*Af~l z1Q&<-Q=L!;9+KhUAZZRJ#p2d!Kl<;&s(tM_J6X+{ifc&*_vOTtch@N;;?{-eVGopLRo@y2dV)gB5g>X>QoI|Y^1uO zrm23_*51B>6%{Qlt>#RX$lV8^&pm5TZWyLbwD<8i&uo?M?QV_Sw#5Wb^oP!cwy_D|9+7f$6_H&BJU4Qk2V&(RT_*~e?Ez6AHU6T?3 zV5vV!dkpx*L|iM_?me0ec0U1 z9!&Yn7l0J;iRL62O|)tA-D)>f^lbGB#K>hj(dbSHJb40z1rcD;<`5+x6@Y7f=Yf(p5=l?%8&*i;Sg!iRa^F+DTv=Iz~^l z`|kTuTCM;-OQINVjKZoWDzjqwSm1N-m+ZHw2p>P+UeX(U6*oE737FPZvm=2GW7irh z1UN_p9j2>Z>C8;OAh}4awqr?&y0B}jHalLqJ==EM8jQ=xS=rCX_P!W6l&Y9hBkM@R zPKM%ucurs%bsg7X10N$Hy#-B{h+(q_z7tGVbr-wD9V)lKHHlZfFMU;>LLYG_Ct@Yf z;g)OD?Hxx7`C7aSDo*#_J1`dSSbuIE{!zfcpuw+1yGDCJ#g(C^lY^wD0X8M^@|J4^Sd!_RV!&mh7+W2--x7Qm~`gfZgb{FN7kjZ&b%3yaNqn{5y zApBhCD09N;DC`yY=kH=c*^RL*@x2AUJpp2WaE^^~>AtNR_DcS}WurYI+jl$ z&%Pynouz@Zwga2D&)bz9r+kf}UuK9_W|%MdqO6AHfRO2}w)u}P+!u(Vf>&(uRw1_riM zwY0#+=;8mc%{_+Of`fJv?{q0{LdMESBq^_`McHx#>*9F<%6 z79~r^F2!|LcV=S`}+oi45_s->%5W|ziN1AcFlEX zkJk_0JE3+cD!Y%vMy0b-*8l>r(7FDQHNZPvFT_-K13{VC<5~**edv)-w-$4a!oAdu zInqaE1?lMZwW6-1F3^JhgKEd6)sKoM(X5n8sL9Tf*6-Ly?gVZCX(rxfADW-xU&FBY z(I~H${ZZJs245N_SbKlojaO|Y#+AQNF>tl7%$2OboaADGfz#ICGUQi%F>Kzy)TmlJ#&Q1UkxrX2RlTm*sV zTjj1&D50T|)brM@Iiow+jT}z=TrDaw z1CSqTzeb9N#np@M+t@e6iML&jN+0D4>_aA=CQtZ}*4|}a`&lI|+?+wmJn3$cQHnjE zaitruXEYEUvmB}6tKB&+gwf{A9!Zs@(JxJpc*(+yZVJ)W)sVsyucJ>5Bk0cvwEk4r zV4$sO)Q{TC;0$V><$8HKDNi?sOd5WFXq=I5g~7ObQkru3F?9uU2xF9eC-EONvOk2> zkZMQ)!mp7Cg6R6aQ*Wi{jjKPkrkSe$MZP0Y0uk>}ie`t*D*TB2tSwOoiMY&&?svTk_NL>|Y=0WF?;<@28l| z;YK_(A!i(~*6|InH-(P1hhbGj~f zHjy+pf}h(M%t6roTc7hq$u!pepA%Z2a2jQ&#F8Tf7g1qG#f~x6KvNTD>25nUFuk%d z7|(Yb?3HWr_y!(gmYAXcKfVk1NSK^&j;#q4nd{Q z&>v3a2Zdf8=k7|nXr;-;w>2JLK_d{mECI zJW+M^%9~fu<>~1TG3Xbz`imBv5X8Mt*oLuK=SYO9f7Ns(vUHylRMZ?k#9}Qh*dr?7 zmfwQ;Q03DtWnB^S_Se3e8Kt5<&fC8yy$hajMZNcgkK8mR_XIqv=e%}vR5Sfs-{Xmk zMj1|}I*5!7e-{_#u@Q3WNr&cTRCj$fn#a<&RWON#@>Q-rp^J<5&>h+Ok3>g0J2oyq zmedi!A!RsG#k==l<33{bl}`E$al!O!V#a-ImEoAr)$$sYh{BFBdlA=}A8&CVh0`04 zzb{Ybzc1%IxxEFTw6~j}c+V?lL?2YBduHHmROHSI z4}Lh}{ax!f&avFV!>L2bK)b(kVWEzdmEe-`1~;R;HC=>TAB-n91KfkD;uwj~@AgrJ z@oWY~=S7%^?f&dQAe0VDLFs|pyFmF&e9&@*arb(kRJ4!SNi;(DI!8lu)RT9jJWpwLZ>^P z`v9%Gg*|g>c!Tl#3-7*fZ+~~j;|ttTWt>-ETxgb;XZ=ak>WwVVT53@Nu)(A9KWVU| zr({?yt1HHw?Pg zi*H$%e8^Bf@$9}rUl~46(V#SL#7e2#-6y{OyzlKn~BgeonBwh2ut#cove1hzIn=2 zyxM0;mz7}*%f|Ht5>{e5s|zt~&$6LYfR&w&iEC3PyKB8eV#aV8wMS$i9AO+cKhmUzY^PHmE6@lV}81fM3Y{P zwLxf;B~a5B(rMS@m-;ZD05qqw+kCZ&jbK^6bWK?awpbMvwx%R&3q4;B z`mB_8Nkf~t&pTA23P?kTew{6WF2WsYYY9-^e3T*m`g^k8WWTfc;5bFz*@23F`py|& zr)K_&m3+TjSv%A2JU50O++S!gct;uX6J2WQt~&&8+|Y>ljojP{A;*?{+|g1?YT|!< z7ten^MfKekNe8`UNc14{_@e*rXUTo|!SDIJD$J>SVJ3I@==BY%m13%hj#j-(&_Scl z&EBbe=iTlT?q=_?-S2#D$MPL9ryn;bBUj|a(tW8&bi#hbN*z}(x_Y8|nCSb3e?8-P2 z?Zn!sAbC`g^GAHO1cUyf0=i^9Kv5zmp>a<{aQ1^$1i!=y+7g$s2X`lB?F+lcCQr}J zT1f0whp+F5&X!v$FI+Kd3}5T(TCqO1xB3QC;$U9YP(N^Jl&Dfuay2$`>K#B_t$Z59 z8q%x{MNRh9z9^{}sRYoOr-0`M-S3U8bcJqzAnBXGTUUhK6TTkq>?3Cv5?}ojvbj#D zw-%Kjx*E$z{{(2QhH4i*PlkWH;iRdoT(BAZ?&FW1VF_q07onIjS_bHL?fy?r?E58I zvyoTrs4|Y1?MUYh2I@o@giUdipZ-it{&tnWy3Q&lNhtL)RUC3pw8bmaeRB`LCzSa7 zLx_oYL;bH>KgP))QFA;4YK^~iOA9GP6|jhmVHsK4$)#^eHlc{iE9fxPf?O%=C?CN5 zi=U-~sYhLD`z^|V01cZjZq)*MhTMxz_s>)!C|9XO+y_Lo(0+De_DG;%bvn2}!Br{H>>>53qBEYc)E>vScf>i+J2I zyu1>FmHh~`hvQ*wsWvP?IBPcQ9D`FevBp&1@bObX+0W4}OrRL-ndRDVipp~O1 zzCP`~H8i+A-3YKDZ|M)4RnNX(GK6mo-w0)UXv31Av!b*Z*fK{5d&|s=|13E)^g!C( z&}^xq^0nxTHr~@I$;niJrO;SbWaZ~OlBZrUrf;boKrGd9cx z=SW9AnK(#yndP~8_4Xs|&gKfMepKYa?d+`UPh>!t(wy>BY~r$cp2aos^@{EgAetYs z&B5853-0&Nv}G)tizcgEfKRgp3=#>;4ItnuK$*BFbV)j>Qgqor;JANYVhzl|ysE z`Tfak5ovQ@YJq7^ajZ1dy>FnerO>rWq?mQ+CD+N4&{k2h*Ioqd?DQ6p<2R^*FYU2S zlD&i2s{7{;uB}a0RP7Sp-IaF#1Ru+g?tkaLHvTw;fCFY-{Fq<59l<)Ml~^9wwC7vs zhnT(fI5D5yrKD$6EL!gSC+zAi}dbI=SxCGs^Yz9BYbuj&`KR{C<;R_PN<0KUD96r9=42dex33h`&iRi1#h;9zM!v^Ae6e!LIrV%4&&rsV_Ak58G?DUSo5u<^JuDxN$c~ zo+4XEWGE|he_EGsdM=mIt4b=$@<)LeL_w%2wiBpa>*LU!wXAtGgPSlYOD?fv? z(&f?vMQ9JP`ET&Vvt6vF+Y6%~wVGp8f%U9fY_$SD9w=%_s-7V{aQMEtyOWGqx&1l< zV}599e)L?DBua=FdcDH(Xh+{Ouh|Z6U=PpLCC2wIfFLXL%a|MLvgAqi%C1@K#%Js< z>;Q^cXGY9WFJiCceCDwGP~@S&Dt3b>J(qWA`j)qf%hM2Zz-jJm(v2h>V?f(E-ljLk z8~)n6Ue`g8RahyeudJXXoXOO(i9F(f7~ZLkLG~~w@Xy~%=oJ;@<9vt3W~GJhYi&9n z6Xy<$e0J>1P78{_v>nP|N4FkbJef04Lm507gwC(4Cfduv~E-=H%-vNp+`dGQsO+?roW?T~@jtv$22 zvaW%xZnZiR!i>YvK#hNjPFl0NxJ`w&iR}8KB8JKdzu$Sm zM=|lOy?bJhJHcU^C=Knyao3T89=`C)z$@yRN{+jtYIMK6ovYkeUB!}rAA9%nXY*_3 z@~&lm ztD}+=%0S9Y0yRQGZm+`H2u{KjTO_q2#0uJ!^k~s z12j`3m7V1nyGs~&z?R@=uM0aI>6mhT<@l7a;Zr;mj-@1|N9$&AU-7_4tfKp%p)Tye zEYTc4m4ctS00d5EWF>`Xj-#rj^i$-y$K6wNJJZePO=^I+#dEBjD;iNi`MqfqqE8jo zu~^RubB*t8^g=tl>60_65eqW|Uym#2Np>%tpJjVAd9pNvqoV(uMR;py;g-Wx7_I9o zjy%7IhuEiYR3yW~DxYm?<$E3NWBb-ICJAuZ9WZokN%F?Q(Q?kST?7@s6M3`e1KWRi zb{X8r<8lX5bl%l~AQJr4|Y6qz>-9fxA-OKsnvG z_p$PHeZ9?2@~v|XlX>xd#ODE_4<6g9%=(@m6wOphnV)gE5#FGuGTK^URlT>oiMjca zL!VsLlb06CDYUlrqQ8TkSARuGds=YL!z%>_b}GOmNIm2oyLM3$BMW1%&8T9hvDY`G~z( zzCGX^DIMTHogUN-B1kJ-fVI}hr@L^3KAn&J6mjcgE(A1t%uFc$JSb}|(ar!|K+)A8 zR=G1H;Hz$YAFfGkw8Rd$kmRV*)!M*9AMqsNWjzVa5fwliO-ps{KpH$(!HIC%(~!)3 z=1%qJ)YMczsx2_S7pVSts$u(*=}hJis*i6H(6BSqnQxoIo;tU6sv9c_=hi9P51JNZ zzQP;`d6q{W5Tc9dzA1a|uLl(sSz4(J_x3R`-dnS^~3j)p#x6 z?))op($7oQjO@_!aUH<^F6yv7#2y(AJXlqgYyF`EZ-EyXsfo6G1p%Hr9V5Y5;K)2K za%SRi3Juvc$j1{zz8h;)E4|$NAN`s+c?_=&`F|O&!7SKTE?n~i`lt)p61XaGnF-sk za(`5V;Kzh2jsI!wNJ4f4K!A+94{&fVEHv;{g@Z``Q@~7?k(88FR}5dqSAoOoN2Rsi zBA1oPj#r6|?Z9;hyV|&SR#1Gc2Tp$KV8VSc`aPJN;NW)KM22N2RIhfLqD_Y2LoljT z_8mUZ6c0S-<;$_`_VSCbYmfGX0mt-QxksBId??`*8u<9GnQZ-Qw_Z}&TS5-(u9mpJ zpD^&-+!!Ap&lFsYTq!F5Di)N4e4GZX;~~dDgn>NuNTHk5beILMjWA?MT}x~%3YYun zM2+tJ0h(3C|8F_c+ru?Ki@tg<2tviv1E)H#gr52ijMugKDZuIK4UQE&zyli|Q3^l+ zkyMZVo)|THv!xEiOV%A54x>LD4<<_<<@r{X&Mv7*p9OA-fF5UAbIjX18dbmlnV}w) zERj?5yex3^=JLcG@b6obKxLOpe0$ziCFtK*3SCVG3t=WhkvNM1n1l!K=75?d33z}K zFl?9RY|Y;BY=8E#z^;6FrOP25sCMRZe*N2@sDOh_02CnMfp$?Yhv}8~1s_6ZzQ3u> zT-pZS>Bt4Z&bB>(D?BB!F-5a(Cvu=9YDM(af15h0nTCDvaFfH8##i9A&^)TmawGE3 zRO;yd{31AX`11vpyJCVEMsf$T1UW;%^d3}hN1)RaNMjcP+TD_z)ab?%tcH+Fp2@zD zCG|S@N6mj*eMy|@;)#t74Ir2CNX zyp_FCMxLzz8dV*8u~4k^oD!8WY3$FY5dt=0^A!e@>5p#K)=Y1A>C0 zh+z%E#j3I3QH1#4v0!#wN>NBWSk$cNj2VHBIm*ECbzf?05-dw?cM;gVT^m7s>^mxHDWZ%;4X z16SE-tUP2Au;6c$`Zf(vAEzMdUYsffkN~-0_!flWJ0txHzkoib@xXajK}AkJs;Z|! zpA791^aQ|F{UUv?5NJQ`n@=$GcV>)w(Ef2zu`5r3!b=U?;+Kd9GerAuU_|s4*7^cF zOu^5Cbj)|If?oXsqFe;zfXpXwVbHeTz2f~q-hIdFHl*D?1iaN}QI21|!2mq<+&>1nkd)9!MJ%v%n>b$* z`@&#nbB^0dBbyd<=RkJ5D-q0<)&Z~>^yw_GGzL>o<6$9pc-jbJbmJzB><2ZfWk8Ld z8-c?hm{}?xOo!!o?QI(n5;|S=L_mguf#!1Lu!6Nqre=2vVSRz)B zrCMqZGftPu2-n9%v}wj3hR{ZL0-;~5^X&g(bF`|H#MP(vWOfZWzwI!*-RsUk6h`Jg(tJDo4NE+A>)O<;o!R-GEkEMx@ zV33$tL7tn#j;E!=D%7&`Agh2XYenfA-Q{-~%%(1(|4v+VTF^fIciJLOmQoAt=LitNC!-T()5afG_JC>+X94Mk4&Mj1 z^zSiS-jN7|mgaCEOSg5gXTu*XfQTTTysKo2SmoGSyS;mF=nMz}bfo)s^-{fWs24~2 zJ5il&=2qY_c%=O}4>1f9I*HOxn6qqLN(i$tx`P1K%jkplUeg2&WP;`X-#$!Em{bPkIX-mLFK^|)TAlvkjCvmXL)#=Q>|0GxTyb^T#~O}o zi!E-1o`bEU-#qX4V&na$Iwv59(4HT{Y?E6E|UYuh`$IlKV;#l)Y6I z_&JalG;9)tRK=1YjT67p%Gx~q&k1Q4z(_9~_7q5?c`E_@buQEdvVf%jlzR5Rkd#bK z6$=RZPf2F~67(M??e7Tvf4+vse~?`R0?eqtrvNa7)iIx`VT{&qy>3xq$_f<4x%Z~L zVe@LGi=RY)rHVB*gli^WSZo|8@I|aUm4bAgIY~wLDAZsm=rcHe6w&aLYaEt*JktP6 zo1D-X3G!u|E^D4-9#^Y=f(7w-`5A$o*3@5Jpu(&h+FqhcO~4NrIKn}!&j|Phh`Ys= zr8WUnh^}xtO6uXYo2S0i;}`1LQ(zkao-LePQlQpQT^~_<1SXW1b$PVe8EuTq=!&fd zKnIj{;$E(OWI;X1>#7f6(UPu|2(i?Vd>QM8*?ac?#1m!KniTB3^BYihHOM%ocWK}A z6Ab)E-&~0XX+58t+_{1&mGuPiQ&_i;oHA>aA#B{~r@@?$p0VDW#nLL&;!+a;npsg( zsNZ)~FCpTG4)8qJlh!w-?q=7@K{0K`PPK6hO!)0b!yuI}w@tX87flg|=hd*(9T8b* z@^#p`h=qu1+JpL@_P7Z8NtFcuXTmR;I=Dy8EbnWAwh_-T18jSi_34 z#6f5^HEI+5Zp1mh_3DFY5C=$6gaT;L&HI?LT*~|$NPvLV7;ITc2wfRbVF{Xk(?BMo zdf~I}&0g5?)w8)U>He21pK9wV%4dUDyWL0TYnkR^0w;5GlErDZPv;-x322SU%$jFq z&#FapPaV$vw150a6I^hpnD}f2L=4$w%E8&_WP!lg{ElUqipo0$0PGPtTBSCVlS7jo zc_Y>Pn7H@stQ^Aoa8>xOZKrr1k-3nra*?q0p8{C0gM?>P=Q18{~iQcDe@gyemTAp+e#vVWj zsAaP6dpq7KF6o^P@^^*&XKWjL(_i%J)>?c{t8U^O!+ornd#vSlhH zyl7<{^x`<32p?1LCSfV-`RucEgRwJ2&)yj?03*NeAFA0Qfx_0GC=AUaxlOeLg#-|Zo5aW3Z!ws3&*4H0cdf1A zCMTN<`%h&T#q<5Ir)3GV&l5h+uN-vK|4@%VJg@sx8U?%We)uXdtA ze%qu(Voii`DE^pzn0XE_^lKJ?QZpzrcWlYk8bK-;a{Jv_48MV(GWqK}k^nqsG{*Y* zBc@~g?R`oFU&N5{ms8ueK|Xg}N}O5y36`?jl563~c2UH{#&`tUsINA4{E+bcD?ywY zBM$kF16yh_x1VW=sMs(l4IOko_0oC}GRICAA;?73uc@!H)h8`&JJ*IAu`xZwp@Nt8n(NSsjG6}$SPlF+1QPprRIuG zoYTyqDMt^B8YZQhvBcB1m!F0ly=(JuOP%{WDIgT!qAzb@(g*-LZ~LK!Pvs{754JVe zu2&ogUH6ahK)qmkyJ35{7pnFB!Cd=&7UEQWOT=o?1J+>k!g*5&jAiZ9sa{{Zsu-f1Zuv*8$JQ$iVs-~ICb zysz)Le*eJl=ek_i;XGf@{k-q{dB5)a@o$>8uIJB$& zyvQ+vKAJJ`XPCKV=cLW;iz17X7{^z;?Pi8M9zY!uG+fX9V`)l#cr z1WVWzK96H9r(L&6;-KR#SHU)7cP-QZ8$V+(&nF)W7mFn8J){F?uPUc1bk z>w81-IzAT*>l-HAYErSUyN(F@NZR}IIy$-*XQGT$NVDrVU%H;F=NjJ{s=E{O=#0p6 zd)mA%p9pWy_ai9>%SG#5#6s+F2^V5;5ynlrox_BpJDB3`uE8e=*Gk6CfF;^+S|9uN ztD+rd(r|)Y-ROVMyi(p#b9kBI&8y?9vm`~$fJ?CMmmR+I65aO!qPfL0upbNpN=)mV zU-TY*e+&oQGv!sc7hPFz%eL2;k(dNA6?n#MQUu3DDcb8OR+egp5?s`>b5UcVuRaa9dh2A^lcdfOGh~W_&A*XQYE#^ zdgTvoh#czMb05XtBUF}LU+m_o8=Xr&J1dmbY2nWb-!&F~pZmFtX?GxGKCDog!68e{ zLRsaHh?ZQO_xNlzdm=?H#MG$DfleA{0%*_#S^a;)T38C ziqvtov8>v)TtxuwO?`IvR?M#dpY=7x@y*`HZtM#i7p_z|P^f2=)O~qSlW%f<1xXo{ zEgzinZ#pl&Lz05^+CjGh%|e1c6{Tf4Pr zQ6bk^ue3AXZr0k)Xjzh8pLToq$c3Iybh(Y~z z!ea@^$2oP-4m~xg=)Tybaq^{&B&H)HXTMP`nc-i$jmY)xD*1Z@9YDGDFb zQtjU%?Pa{yT}rB9qnWYn4NU_Ry{mnMZ5*Rermizb5uFo2O79=AG)u z*liY0tGm_w{oS;cTuzwK2`W_yZXUBR2}3bDjtsaR{SQp$b@sS7Xr$ znSQlAMeL*cj0EOxdp?(?b5Zn_=h4hgQY$@nvnvMeI~Ky|PdLkhcAc zAFc?6*h*zQGe_`oC`31uhBmFlZyM$rPdhv-)52<=B^>a+?{S32?5^G< zg-g7##+W=ghd`{3R3lkoc!#ZXr9D=&Eyqyo9}>EMk;z*bpb(X_u7tShmz^R5vDGsl zhXiW)kIjcD01*HEvh%_o~k)A!dboGF6}9nLy~0JiJv1! zU7nx~Drdod+$Yuhm)E3H9SXIf4e9AQ)K^K5B#&@AZ*gw`Qs%_@KRs2 z_A%jBn>-KN2b^Rz?!GREYp;}@GWGi3H|voTP{^vRD4YtHt|E)o4E!W_z^?(q~{WyIo4BaQb_pHh6zre;ho~J>+EhHycv%(Qb zms&!?;Dc2?{|C^xo?hZU`}U7j^}_c z!YB*F4{8eABVcDjOqF(+FTyc1(LEOYtHqJ`Ld;WG9!Q;ouVldk;!H8+jud11bfGoc zgI2|QZFnzOw zutKi!1L8OSDwmd~T|uM^d%`)}y=Nl)cVkHDaxosU(8O~XUvjzBCQ{w0r@>THli&F7 zyo$t};vbF;aEpDny`QOg@8+U}ZMcpu&|C(4K>bq(g4Y1)o2EJgp13yH6X5Z06&=5tSZ}nMV}yWS0^(| z9>Dm5Rjt7T6i$0t0r}=K5@HKzCJ43Ir+?jh@btgY%z(hI$_F9QI?|72DEHeq2laaD z=QAR0Y{BO>CpC@zHl%1Fa=POHv=FQd@w4gmiLyushoI-x#tIY>&;k>L6|;oLCnBRz z<&%SIT_@KN>P}UN`o=LFkA&rePHjOr@*FE$#Qv1Wz+j~=T!M(fx zaGOapJ|u>=DiisrTPN^ZjrBjV4E6&s2G#4Q1U(n*BCT0{oP4 z8~VHikaxaMXqe8S%UVRFg&Y`N&QA~N{Y^U!A^v$@Oj##qV6*{#)CFBPkL(TClRME6VFOq0Oz zaY+f$oyflQM%Obj-EQXGdS{}Jh3lic7AtaRjyroUpZoaR@7Z?C?(%I5CRMVKW-1!g z`$LrQk=%rq34@Wv$Z(KTGDW77wF$XCUZjD~s;zY7p*Z%p+y~$f_h)2nU4FJ|IOGY+ zgG}1@iK~T?N<#c9ED4ACbAn7&`1!9QSr*ebZqicF@DV-8vDvSwQ(jFa(vd-78oU`Fb13d;b`UE-wmh=o zD!;?L`6_a!q#@$rkN^}o4P|ZfZPfQWUqCB(+1+4V1`sigI>_#83Yut`-trhL%{x!S zXO&D=^GW>11`aCav#M9kp-h>*&S`9RB@@jJgWwjm!gyzMD0n?|e2xc^W>gm`Tp3X}_&pON*`l>C z+`M_y8jVJWM8+&EE^@O*X-EqsX!Ef|#mp}*uK9R-Z)#|)zfwGR?$x`Q-`;}4!V!sy ziRzawU5e$u#>)^P>E-3Mqp5+4?D1pvv()&;n$SCPW0VEDQfU;7b`+7LR)>)?RjRk@ zwrQRZ`hGU^qnxaw;&z<7{LZGkypn>7O3U-Y!c#@?Tb7l@CnPC()${gkAw@;A(6%hQ zn>R1>F+C&@jDBqo+L~$9H#7*bL}{CurB_#1_W-`Y^A|4+<`)*8mzV1)@BCChcXz{O z77}kJAdBauWuvc$mlT)L1+bc8W3@D=@sZrD2@j8lb)RNd<;$ij45vN6)9LaS9YNzqMONn`v`V!Ttd%0@8!1pP3{rYv!#gP}E zvehHd-yn(Z>Dt;_vHq#GasF#4e)b=A_XS{@L+M~kc@gK%?ys#dh2A05nXoouqr3dQ zy;!+#T#(nU=ME`Y(znKBv|`OnOj3-DjCw*2P*2OXY<~DYHa2E({P^*^4fXXFoAN#K z#Z58?$tf|Q3BA5P?6cb6d}pYhvS0{deD6Zri2Yx_e7X7|Q$@uYfK-#q3Of(r>+8`Q zOP_@$8-KPZ;W#C0Et>qj6C)e*%G0A>?~YPa&6vosknt?5QuK*G+UQ|Lt zw&ZHu*A)c7bM&Qsg0^6eLd)wHho4)+39Tiod5Cu0;I`dEo}8g9g(393d$(=i=eL;} z#VYL2AU(uTq?qs(5>#(}`0xRzVRyH*!d8)z>1PaCjs91a<{UL7L%&x4l!nCGdqTbd zQqL<2W2zKtPfyRYCdS4$zt7C1e-_|eB>nvbnZ3QomzI_QuQeUzD6I==6n|;-=?}*d z+W%e=-3Wyx5sVIIZ?nI0`SRP%)z#HL$ZNdHHhf_lNt5jXpRe<=W5+6Zy=>2dfRKbo zx4AU2*_`=j0}vTm%3R5hFBOrHFt5HnqqlyKBJdCeBt5Q-TL5NH%fSLi}!X0;ZbW9~a5A5TlWWpN< zqeu2ivp81cE+9udQnPiD_$cYoBTrb=g6{rQjW0I|g$5Z3+IH2E)q*yAb7v@M`Qf5t zmqiK;a&T}Eu93R%jme7@p5=-Q9&P)cIWRE57_OH6ctQrnYyn`jec$@~J*y(0>fGP$ z*P>(rfg<12YDmwzmT{pDWMaq(n$+W%-P0l>b3t%Q-YfSq)i0Ln1}Izl`1t%`X9u=(|H4)qd8S0)q5uHE20f}*rXKo*+7(;@G9PU|5_F`-23e9+sjWBys;(@4l^p1 zlap;eoaSmoR&wkFSyc$S$BYU(smjMeJ`kqE$K<<$!129JmN2t%x~g`o%I*!zXmfyK zL9`H|aA1^@{ZtmmhRQ0Toif1W(=B+;DiWE~r%#{m>A*tzLiTC+4E~<9f>s=NLIvQE zPX69hTVG$_>x_QK7?rp%=@I(OC0Q6`Y0Az2Kmy|Sw6DK3Hvzcj9h4fV=8k3XJjzeCyEl6c@ zRN}O#6iLh8d+=_6;AD{V?2(ekq=p-|`%_7x@YT1&%^R{$@Y;;Y-$bf9Op#2*IVk6y@h+NOY#P!`ANKcOJ67+!a>8fjn*_r2 zSIAW7`^%>6G$rTV*Y|S;tP)hOx@U7l>(fi5c>@p_Y{Asv=WUv!*FpBJet-8pKi%~a zB!OPCw$6U7U9r>)X`cwKpXWSb-|KDd@Bn4y z=iZn8mtc7H4Nzl$Zx|<)D?xs9v z@jOYEq8nS)k2N|GmShyp6hcEv6b8pu(9pOn*-49uNKN1kOW`Q zF4fFl15Z<$*N4nr2JHpb)0}1V?s|puQtWyJk)?#~^9dq0BsG-2=Al_f zgMadXFYMh`JM!05DYJ~ra^v?5jZzIbYfFAgd(Rs^k+zS%k|y(}pZ!Qp&b8~;g_4qz zRso9nf{~+$sOTdEvjw=ji%7TSRVY&?3V$61%_TKWR0HM3HDE<~CVKG#85z^~C5;P4 z$kHWJaUaLT1h_dmKCBxLG}{HlSnR~a$DrLR!tk*3#Ki5)?Wb&PY(0>FE9vLw_b5GG zY;<&#<I5@ZpGSF$e0l=v3R;S;rhD+YNfTYhK0ic-3hzO#zjoO{y;A4~**Fg#w&q}m)rfB&wze%yQECVS}nzbEji*>9DZ-Jhy{ zftmm0VXG6izn8^X+`Zk8#3k@x9{fh1u~2tEt)3LXPn0J&414G9uOq=7ceG+(?p7M| zO@!hyMgoqf6DO;gv(M9{@)Pkyo6C|~m4?^}nxDQRpphT)QBZQGQ>QUHr&9-a2w26E zwb?T{V6$ae6LR@LCc1L);YXDG@0os9BID)($bK3WC8zVvqgqHCO5&>SFZ|qTQD+&+ zBD7kMl84f_b)`M&Hb8pHNY8|E;lYIBJ{)MBV1}3mqMG}QoB1AW$nNRbZ#9Kz^P9}w z7c>$do(WU>#EE02H}>`+Zsu zPGNj!#^d9VZn+53ImI-TrcG!g#8wB^gN%HJ27eOIWnzaQZjCnG_4$Z>)0e9S@=U|f zywWNJ;1OCLs`Bw|VS505v4XSLR>t4S4(B_G`ye?6G3F>1PK`RnYE$@Pzf;y(Z{oQKDoe%88iG0bs~bBjCcC-0ITo?zJ#1b; z#Wr;ZBoi8MV6C1Zny;J(+5Kg=3PRr|2xwK4ik1iqYgTqj66&OKu$Aw|?#6!KT3@JH z8Z5GzcL3ys^KBx@=h`E@#V@;r1usy!(4txZjn^NWT$~FC^N+xajXwS5)D3n!qr--K zG3Dvqk~T52pYScF#(oEnUBFIQ0jND^uOrAq)~^@zqABuS7H*s(YZHK>ZW=cdLK5=Uz1&P$@J4ShATK@vXqYx9{M_@!;Oq zL%c(cw9MKxA^3xw0fWP*95CnmDtt~K4BdMmkEfLJ?c;1zBWjcH5TV&&Sk6f^^ zjkfi#m&o+Bb@_TUpT^IYoZTv7$7aqOMZw$jb<7vHMjFfUiZR!<**;GIKsh{DO{-k0 zB@^U#n!=Si(%Ic0BW`_iXLi8sI!XFqieZ7?I-PL0e_<%jgZ;)}1A1LVP@4L)vP z^jHP(c>ALRddQ4fkPlO)?CP~iKdZOOi)sEmtSW};cH_FvBwbGIte?!$tK#bY`j!T@ z(M&F{K*2#f!hu=aJ#rvw%RI>E1kS=Es=TTAz`sLFCtvDDWyQcrRUElSRd!-$<>EwrT9{`kR)V%(1jGf9x_T z?FcDdkJLf>4}$T=(?|JnmZqkKO{iqvP@VtUBLG!CSESX^)GefdpOw4ZSHT=J+@nuF zCG;i`If5&6cz*n;z$x>toRu|urKI=1T2E&iqUQ=c=fniI%)4SL`qUo3$ zzY4tjg?Jif_653ta_KD|GKTnIS&C;gsIB?hW>40u3Rr6;*-=i=Cq!Lv&3u@88&UHg zvERzt)^$Y`oBgDI@fcz{JwSxJ$8^Wo4|3v98dCvP_amK8(2gH8rcWV;V>-Bh&c^fb z4*lO%%E(bnjL*+&-Q5j$yE-r#@n6S(TRx}{no|F=He5_?IL{b@TgHu@+*)=BIG#w9 zCL~zhrNoHr8%AJSnxCt1l7UWI6(RQ9UY0SVDR?hGP zIeYZtTF}n1%&#_>SOYN;Y|Rv)8(+gGHH$pRUu& z3z|)}Ee+o_s5xuOUNFv&N{I#+G3C|?B!7OeYP}Tshdfrb)g>e_3@k9B&BpXU*H9Bm z7kX58>n9V_NQTp*@`RxwEMsVCY3buG9OpX_#v0)~H7^a!&A(QyzJQy^jjEfmU^D%p zObOWDSa-2K#5Vt?e_o4R3psR;vGL zx&1CRBD46;l79}xeN&n;T!|Iqng1gyUby$yq#*W4M|Y87fj{X3zHBQ!&>Tl6KfhOD zR;P!sfzMaV1juR}7gOLqyrwa08usp(TVtO9?>kvV%9N3lqGl(Eo*@?`dkZ+?oRKSv zrbh3pd!n)A;~*(<#~n9T1I4jCO0;gzlw#2hmXxi z1bQ^1b^QQx(LbZx{t&9WGCQyZU}ByvxUL;sW5>j_6;!aNH0zN^@o0tOE}cpL%1;yz z0ZcEL)yYy39p`;LhzJa$LV4exRv){Tqr35w*}F|GilOV?byDo&K9`oakkw9ZEFqQa|JiR8=~64aFO(l%N;=5D-LI%15TIUFJwPDwF3G^EM3lR z+57U7t!?*Z(e7y6>u(ZX)FBh}hx|=WPsN^x*JlwwlHm9tdcRTvf2x1hfm1`}Pp2<@ zJ-&tZm|IBB>0ToIf~)Dtk1BhOXVXYuua{N~bgmI!`mfJhoUyK)!E?nAjI&LeU0i8? zHG72OV91H{ufx$jU-pejRU#VhC-eu}=a}Dv15(3Edl)Ymd8xv6RY;FNP9fkT9D)ZY z;Vh?_e5w1r-I`Vy*Y{{#pCep!m@!kuTtp{N*-66+X^9sO~d%HTHgh)^3)~uBx}N=bks%&>jla@bOw2J2e3+8*Cl~xlr&)#d zyp9;|q}^SezO)HMN;|^{$%zAh&hBeQa6W?_<{sACh+fY6IW6n74C4mK_v2x*Oui;L^J{o_5YBnB^)o0li};HKi;#9XcTw_uW~ zw~&_H3=OYctF5lBE$2*Vk#88RW+10@Vawtei~N|4C`!sNC#-zj`;rJ}EZ+Wa2kd12 zKo4Qgs-k(Txbg2KPS@Mp`vZ8Pk7?R9L=s-vihjBbuQY;#9k3ICB1Q0;>leaiA2`Xb zi&WdlPkcsw>Rj(mS>3lsr0qZNzBS!FR#H+z zDk>`48yy+Jt7~j1mcNjJws;68S|L{~*K6f89daNm1zc34r zmzS~aj_IrW=3nC0pLZ#_yr+;oskm6wZzjX1o{>jsWUW_m#V)U)V7R)bhGgRGyx`35 z*-A*TnSuLc55<|H@F=C7wS5Cul~(>do=ZzaOT+c|<~_rzs;U*5h>lFgh>qOfzki!W zM@K(`-+nqex@)lS1m)yRURG5JaUHsR^JYGf6JOHRWp5cDuWHd2=}sz;YUXSoRT@X3=KsmaO4eEd@iii)o__!Hi13$jFN z@aMvxBy)4~=J*bD-N%`Q%X)foe@VLx(6{+N+v)J;ZoOW!a5w7P5 uvq8`lqB7lc;B`+_u39=WddgwN6pfeV16bpi*6h%Oh zVj0D;q0*!)C@3AJ*LO4X{eIu?t@p*9&|Mk`yP2-$Tp$y+8Z2_u2O#je?tJ zw%lyWlqvH(@g9sRQx>eBGG$uYtQkPdv2*>`fiDv!1BaTD@p8rIDN`1@sxZDPsV+z; z;Z0c!LI3q@E!bWlSE|-RJl2B29EHL*P{`qjaHL9G8BYZ?0sT^WpfE_t3;gRCuss;G z1!R1AgAi*W?hrWe0fyRwK=$mvj_2@svVUC=W@`^zU_DDH3zTb>z)y-d@By+1njt9Q z7tjHR{`FG_hi(8`+!YE5kHrfh3W3qFFbKjH7y!5g5${bVtp%fj-x8si2YlgpT(R7E z3R(~%mjW$VB*fO<779ZkY#ogA_T!*JfKhoNKo=Nfi?jvXg5XGF$A4OFVZfhWoFGn! z@V|^^JejBB@c+7D1;!Ka9)MI+m1LsEi|0*6kYs<|sD>A!6v}0P9R`Bg0{{Q&Q0W!C zzgqcnIbQkbuBoG8C%;b3#0P4fa@=r@NfahMoq2vI$-djxqq=3?q!R`bP51bbXj-g?_*<=M9#RjW`xCpkI0`v=j zH}DFA!nhJ12@oO*M+QDbL3Akz5+wKJ2B1+m6h))d5CbJr0v*LBpw(;!5~?KWl_Xh+ z9*`)936sN!J^=yt0lq4_MntADL;)Zljm(pcq7wskAzVF0u0gAUR1y-L5(u0T$n&KJ zGd;8vJQJx5@&Z~QLLCoA^mQkweek|;KExN81RZEc_@KE#K^{tP4bZ@403R5s9`B$* zyHhzq0R(#?I7H6|X*Ce649XNr&@wQB9>nm%af8^wz`?#SnsI`RK%p*xq|pTVIZ$P4 ziIB|EVF78QxSkpTpsQFO1YuOIKVPUoq@D}`^b&>g)X+pg8%EfPu&X z3MQ5e(P;wQsXC4%7{?Tdl^h001@;PpVmX8mHd7nS_Y+bf28l`$s3L>8BCUofWXmML z<=Id@0s|-$h8`+MnuKx_2ghKVEHH*6v70)SbF37Nk~3w zB40+wU^(^@C0MF+mxE#MI)kTFqVz&5fVV;5j>D2PB(XsS0Z!0q7*aLHmmb1b)8P!9 zucsFSY2YFB8eaoJ4HGg0kw}FG5onN*eKarwTqxxP5E1S^bS8m>V-tC7zK&0%DCr(F zrpVii!s3Q#^b8qIMPYEY6pE6Brg7kbaH^lg+rWTRSONAbtP~g(02z-EcQ%m&bn!5wdUgfFw zwAZU)_Abp0^(yArG*} zdot)sltd>3Q%OV&8Ua$%NMJl*UKoia1Pp<530ws#Bv7W(7&ruyj0-UYhCn1Tdp`pJ z#zw7m_X0@+ef>fZBrX}D@c~-&fMKF^z*=Bw;y;f8W`ng4(SibXK!?m%i9`xiFcFf8 zXQOkc- z#M2dY2*YUm3SSc2)15$+@g)!@k>D=zbjRwELVFJ=k^pE7%buxMi2*dmLKRX9K7>O9 z`~wO}#c`x07|sh4C<1wEeK{yGnWz=|X@x9%2H<8yD29+D_aaG!L14a*!Xtnu@WH`# zTCQ9pF$h$GK%@q*41lR{5-%|y3r5IQ-cY$Rm`8&Jf#9AB6%N2cEIq{CfdmHLEF#Z? zg;3$>1hNAUMHINBBwlJY4=`D|Mu(vqVL)I6lpwvFh){W=kUj(-h#Jer@kKa4sh&SS$p*xL3RL5bh#idgKx!O(IaCx#jgd-KL^u-) z92LmHAVfTk7l(!i)KMYzQ;5l0C0(EudxA+?wm@jWa5xBecMO5g@IatYIBx`7K?hM; zz$HWIepCQJPyqpOoNr)=imC#U7+61fuo4mA$_OIfa?xPE7Nn#Gixe0? z6&x^Wte+1&1WfUi1qP^DdZsVQokb_9LZHAul@N+HY7^3U7bKcqEhh+(!B7l0h$JPu zYkeII!eAW6Pb3Ioiquprj81?^_&mIi0uUEUiv>xUnjj2~>L4H}9E@IpqSuNrTs4A^ zlVco!Lr{DKA7jJ?u9l-T$YeeYnmtX)RC^PV31?ma8Sf6kI?tu>wHB z9i(8E#1n1+1ndr+Ow>?iA!ss}ScHl%@CuT{+)*ARwKvR@%=GexQuI{5 z77QlYvuR2s*bByh^E`+SWEPYx*XbZevG}Oz5PP5t(0Z&&#tx)IWKta6;H&lH1dF6b zQvrc+Fh8=C?hfz+Fu1L&mNexyNKc*w9Y!UqC>V(!#h@ghcq}MK=LL75 zi8NGCErdmcs-#A#1c*pd3QiV4*K62X1~`Q7uEKI~fv{kHfSej)&+wu!(OjN3fTEHj z3}jC|*qaFT#tL{Oi5^7bGZ+LCNQ!a@a2NQ&saky{AP0axWBDKg6pS#6-{9bj)zL&85Faoz ztd_0_KrxY?2)=+A;=>2%FP35OBWd_XKHV+|^t zFR&^)kbt7rp`|RDC!Zo0g@j-XBzwLbrVN1?G*BuYAymPMOuQ7s2SWuc2u~YG^7NyN zVBShqfj|NScsk(Z0V)mAV**e?ei#KM zL?$!HJV;7}uRRDu@)h#gJP6tz!a%Wg>L3Rr-ShKN8zo?Ll>qc(kqSS12$><`66Ee$ zEzZyAPQ@a$kB6V1j_C`c6MWb~5`vOP0FL*-qL^N2Ay(k6#wgVg1l`Aj&qC3>0B+(3 zbZZzM28>b;@|Ag_pgeBfPv93E2O!BVL7V1Xyz8-awv4SX&a=s+2L zs~T^h@OeQrBWFXXP!y6t1)_W5RC7kIRW9}2^M+FNc<~^c3`L+|khbz(R{pI%_Opduzo>Ik)B1Al6{aW7zPVO+o}LKCjg}fp~(gVfd)jd9%>IVMP=mH zB0fXJR8eqzqai6tz@@MffcD@ORHz3IA(vtW$^f;o)q@)%)6iflEg1k!EM6ej@t{E> zPXvz&_+>20Lm22CLh_N3!Dv6OFNcCgdIs8)Ly%%lh)k>n!T3xBNGca{nF@tHNI?no z6GA)kVs*{2rs?|EEod|B=Y5A z0Yn)h(4jeKszi#_`)E)eEFBnU@2ghHNNi$=gbxYwGnzNmXwo_+(H^5gvSoCtH_Tf| zVG=PuERs+NwGRZ*c`6i%g@&-OL3jdQr-nkL?mmFKL3y(T9()W~L15Dz){Qa+783glaVdt|c zQ&vv#^lvN_r~ALv_xqTy_bfn3H~gcd z-$$B8E&4}E|54KKYNP)k=|4#NJ^koE?*1Ql|2@fG^go>RA5Qu`Ba(kO=|6$f_l!vX z37r0k?!RAr_fK^HPYUXv6x4T2ZT;i!|8e*Kxcl$rp#C36&vj{T%^ruXDGN&7`&WHT%l!z9WLoUuo|VojCtN zjRmc*=U3kQ3yrpxO&RQ}j>~Jz=XSph&C6;_$ssZedYYu^j=wrD{GQRbiK(O9l^36I zg)(~~ys=}l@6Wsa{(CRL&WrN@Lt69GtmxXVeECROsdI6~{&|I$wqAU=)iZge={C~R z$+qjxS29n-xFb(@pZ~S&o6<<80Bl2zsy_1H-}G6!ELt3)DcM=t_v+ttmhh?7}iSW68xe%9P`_m1qivc);P;rv~vx*DgD{uKSGf%0n8HsaS3 zYPhiIs!r>W$l7* z3Q!LSaI5@z-|4^CujDDFlVjbD&W6u6r%Kn~yXr9Al)UgyRca0Uaw!igjoFdEray%^ zvq5MMtTL3pJI(Gz@U8fO*=cs^Hl@WAC5I`Y{W(>QZ${Q2O-%|mFKPdGEr4mP*$U;` z4#!rj&=1_$+-&E+Uq)bl32Rp-r)57T%4h%mZl&4EKQ(f!TOKi4I!CdQ675zt-DUKk z`M99+$mdPxikfY!FO0ldJGB!U^HXxs+=JM7CDtw z4YNP|IG=U`N8R=<#Ye1eR`+ynk~{Sz&a^>htxRo(&Vu^fo0>7~P!S&% z!Ctb>E&LBe(f@IL+T@o{LgF{jBUuUTeyd;le%|o+3+m=R&FP*0v1rxuEkE`|iCvie zYm<^HA1$W&e~HcfFH7QYf=Tbs(VFU~l@~i1YQCk;pH={1nE+uL2!B5}+I$o9p(TMU z6R<>J{If>^!)i@c-2t{I>uEh)Y;*ba8lRf}6ETzunQq+;6S@RvcjD ztw&BKeq(MH9`~kBOkajieZ9cM{B{f1$N$T)aEHXCe{JK?Z+63Dw`_BP6|d=CL@Q_e zhpsz@a~pqlDRs-=i*HKwOF$3Gk=&HjZ_I5vAlgXx8E=HQfblbiegFSnT9j|j$fMwG z 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ΝOF-03Z`c8 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

    E}Us>L2AX0JgV|I^0i`7ZJ(K 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 literal 0 HcmV?d00001 diff --git a/ReadmeFiles/saga_workflow.png b/ReadmeFiles/saga_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..03f1d1a5a56d9ad7c81eef027b9b2c61ead21a9a GIT binary patch literal 110226 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

    ;Tr_&xVz+{f<>N6C%-LgUq8x{=}A& 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

    joObP!(j{>RNrVaZvXZD-VG7oNlq+V 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 kM?T2MY!x|% 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 literal 0 HcmV?d00001 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..b2820be --- /dev/null +++ b/build.sh @@ -0,0 +1,14 @@ +dotnet publish src/MassTransitAdvancedExample.sln -c Release + +mkdir -p appsettings + +chmod +x ./scripts/wait-for-it.sh +chmod +x ./scripts/buildservice.sh + +./scripts/buildservice.sh api_service ApiService +./scripts/buildservice.sh cart_service CartService +./scripts/buildservice.sh delivery_service DeliveryService +./scripts/buildservice.sh feedback_service FeedbackService +./scripts/buildservice.sh history_service HistoryService +./scripts/buildservice.sh orchestrator_service OrderOrchestratorService +./scripts/buildservice.sh payment_service PaymentService \ No newline at end of file diff --git a/configs/grafana/dashboards.yml b/configs/grafana/dashboards.yml new file mode 100644 index 0000000..6ebae10 --- /dev/null +++ b/configs/grafana/dashboards.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +providers: + - name: 'rabbitmq' + orgId: 1 + folder: '' + type: file + disableDeletion: true + options: + path: /dashboards \ No newline at end of file diff --git a/configs/grafana/dashboards/masstransit.json b/configs/grafana/dashboards/masstransit.json new file mode 100644 index 0000000..997b3c7 --- /dev/null +++ b/configs/grafana/dashboards/masstransit.json @@ -0,0 +1,1653 @@ +{ + "annotations": { + "list": [ + { + "$$hashKey": "object:636", + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "iteration": 1586724089619, + "links": [], + "panels": [ + { + "cacheTimeout": null, + "datasource": null, + "gridPos": { + "h": 8, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 20, + "links": [], + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "mean" + ], + "defaults": { + "mappings": [ + { + "$$hashKey": "object:4271", + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [], + "values": false + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal" + }, + "pluginVersion": "6.7.2", + "targets": [ + { + "expr": "rate(process_cpu_seconds_total[5m])", + "interval": "", + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU", + "type": "stat" + }, + { + "dashboardFilter": "", + "dashboardTags": [], + "datasource": null, + "folderId": -1, + "gridPos": { + "h": 8, + "w": 5, + "x": 4, + "y": 0 + }, + "id": 24, + "limit": 10, + "nameFilter": "", + "onlyAlertsOnDashboard": false, + "show": "current", + "sortOrder": 1, + "stateFilter": [], + "timeFrom": null, + "timeShift": null, + "title": "Alerts", + "type": "alertlist" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 6, + "x": 9, + "y": 0 + }, + "hiddenSeries": false, + "id": 22, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.7.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_working_set_bytes", + "interval": "", + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Memory", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:4320", + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:4321", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 7, + "x": 15, + "y": 0 + }, + "hiddenSeries": false, + "id": 26, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "mt_activity_in_progress", + "interval": "", + "legendFormat": "{{consumer_type}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Activity Execution", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 18, + "panels": [], + "title": "Consume", + "type": "row" + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 5 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "5m", + "frequency": "1m", + "handler": 1, + "name": "Consume Duration alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 11, + "x": 0, + "y": 9 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(mt_consume_duration_seconds_bucket[5m])) by (message_type,le)) ", + "interval": "", + "legendFormat": "{{message_type}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 5 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Consume Duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:3916", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:3917", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 42 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "10s", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "10s", + "frequency": "5s", + "handler": 1, + "name": "Delivery Duration Long", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 11, + "x": 11, + "y": 9 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(mt_delivery_duration_seconds_bucket[5m])) by (message_type,le))", + "interval": "", + "legendFormat": "{{message_type}}", + "refId": "A" + } + ], + "thresholds": [ + { + "$$hashKey": "object:5930", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 62, + "yaxis": "left" + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Delivery Duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:703", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:704", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "gridPos": { + "h": 8, + "w": 7, + "x": 0, + "y": 20 + }, + "id": 29, + "options": { + "displayMode": "lcd", + "fieldOptions": { + "calcs": [ + "mean" + ], + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 10 + } + ] + }, + "title": "", + "unit": "none" + }, + "overrides": [], + "values": false + }, + "orientation": "horizontal", + "showUnfilled": true + }, + "pluginVersion": "6.7.2", + "targets": [ + { + "expr": "rate(mt_consume_total{message_type=~\"[[SagaMessageType]]\"}[5m])", + "interval": "", + "legendFormat": "{{consumer_type}}/{{message_type}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Saga Message Rate", + "type": "bargauge" + }, + { + "cacheTimeout": null, + "datasource": null, + "gridPos": { + "h": 8, + "w": 4, + "x": 7, + "y": 20 + }, + "id": 31, + "links": [], + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "mean" + ], + "defaults": { + "mappings": [ + { + "$$hashKey": "object:7339", + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "yellow", + "value": null + }, + { + "color": "green", + "value": 300 + } + ] + }, + "unit": "none" + }, + "overrides": [], + "values": false + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal" + }, + "pluginVersion": "6.7.2", + "targets": [ + { + "expr": "mt_consume_total{message_type=\"RoutingSlipCompleted\"}", + "format": "time_series", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Fulfilled Orders", + "type": "stat" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 16, + "panels": [], + "title": "Publish", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 29 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(mt_publish_total{message_type=~\"[[PublishMessageTypes]]\"}[5m])", + "interval": "", + "legendFormat": "{{message_type}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Publish Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:1183", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:1184", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 7, + "x": 8, + "y": 29 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(mt_send_total{message_type=~\"[[SendMessageTypes]]\"}[5m])", + "interval": "", + "legendFormat": "{{message_type}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Send Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:1183", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:1184", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 7, + "x": 15, + "y": 29 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "avg": false, + "current": false, + "hideEmpty": true, + "hideZero": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(mt_receive_total[5m])", + "interval": "", + "legendFormat": "{{endpoint_address}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Receive Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:1183", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:1184", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 22, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "$$hashKey": "object:1733", + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus", + "definition": "label_values(consumer_type)", + "hide": 0, + "includeAll": true, + "index": -1, + "label": null, + "multi": false, + "name": "ConsumerType", + "options": [ + { + "$$hashKey": "object:1733", + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "$$hashKey": "object:1734", + "selected": false, + "text": "AllocateInventoryActivity", + "value": "AllocateInventoryActivity" + }, + { + "$$hashKey": "object:1735", + "selected": false, + "text": "AllocateInventoryConsumer", + "value": "AllocateInventoryConsumer" + }, + { + "$$hashKey": "object:1736", + "selected": false, + "text": "AllocationState", + "value": "AllocationState" + }, + { + "$$hashKey": "object:1737", + "selected": false, + "text": "CancelScheduledMessageConsumer", + "value": "CancelScheduledMessageConsumer" + }, + { + "$$hashKey": "object:1738", + "selected": false, + "text": "FulfillOrderConsumer", + "value": "FulfillOrderConsumer" + }, + { + "$$hashKey": "object:1739", + "selected": false, + "text": "InventoryAllocated_", + "value": "InventoryAllocated_" + }, + { + "$$hashKey": "object:1740", + "selected": false, + "text": "OrderState", + "value": "OrderState" + }, + { + "$$hashKey": "object:1741", + "selected": false, + "text": "PaymentActivity", + "value": "PaymentActivity" + }, + { + "$$hashKey": "object:1742", + "selected": false, + "text": "RoutingSlipBatchEventConsumer", + "value": "RoutingSlipBatchEventConsumer" + }, + { + "$$hashKey": "object:1743", + "selected": false, + "text": "RoutingSlipCompleted_", + "value": "RoutingSlipCompleted_" + }, + { + "$$hashKey": "object:1744", + "selected": false, + "text": "RoutingSlipEventConsumer", + "value": "RoutingSlipEventConsumer" + }, + { + "$$hashKey": "object:1745", + "selected": false, + "text": "ScheduleMessageConsumer", + "value": "ScheduleMessageConsumer" + }, + { + "$$hashKey": "object:1746", + "selected": false, + "text": "SubmitOrderConsumer", + "value": "SubmitOrderConsumer" + } + ], + "query": "label_values(consumer_type)", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "$$hashKey": "object:1623", + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus", + "definition": "label_values(message_type)", + "hide": 0, + "includeAll": true, + "index": -1, + "label": null, + "multi": false, + "name": "MessageType", + "options": [ + { + "$$hashKey": "object:1623", + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "$$hashKey": "object:1624", + "selected": false, + "text": "AllocateInventory", + "value": "AllocateInventory" + }, + { + "$$hashKey": "object:1625", + "selected": false, + "text": "AllocateInventoryArguments", + "value": "AllocateInventoryArguments" + }, + { + "$$hashKey": "object:1626", + "selected": false, + "text": "AllocateInventoryLog", + "value": "AllocateInventoryLog" + }, + { + "$$hashKey": "object:1627", + "selected": false, + "text": "AllocationCreated", + "value": "AllocationCreated" + }, + { + "$$hashKey": "object:1628", + "selected": false, + "text": "AllocationHoldDurationExpired", + "value": "AllocationHoldDurationExpired" + }, + { + "$$hashKey": "object:1629", + "selected": false, + "text": "AllocationReleaseRequested", + "value": "AllocationReleaseRequested" + }, + { + "$$hashKey": "object:1630", + "selected": false, + "text": "Batch`1", + "value": "Batch`1" + }, + { + "$$hashKey": "object:1631", + "selected": false, + "text": "CancelScheduledMessage", + "value": "CancelScheduledMessage" + }, + { + "$$hashKey": "object:1632", + "selected": false, + "text": "CheckOrder", + "value": "CheckOrder" + }, + { + "$$hashKey": "object:1633", + "selected": false, + "text": "FulfillOrder", + "value": "FulfillOrder" + }, + { + "$$hashKey": "object:1634", + "selected": false, + "text": "InventoryAllocated", + "value": "InventoryAllocated" + }, + { + "$$hashKey": "object:1635", + "selected": false, + "text": "OrderAccepted", + "value": "OrderAccepted" + }, + { + "$$hashKey": "object:1636", + "selected": false, + "text": "OrderFulfillmentCompleted", + "value": "OrderFulfillmentCompleted" + }, + { + "$$hashKey": "object:1637", + "selected": false, + "text": "OrderFulfillmentFaulted", + "value": "OrderFulfillmentFaulted" + }, + { + "$$hashKey": "object:1638", + "selected": false, + "text": "OrderNotFound", + "value": "OrderNotFound" + }, + { + "$$hashKey": "object:1639", + "selected": false, + "text": "OrderStatus", + "value": "OrderStatus" + }, + { + "$$hashKey": "object:1640", + "selected": false, + "text": "OrderSubmissionAccepted", + "value": "OrderSubmissionAccepted" + }, + { + "$$hashKey": "object:1641", + "selected": false, + "text": "OrderSubmitted", + "value": "OrderSubmitted" + }, + { + "$$hashKey": "object:1642", + "selected": false, + "text": "PaymentArguments", + "value": "PaymentArguments" + }, + { + "$$hashKey": "object:1766", + "selected": false, + "text": "RoutingSlip", + "value": "RoutingSlip" + }, + { + "$$hashKey": "object:1767", + "selected": false, + "text": "RoutingSlipActivityCompensated", + "value": "RoutingSlipActivityCompensated" + }, + { + "$$hashKey": "object:1768", + "selected": false, + "text": "RoutingSlipActivityCompleted", + "value": "RoutingSlipActivityCompleted" + }, + { + "$$hashKey": "object:1769", + "selected": false, + "text": "RoutingSlipActivityFaulted", + "value": "RoutingSlipActivityFaulted" + }, + { + "$$hashKey": "object:1770", + "selected": false, + "text": "RoutingSlipCompleted", + "value": "RoutingSlipCompleted" + }, + { + "$$hashKey": "object:1771", + "selected": false, + "text": "RoutingSlipFaulted", + "value": "RoutingSlipFaulted" + }, + { + "$$hashKey": "object:1772", + "selected": false, + "text": "Scheduled", + "value": "Scheduled" + }, + { + "$$hashKey": "object:1773", + "selected": false, + "text": "ScheduleMessage", + "value": "ScheduleMessage" + }, + { + "$$hashKey": "object:1774", + "selected": false, + "text": "ScheduleMessage`1", + "value": "ScheduleMessage`1" + }, + { + "$$hashKey": "object:1775", + "selected": false, + "text": "SubmitOrder", + "value": "SubmitOrder" + } + ], + "query": "label_values(message_type)", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "$$hashKey": "object:3123", + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus", + "definition": "label_values(mt_publish_total,message_type)", + "hide": 0, + "includeAll": true, + "index": -1, + "label": null, + "multi": false, + "name": "PublishMessageTypes", + "options": [ + { + "$$hashKey": "object:3123", + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "$$hashKey": "object:3124", + "selected": false, + "text": "OrderSubmitted", + "value": "OrderSubmitted" + }, + { + "$$hashKey": "object:3125", + "selected": false, + "text": "AllocateInventory", + "value": "AllocateInventory" + }, + { + "$$hashKey": "object:3126", + "selected": false, + "text": "RoutingSlipActivityCompleted", + "value": "RoutingSlipActivityCompleted" + }, + { + "$$hashKey": "object:3127", + "selected": false, + "text": "AllocationCreated", + "value": "AllocationCreated" + }, + { + "$$hashKey": "object:3128", + "selected": false, + "text": "RoutingSlipCompleted", + "value": "RoutingSlipCompleted" + }, + { + "$$hashKey": "object:3129", + "selected": false, + "text": "RoutingSlipActivityFaulted", + "value": "RoutingSlipActivityFaulted" + }, + { + "$$hashKey": "object:3130", + "selected": false, + "text": "AllocationReleaseRequested", + "value": "AllocationReleaseRequested" + }, + { + "$$hashKey": "object:3131", + "selected": false, + "text": "RoutingSlipFaulted", + "value": "RoutingSlipFaulted" + }, + { + "$$hashKey": "object:3132", + "selected": false, + "text": "RoutingSlipActivityCompensated", + "value": "RoutingSlipActivityCompensated" + } + ], + "query": "label_values(mt_publish_total,message_type)", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "$$hashKey": "object:3040", + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus", + "definition": "label_values(mt_send_total,message_type)", + "hide": 0, + "includeAll": true, + "index": -1, + "label": null, + "multi": false, + "name": "SendMessageTypes", + "options": [ + { + "$$hashKey": "object:3040", + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "$$hashKey": "object:3041", + "selected": false, + "text": "OrderSubmissionAccepted", + "value": "OrderSubmissionAccepted" + }, + { + "$$hashKey": "object:3042", + "selected": false, + "text": "FulfillOrder", + "value": "FulfillOrder" + }, + { + "$$hashKey": "object:3043", + "selected": false, + "text": "OrderStatus", + "value": "OrderStatus" + }, + { + "$$hashKey": "object:3044", + "selected": false, + "text": "RoutingSlip", + "value": "RoutingSlip" + }, + { + "$$hashKey": "object:3045", + "selected": false, + "text": "ScheduleMessage`1", + "value": "ScheduleMessage`1" + }, + { + "$$hashKey": "object:3046", + "selected": false, + "text": "InventoryAllocated", + "value": "InventoryAllocated" + }, + { + "$$hashKey": "object:3047", + "selected": false, + "text": "RoutingSlipCompleted", + "value": "RoutingSlipCompleted" + }, + { + "$$hashKey": "object:3048", + "selected": false, + "text": "RoutingSlipFaulted", + "value": "RoutingSlipFaulted" + }, + { + "$$hashKey": "object:3049", + "selected": false, + "text": "CancelScheduledMessage", + "value": "CancelScheduledMessage" + }, + { + "$$hashKey": "object:3050", + "selected": false, + "text": "Scheduled", + "value": "Scheduled" + }, + { + "$$hashKey": "object:3051", + "selected": false, + "text": "OrderNotFound", + "value": "OrderNotFound" + } + ], + "query": "label_values(mt_send_total,message_type)", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "$$hashKey": "object:6865", + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus", + "definition": "label_values(mt_saga_in_progress,consumer_type)", + "hide": 0, + "includeAll": true, + "index": -1, + "label": null, + "multi": false, + "name": "SagaType", + "options": [ + { + "$$hashKey": "object:6865", + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "$$hashKey": "object:6866", + "selected": false, + "text": "OrderState", + "value": "OrderState" + }, + { + "$$hashKey": "object:6867", + "selected": false, + "text": "AllocationState", + "value": "AllocationState" + } + ], + "query": "label_values(mt_saga_in_progress,consumer_type)", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "$$hashKey": "object:6579", + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus", + "definition": "label_values(mt_saga_in_progress,message_type)", + "hide": 0, + "includeAll": true, + "index": -1, + "label": null, + "multi": false, + "name": "SagaMessageType", + "options": [ + { + "$$hashKey": "object:6579", + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "$$hashKey": "object:6580", + "selected": false, + "text": "OrderSubmitted", + "value": "OrderSubmitted" + }, + { + "$$hashKey": "object:6581", + "selected": false, + "text": "CheckOrder", + "value": "CheckOrder" + }, + { + "$$hashKey": "object:6582", + "selected": false, + "text": "OrderAccepted", + "value": "OrderAccepted" + }, + { + "$$hashKey": "object:6583", + "selected": false, + "text": "AllocationCreated", + "value": "AllocationCreated" + }, + { + "$$hashKey": "object:6584", + "selected": false, + "text": "OrderFulfillmentCompleted", + "value": "OrderFulfillmentCompleted" + }, + { + "$$hashKey": "object:6585", + "selected": false, + "text": "OrderFulfillmentFaulted", + "value": "OrderFulfillmentFaulted" + }, + { + "$$hashKey": "object:6586", + "selected": false, + "text": "AllocationReleaseRequested", + "value": "AllocationReleaseRequested" + }, + { + "$$hashKey": "object:6587", + "selected": false, + "text": "AllocationHoldDurationExpired", + "value": "AllocationHoldDurationExpired" + } + ], + "query": "label_values(mt_saga_in_progress,message_type)", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "MassTransit Sample", + "uid": "vOtk-UjWz", + "variables": { + "list": [] + }, + "version": 1 + } diff --git a/configs/grafana/datasources.yml b/configs/grafana/datasources.yml new file mode 100644 index 0000000..4a587e2 --- /dev/null +++ b/configs/grafana/datasources.yml @@ -0,0 +1,44 @@ +apiVersion: 1 + +datasources: + # name of the datasource. Required + - name: prometheus + # datasource type. Required + type: prometheus + # access mode. direct or proxy. Required + access: proxy + # org id. will default to orgId 1 if not specified + orgId: 1 + # url + url: http://prometheus:9090 + # database password, if used + # password: + # database user, if used + # user: + # database name, if used + # database: + # enable/disable basic auth + # basicAuth: + # basic auth username + # basicAuthUser: + # basic auth password + # basicAuthPassword: + # enable/disable with credentials headers + # withCredentials: + # mark as default datasource. Max one per org + isDefault: true + # fields that will be converted to json and stored in json_data + # jsonData: + # graphiteVersion: "1.1" + # tlsAuth: true + # tlsAuthWithCACert: true + # httpHeaderName1: "Authorization" + # json object of data that will be encrypted. + # secureJsonData: + # tlsCACert: "..." + # tlsClientCert: "..." + # tlsClientKey: "..." + # httpHeaderValue1: "Bearer xf5yhfkpsnmgo" + version: 1 + # allow users to edit datasources from the UI. + editable: false \ No newline at end of file diff --git a/configs/prometheus.yml b/configs/prometheus.yml new file mode 100644 index 0000000..5ddca0f --- /dev/null +++ b/configs/prometheus.yml @@ -0,0 +1,27 @@ +global: + scrape_interval: 5s + +alerting: + alertmanagers: + - static_configs: + - targets: + # - 'alertmanager:9093' + +# Load rules once and periodically evaluate them according to the global 'evaluation_interval'. +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" + +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'rabbitmq-server' + static_configs: + - targets: + - 'rabbitmq:15692' + - job_name: 'api-service' + static_configs: + - targets: + - 'api_service:80' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f7ca997 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,123 @@ +version: '3.2' + +services: + rabbitmq: + image: masstransit/rabbitmq + container_name: rabbitmq + hostname: rabbitmq + ports: + - 5672:5672 + - 15672:15672 + + database: + image: postgres:latest + restart: always + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "postgres" + ports: + - 5432:5432 + volumes: + - "./data:/var/lib/postgresql/data" + + api_service: + image: api_service + container_name: api_service + restart: always + volumes: + - "./scripts/wait-for-it.sh:/app/wait-for-it.sh" + - "./appsettings/api_service.json:/app/appsettings.json" + ports: + - 80:80 + depends_on: + - rabbitmq + entrypoint: ["./wait-for-it.sh", "-s", "rabbitmq:5672", "--", "dotnet", "ApiService.dll"] + + cart_service: + image: cart_service + container_name: cart_service + restart: always + volumes: + - "./scripts/wait-for-it.sh:/app/wait-for-it.sh" + - "./appsettings/cart_service.json:/app/appsettings.json" + depends_on: + - rabbitmq + - database + entrypoint: ["./wait-for-it.sh", "-s", "rabbitmq:5672", "--", "dotnet", "CartService.dll"] + + delivery_service: + image: delivery_service + container_name: delivery_service + restart: always + volumes: + - "./scripts/wait-for-it.sh:/app/wait-for-it.sh" + - "./appsettings/delivery_service.json:/app/appsettings.json" + depends_on: + - rabbitmq + entrypoint: ["./wait-for-it.sh", "-s", "rabbitmq:5672", "--", "dotnet", "DeliveryService.dll"] + + + feedback_service: + image: feedback_service + container_name: feedback_service + restart: always + volumes: + - "./scripts/wait-for-it.sh:/app/wait-for-it.sh" + - "./appsettings/feedback_service.json:/app/appsettings.json" + depends_on: + - rabbitmq + - database + entrypoint: ["./wait-for-it.sh", "-s", "rabbitmq:5672", "--", "dotnet", "FeedbackService.dll"] + + + history_service: + image: history_service + container_name: history_service + restart: always + volumes: + - "./scripts/wait-for-it.sh:/app/wait-for-it.sh" + - "./appsettings/history_service.json:/app/appsettings.json" + depends_on: + - rabbitmq + - database + entrypoint: ["./wait-for-it.sh", "-s", "rabbitmq:5672", "--", "dotnet", "HistoryService.dll"] + + orchestrator_service: + image: orchestrator_service + container_name: orchestrator_service + restart: always + volumes: + - "./scripts/wait-for-it.sh:/app/wait-for-it.sh" + - "./appsettings/orchestrator_service.json:/app/appsettings.json" + depends_on: + - rabbitmq + - database + entrypoint: ["./wait-for-it.sh", "-s", "rabbitmq:5672", "--", "dotnet", "OrderOrchestratorService.dll"] + + payment_service: + image: payment_service + container_name: payment_service + restart: always + volumes: + - "./scripts/wait-for-it.sh:/app/wait-for-it.sh" + - "./appsettings/payment_service.json:/app/appsettings.json" + depends_on: + - rabbitmq + - database + entrypoint: ["./wait-for-it.sh", "-s", "rabbitmq:5672", "--", "dotnet", "PaymentService.dll"] + + prometheus: + image: prom/prometheus + ports: + - 9090:9090 + volumes: + - "./configs/prometheus.yml:/etc/prometheus/prometheus.yml" + + grafana: + image: grafana/grafana + ports: + - 3000:3000 + volumes: + - "./configs/grafana/dashboards.yml:/etc/grafana/provisioning/dashboards/rabbitmq.yaml" + - "./configs/grafana/datasources.yml:/etc/grafana/provisioning/datasources/prometheus.yaml" + - "./configs/grafana/dashboards:/dashboards" \ No newline at end of file diff --git a/scripts/buildservice.sh b/scripts/buildservice.sh new file mode 100644 index 0000000..11a631b --- /dev/null +++ b/scripts/buildservice.sh @@ -0,0 +1,5 @@ +SERVICE_NAME=$1 +SERVICE_PROJECT=$2 + +docker build -t $SERVICE_NAME src/$SERVICE_PROJECT +cp src/$SERVICE_PROJECT/appsettings.json appsettings/$SERVICE_NAME.json \ No newline at end of file diff --git a/scripts/wait-for-it.sh b/scripts/wait-for-it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/scripts/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/specs/saga_workflow.def b/specs/saga_workflow.def new file mode 100644 index 0000000..9351475 --- /dev/null +++ b/specs/saga_workflow.def @@ -0,0 +1,21 @@ +autogen definitions fsm; + +method = case; +type = loop; +prefix = server_fsm; + +state = initial, cart_request_pending, money_reservation_request_pending, awaiting_confirmation, money_unreservation_request_pending, archive_order_request_pending, finalized, awaiting_delivery, awaiting_feedback, add_feedback_request_pending; +event = order_submitted, cart_request_completed, money_reservation_request_completed, order_rejected, money_unreservation_request_completed, archive_order_request_completed, order_confirmed, order_delivered, received_feedback, feedback_receiving_timeout, add_feedback_request_completed; + +transition = +{ tst = initial; tev = order_submitted; next = cart_request_pending; } +{ tst = cart_request_pending; tev = cart_request_completed; next = money_reservation_request_pending; } +{ tst = money_reservation_request_pending; tev = money_reservation_request_completed; next = awaiting_confirmation; } +{ tst = awaiting_confirmation; tev = order_rejected; next = money_unreservation_request_pending; } +{ tst = money_unreservation_request_pending; tev = money_reservation_request_completed; next = archive_order_request_pending; } +{ tst = archive_order_request_pending; tev = archive_order_request_completed; next = finalized; } +{ tst = awaiting_confirmation; tev = order_confirmed; next = awaiting_delivery; } +{ tst = awaiting_delivery; tev = order_delivered; next = awaiting_feedback; } +{ tst = awaiting_feedback; tev = received_feedback; next = add_feedback_request_pending; } +{ tst = awaiting_feedback; tev = feedback_receiving_timeout; next = archive_order_request_pending; } +{ tst = add_feedback_request_pending; tev = add_feedback_request_completed; next = archive_order_request_pending; } \ No newline at end of file diff --git a/src/ApiService/ApiService.csproj b/src/ApiService/ApiService.csproj new file mode 100644 index 0000000..bd55db2 --- /dev/null +++ b/src/ApiService/ApiService.csproj @@ -0,0 +1,27 @@ + + + + enable + net6.0 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ApiService/Configurations/RabbitMqConfiguration.cs b/src/ApiService/Configurations/RabbitMqConfiguration.cs new file mode 100644 index 0000000..dd0acee --- /dev/null +++ b/src/ApiService/Configurations/RabbitMqConfiguration.cs @@ -0,0 +1,9 @@ +namespace ApiService.Configurations; + +public class RabbitMqConfiguration +{ + public string? Hostname { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } +} \ No newline at end of file diff --git a/src/ApiService/Consumers/FeedbackRequestedConsumer.cs b/src/ApiService/Consumers/FeedbackRequestedConsumer.cs new file mode 100644 index 0000000..43c4749 --- /dev/null +++ b/src/ApiService/Consumers/FeedbackRequestedConsumer.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using ApiService.Contracts.UserApi; +using MassTransit; + +namespace ApiService.Consumers +{ + public class FeedbackRequestedConsumer : IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } +} diff --git a/src/ApiService/Consumers/FeedbackRequestedConsumerDefinition.cs b/src/ApiService/Consumers/FeedbackRequestedConsumerDefinition.cs new file mode 100644 index 0000000..d9b0ec9 --- /dev/null +++ b/src/ApiService/Consumers/FeedbackRequestedConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace ApiService.Consumers +{ + public class FeedbackRequestedConsumerDefinition : ConsumerDefinition + { + public FeedbackRequestedConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + endpointConfigurator.UseDelayedRedelivery(r => r.Interval(5, 1000)); + endpointConfigurator.UseMessageRetry(r => r.Interval(5, 5000)); + endpointConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/ApiService/Consumers/GetArchivedOrderResponseConsumer.cs b/src/ApiService/Consumers/GetArchivedOrderResponseConsumer.cs new file mode 100644 index 0000000..c911481 --- /dev/null +++ b/src/ApiService/Consumers/GetArchivedOrderResponseConsumer.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using MassTransit; +using Microsoft.Extensions.Logging; + +namespace ApiService.Consumers +{ + public class GetArchivedOrderResponseConsumer : IConsumer + { + private readonly ILogger _logger; + + public GetArchivedOrderResponseConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation($"[ArchivedOrder] {context.Message.OrderId} " + + $"{context.Message.FeedbackText} {context.Message.FeedbackStars}" + + $"{context.Message.ConfirmDate} { context.Message.Cart.Count}"); + return Task.CompletedTask; + } + } +} diff --git a/src/ApiService/Consumers/GetArchivedOrderResponseConsumerDefinition.cs b/src/ApiService/Consumers/GetArchivedOrderResponseConsumerDefinition.cs new file mode 100644 index 0000000..6d124f7 --- /dev/null +++ b/src/ApiService/Consumers/GetArchivedOrderResponseConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace ApiService.Consumers +{ + public class GetArchivedOrderResponseConsumerDefinition : ConsumerDefinition + { + public GetArchivedOrderResponseConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + endpointConfigurator.UseDelayedRedelivery(r => r.Interval(5, 1000)); + endpointConfigurator.UseMessageRetry(r => r.Interval(5, 5000)); + endpointConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/ApiService/Consumers/NewOrderConfirmationRequestedConsumer.cs b/src/ApiService/Consumers/NewOrderConfirmationRequestedConsumer.cs new file mode 100644 index 0000000..c23d352 --- /dev/null +++ b/src/ApiService/Consumers/NewOrderConfirmationRequestedConsumer.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using MassTransit; +using Microsoft.Extensions.Logging; + +namespace ApiService.Consumers +{ + public class NewOrderConfirmationRequestedConsumer : IConsumer + { + private readonly ILogger _logger; + + public NewOrderConfirmationRequestedConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation($"NewOrderConfirmationRequested {context.Message.OrderId}"); + return Task.CompletedTask; + } + } +} diff --git a/src/ApiService/Consumers/NewOrderConfirmationRequestedConsumerDefinition.cs b/src/ApiService/Consumers/NewOrderConfirmationRequestedConsumerDefinition.cs new file mode 100644 index 0000000..3240ac1 --- /dev/null +++ b/src/ApiService/Consumers/NewOrderConfirmationRequestedConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace ApiService.Consumers +{ + public class NewOrderConfirmationRequestedConsumerDefinition : ConsumerDefinition + { + public NewOrderConfirmationRequestedConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + endpointConfigurator.UseDelayedRedelivery(r => r.Interval(5, 1000)); + endpointConfigurator.UseMessageRetry(r => r.Interval(5, 5000)); + endpointConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/ApiService/Consumers/OrderRejectedConsumer.cs b/src/ApiService/Consumers/OrderRejectedConsumer.cs new file mode 100644 index 0000000..a3d001c --- /dev/null +++ b/src/ApiService/Consumers/OrderRejectedConsumer.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using ApiService.Contracts.UserApi; +using MassTransit; +using Microsoft.Extensions.Logging; + +namespace ApiService.Consumers +{ + public class OrderRejectedConsumer : IConsumer + { + private readonly ILogger _logger; + + public OrderRejectedConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation($"OrderRejected {context.Message.OrderId}"); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/ApiService/Consumers/OrderRejectedConsumerDefinition.cs b/src/ApiService/Consumers/OrderRejectedConsumerDefinition.cs new file mode 100644 index 0000000..a3f19ed --- /dev/null +++ b/src/ApiService/Consumers/OrderRejectedConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace ApiService.Consumers +{ + public class OrderRejectedConsumerDefinition : ConsumerDefinition + { + public OrderRejectedConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + endpointConfigurator.UseDelayedRedelivery(r => r.Interval(5, 1000)); + endpointConfigurator.UseMessageRetry(r => r.Interval(5, 5000)); + endpointConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/ApiService/Controllers/OrdersController.cs b/src/ApiService/Controllers/OrdersController.cs new file mode 100644 index 0000000..02d39fe --- /dev/null +++ b/src/ApiService/Controllers/OrdersController.cs @@ -0,0 +1,258 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using ApiService.Contracts.MonitoringApi; +using ApiService.Contracts.UserApi; +using ApiService.Models.Interfaces; +using CartService.Contracts; +using MassTransit; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace ApiService.Controllers +{ + [ApiController] + [Route("api/orders")] + public class OrdersController : ControllerBase + { + private readonly ISendEndpointProvider _sendEndpointProvider; + private readonly IRoutingConfiguration _routingConfiguration; + private readonly IPublishEndpoint _publishEndpoint; + private readonly IRequestClient _allOrderStatesClient; + private readonly IRequestClient _orderStateClient; + private readonly IRequestClient _archiverOrderClient; + private readonly IRequestClient _abortOrderClient; + private readonly ILogger _logger; + + public OrdersController(ISendEndpointProvider sendEndpointProvider, + IRoutingConfiguration routingConfiguration, + IPublishEndpoint publishEndpoint, + IRequestClient allOrderStatesClient, + IRequestClient orderStateClient, + IRequestClient archiverOrderClient, + IRequestClient abortOrderClient, + ILogger logger) + { + _sendEndpointProvider = sendEndpointProvider; + _routingConfiguration = routingConfiguration; + _publishEndpoint = publishEndpoint; + _allOrderStatesClient = allOrderStatesClient; + _orderStateClient = orderStateClient; + _archiverOrderClient = archiverOrderClient; + _abortOrderClient = abortOrderClient; + _logger = logger; + } + + [HttpPost] + [Route("{id}/cart-position/add")] + public async Task PostCartPosition([FromRoute][Required] Guid id, + [FromQuery][Required] string name, + [FromQuery][Required] int amount) + { + try + { + var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri(_routingConfiguration.CartServiceAddress!)); + + await endpoint.Send(new + { + OrderId = id, + Amount = amount, + Name = name, + }); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, e); + } + } + + [HttpPost] + [Route("{id}/submit")] + public async Task PostCartPosition([FromRoute][Required] Guid id) + { + try + { + await _publishEndpoint.Publish(new + { + OrderId = id + }); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, e); + } + } + + [HttpPost] + [Route("{id}/confirm")] + public async Task SubmitOrder([FromRoute][Required] Guid id, + [FromQuery][Required] string name) + { + try + { + await _publishEndpoint.Publish(new + { + OrderId = id, + ConfirmManager = name + }); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + + return StatusCode(500, e); + } + } + + [HttpPost] + [Route("{id}/reject")] + public async Task RejectOrder([FromRoute][Required] Guid id, + [FromQuery][Required] string name, + [FromQuery][Required] string reason) + { + try + { + await _publishEndpoint.Publish(new + { + OrderId = id, + RejectManager = name, + Reason = reason + }); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + + return StatusCode(500, e); + } + } + + [HttpGet] + [Route("")] + public async Task GetAllOrdersState() + { + try + { + var response = await _allOrderStatesClient.GetResponse(new { }); + + return Ok(response.Message); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + + return StatusCode(500, e); + } + } + + [HttpGet] + [Route("{id}/state")] + public async Task GetOrderState([FromRoute][Required] Guid id) + { + try + { + var response = await _orderStateClient.GetResponse(new + { + OrderId = id + }); + + return Ok(response.Message); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + + return StatusCode(500, e); + } + } + + [HttpPost] + [Route("{id}/send-feedback")] + public async Task GetOrderState([FromRoute][Required] Guid id, + [FromQuery][Required] string text, + [FromQuery][Required] int startAmount) + { + try + { + await _publishEndpoint.Publish(new + { + + OrderId = id, + Text = text, + StarsAmount = startAmount + }); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + + return StatusCode(500, e); + } + } + + [HttpPost] + [Route("{id}/archive")] + public async Task GetOrderArchiveState([FromRoute][Required] Guid id) + { + try + { + var stopWatch = new Stopwatch(); + stopWatch.Start(); + /*await _publishEndpoint.Publish(new + { + OrderId = id + });*/ + + var archivedOrder = (await _archiverOrderClient.GetResponse(new + { + OrderId = id + })).Message; + + stopWatch.Stop(); + + return Ok(new { archivedOrder, stopWatch.ElapsedMilliseconds}); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + + return StatusCode(500, e); + } + } + + [HttpPost] + [Route("{id}/abort")] + public async Task AbortOrder([FromRoute][Required] Guid id) + { + try + { + var abortedOrder = (await _abortOrderClient.GetResponse(new + { + OrderId = id + })).Message; + + return Ok(abortedOrder); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + + return StatusCode(500, e); + } + } + } +} diff --git a/src/ApiService/Dockerfile b/src/ApiService/Dockerfile new file mode 100644 index 0000000..289d714 --- /dev/null +++ b/src/ApiService/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +COPY ./bin/Release/net6.0 . + +EXPOSE 80 + +ENTRYPOINT ["dotnet", "ApiService.dll"] \ No newline at end of file diff --git a/src/ApiService/Models/Implementations/RoutingConfiguration.cs b/src/ApiService/Models/Implementations/RoutingConfiguration.cs new file mode 100644 index 0000000..9d51512 --- /dev/null +++ b/src/ApiService/Models/Implementations/RoutingConfiguration.cs @@ -0,0 +1,11 @@ +using ApiService.Models.Interfaces; + +namespace ApiService.Models.Implementations +{ + public class RoutingConfiguration : IRoutingConfiguration + { + public string? CartServiceAddress { get; set; } + + public string? ApiServiceAddress { get; set; } + } +} diff --git a/src/ApiService/Models/Interfaces/IRoutingConfiguration.cs b/src/ApiService/Models/Interfaces/IRoutingConfiguration.cs new file mode 100644 index 0000000..daff667 --- /dev/null +++ b/src/ApiService/Models/Interfaces/IRoutingConfiguration.cs @@ -0,0 +1,8 @@ +namespace ApiService.Models.Interfaces +{ + public interface IRoutingConfiguration + { + string? CartServiceAddress { get; set; } + string? ApiServiceAddress { get; set; } + } +} diff --git a/src/ApiService/Program.cs b/src/ApiService/Program.cs new file mode 100644 index 0000000..2a456c2 --- /dev/null +++ b/src/ApiService/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace ApiService +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/ApiService/Properties/launchSettings.json b/src/ApiService/Properties/launchSettings.json new file mode 100644 index 0000000..a2f62dd --- /dev/null +++ b/src/ApiService/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:57478", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "UserWebApi": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/ApiService/Startup.cs b/src/ApiService/Startup.cs new file mode 100644 index 0000000..9b01eb1 --- /dev/null +++ b/src/ApiService/Startup.cs @@ -0,0 +1,113 @@ +using ApiService.Configurations; +using ApiService.Consumers; +using ApiService.Contracts.ManagerApi; +using ApiService.Contracts.MonitoringApi; +using ApiService.Contracts.UserApi; +using ApiService.Models.Implementations; +using ApiService.Models.Interfaces; +using MassTransit; +using MassTransit.PrometheusIntegration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Prometheus; + +namespace ApiService +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + + services.AddControllers(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "UserWebApi", Version = "v1" }); + }); + + var routingSection = Configuration.GetSection("RoutingConfiguration"); + var routingConfig = routingSection.Get(); + + var rabbitMqSection = Configuration.GetSection("RabbitMqConfiguration"); + var rabbitMqConfig = rabbitMqSection.Get(); + + services.AddTransient(services => routingConfig); + + services.AddMassTransit(cfg => + { + cfg.AddConsumer(typeof(NewOrderConfirmationRequestedConsumerDefinition)) + .Endpoint(e => + { + e.Name = routingConfig.ApiServiceAddress; + }); + + cfg.AddConsumer(typeof(OrderRejectedConsumerDefinition)) + .Endpoint(e => + { + e.Name = routingConfig.ApiServiceAddress; + }); + + cfg.AddConsumer(typeof(FeedbackRequestedConsumerDefinition)) + .Endpoint(e => + { + e.Name = routingConfig.ApiServiceAddress; + }); + + cfg.AddConsumer(typeof(GetArchivedOrderResponseConsumerDefinition)) + .Endpoint(e => + { + e.Name = routingConfig.ApiServiceAddress; + }); + + cfg.AddRequestClient(); + cfg.AddRequestClient(); + cfg.AddRequestClient(); + cfg.AddRequestClient(); + + cfg.UsingRabbitMq((context, config) => + { + config.UseBsonSerializer(); + config.ConfigureEndpoints(context); + + config.Host(rabbitMqConfig.Hostname, rabbitMqConfig.VirtualHost, h => + { + h.Username(rabbitMqConfig.Username); + h.Password(rabbitMqConfig.Password); + }); + + config.UsePrometheusMetrics(serviceName: "api_service"); + }); + }) + .AddMassTransitHostedService(true); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "UserWebApi v1")); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapMetrics(); + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/ApiService/appsettings.Development.json b/src/ApiService/appsettings.Development.json new file mode 100644 index 0000000..1073e90 --- /dev/null +++ b/src/ApiService/appsettings.Development.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "MassTransit": "Debug", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "RoutingConfiguration": { + "ApiServiceAddress": "api-service", + "CartServiceAddress": "exchange:cart-service" + }, + "RabbitMqConfiguration": { + "Hostname": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + }, + "AllowedHosts": "*" +} diff --git a/src/ApiService/appsettings.json b/src/ApiService/appsettings.json new file mode 100644 index 0000000..ee5f1e0 --- /dev/null +++ b/src/ApiService/appsettings.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "MassTransit": "Debug", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "RoutingConfiguration": { + "ApiServiceAddress": "api-service", + "CartServiceAddress": "exchange:cart-service" + }, + "RabbitMqConfiguration": { + "Hostname": "rabbitmq", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + }, + "AllowedHosts": "*" +} diff --git a/src/CartService/CartService.csproj b/src/CartService/CartService.csproj new file mode 100644 index 0000000..3dbc5ec --- /dev/null +++ b/src/CartService/CartService.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + true + dotnet-CartService-85D9867A-C62D-4064-A417-4FF1079C3880 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/CartService/Configurations/EndpointsConfiguration.cs b/src/CartService/Configurations/EndpointsConfiguration.cs new file mode 100644 index 0000000..11e571b --- /dev/null +++ b/src/CartService/Configurations/EndpointsConfiguration.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Configurations +{ + public class EndpointsConfiguration + { + public string? CartServiceAddress { get; set; } + } +} diff --git a/src/CartService/Configurations/RabbitMqConfiguration.cs b/src/CartService/Configurations/RabbitMqConfiguration.cs new file mode 100644 index 0000000..d9f92e5 --- /dev/null +++ b/src/CartService/Configurations/RabbitMqConfiguration.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Configurations +{ + public class RabbitMqConfiguration + { + public string? Hostname { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + } +} diff --git a/src/CartService/Consumers/AddCartPositionConsumer.cs b/src/CartService/Consumers/AddCartPositionConsumer.cs new file mode 100644 index 0000000..493da03 --- /dev/null +++ b/src/CartService/Consumers/AddCartPositionConsumer.cs @@ -0,0 +1,68 @@ +using CartService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using CartService.Contracts; + +namespace CartService.Consumers +{ + public class AddCartPositionConsumer : IConsumer + { + + private readonly ILogger _logger; + private readonly ICartRepository _cartRepository; + private readonly IGoodRepository _goodRepository; + private readonly ICartPositionRepository _cartPositionRepository; + private readonly Random _random; + + public AddCartPositionConsumer(ILogger logger, + ICartRepository cartRepository, + IGoodRepository goodRepository, + ICartPositionRepository cartPositionRepository) + { + _random = new Random(); + + _logger = logger; + _cartRepository = cartRepository; + _goodRepository = goodRepository; + _cartPositionRepository = cartPositionRepository; + } + + public async Task Consume(ConsumeContext context) + { + + var newCartPosition = context.Message; + + if (!await _cartRepository.CartExistsAsync(newCartPosition.OrderId)) + { + await _cartRepository.AddCartAsync(newCartPosition.OrderId); + } + + if (!await _goodRepository.GoodExistsAsync(newCartPosition.Name)) + { + await _goodRepository.AddGoodAsync(Guid.NewGuid(), newCartPosition.Name, _random.Next(100, 150)); + } + + var cart = await _cartRepository.GetCartWithCartPositionsAsync(newCartPosition.OrderId); + + var cartPosition = cart.CartPositions!.FirstOrDefault(cp => cp.Good!.Name == newCartPosition.Name); + + if (cartPosition != null) + { + await _cartPositionRepository.UpdateCartPositionAsync(cartPosition.Id, cartPosition.CartId, cartPosition.GoodId, cartPosition.Amount + newCartPosition.Amount); + } + else + { + var good = await _goodRepository.GetGoodByNameAsync(newCartPosition.Name); + + await _cartPositionRepository.AddCartPositionAsync(Guid.NewGuid(), newCartPosition.OrderId, good.Id, newCartPosition.Amount); + } + + + + //to-do: respond + } + } +} diff --git a/src/CartService/Consumers/AddCartPositionConsumerDefinition.cs b/src/CartService/Consumers/AddCartPositionConsumerDefinition.cs new file mode 100644 index 0000000..c11ffbd --- /dev/null +++ b/src/CartService/Consumers/AddCartPositionConsumerDefinition.cs @@ -0,0 +1,27 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Consumers +{ + public class AddCartPositionConsumerDefinition : ConsumerDefinition + { + public AddCartPositionConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Immediate(5)); + consumerConfigurator.UseMessageRetry(r => r.Incremental(5, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/CartService/Consumers/GetCartConsumer.cs b/src/CartService/Consumers/GetCartConsumer.cs new file mode 100644 index 0000000..e9a253f --- /dev/null +++ b/src/CartService/Consumers/GetCartConsumer.cs @@ -0,0 +1,77 @@ +using CartService.Database.Models; +using CartService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CartService.Contracts; +using DbCartPosition = CartService.Database.Models.CartPosition; +using DtoCartPosition = Contracts.Shared.CartPosition; + +namespace CartService.Consumers +{ + public class GetCartConsumer : IConsumer + { + + private readonly ILogger _logger; + private readonly ICartRepository _cartRepository; + + public GetCartConsumer(ILogger logger, + ICartRepository cartRepository) + { + _logger = logger; + _cartRepository = cartRepository; + } + + public async Task Consume(ConsumeContext context) + { + if (context.RequestId != null) + { + var message = context.Message; + + var cart = await _cartRepository.GetCartWithCartPositionsAsync(message.OrderId); + + await context.RespondAsync(new + { + OrderId = message.OrderId, + CartContent = cart.CartPositions!.Select(c => ConvertToContractCartPosition(c)).ToList(), + TotalPrice = CountPrice(cart.CartPositions!.ToList()) + }) ; + } + } + + private int CountPrice(List cartPositions) + { + int totalPrice = 0; + + foreach (var position in cartPositions) + { + totalPrice += position.Good!.Price * position.Amount; + } + + return totalPrice; + } + + //private int CountPrice(List cartPositions) + //{ + // return cartPositions.Aggregate(0, (total, next) => + // { + // total += next.Amount * next.Good!.Price; + // return total; + // }); + + //} + + private DtoCartPosition ConvertToContractCartPosition(DbCartPosition dbCartPosition) + { + return new DtoCartPosition() + { + Amount = dbCartPosition.Amount, + Name = dbCartPosition.Good!.Name, + Price = dbCartPosition.Good!.Price + }; + } + } +} diff --git a/src/CartService/Consumers/GetCartConsumerDefinition.cs b/src/CartService/Consumers/GetCartConsumerDefinition.cs new file mode 100644 index 0000000..752c07b --- /dev/null +++ b/src/CartService/Consumers/GetCartConsumerDefinition.cs @@ -0,0 +1,27 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Consumers +{ + public class GetCartConsumerDefinition : ConsumerDefinition + { + public GetCartConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Exponential(5, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(12), TimeSpan.FromSeconds(1.5))); + consumerConfigurator.UseMessageRetry(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/CartService/Consumers/RemoveCartPositionConsumer.cs b/src/CartService/Consumers/RemoveCartPositionConsumer.cs new file mode 100644 index 0000000..84369d0 --- /dev/null +++ b/src/CartService/Consumers/RemoveCartPositionConsumer.cs @@ -0,0 +1,53 @@ +using CartService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CartService.Contracts; + +namespace CartService.Consumers +{ + public class RemoveCartPositionConsumer : IConsumer + { + + readonly ILogger _logger; + private readonly ICartRepository _cartRepository; + private readonly ICartPositionRepository _cartPositionRepository; + + public RemoveCartPositionConsumer(ILogger logger, + ICartRepository cartRepository, + IGoodRepository goodRepository, + ICartPositionRepository cartPositionRepository) + { + _logger = logger; + _cartRepository = cartRepository; + _cartPositionRepository = cartPositionRepository; + } + + public async Task Consume(ConsumeContext context) + { + var cartPositionForRemove = context.Message; + + var cart = await _cartRepository.GetCartWithCartPositionsAsync(cartPositionForRemove.OrderId); + + var cartPosition = cart.CartPositions!.FirstOrDefault(cp => cp.Good!.Name == cartPositionForRemove.Name); + + if (cartPosition != null) + { + if (cartPosition.Amount > cartPositionForRemove.Amount) + { + await _cartPositionRepository.UpdateCartPositionAsync(cartPosition.Id, cartPosition.CartId, cartPosition.GoodId, cartPosition.Amount - cartPositionForRemove.Amount); + } + else + { + await _cartPositionRepository.RemoveCartPositionAsync(cartPosition.Id); + } + } + + //to-do: respond + } + } +} diff --git a/src/CartService/Consumers/RemoveCartPositionConsumerDefinition.cs b/src/CartService/Consumers/RemoveCartPositionConsumerDefinition.cs new file mode 100644 index 0000000..0d6daea --- /dev/null +++ b/src/CartService/Consumers/RemoveCartPositionConsumerDefinition.cs @@ -0,0 +1,27 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Consumers +{ + public class RemoveCartPositionConsumerDefinition : ConsumerDefinition + { + public RemoveCartPositionConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Incremental(5, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2))); + consumerConfigurator.UseMessageRetry(r => r.Immediate(5)); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/CartService/Database/Configurations/CartConfiguration.cs b/src/CartService/Database/Configurations/CartConfiguration.cs new file mode 100644 index 0000000..881f20c --- /dev/null +++ b/src/CartService/Database/Configurations/CartConfiguration.cs @@ -0,0 +1,24 @@ +using CartService.Database.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Configurations +{ + internal class CartConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(c => c.Id).IsUnique(); + builder.HasKey(c => c.Id); + + builder.HasMany(c => c.CartPositions) + .WithOne(cp => cp.Cart!) + .HasForeignKey(cp => cp.CartId); + } + } +} diff --git a/src/CartService/Database/Configurations/CartPositionConfiguration.cs b/src/CartService/Database/Configurations/CartPositionConfiguration.cs new file mode 100644 index 0000000..be8e367 --- /dev/null +++ b/src/CartService/Database/Configurations/CartPositionConfiguration.cs @@ -0,0 +1,26 @@ +using CartService.Database.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Configurations +{ + public class CartPositionConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(cp => cp.Id); + builder.HasIndex(cp => cp.Id).IsUnique(); + + builder.HasOne(cp => cp.Good); + + builder.Property(cp => cp.CartId).IsRequired(); + builder.Property(cp => cp.GoodId).IsRequired(); + builder.Property(cp => cp.Amount).IsRequired(); + } + } +} diff --git a/src/CartService/Database/Configurations/GoodConfiguration.cs b/src/CartService/Database/Configurations/GoodConfiguration.cs new file mode 100644 index 0000000..2820394 --- /dev/null +++ b/src/CartService/Database/Configurations/GoodConfiguration.cs @@ -0,0 +1,23 @@ +using CartService.Database.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Configurations +{ + public class GoodConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(g => g.Id); + builder.HasIndex(g => g.Id).IsUnique(); + + builder.Property(g => g.Name).IsRequired(); + builder.Property(g => g.Price).IsRequired(); + } + } +} diff --git a/src/CartService/Database/Models/Cart.cs b/src/CartService/Database/Models/Cart.cs new file mode 100644 index 0000000..6a3ac90 --- /dev/null +++ b/src/CartService/Database/Models/Cart.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Models +{ + public class Cart + { + public Guid Id { get; set; } + + public ICollection? CartPositions { get; set; } + + public Cart(Guid id) + { + Id = id; + } + } +} diff --git a/src/CartService/Database/Models/CartPosition.cs b/src/CartService/Database/Models/CartPosition.cs new file mode 100644 index 0000000..9ab6c47 --- /dev/null +++ b/src/CartService/Database/Models/CartPosition.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Models +{ + public class CartPosition + { + public Guid Id { get; set; } + + public Guid CartId { get; set; } + + public Cart? Cart { get; set; } + + public Guid GoodId { get; set; } + + public Good? Good { get; set; } + + public int Amount { get; set; } + + public CartPosition(Guid id, + Guid cartId, + Guid goodId, + int amount) + { + Id = id; + CartId = cartId; + GoodId = goodId; + Amount = amount; + } + } +} diff --git a/src/CartService/Database/Models/Good.cs b/src/CartService/Database/Models/Good.cs new file mode 100644 index 0000000..3a2bb7e --- /dev/null +++ b/src/CartService/Database/Models/Good.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Models +{ + public class Good + { + public Guid Id { get; set; } + + public string? Name { get; set; } + + public int Price { get; set; } + + public Good(Guid id, + string? name, + int price) + { + Id = id; + Name = name; + Price = price; + } + } +} diff --git a/src/CartService/Database/NpgSqlContext.cs b/src/CartService/Database/NpgSqlContext.cs new file mode 100644 index 0000000..60ca622 --- /dev/null +++ b/src/CartService/Database/NpgSqlContext.cs @@ -0,0 +1,35 @@ +using CartService.Database.Configurations; +using CartService.Database.Models; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database +{ + public class NpgSqlContext : DbContext + { + public DbSet? Carts { get; set; } + public DbSet? CartPositions { get; set; } + public DbSet? Goods { get; set; } + + public NpgSqlContext() + { + + } + + public NpgSqlContext(DbContextOptions options) : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new CartConfiguration()); + modelBuilder.ApplyConfiguration(new CartPositionConfiguration()); + modelBuilder.ApplyConfiguration(new GoodConfiguration()); + } + } +} diff --git a/src/CartService/Database/Repositories/CartPositionRepository.cs b/src/CartService/Database/Repositories/CartPositionRepository.cs new file mode 100644 index 0000000..f166bcc --- /dev/null +++ b/src/CartService/Database/Repositories/CartPositionRepository.cs @@ -0,0 +1,65 @@ +using CartService.Database.Models; +using CartService.Database.Repositories.Interfaces; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Repositories +{ + public class CartPositionRepository : ICartPositionRepository + { + private readonly NpgSqlContext _dbContext; + + public CartPositionRepository(NpgSqlContext dbContext) + { + _dbContext = dbContext; + } + + public async Task AddCartPositionAsync(Guid id, Guid cartId, Guid goodId, int amount) + { + var cartPosition = new CartPosition(id, cartId, goodId, amount); + + await _dbContext.CartPositions!.AddAsync(cartPosition); + + await _dbContext.SaveChangesAsync(); + } + + public async Task CartPositionExistsForCartAsync(Guid cartId, string name) + { + var cart = await _dbContext.CartPositions! + .Include(cp => cp.Good) + .AsNoTracking() + .Where(cp => cp.CartId == cartId) + .FirstOrDefaultAsync(cp => cp.Good!.Name == name); + + return cart != null; + } + + public async Task RemoveCartPositionAsync(Guid id) + { + var cartPosition = await _dbContext.CartPositions!.FirstOrDefaultAsync(cp => cp.Id == id); + + _dbContext.Remove(cartPosition!); + + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateCartPositionAsync(Guid id, Guid cartId, Guid goodId, int amount) + { + var cartPosition = await _dbContext.CartPositions!.FirstOrDefaultAsync(cp => cp.Id == id); + + if (cartPosition != null) + { + cartPosition.Id = id; + cartPosition.CartId = cartId; + cartPosition.GoodId = goodId; + cartPosition.Amount = amount; + } + + await _dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/CartService/Database/Repositories/CartRepository.cs b/src/CartService/Database/Repositories/CartRepository.cs new file mode 100644 index 0000000..2e5f94b --- /dev/null +++ b/src/CartService/Database/Repositories/CartRepository.cs @@ -0,0 +1,51 @@ +using CartService.Database.Models; +using CartService.Database.Repositories.Interfaces; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Repositories +{ + public class CartRepository : ICartRepository + { + + private readonly NpgSqlContext _dbContext; + + public CartRepository(NpgSqlContext dbContext) + { + _dbContext = dbContext; + } + + public async Task AddCartAsync(Guid id) + { + var cart = new Cart(id); + + await _dbContext.Carts!.AddAsync(cart); + + await _dbContext.SaveChangesAsync(); + } + + public async Task GetCartWithCartPositionsAsync(Guid id) + { + var cart = await _dbContext.Carts! + .Include(c => c.CartPositions!) + .ThenInclude(cp => cp.Good) + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == id); + + return cart!; + } + + public async Task CartExistsAsync(Guid id) + { + var cart = await _dbContext.Carts! + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == id); + + return cart != null; + } + } +} diff --git a/src/CartService/Database/Repositories/GoodRepository.cs b/src/CartService/Database/Repositories/GoodRepository.cs new file mode 100644 index 0000000..7d796a0 --- /dev/null +++ b/src/CartService/Database/Repositories/GoodRepository.cs @@ -0,0 +1,51 @@ +using CartService.Database.Models; +using CartService.Database.Repositories.Interfaces; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Repositories +{ + public class GoodRepository : IGoodRepository + { + + private readonly NpgSqlContext _dbContext; + + public GoodRepository(NpgSqlContext dbContext) + { + _dbContext = dbContext; + } + + public async Task AddGoodAsync(Guid id, + string? name, + int price) + { + var good = new Good(id, name, price); + + await _dbContext.Goods!.AddAsync(good); + + await _dbContext.SaveChangesAsync(); + } + + public async Task GetGoodByNameAsync(string name) + { + var good = await _dbContext.Goods! + .AsNoTracking() + .FirstOrDefaultAsync(g => g.Name == name); + + return good!; + } + + public async Task GoodExistsAsync(string name) + { + var good = await _dbContext.Goods! + .AsNoTracking() + .FirstOrDefaultAsync(g => g.Name == name); + + return good != null; + } + } +} diff --git a/src/CartService/Database/Repositories/Interfaces/ICartPositionRepository.cs b/src/CartService/Database/Repositories/Interfaces/ICartPositionRepository.cs new file mode 100644 index 0000000..1778efc --- /dev/null +++ b/src/CartService/Database/Repositories/Interfaces/ICartPositionRepository.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Repositories.Interfaces +{ + public interface ICartPositionRepository + { + public Task AddCartPositionAsync(Guid id, + Guid cartId, + Guid goodId, + int amount); + + public Task UpdateCartPositionAsync(Guid id, + Guid cartId, + Guid goodId, + int amount); + + public Task CartPositionExistsForCartAsync(Guid cartId, string name); + + public Task RemoveCartPositionAsync(Guid id); + } +} diff --git a/src/CartService/Database/Repositories/Interfaces/ICartRepository.cs b/src/CartService/Database/Repositories/Interfaces/ICartRepository.cs new file mode 100644 index 0000000..5ec1f91 --- /dev/null +++ b/src/CartService/Database/Repositories/Interfaces/ICartRepository.cs @@ -0,0 +1,18 @@ +using CartService.Database.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Repositories.Interfaces +{ + public interface ICartRepository + { + public Task AddCartAsync(Guid id); + + public Task CartExistsAsync(Guid id); + + public Task GetCartWithCartPositionsAsync(Guid id); + } +} diff --git a/src/CartService/Database/Repositories/Interfaces/IGoodRepository.cs b/src/CartService/Database/Repositories/Interfaces/IGoodRepository.cs new file mode 100644 index 0000000..0551742 --- /dev/null +++ b/src/CartService/Database/Repositories/Interfaces/IGoodRepository.cs @@ -0,0 +1,20 @@ +using CartService.Database.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CartService.Database.Repositories.Interfaces +{ + public interface IGoodRepository + { + public Task GoodExistsAsync(string name); + + public Task GetGoodByNameAsync(string name); + + public Task AddGoodAsync(Guid id, + string? name, + int price); + } +} diff --git a/src/CartService/Dockerfile b/src/CartService/Dockerfile new file mode 100644 index 0000000..7911eba --- /dev/null +++ b/src/CartService/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +COPY ./bin/Release/net6.0 . + +ENTRYPOINT ["dotnet", "CartService.dll"] \ No newline at end of file diff --git a/src/CartService/Migrations/20211028164439_Initial.Designer.cs b/src/CartService/Migrations/20211028164439_Initial.Designer.cs new file mode 100644 index 0000000..4777d3a --- /dev/null +++ b/src/CartService/Migrations/20211028164439_Initial.Designer.cs @@ -0,0 +1,112 @@ +// +using System; +using CartService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace CartService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + [Migration("20211028164439_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("CartService.Database.Models.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("CartService.Database.Models.CartPosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.HasIndex("GoodId"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("CartPositions"); + }); + + modelBuilder.Entity("CartService.Database.Models.Good", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("CartService.Database.Models.CartPosition", b => + { + b.HasOne("CartService.Database.Models.Cart", "Cart") + .WithMany("CartPositions") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CartService.Database.Models.Good", "Good") + .WithMany() + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("CartService.Database.Models.Cart", b => + { + b.Navigation("CartPositions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/CartService/Migrations/20211028164439_Initial.cs b/src/CartService/Migrations/20211028164439_Initial.cs new file mode 100644 index 0000000..604492b --- /dev/null +++ b/src/CartService/Migrations/20211028164439_Initial.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace CartService.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Carts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Carts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Goods", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Price = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Goods", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CartPositions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CartId = table.Column(type: "uuid", nullable: false), + GoodId = table.Column(type: "uuid", nullable: false), + Amount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CartPositions", x => x.Id); + table.ForeignKey( + name: "FK_CartPositions_Carts_CartId", + column: x => x.CartId, + principalTable: "Carts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CartPositions_Goods_GoodId", + column: x => x.GoodId, + principalTable: "Goods", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CartPositions_CartId", + table: "CartPositions", + column: "CartId"); + + migrationBuilder.CreateIndex( + name: "IX_CartPositions_GoodId", + table: "CartPositions", + column: "GoodId"); + + migrationBuilder.CreateIndex( + name: "IX_CartPositions_Id", + table: "CartPositions", + column: "Id", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Carts_Id", + table: "Carts", + column: "Id", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Goods_Id", + table: "Goods", + column: "Id", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CartPositions"); + + migrationBuilder.DropTable( + name: "Carts"); + + migrationBuilder.DropTable( + name: "Goods"); + } + } +} diff --git a/src/CartService/Migrations/NpgSqlContextModelSnapshot.cs b/src/CartService/Migrations/NpgSqlContextModelSnapshot.cs new file mode 100644 index 0000000..bf6438e --- /dev/null +++ b/src/CartService/Migrations/NpgSqlContextModelSnapshot.cs @@ -0,0 +1,110 @@ +// +using System; +using CartService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace CartService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + partial class NpgSqlContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("CartService.Database.Models.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("CartService.Database.Models.CartPosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.HasIndex("GoodId"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("CartPositions"); + }); + + modelBuilder.Entity("CartService.Database.Models.Good", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("CartService.Database.Models.CartPosition", b => + { + b.HasOne("CartService.Database.Models.Cart", "Cart") + .WithMany("CartPositions") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CartService.Database.Models.Good", "Good") + .WithMany() + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("CartService.Database.Models.Cart", b => + { + b.Navigation("CartPositions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/CartService/Program.cs b/src/CartService/Program.cs new file mode 100644 index 0000000..0e4a3eb --- /dev/null +++ b/src/CartService/Program.cs @@ -0,0 +1,88 @@ +using CartService.Consumers; +using CartService.Database; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using System; +using CartService.Configurations; +using CartService.Database.Repositories.Interfaces; +using CartService.Database.Repositories; +using System.Reflection; + +namespace CartService +{ + public class Program + { + public static void Main(string[] args) + { + Console.Title = Assembly.GetExecutingAssembly().GetName().Name!; + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + var contextOptions = new DbContextOptionsBuilder() + .UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")) + .Options; + + using (var context = new NpgSqlContext(contextOptions)) + { + context.Database.Migrate(); + } + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddDbContext(opt => + opt.UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")), + ServiceLifetime.Transient, + ServiceLifetime.Transient); + + var endpointsSection = hostContext.Configuration.GetSection("EndpointsConfiguration"); + var endpointsConfig = endpointsSection.Get(); + + var rabbitMqSection = hostContext.Configuration.GetSection("RabbitMqConfiguration"); + var rabbitMqConfig = rabbitMqSection.Get(); + + services.AddMassTransit(x => + { + x.AddConsumer(typeof(AddCartPositionConsumerDefinition)) + .Endpoint(cfg => + { + cfg.Name = endpointsConfig.CartServiceAddress; + }); + + x.AddConsumer(typeof(RemoveCartPositionConsumerDefinition)) + .Endpoint(cfg => + { + cfg.Name = endpointsConfig.CartServiceAddress; + }); + + x.AddConsumer(typeof(GetCartConsumerDefinition)) + .Endpoint(cfg => + { + cfg.Name = endpointsConfig.CartServiceAddress; + }); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseBsonSerializer(); + cfg.ConfigureEndpoints(context); + + cfg.Host(rabbitMqConfig.Hostname, rabbitMqConfig.VirtualHost, h => + { + h.Username(rabbitMqConfig.Username); + h.Password(rabbitMqConfig.Password); + }); + }); + + }).AddMassTransitHostedService(true); + }); + + } +} diff --git a/src/CartService/Properties/launchSettings.json b/src/CartService/Properties/launchSettings.json new file mode 100644 index 0000000..b256c28 --- /dev/null +++ b/src/CartService/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "CartService": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/CartService/appsettings.Development.json b/src/CartService/appsettings.Development.json new file mode 100644 index 0000000..2e04b55 --- /dev/null +++ b/src/CartService/appsettings.Development.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=localhost;Port=5432;Database=CartServiceDb;" + }, + "EndpointsConfiguration": { + "CartServiceAddress": "cart-service" + }, + "RabbitMqConfiguration": { + "Hostname": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } + +} diff --git a/src/CartService/appsettings.json b/src/CartService/appsettings.json new file mode 100644 index 0000000..edb9954 --- /dev/null +++ b/src/CartService/appsettings.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=database;Port=5432;Database=CartServiceDb;" + }, + "EndpointsConfiguration": { + "CartServiceAddress": "cart-service" + }, + "RabbitMqConfiguration": { + "Hostname": "rabbitmq", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } + +} diff --git a/src/Contracts/ApiService.Contracts/ApiService.Contracts.csproj b/src/Contracts/ApiService.Contracts/ApiService.Contracts.csproj new file mode 100644 index 0000000..18fdaa4 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/ApiService.Contracts.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + + + + + + + + + + + diff --git a/src/Contracts/ApiService.Contracts/ManagerApi/ConfirmOrder.cs b/src/Contracts/ApiService.Contracts/ManagerApi/ConfirmOrder.cs new file mode 100644 index 0000000..9b2321c --- /dev/null +++ b/src/Contracts/ApiService.Contracts/ManagerApi/ConfirmOrder.cs @@ -0,0 +1,11 @@ +using System; + +namespace ApiService.Contracts.ManagerApi +{ + public interface ConfirmOrder + { + public Guid OrderId { get; set; } + + public string ConfirmManager { get; set; } + } +} diff --git a/src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrder.cs b/src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrder.cs new file mode 100644 index 0000000..496cfdc --- /dev/null +++ b/src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrder.cs @@ -0,0 +1,9 @@ +using System; + +namespace ApiService.Contracts.ManagerApi +{ + public interface GetArchivedOrder + { + public Guid OrderId { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrderResponse.cs b/src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrderResponse.cs new file mode 100644 index 0000000..75fecd6 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/ManagerApi/GetArchivedOrderResponse.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Contracts.Shared; + +namespace ApiService.Contracts.ManagerApi +{ + public interface GetArchivedOrderResponse + { + public Guid OrderId { get; set; } + + public List Cart { get; set; } + + public int TotalPrice { get; set; } + + public bool IsConfirmed { get; set; } + + public DateTimeOffset SubmitDate { get; set; } + + public string Manager { get; set; } + + public DateTimeOffset? ConfirmDate { get; set; } + + public DateTimeOffset? DeliveredDate { get; set; } + + public string FeedbackText { get; set; } + public int FeedbackStars { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/ApiService.Contracts/ManagerApi/NewOrderConfirmationRequested.cs b/src/Contracts/ApiService.Contracts/ManagerApi/NewOrderConfirmationRequested.cs new file mode 100644 index 0000000..c84f900 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/ManagerApi/NewOrderConfirmationRequested.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Contracts.Shared; + +namespace ApiService.Contracts.ManagerApi +{ + public interface NewOrderConfirmationRequested + { + public Guid OrderId { get; set; } + + public List Cart { get; set; } + + public int TotalPrice { get; set; } + + } +} diff --git a/src/Contracts/ApiService.Contracts/ManagerApi/RejectOrder.cs b/src/Contracts/ApiService.Contracts/ManagerApi/RejectOrder.cs new file mode 100644 index 0000000..9564242 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/ManagerApi/RejectOrder.cs @@ -0,0 +1,13 @@ +using System; + +namespace ApiService.Contracts.ManagerApi +{ + public interface RejectOrder + { + public Guid OrderId { get; set; } + + public string RejectManager { get; set; } + + public string Reason { get; set; } + } +} diff --git a/src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersState.cs b/src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersState.cs new file mode 100644 index 0000000..99dedc0 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersState.cs @@ -0,0 +1,7 @@ +namespace ApiService.Contracts.MonitoringApi +{ + public interface GetAllOrdersState + { + + } +} \ No newline at end of file diff --git a/src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersStateResponse.cs b/src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersStateResponse.cs new file mode 100644 index 0000000..ede0fa3 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/MonitoringApi/GetAllOrdersStateResponse.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace ApiService.Contracts.MonitoringApi +{ + public interface GetAllOrdersStateResponse + { + Dictionary States { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderState.cs b/src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderState.cs new file mode 100644 index 0000000..b02d2d9 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderState.cs @@ -0,0 +1,9 @@ +using System; + +namespace ApiService.Contracts.MonitoringApi +{ + public interface GetOrderState + { + Guid OrderId { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderStateResponse.cs b/src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderStateResponse.cs new file mode 100644 index 0000000..4990336 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/MonitoringApi/GetOrderStateResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace ApiService.Contracts.MonitoringApi +{ + public interface GetOrderStateResponse + { + public Guid OrderId { get; set; } + public int State { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/ApiService.Contracts/UserApi/AbortOrder.cs b/src/Contracts/ApiService.Contracts/UserApi/AbortOrder.cs new file mode 100644 index 0000000..d13d040 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/UserApi/AbortOrder.cs @@ -0,0 +1,9 @@ +using System; + +namespace ApiService.Contracts.UserApi +{ + public interface AbortOrder + { + public Guid OrderId { get; set; } + } +} diff --git a/src/Contracts/ApiService.Contracts/UserApi/FeedbackReceived.cs b/src/Contracts/ApiService.Contracts/UserApi/FeedbackReceived.cs new file mode 100644 index 0000000..dc7f22b --- /dev/null +++ b/src/Contracts/ApiService.Contracts/UserApi/FeedbackReceived.cs @@ -0,0 +1,13 @@ +using System; + +namespace ApiService.Contracts.UserApi +{ + public interface FeedbackReceived + { + Guid OrderId { get; } + + string Text { get; } + + public int StarsAmount { get; } + } +} diff --git a/src/Contracts/ApiService.Contracts/UserApi/FeedbackRequested.cs b/src/Contracts/ApiService.Contracts/UserApi/FeedbackRequested.cs new file mode 100644 index 0000000..2f39d7a --- /dev/null +++ b/src/Contracts/ApiService.Contracts/UserApi/FeedbackRequested.cs @@ -0,0 +1,9 @@ +using System; + +namespace ApiService.Contracts.UserApi +{ + public interface FeedbackRequested + { + public Guid OrderId { get; set; } + } +} diff --git a/src/Contracts/ApiService.Contracts/UserApi/OrderAborted.cs b/src/Contracts/ApiService.Contracts/UserApi/OrderAborted.cs new file mode 100644 index 0000000..2aeda06 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/UserApi/OrderAborted.cs @@ -0,0 +1,9 @@ +using System; + +namespace ApiService.Contracts.UserApi +{ + public interface OrderAborted + { + public Guid OrderId { get; set; } + } +} diff --git a/src/Contracts/ApiService.Contracts/UserApi/OrderRejected.cs b/src/Contracts/ApiService.Contracts/UserApi/OrderRejected.cs new file mode 100644 index 0000000..f68f3d6 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/UserApi/OrderRejected.cs @@ -0,0 +1,10 @@ +using System; + +namespace ApiService.Contracts.UserApi +{ + public interface OrderRejected + { + public Guid OrderId { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/ApiService.Contracts/UserApi/OrderSubmitted.cs b/src/Contracts/ApiService.Contracts/UserApi/OrderSubmitted.cs new file mode 100644 index 0000000..f42fbd7 --- /dev/null +++ b/src/Contracts/ApiService.Contracts/UserApi/OrderSubmitted.cs @@ -0,0 +1,12 @@ +using System; + +namespace ApiService.Contracts.UserApi +{ + public interface OrderSubmitted + { + public Guid OrderId { get; set; } + + public string UserName { get; set; } + + } +} diff --git a/src/Contracts/ApiService.Contracts/UserApi/SendFeedback.cs b/src/Contracts/ApiService.Contracts/UserApi/SendFeedback.cs new file mode 100644 index 0000000..b6b22ef --- /dev/null +++ b/src/Contracts/ApiService.Contracts/UserApi/SendFeedback.cs @@ -0,0 +1,13 @@ +using System; + +namespace ApiService.Contracts.UserApi +{ + public interface SendFeedback + { + public Guid OrderId { get; set; } + + public string Text { get; set; } + + public int StarsAmount { get; set; } + } +} diff --git a/src/Contracts/CartService.Contracts/AddCartPosition.cs b/src/Contracts/CartService.Contracts/AddCartPosition.cs new file mode 100644 index 0000000..571cc86 --- /dev/null +++ b/src/Contracts/CartService.Contracts/AddCartPosition.cs @@ -0,0 +1,13 @@ +using System; + +namespace CartService.Contracts +{ + public interface AddCartPosition + { + Guid OrderId { get; } + + string Name { get; } + + int Amount { get; } + } +} diff --git a/src/Contracts/CartService.Contracts/CartService.Contracts.csproj b/src/Contracts/CartService.Contracts/CartService.Contracts.csproj new file mode 100644 index 0000000..162ed0c --- /dev/null +++ b/src/Contracts/CartService.Contracts/CartService.Contracts.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + + + + + + + + + + + diff --git a/src/Contracts/CartService.Contracts/GetCart.cs b/src/Contracts/CartService.Contracts/GetCart.cs new file mode 100644 index 0000000..f63535a --- /dev/null +++ b/src/Contracts/CartService.Contracts/GetCart.cs @@ -0,0 +1,9 @@ +using System; + +namespace CartService.Contracts +{ + public interface GetCart + { + public Guid OrderId { get; set; } + } +} diff --git a/src/Contracts/CartService.Contracts/GetCartResponse.cs b/src/Contracts/CartService.Contracts/GetCartResponse.cs new file mode 100644 index 0000000..15ac79e --- /dev/null +++ b/src/Contracts/CartService.Contracts/GetCartResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Contracts.Shared; + +namespace CartService.Contracts +{ + public interface GetCartResponse + { + public Guid OrderId { get; set; } + + public List CartContent { get; set; } + + public int TotalPrice { get; set; } + } +} diff --git a/src/Contracts/CartService.Contracts/RemoveCartPosition.cs b/src/Contracts/CartService.Contracts/RemoveCartPosition.cs new file mode 100644 index 0000000..a59e7bc --- /dev/null +++ b/src/Contracts/CartService.Contracts/RemoveCartPosition.cs @@ -0,0 +1,13 @@ +using System; + +namespace CartService.Contracts +{ + public interface RemoveCartPosition + { + Guid OrderId { get; } + + string Name { get; } + + int Amount { get; } + } +} diff --git a/src/Contracts/Contracts.Shared/CartPosition.cs b/src/Contracts/Contracts.Shared/CartPosition.cs new file mode 100644 index 0000000..bbd5509 --- /dev/null +++ b/src/Contracts/Contracts.Shared/CartPosition.cs @@ -0,0 +1,11 @@ +namespace Contracts.Shared +{ + public class CartPosition + { + public string Name { get; set; } + + public int Amount { get; set; } + + public int Price { get; set; } + } +} diff --git a/src/Contracts/Contracts.Shared/Contracts.Shared.csproj b/src/Contracts/Contracts.Shared/Contracts.Shared.csproj new file mode 100644 index 0000000..dbc1517 --- /dev/null +++ b/src/Contracts/Contracts.Shared/Contracts.Shared.csproj @@ -0,0 +1,7 @@ + + + + net6.0 + + + diff --git a/src/Contracts/DeliveryService.Contracts/DeliveryOrder.cs b/src/Contracts/DeliveryService.Contracts/DeliveryOrder.cs new file mode 100644 index 0000000..4355300 --- /dev/null +++ b/src/Contracts/DeliveryService.Contracts/DeliveryOrder.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using Contracts.Shared; + +namespace DeliveryService.Contracts +{ + public interface DeliveryOrder + { + public Guid OrderId { get; set; } + + public List Cart { get; set; } + } +} diff --git a/src/Contracts/DeliveryService.Contracts/DeliveryService.Contracts.csproj b/src/Contracts/DeliveryService.Contracts/DeliveryService.Contracts.csproj new file mode 100644 index 0000000..e1a106b --- /dev/null +++ b/src/Contracts/DeliveryService.Contracts/DeliveryService.Contracts.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + + + + + + + + + + + + diff --git a/src/Contracts/DeliveryService.Contracts/OrderDelivered.cs b/src/Contracts/DeliveryService.Contracts/OrderDelivered.cs new file mode 100644 index 0000000..46da0b5 --- /dev/null +++ b/src/Contracts/DeliveryService.Contracts/OrderDelivered.cs @@ -0,0 +1,9 @@ +using System; + +namespace DeliveryService.Contracts +{ + public interface OrderDelivered + { + public Guid OrderId { get; set; } + } +} diff --git a/src/Contracts/FeedbackService.Contracts/AddFeedback.cs b/src/Contracts/FeedbackService.Contracts/AddFeedback.cs new file mode 100644 index 0000000..c5ee3cd --- /dev/null +++ b/src/Contracts/FeedbackService.Contracts/AddFeedback.cs @@ -0,0 +1,13 @@ +using System; + +namespace FeedbackService.Contracts +{ + public interface AddFeedback + { + public Guid OrderId { get; set; } + + public string Text { get; set; } + + public int StarsAmount { get; set; } + } +} diff --git a/src/Contracts/FeedbackService.Contracts/FeedbackAdded.cs b/src/Contracts/FeedbackService.Contracts/FeedbackAdded.cs new file mode 100644 index 0000000..dbd5007 --- /dev/null +++ b/src/Contracts/FeedbackService.Contracts/FeedbackAdded.cs @@ -0,0 +1,9 @@ +using System; + +namespace FeedbackService.Contracts +{ + public interface FeedbackAdded + { + public Guid OrderId { get; set; } + } +} diff --git a/src/Contracts/FeedbackService.Contracts/FeedbackService.Contracts.csproj b/src/Contracts/FeedbackService.Contracts/FeedbackService.Contracts.csproj new file mode 100644 index 0000000..91e8aa5 --- /dev/null +++ b/src/Contracts/FeedbackService.Contracts/FeedbackService.Contracts.csproj @@ -0,0 +1,11 @@ + + + + net6.0 + + + + + + + diff --git a/src/Contracts/FeedbackService.Contracts/GetOrderFeedback.cs b/src/Contracts/FeedbackService.Contracts/GetOrderFeedback.cs new file mode 100644 index 0000000..ce87a91 --- /dev/null +++ b/src/Contracts/FeedbackService.Contracts/GetOrderFeedback.cs @@ -0,0 +1,9 @@ +using System; + +namespace FeedbackService.Contracts +{ + public interface GetOrderFeedback + { + public Guid OrderId { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/FeedbackService.Contracts/GetOrderFeedbackResponse.cs b/src/Contracts/FeedbackService.Contracts/GetOrderFeedbackResponse.cs new file mode 100644 index 0000000..adca89e --- /dev/null +++ b/src/Contracts/FeedbackService.Contracts/GetOrderFeedbackResponse.cs @@ -0,0 +1,12 @@ +using System; + +namespace FeedbackService.Contracts +{ + public interface GetOrderFeedbackResponse + { + public Guid OrderId { get; set; } + + public string Text { get; set; } + public int StarsAmount { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/HistoryService.Contracts/ArchiveOrder.cs b/src/Contracts/HistoryService.Contracts/ArchiveOrder.cs new file mode 100644 index 0000000..b6e6511 --- /dev/null +++ b/src/Contracts/HistoryService.Contracts/ArchiveOrder.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Contracts.Shared; + +namespace HistoryService.Contracts +{ + public interface ArchiveOrder + { + public Guid OrderId { get; set; } + + public List Cart { get; set; } + + public int TotalPrice { get; set; } + + public bool IsConfirmed { get; set; } + + public DateTimeOffset SubmitDate { get; set; } + + public string Manager { get; set; } + + public DateTimeOffset? ConfirmDate { get; set; } + + public DateTimeOffset? DeliveredDate { get; set; } + } +} diff --git a/src/Contracts/HistoryService.Contracts/GetOrderFromArchive.cs b/src/Contracts/HistoryService.Contracts/GetOrderFromArchive.cs new file mode 100644 index 0000000..6873638 --- /dev/null +++ b/src/Contracts/HistoryService.Contracts/GetOrderFromArchive.cs @@ -0,0 +1,9 @@ +using System; + +namespace HistoryService.Contracts +{ + public interface GetOrderFromArchive + { + public Guid OrderId { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/HistoryService.Contracts/GetOrderFromArchiveResponse.cs b/src/Contracts/HistoryService.Contracts/GetOrderFromArchiveResponse.cs new file mode 100644 index 0000000..73ad0e9 --- /dev/null +++ b/src/Contracts/HistoryService.Contracts/GetOrderFromArchiveResponse.cs @@ -0,0 +1,19 @@ +using System; + +namespace HistoryService.Contracts +{ + public interface GetOrderFromArchiveResponse + { + public Guid OrderId { get; set; } + + public bool IsConfirmed { get; set; } + + public DateTimeOffset SubmitDate { get; set; } + + public string Manager { get; set; } + + public DateTimeOffset? ConfirmDate { get; set; } + + public DateTimeOffset? DeliveredDate { get; set; } + } +} \ No newline at end of file diff --git a/src/Contracts/HistoryService.Contracts/HistoryService.Contracts.csproj b/src/Contracts/HistoryService.Contracts/HistoryService.Contracts.csproj new file mode 100644 index 0000000..162ed0c --- /dev/null +++ b/src/Contracts/HistoryService.Contracts/HistoryService.Contracts.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + + + + + + + + + + + diff --git a/src/Contracts/HistoryService.Contracts/OrderAdded.cs b/src/Contracts/HistoryService.Contracts/OrderAdded.cs new file mode 100644 index 0000000..ae3f14c --- /dev/null +++ b/src/Contracts/HistoryService.Contracts/OrderAdded.cs @@ -0,0 +1,9 @@ +using System; + +namespace HistoryService.Contracts +{ + public interface OrderAdded + { + public Guid OrderId { get; set; } + } +} diff --git a/src/Contracts/PaymentService.Contracts/ErrorReservingMoney.cs b/src/Contracts/PaymentService.Contracts/ErrorReservingMoney.cs new file mode 100644 index 0000000..0181342 --- /dev/null +++ b/src/Contracts/PaymentService.Contracts/ErrorReservingMoney.cs @@ -0,0 +1,11 @@ +using System; + +namespace PaymentService.Contracts +{ + public interface ErrorReservingMoney + { + public Guid OrderId { get; } + + public string Reason { get; } + } +} diff --git a/src/Contracts/PaymentService.Contracts/MoneyReserved.cs b/src/Contracts/PaymentService.Contracts/MoneyReserved.cs new file mode 100644 index 0000000..7cc9c72 --- /dev/null +++ b/src/Contracts/PaymentService.Contracts/MoneyReserved.cs @@ -0,0 +1,10 @@ +using System; + +namespace PaymentService.Contracts +{ + public interface MoneyReserved + { + Guid OrderId { get; } + int Amount { get; } + } +} \ No newline at end of file diff --git a/src/Contracts/PaymentService.Contracts/MoneyUnreserved.cs b/src/Contracts/PaymentService.Contracts/MoneyUnreserved.cs new file mode 100644 index 0000000..e211e03 --- /dev/null +++ b/src/Contracts/PaymentService.Contracts/MoneyUnreserved.cs @@ -0,0 +1,10 @@ +using System; + +namespace PaymentService.Contracts +{ + public interface MoneyUnreserved + { + Guid OrderId { get; } + int Amount { get; } + } +} \ No newline at end of file diff --git a/src/Contracts/PaymentService.Contracts/PaymentService.Contracts.csproj b/src/Contracts/PaymentService.Contracts/PaymentService.Contracts.csproj new file mode 100644 index 0000000..91e8aa5 --- /dev/null +++ b/src/Contracts/PaymentService.Contracts/PaymentService.Contracts.csproj @@ -0,0 +1,11 @@ + + + + net6.0 + + + + + + + diff --git a/src/Contracts/PaymentService.Contracts/ReserveMoney.cs b/src/Contracts/PaymentService.Contracts/ReserveMoney.cs new file mode 100644 index 0000000..9616354 --- /dev/null +++ b/src/Contracts/PaymentService.Contracts/ReserveMoney.cs @@ -0,0 +1,11 @@ +using System; + +namespace PaymentService.Contracts +{ + public interface ReserveMoney + { + public Guid OrderId { get; set; } + + public int Amount { get; set; } + } +} diff --git a/src/Contracts/PaymentService.Contracts/UnreserveMoney.cs b/src/Contracts/PaymentService.Contracts/UnreserveMoney.cs new file mode 100644 index 0000000..cedb194 --- /dev/null +++ b/src/Contracts/PaymentService.Contracts/UnreserveMoney.cs @@ -0,0 +1,11 @@ +using System; + +namespace PaymentService.Contracts +{ + public interface UnreserveMoney + { + public Guid OrderId { get; set; } + + public int Amount { get; set; } + } +} diff --git a/src/DeliveryService/Configurations/EndpointsConfiguration.cs b/src/DeliveryService/Configurations/EndpointsConfiguration.cs new file mode 100644 index 0000000..f0b3553 --- /dev/null +++ b/src/DeliveryService/Configurations/EndpointsConfiguration.cs @@ -0,0 +1,8 @@ +namespace DeliveryService.Configurations +{ + public class EndpointsConfiguration + { + public string? DeliveryServiceAddress { get; set; } + public string? SchedulerQueueName { get; set; } + } +} \ No newline at end of file diff --git a/src/DeliveryService/Configurations/RabbitMqConfiguration.cs b/src/DeliveryService/Configurations/RabbitMqConfiguration.cs new file mode 100644 index 0000000..b8b4f08 --- /dev/null +++ b/src/DeliveryService/Configurations/RabbitMqConfiguration.cs @@ -0,0 +1,10 @@ +namespace DeliveryService.Configurations +{ + public class RabbitMqConfiguration + { + public string? Hostname { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + } +} \ No newline at end of file diff --git a/src/DeliveryService/Consumers/DeliveryOrderConsumer.cs b/src/DeliveryService/Consumers/DeliveryOrderConsumer.cs new file mode 100644 index 0000000..e97dcbb --- /dev/null +++ b/src/DeliveryService/Consumers/DeliveryOrderConsumer.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using DeliveryService.Contracts; +using MassTransit; +using Microsoft.Extensions.Logging; + +namespace DeliveryService.Consumers +{ + public class DeliveryOrderConsumer : IConsumer + { + private readonly ILogger _logger; + + public DeliveryOrderConsumer(ILogger logger) + { + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + _logger.LogInformation("[{consumerName}] Received delivery request for order {orderId}.", + nameof(DeliveryOrderConsumer), context.Message.OrderId); + + + await context.SchedulePublish(DateTime.UtcNow + TimeSpan.FromSeconds(30), + new + { + OrderId = context.Message.OrderId + }); + } + } +} \ No newline at end of file diff --git a/src/DeliveryService/Consumers/DeliveryOrderConsumerDefinition.cs b/src/DeliveryService/Consumers/DeliveryOrderConsumerDefinition.cs new file mode 100644 index 0000000..1bdb147 --- /dev/null +++ b/src/DeliveryService/Consumers/DeliveryOrderConsumerDefinition.cs @@ -0,0 +1,27 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DeliveryService.Consumers +{ + public class DeliveryOrderConsumerDefinition : ConsumerDefinition + { + public DeliveryOrderConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/DeliveryService/DeliveryService.csproj b/src/DeliveryService/DeliveryService.csproj new file mode 100644 index 0000000..6331041 --- /dev/null +++ b/src/DeliveryService/DeliveryService.csproj @@ -0,0 +1,37 @@ + + + + Exe + net6.0 + enable + true + + + + + + + + + + + + + + + + Always + + + + + + + + + + Always + + + + diff --git a/src/DeliveryService/Dockerfile b/src/DeliveryService/Dockerfile new file mode 100644 index 0000000..f866599 --- /dev/null +++ b/src/DeliveryService/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +COPY ./bin/Release/net6.0 . + +ENTRYPOINT ["dotnet", "DeliveryService.dll"] \ No newline at end of file diff --git a/src/DeliveryService/Program.cs b/src/DeliveryService/Program.cs new file mode 100644 index 0000000..d8c4a89 --- /dev/null +++ b/src/DeliveryService/Program.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; +using DeliveryService.Configurations; +using DeliveryService.Consumers; +using MassTransit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace DeliveryService +{ + class Program + { + static void Main(string[] args) + { + Console.Title = Assembly.GetExecutingAssembly().GetName().Name!; + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + var endpointsConfigSection = hostContext.Configuration.GetSection("EndpointsConfiguration"); + var endpointsConfig = endpointsConfigSection.Get(); + + var rabbitMqConfigSection = hostContext.Configuration.GetSection("RabbitMqConfiguration"); + var rabbitMqConfig = rabbitMqConfigSection.Get(); + + services.AddMassTransit(c => + { + c.AddConsumer(typeof(DeliveryOrderConsumerDefinition)) + .Endpoint(configurator => + { + configurator.Name = endpointsConfig.DeliveryServiceAddress; + }); + + c.AddDelayedMessageScheduler(); + c.UsingRabbitMq((context, configurator) => + { + configurator.UseBsonSerializer(); + configurator.Host(rabbitMqConfig.Hostname, rabbitMqConfig.VirtualHost, h => + { + h.Username(rabbitMqConfig.Username); + h.Password(rabbitMqConfig.Password); + }); + + + configurator.UseDelayedMessageScheduler(); + configurator.ConfigureEndpoints(context); + }); + }); + + services.AddMassTransitHostedService(true); + }); + } +} diff --git a/src/DeliveryService/Properties/launchSettings.json b/src/DeliveryService/Properties/launchSettings.json new file mode 100644 index 0000000..749e1e8 --- /dev/null +++ b/src/DeliveryService/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "DeliveryService": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/DeliveryService/appsettings.Development.json b/src/DeliveryService/appsettings.Development.json new file mode 100644 index 0000000..29a8aa4 --- /dev/null +++ b/src/DeliveryService/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "EndpointsConfiguration": { + "DeliveryServiceAddress": "delivery-service", + "SchedulerQueueName": "delivery-service-scheduler" + }, + "RabbitMqConfiguration": { + "Hostname": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } + } \ No newline at end of file diff --git a/src/DeliveryService/appsettings.json b/src/DeliveryService/appsettings.json new file mode 100644 index 0000000..59fdfb0 --- /dev/null +++ b/src/DeliveryService/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "EndpointsConfiguration": { + "DeliveryServiceAddress": "delivery-service", + "SchedulerQueueName": "delivery-service-scheduler" + }, + "RabbitMqConfiguration": { + "Hostname": "rabbitmq", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } +} \ No newline at end of file diff --git a/src/FeedbackService/Configurations/EndpointsConfiguration.cs b/src/FeedbackService/Configurations/EndpointsConfiguration.cs new file mode 100644 index 0000000..a458280 --- /dev/null +++ b/src/FeedbackService/Configurations/EndpointsConfiguration.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Configurations +{ + public class EndpointsConfiguration + { + public string? FeedbackServiceAddress { get; set; } + } +} diff --git a/src/FeedbackService/Configurations/RabbitMqConfiguration.cs b/src/FeedbackService/Configurations/RabbitMqConfiguration.cs new file mode 100644 index 0000000..eca0d29 --- /dev/null +++ b/src/FeedbackService/Configurations/RabbitMqConfiguration.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Configurations +{ + public class RabbitMqConfiguration + { + public string? Hostname { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + } +} diff --git a/src/FeedbackService/Consumers/AddFeedbackConsumer.cs b/src/FeedbackService/Consumers/AddFeedbackConsumer.cs new file mode 100644 index 0000000..0b83e44 --- /dev/null +++ b/src/FeedbackService/Consumers/AddFeedbackConsumer.cs @@ -0,0 +1,45 @@ +using FeedbackService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FeedbackService.Contracts; + +namespace FeedbackService.Consumers +{ + public class AddFeedbackConsumer : IConsumer + { + private readonly IFeedbackRepository _feedbackRepository; + private readonly ILogger _logger; + + + public AddFeedbackConsumer(ILogger logger, + IFeedbackRepository feedbackRepository) + { + _logger = logger; + _feedbackRepository = feedbackRepository; + } + + public async Task Consume(ConsumeContext context) + { + + _logger.LogInformation("[{consumerName}] Received feedback add request for order {orderId}.", + nameof(AddFeedbackConsumer), context.Message.OrderId); + + var message = context.Message; + + await _feedbackRepository.AddFeedbackAsync(message.OrderId, message.Text, message.StarsAmount); + + if (context.RequestId != null) + { + await context.RespondAsync(new + { + OrderId = message.OrderId + }); + } + } + } +} diff --git a/src/FeedbackService/Consumers/AddFeedbackConsumerDefinition.cs b/src/FeedbackService/Consumers/AddFeedbackConsumerDefinition.cs new file mode 100644 index 0000000..17660a4 --- /dev/null +++ b/src/FeedbackService/Consumers/AddFeedbackConsumerDefinition.cs @@ -0,0 +1,27 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Consumers +{ + public class AddFeedbackConsumerDefinition : ConsumerDefinition + { + public AddFeedbackConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/FeedbackService/Consumers/GetOrderFeedbackConsumer.cs b/src/FeedbackService/Consumers/GetOrderFeedbackConsumer.cs new file mode 100644 index 0000000..3f193fe --- /dev/null +++ b/src/FeedbackService/Consumers/GetOrderFeedbackConsumer.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using FeedbackService.Contracts; +using FeedbackService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.Extensions.Logging; + +namespace FeedbackService.Consumers +{ + public class GetOrderFeedbackConsumer : IConsumer + { + private readonly ILogger _logger; + private readonly IFeedbackRepository _feedbackRepository; + + public GetOrderFeedbackConsumer(ILogger logger, + IFeedbackRepository feedbackRepository) + { + _logger = logger; + _feedbackRepository = feedbackRepository; + } + + public async Task Consume(ConsumeContext context) + { + var feedback = await _feedbackRepository.FindFeedbackAsync(context.Message.OrderId); + + await context.RespondAsync(new + { + OrderId = context.Message.OrderId, + Text = feedback?.Text, + StarsAmount = feedback?.StarsAmount + }); + } + } +} \ No newline at end of file diff --git a/src/FeedbackService/Consumers/GetOrderFeedbackConsumerDefinition.cs b/src/FeedbackService/Consumers/GetOrderFeedbackConsumerDefinition.cs new file mode 100644 index 0000000..c8a6283 --- /dev/null +++ b/src/FeedbackService/Consumers/GetOrderFeedbackConsumerDefinition.cs @@ -0,0 +1,27 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Consumers +{ + public class GetOrderFeedbackConsumerDefinition : ConsumerDefinition + { + public GetOrderFeedbackConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/FeedbackService/Database/Models/Feedback.cs b/src/FeedbackService/Database/Models/Feedback.cs new file mode 100644 index 0000000..eb6edd6 --- /dev/null +++ b/src/FeedbackService/Database/Models/Feedback.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Database.Models +{ + public class Feedback + { + public Guid Id { get; set; } + public string? Text { get; set; } + public int StarsAmount { get; set; } + + public Feedback(Guid id, + string? text, + int starsAmount) + { + Id = id; + Text = text; + StarsAmount = starsAmount; + } + } +} diff --git a/src/FeedbackService/Database/NpgSqlContext.cs b/src/FeedbackService/Database/NpgSqlContext.cs new file mode 100644 index 0000000..325d9e4 --- /dev/null +++ b/src/FeedbackService/Database/NpgSqlContext.cs @@ -0,0 +1,26 @@ +using FeedbackService.Database.Models; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Database +{ + public class NpgSqlContext : DbContext + { + + public DbSet? Feedbacks { get; set; } + + public NpgSqlContext() + { + + } + + public NpgSqlContext(DbContextOptions options) : base(options) + { + + } + } +} diff --git a/src/FeedbackService/Database/Repositories/FeedbackRepository.cs b/src/FeedbackService/Database/Repositories/FeedbackRepository.cs new file mode 100644 index 0000000..ee52b7f --- /dev/null +++ b/src/FeedbackService/Database/Repositories/FeedbackRepository.cs @@ -0,0 +1,50 @@ +using FeedbackService.Database.Models; +using FeedbackService.Database.Repositories.Interfaces; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Database.Repositories +{ + public class FeedbackRepository : IFeedbackRepository + { + + private readonly NpgSqlContext _context; + + public FeedbackRepository(NpgSqlContext context) + { + _context = context; + } + + public async Task AddFeedbackAsync(Guid id, string text, int starsAmount) + { + var feedback = new Feedback(id, text, starsAmount); + + await _context.Feedbacks! + .AddAsync(feedback); + + await _context.SaveChangesAsync(); + } + + public async Task GetFeedbackAsync(Guid id) + { + var feedback = await _context.Feedbacks! + .AsNoTracking() + .FirstOrDefaultAsync(f => f.Id == id); + + return feedback!; + } + + public async Task FindFeedbackAsync(Guid id) + { + var feedback = await _context.Feedbacks! + .AsNoTracking() + .FirstOrDefaultAsync(f => f.Id == id); + + return feedback; + } + } +} diff --git a/src/FeedbackService/Database/Repositories/Interfaces/IFeedbackRepository.cs b/src/FeedbackService/Database/Repositories/Interfaces/IFeedbackRepository.cs new file mode 100644 index 0000000..b4c3142 --- /dev/null +++ b/src/FeedbackService/Database/Repositories/Interfaces/IFeedbackRepository.cs @@ -0,0 +1,19 @@ +using FeedbackService.Database.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FeedbackService.Database.Repositories.Interfaces +{ + public interface IFeedbackRepository + { + public Task AddFeedbackAsync(Guid id, + string text, + int starsAmount); + + public Task GetFeedbackAsync(Guid id); + public Task FindFeedbackAsync(Guid id); + } +} diff --git a/src/FeedbackService/Dockerfile b/src/FeedbackService/Dockerfile new file mode 100644 index 0000000..0e53055 --- /dev/null +++ b/src/FeedbackService/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +COPY ./bin/Release/net6.0 . + +ENTRYPOINT ["dotnet", "FeedbackService.dll"] \ No newline at end of file diff --git a/src/FeedbackService/FeedbackService.csproj b/src/FeedbackService/FeedbackService.csproj new file mode 100644 index 0000000..ba0efba --- /dev/null +++ b/src/FeedbackService/FeedbackService.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + true + dotnet-FeedbackService-82A53F3F-FE0A-4157-A449-820E9FB32243 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/FeedbackService/Migrations/20211031093642_Initial.Designer.cs b/src/FeedbackService/Migrations/20211031093642_Initial.Designer.cs new file mode 100644 index 0000000..fe03579 --- /dev/null +++ b/src/FeedbackService/Migrations/20211031093642_Initial.Designer.cs @@ -0,0 +1,43 @@ +// +using System; +using FeedbackService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace FeedbackService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + [Migration("20211031093642_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("FeedbackService.Database.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("StarsAmount") + .HasColumnType("integer"); + + b.Property("Text") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Feedbacks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/FeedbackService/Migrations/20211031093642_Initial.cs b/src/FeedbackService/Migrations/20211031093642_Initial.cs new file mode 100644 index 0000000..9b45ec6 --- /dev/null +++ b/src/FeedbackService/Migrations/20211031093642_Initial.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace FeedbackService.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Feedbacks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Text = table.Column(type: "text", nullable: true), + StarsAmount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Feedbacks", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Feedbacks"); + } + } +} diff --git a/src/FeedbackService/Migrations/NpgSqlContextModelSnapshot.cs b/src/FeedbackService/Migrations/NpgSqlContextModelSnapshot.cs new file mode 100644 index 0000000..392145d --- /dev/null +++ b/src/FeedbackService/Migrations/NpgSqlContextModelSnapshot.cs @@ -0,0 +1,41 @@ +// +using System; +using FeedbackService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace FeedbackService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + partial class NpgSqlContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("FeedbackService.Database.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("StarsAmount") + .HasColumnType("integer"); + + b.Property("Text") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Feedbacks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/FeedbackService/Program.cs b/src/FeedbackService/Program.cs new file mode 100644 index 0000000..d1edc4e --- /dev/null +++ b/src/FeedbackService/Program.cs @@ -0,0 +1,82 @@ +using FeedbackService.Configurations; +using FeedbackService.Consumers; +using FeedbackService.Database; +using FeedbackService.Database.Repositories; +using FeedbackService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace FeedbackService +{ + public class Program + { + public static void Main(string[] args) + { + Console.Title = Assembly.GetExecutingAssembly().GetName().Name!; + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + var contextOptions = new DbContextOptionsBuilder() + .UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")) + .Options; + + using (var context = new NpgSqlContext(contextOptions)) + { + context.Database.Migrate(); + } + + services.AddTransient(); + + services.AddDbContext(opt => + opt.UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")), + ServiceLifetime.Transient, + ServiceLifetime.Transient); + + var endpointsSection = hostContext.Configuration.GetSection("EndpointsConfiguration"); + var endpointsConfig = endpointsSection.Get(); + + var rabbitMqSection = hostContext.Configuration.GetSection("RabbitMqConfiguration"); + var rabbitMqConfig = rabbitMqSection.Get(); + + services.AddMassTransit(x => + { + x.AddConsumer(typeof(AddFeedbackConsumerDefinition)) + .Endpoint(cfg => + { + cfg.Name = endpointsConfig.FeedbackServiceAddress; + }); + + x.AddConsumer(typeof(GetOrderFeedbackConsumerDefinition)) + .Endpoint(cfg => + { + cfg.Name = endpointsConfig.FeedbackServiceAddress; + }); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseBsonSerializer(); + cfg.ConfigureEndpoints(context); + + cfg.Host(rabbitMqConfig.Hostname, rabbitMqConfig.VirtualHost, h => + { + h.Username(rabbitMqConfig.Username); + h.Password(rabbitMqConfig.Password); + }); + }); + + }).AddMassTransitHostedService(true); + }); + } +} diff --git a/src/FeedbackService/Properties/launchSettings.json b/src/FeedbackService/Properties/launchSettings.json new file mode 100644 index 0000000..96fb40c --- /dev/null +++ b/src/FeedbackService/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "FeedbackService": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/FeedbackService/appsettings.Development.json b/src/FeedbackService/appsettings.Development.json new file mode 100644 index 0000000..90d5f80 --- /dev/null +++ b/src/FeedbackService/appsettings.Development.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + + "EndpointsConfiguration": { + "FeedbackServiceAddress": "feedback-service" + }, + + "RabbitMqConfiguration": { + "Hostname": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + }, + + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=localhost;Port=5432;Database=FeedbackServiceDb;" + } +} diff --git a/src/FeedbackService/appsettings.json b/src/FeedbackService/appsettings.json new file mode 100644 index 0000000..3d792a4 --- /dev/null +++ b/src/FeedbackService/appsettings.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + + "EndpointsConfiguration": { + "FeedbackServiceAddress": "feedback-service" + }, + + "RabbitMqConfiguration": { + "Hostname": "rabbitmq", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + }, + + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=database;Port=5432;Database=FeedbackServiceDb;" + } +} diff --git a/src/HistoryService/Configurations/EndpointsConfiguration.cs b/src/HistoryService/Configurations/EndpointsConfiguration.cs new file mode 100644 index 0000000..d08c4b1 --- /dev/null +++ b/src/HistoryService/Configurations/EndpointsConfiguration.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HistoryService.Configurations +{ + public class EndpointsConfiguration + { + public string? HistoryServiceAddress { get; set; } + } +} diff --git a/src/HistoryService/Configurations/RabbitMqConfiguration.cs b/src/HistoryService/Configurations/RabbitMqConfiguration.cs new file mode 100644 index 0000000..4df1b3d --- /dev/null +++ b/src/HistoryService/Configurations/RabbitMqConfiguration.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HistoryService.Configurations +{ + public class RabbitMqConfiguration + { + public string? Hostname { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + } +} diff --git a/src/HistoryService/Consumers/ArchivedOrderConsumer.cs b/src/HistoryService/Consumers/ArchivedOrderConsumer.cs new file mode 100644 index 0000000..15dccac --- /dev/null +++ b/src/HistoryService/Consumers/ArchivedOrderConsumer.cs @@ -0,0 +1,44 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using HistoryService.Contracts; +using HistoryService.Database.Repositories.Interfaces; + +namespace HistoryService.Consumers +{ + public class ArchivedOrderConsumer : IConsumer + { + private readonly ILogger _logger; + private readonly IArchivedOrderRepository _archivedOrderRepository; + + public ArchivedOrderConsumer(ILogger logger, + IArchivedOrderRepository archivedOrderRepository) + { + _archivedOrderRepository = archivedOrderRepository; + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + _logger.LogInformation("[{consumerName}] Received order archive request for order {orderId}.", + nameof(ArchivedOrderConsumer), context.Message.OrderId); + + var message = context.Message; + + await _archivedOrderRepository.AddOrderAsync(message.OrderId, + message.IsConfirmed, + message.SubmitDate, + message.Manager, + message.ConfirmDate, + message.DeliveredDate); + + if (context.RequestId != null) + { + await context.RespondAsync(new + { + OrderId = message.OrderId + }); + } + } + } +} diff --git a/src/HistoryService/Consumers/ArchivedOrderConsumerDefinition.cs b/src/HistoryService/Consumers/ArchivedOrderConsumerDefinition.cs new file mode 100644 index 0000000..5094780 --- /dev/null +++ b/src/HistoryService/Consumers/ArchivedOrderConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace HistoryService.Consumers +{ + public class ArchivedOrderConsumerDefinition : ConsumerDefinition + { + public ArchivedOrderConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/HistoryService/Consumers/GetOrderFromArchiveConsumer.cs b/src/HistoryService/Consumers/GetOrderFromArchiveConsumer.cs new file mode 100644 index 0000000..339215e --- /dev/null +++ b/src/HistoryService/Consumers/GetOrderFromArchiveConsumer.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using HistoryService.Contracts; +using HistoryService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.Extensions.Logging; + +namespace HistoryService.Consumers +{ + public class GetOrderFromArchiveConsumer : IConsumer + { + private readonly ILogger _logger; + private readonly IArchivedOrderRepository _archivedOrderRepository; + + public GetOrderFromArchiveConsumer(ILogger logger, + IArchivedOrderRepository archivedOrderRepository) + { + _logger = logger; + _archivedOrderRepository = archivedOrderRepository; + } + + public async Task Consume(ConsumeContext context) + { + var archiveOrder = await _archivedOrderRepository.GetOrderAsync(context.Message.OrderId); + + await context.RespondAsync(new + { + + OrderId = archiveOrder.Id, + IsConfirmed = archiveOrder.IsConfirmed, + SubmitDate = archiveOrder.SubmitDate, + Manager = archiveOrder.Manager, + ConfirmDate = archiveOrder.ConfirmDate, + DeliveredDate = archiveOrder.DeliveredDate + }); + } + } +} \ No newline at end of file diff --git a/src/HistoryService/Consumers/GetOrderFromArchiveConsumerDefinition.cs b/src/HistoryService/Consumers/GetOrderFromArchiveConsumerDefinition.cs new file mode 100644 index 0000000..eba6c70 --- /dev/null +++ b/src/HistoryService/Consumers/GetOrderFromArchiveConsumerDefinition.cs @@ -0,0 +1,27 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HistoryService.Consumers +{ + public class GetOrderFromArchiveConsumerDefinition : ConsumerDefinition + { + public GetOrderFromArchiveConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/HistoryService/Database/Models/ArchivedOrder.cs b/src/HistoryService/Database/Models/ArchivedOrder.cs new file mode 100644 index 0000000..53b002d --- /dev/null +++ b/src/HistoryService/Database/Models/ArchivedOrder.cs @@ -0,0 +1,34 @@ +using System; + +namespace HistoryService.Database.Models +{ + public class ArchivedOrder + { + public Guid Id { get; set; } + + public bool IsConfirmed { get; set; } + + public DateTimeOffset SubmitDate { get; set; } + + public string? Manager { get; set; } + + public DateTimeOffset? ConfirmDate { get; set; } + + public DateTimeOffset? DeliveredDate { get; set; } + + public ArchivedOrder(Guid id, + bool isConfirmed, + DateTimeOffset submitDate, + string manager, + DateTimeOffset? confirmDate, + DateTimeOffset? deliveredDate) + { + Id = id; + IsConfirmed = isConfirmed; + SubmitDate = submitDate; + Manager = manager; + ConfirmDate = confirmDate; + DeliveredDate = deliveredDate; + } + } +} diff --git a/src/HistoryService/Database/NpgSqlContext.cs b/src/HistoryService/Database/NpgSqlContext.cs new file mode 100644 index 0000000..3246a00 --- /dev/null +++ b/src/HistoryService/Database/NpgSqlContext.cs @@ -0,0 +1,21 @@ +using HistoryService.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace HistoryService.Database +{ + public class NpgSqlContext : DbContext + { + public DbSet? ArchivedOrders { get; set; } + + public NpgSqlContext() + { + + } + + public NpgSqlContext(DbContextOptions options) : base(options) + { + + } + + } +} diff --git a/src/HistoryService/Database/Repositories/ArchivedOrderRepository.cs b/src/HistoryService/Database/Repositories/ArchivedOrderRepository.cs new file mode 100644 index 0000000..3934fb8 --- /dev/null +++ b/src/HistoryService/Database/Repositories/ArchivedOrderRepository.cs @@ -0,0 +1,42 @@ +using HistoryService.Database.Models; +using HistoryService.Database.Repositories.Interfaces; +using Microsoft.EntityFrameworkCore; +using System; +using System.Threading.Tasks; + +namespace HistoryService.Database.Repositories +{ + public class ArchivedOrderRepository : IArchivedOrderRepository + { + private readonly NpgSqlContext _context; + + public ArchivedOrderRepository(NpgSqlContext context) + { + _context = context; + } + + public async Task AddOrderAsync(Guid id, + bool isConfirmed, + DateTimeOffset submitDate, + string manager, + DateTimeOffset? confirmDate, + DateTimeOffset? deliveredDate) + { + var archivedOrder = new ArchivedOrder(id, isConfirmed, submitDate, manager, confirmDate, deliveredDate); + + await _context.ArchivedOrders! + .AddAsync(archivedOrder); + + await _context.SaveChangesAsync(); + } + + public async Task GetOrderAsync(Guid id) + { + var archivedOrder = await _context.ArchivedOrders! + .AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == id); + + return archivedOrder!; + } + } +} diff --git a/src/HistoryService/Database/Repositories/Interfaces/IArchivedOrderRepository.cs b/src/HistoryService/Database/Repositories/Interfaces/IArchivedOrderRepository.cs new file mode 100644 index 0000000..188805b --- /dev/null +++ b/src/HistoryService/Database/Repositories/Interfaces/IArchivedOrderRepository.cs @@ -0,0 +1,19 @@ +using HistoryService.Database.Models; +using System; +using System.Threading.Tasks; + +namespace HistoryService.Database.Repositories.Interfaces +{ + public interface IArchivedOrderRepository + { + public Task AddOrderAsync(Guid id, + bool isConfirmed, + DateTimeOffset submitDate, + string manager, + DateTimeOffset? confirmDate, + DateTimeOffset? deliveredDate); + + public Task GetOrderAsync(Guid id); + + } +} diff --git a/src/HistoryService/Dockerfile b/src/HistoryService/Dockerfile new file mode 100644 index 0000000..c282965 --- /dev/null +++ b/src/HistoryService/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +COPY ./bin/Release/net6.0 . + +ENTRYPOINT ["dotnet", "HistoryService.dll"] \ No newline at end of file diff --git a/src/HistoryService/HistoryService.csproj b/src/HistoryService/HistoryService.csproj new file mode 100644 index 0000000..1fa6d7a --- /dev/null +++ b/src/HistoryService/HistoryService.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + true + dotnet-HistoryService-7D9C4E29-7145-4EA2-B4D2-C14532CDFC52 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/HistoryService/Migrations/20211031111914_Initial.Designer.cs b/src/HistoryService/Migrations/20211031111914_Initial.Designer.cs new file mode 100644 index 0000000..539beef --- /dev/null +++ b/src/HistoryService/Migrations/20211031111914_Initial.Designer.cs @@ -0,0 +1,55 @@ +// +using System; +using HistoryService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace HistoryService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + [Migration("20211031111914_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("HistoryService.Database.Models.ArchievedOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConfirmDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("Manager") + .HasColumnType("text"); + + b.Property("SubmitDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ArchievedOrders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/HistoryService/Migrations/20211031111914_Initial.cs b/src/HistoryService/Migrations/20211031111914_Initial.cs new file mode 100644 index 0000000..211d577 --- /dev/null +++ b/src/HistoryService/Migrations/20211031111914_Initial.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HistoryService.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ArchievedOrders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TotalPrice = table.Column(type: "integer", nullable: false), + IsConfirmed = table.Column(type: "boolean", nullable: false), + SubmitDate = table.Column(type: "timestamp with time zone", nullable: false), + Manager = table.Column(type: "text", nullable: true), + ConfirmDate = table.Column(type: "timestamp with time zone", nullable: true), + DeliveredDate = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ArchievedOrders", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ArchievedOrders"); + } + } +} diff --git a/src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.Designer.cs b/src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.Designer.cs new file mode 100644 index 0000000..55851f6 --- /dev/null +++ b/src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.Designer.cs @@ -0,0 +1,52 @@ +// +using System; +using HistoryService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace HistoryService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + [Migration("20211102100836_RemovedTotalPrice")] + partial class RemovedTotalPrice + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("HistoryService.Database.Models.ArchievedOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConfirmDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("Manager") + .HasColumnType("text"); + + b.Property("SubmitDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ArchievedOrders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.cs b/src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.cs new file mode 100644 index 0000000..3e84a62 --- /dev/null +++ b/src/HistoryService/Migrations/20211102100836_RemovedTotalPrice.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HistoryService.Migrations +{ + public partial class RemovedTotalPrice : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TotalPrice", + table: "ArchievedOrders"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TotalPrice", + table: "ArchievedOrders", + type: "integer", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.Designer.cs b/src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.Designer.cs new file mode 100644 index 0000000..e461688 --- /dev/null +++ b/src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.Designer.cs @@ -0,0 +1,52 @@ +// +using System; +using HistoryService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace HistoryService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + [Migration("20211111144256_ChangedArchievedToArchived")] + partial class ChangedArchievedToArchived + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("HistoryService.Database.Models.ArchivedOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConfirmDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("Manager") + .HasColumnType("text"); + + b.Property("SubmitDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ArchivedOrders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.cs b/src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.cs new file mode 100644 index 0000000..e839ba5 --- /dev/null +++ b/src/HistoryService/Migrations/20211111144256_ChangedArchievedToArchived.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HistoryService.Migrations +{ + public partial class ChangedArchievedToArchived : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ArchievedOrders"); + + migrationBuilder.CreateTable( + name: "ArchivedOrders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + IsConfirmed = table.Column(type: "boolean", nullable: false), + SubmitDate = table.Column(type: "timestamp with time zone", nullable: false), + Manager = table.Column(type: "text", nullable: true), + ConfirmDate = table.Column(type: "timestamp with time zone", nullable: true), + DeliveredDate = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ArchivedOrders", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ArchivedOrders"); + + migrationBuilder.CreateTable( + name: "ArchievedOrders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ConfirmDate = table.Column(type: "timestamp with time zone", nullable: true), + DeliveredDate = table.Column(type: "timestamp with time zone", nullable: true), + IsConfirmed = table.Column(type: "boolean", nullable: false), + Manager = table.Column(type: "text", nullable: true), + SubmitDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ArchievedOrders", x => x.Id); + }); + } + } +} diff --git a/src/HistoryService/Migrations/NpgSqlContextModelSnapshot.cs b/src/HistoryService/Migrations/NpgSqlContextModelSnapshot.cs new file mode 100644 index 0000000..deae248 --- /dev/null +++ b/src/HistoryService/Migrations/NpgSqlContextModelSnapshot.cs @@ -0,0 +1,50 @@ +// +using System; +using HistoryService.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace HistoryService.Migrations +{ + [DbContext(typeof(NpgSqlContext))] + partial class NpgSqlContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("HistoryService.Database.Models.ArchivedOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConfirmDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("Manager") + .HasColumnType("text"); + + b.Property("SubmitDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ArchivedOrders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/HistoryService/Program.cs b/src/HistoryService/Program.cs new file mode 100644 index 0000000..b19fce7 --- /dev/null +++ b/src/HistoryService/Program.cs @@ -0,0 +1,84 @@ +using HistoryService.Configurations; +using HistoryService.Consumers; +using HistoryService.Database; +using HistoryService.Database.Repositories; +using HistoryService.Database.Repositories.Interfaces; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace HistoryService +{ + public class Program + { + public static void Main(string[] args) + { + Console.Title = Assembly.GetExecutingAssembly().GetName().Name!; + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + + var contextOptions = new DbContextOptionsBuilder() + .UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")) + .Options; + + using (var context = new NpgSqlContext(contextOptions)) + { + context.Database.Migrate(); + } + + services.AddTransient(); + + services.AddDbContext(opt => + opt.UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")), + ServiceLifetime.Transient, + ServiceLifetime.Transient); + + var endpointsSection = hostContext.Configuration.GetSection("EndpointsConfiguration"); + var endpointsConfig = endpointsSection.Get(); + + var rabbitMqSection = hostContext.Configuration.GetSection("RabbitMqConfiguration"); + var rabbitMqConfig = rabbitMqSection.Get(); + + services.AddMassTransit(x => + { + x.AddConsumer(typeof(ArchivedOrderConsumerDefinition)) + .Endpoint(cfg => + { + cfg.Name = endpointsConfig.HistoryServiceAddress; + }); + + x.AddConsumer(typeof(GetOrderFromArchiveConsumerDefinition)) + .Endpoint(cfg => + { + cfg.Name = endpointsConfig.HistoryServiceAddress; + }); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseBsonSerializer(); + cfg.ConfigureEndpoints(context); + + cfg.Host(rabbitMqConfig.Hostname, rabbitMqConfig.VirtualHost, h => + { + h.Username(rabbitMqConfig.Username); + h.Password(rabbitMqConfig.Password); + }); + }); + + }).AddMassTransitHostedService(true); + }); + } +} diff --git a/src/HistoryService/Properties/launchSettings.json b/src/HistoryService/Properties/launchSettings.json new file mode 100644 index 0000000..246db67 --- /dev/null +++ b/src/HistoryService/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HistoryService": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/HistoryService/appsettings.Development.json b/src/HistoryService/appsettings.Development.json new file mode 100644 index 0000000..1a07382 --- /dev/null +++ b/src/HistoryService/appsettings.Development.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + + "EndpointsConfiguration": { + "HistoryServiceAddress": "history-service" + }, + + "RabbitMqConfiguration": { + "Hostname": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + }, + + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=localhost;Port=5432;Database=HistoryServiceDb;" + } +} diff --git a/src/HistoryService/appsettings.json b/src/HistoryService/appsettings.json new file mode 100644 index 0000000..762f922 --- /dev/null +++ b/src/HistoryService/appsettings.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + + "EndpointsConfiguration": { + "HistoryServiceAddress": "history-service" + }, + + "RabbitMqConfiguration": { + "Hostname": "rabbitmq", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + }, + + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=database;Port=5432;Database=HistoryServiceDb;" + } +} diff --git a/src/MassTransitAdvancedExample.sln b/src/MassTransitAdvancedExample.sln new file mode 100644 index 0000000..72538ca --- /dev/null +++ b/src/MassTransitAdvancedExample.sln @@ -0,0 +1,130 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiService", "ApiService\ApiService.csproj", "{1A5B5DF9-FEFB-4312-B709-9017ACCEBC7F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CartService", "CartService\CartService.csproj", "{BAA754EE-7039-4CB4-ABCB-D74E26FC2400}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService", "PaymentService\PaymentService.csproj", "{EFE6E120-2932-46F6-95D3-BA165C6F1606}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeliveryService", "DeliveryService\DeliveryService.csproj", "{01372D1D-2DBD-41E7-B49B-21577ED9A3C1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrderOrchestratorService", "OrderOrchestratorService\OrderOrchestratorService.csproj", "{A4F134A4-D510-469D-B04D-2D7ADC61282B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeedbackService", "FeedbackService\FeedbackService.csproj", "{46C26DBD-31BF-40D0-A4A2-58937434D4FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HistoryService", "HistoryService\HistoryService.csproj", "{681AA9C6-381C-46E3-8AF2-8055129AD1DE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CartService.Contracts", "Contracts\CartService.Contracts\CartService.Contracts.csproj", "{85EB029C-0248-4CAB-9B73-D2B5E39A619F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeliveryService.Contracts", "Contracts\DeliveryService.Contracts\DeliveryService.Contracts.csproj", "{45B7E8EA-474B-4837-AADC-17FE720B0A8E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contracts.Shared", "Contracts\Contracts.Shared\Contracts.Shared.csproj", "{80793CD6-2451-4BC7-9873-6A1835673C75}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeedbackService.Contracts", "Contracts\FeedbackService.Contracts\FeedbackService.Contracts.csproj", "{97835F17-B421-406E-A622-677201E87B65}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HistoryService.Contracts", "Contracts\HistoryService.Contracts\HistoryService.Contracts.csproj", "{C6E86BC1-7C80-4C8A-9B97-E234180BD497}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Contracts", "Contracts\PaymentService.Contracts\PaymentService.Contracts.csproj", "{849B352B-FAE2-4605-9FA1-A3DC994DD403}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiService.Contracts", "Contracts\ApiService.Contracts\ApiService.Contracts.csproj", "{E752B93D-4FF9-48A1-93FA-9AD6C4166E87}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Contracts", "Contracts", "{43BF4E21-BF07-46CE-B3FE-56EB96950B38}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{883D3BA4-2A79-4613-9BAF-8C3F76D3382F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrderOrchestratorService.Tests", "Tests\OrderOrchestratorService.Tests\OrderOrchestratorService.Tests.csproj", "{727F83C4-F547-4AC7-8BCF-4E7636C7FD1D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaymentService.Tests", "Tests\PaymentService.Tests\PaymentService.Tests.csproj", "{8A11255F-641A-4B32-9E9A-0CA7A4657C1B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A5B5DF9-FEFB-4312-B709-9017ACCEBC7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A5B5DF9-FEFB-4312-B709-9017ACCEBC7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A5B5DF9-FEFB-4312-B709-9017ACCEBC7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A5B5DF9-FEFB-4312-B709-9017ACCEBC7F}.Release|Any CPU.Build.0 = Release|Any CPU + {BAA754EE-7039-4CB4-ABCB-D74E26FC2400}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAA754EE-7039-4CB4-ABCB-D74E26FC2400}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAA754EE-7039-4CB4-ABCB-D74E26FC2400}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAA754EE-7039-4CB4-ABCB-D74E26FC2400}.Release|Any CPU.Build.0 = Release|Any CPU + {EFE6E120-2932-46F6-95D3-BA165C6F1606}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFE6E120-2932-46F6-95D3-BA165C6F1606}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFE6E120-2932-46F6-95D3-BA165C6F1606}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFE6E120-2932-46F6-95D3-BA165C6F1606}.Release|Any CPU.Build.0 = Release|Any CPU + {01372D1D-2DBD-41E7-B49B-21577ED9A3C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01372D1D-2DBD-41E7-B49B-21577ED9A3C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01372D1D-2DBD-41E7-B49B-21577ED9A3C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01372D1D-2DBD-41E7-B49B-21577ED9A3C1}.Release|Any CPU.Build.0 = Release|Any CPU + {A4F134A4-D510-469D-B04D-2D7ADC61282B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4F134A4-D510-469D-B04D-2D7ADC61282B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4F134A4-D510-469D-B04D-2D7ADC61282B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4F134A4-D510-469D-B04D-2D7ADC61282B}.Release|Any CPU.Build.0 = Release|Any CPU + {46C26DBD-31BF-40D0-A4A2-58937434D4FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46C26DBD-31BF-40D0-A4A2-58937434D4FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46C26DBD-31BF-40D0-A4A2-58937434D4FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46C26DBD-31BF-40D0-A4A2-58937434D4FB}.Release|Any CPU.Build.0 = Release|Any CPU + {681AA9C6-381C-46E3-8AF2-8055129AD1DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {681AA9C6-381C-46E3-8AF2-8055129AD1DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {681AA9C6-381C-46E3-8AF2-8055129AD1DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {681AA9C6-381C-46E3-8AF2-8055129AD1DE}.Release|Any CPU.Build.0 = Release|Any CPU + {85EB029C-0248-4CAB-9B73-D2B5E39A619F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85EB029C-0248-4CAB-9B73-D2B5E39A619F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85EB029C-0248-4CAB-9B73-D2B5E39A619F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85EB029C-0248-4CAB-9B73-D2B5E39A619F}.Release|Any CPU.Build.0 = Release|Any CPU + {45B7E8EA-474B-4837-AADC-17FE720B0A8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45B7E8EA-474B-4837-AADC-17FE720B0A8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45B7E8EA-474B-4837-AADC-17FE720B0A8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45B7E8EA-474B-4837-AADC-17FE720B0A8E}.Release|Any CPU.Build.0 = Release|Any CPU + {80793CD6-2451-4BC7-9873-6A1835673C75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80793CD6-2451-4BC7-9873-6A1835673C75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80793CD6-2451-4BC7-9873-6A1835673C75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80793CD6-2451-4BC7-9873-6A1835673C75}.Release|Any CPU.Build.0 = Release|Any CPU + {97835F17-B421-406E-A622-677201E87B65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97835F17-B421-406E-A622-677201E87B65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97835F17-B421-406E-A622-677201E87B65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97835F17-B421-406E-A622-677201E87B65}.Release|Any CPU.Build.0 = Release|Any CPU + {C6E86BC1-7C80-4C8A-9B97-E234180BD497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6E86BC1-7C80-4C8A-9B97-E234180BD497}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6E86BC1-7C80-4C8A-9B97-E234180BD497}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6E86BC1-7C80-4C8A-9B97-E234180BD497}.Release|Any CPU.Build.0 = Release|Any CPU + {849B352B-FAE2-4605-9FA1-A3DC994DD403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {849B352B-FAE2-4605-9FA1-A3DC994DD403}.Debug|Any CPU.Build.0 = Debug|Any CPU + {849B352B-FAE2-4605-9FA1-A3DC994DD403}.Release|Any CPU.ActiveCfg = Release|Any CPU + {849B352B-FAE2-4605-9FA1-A3DC994DD403}.Release|Any CPU.Build.0 = Release|Any CPU + {E752B93D-4FF9-48A1-93FA-9AD6C4166E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E752B93D-4FF9-48A1-93FA-9AD6C4166E87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E752B93D-4FF9-48A1-93FA-9AD6C4166E87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E752B93D-4FF9-48A1-93FA-9AD6C4166E87}.Release|Any CPU.Build.0 = Release|Any CPU + {727F83C4-F547-4AC7-8BCF-4E7636C7FD1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {727F83C4-F547-4AC7-8BCF-4E7636C7FD1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {727F83C4-F547-4AC7-8BCF-4E7636C7FD1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {727F83C4-F547-4AC7-8BCF-4E7636C7FD1D}.Release|Any CPU.Build.0 = Release|Any CPU + {8A11255F-641A-4B32-9E9A-0CA7A4657C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A11255F-641A-4B32-9E9A-0CA7A4657C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A11255F-641A-4B32-9E9A-0CA7A4657C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A11255F-641A-4B32-9E9A-0CA7A4657C1B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {85EB029C-0248-4CAB-9B73-D2B5E39A619F} = {43BF4E21-BF07-46CE-B3FE-56EB96950B38} + {45B7E8EA-474B-4837-AADC-17FE720B0A8E} = {43BF4E21-BF07-46CE-B3FE-56EB96950B38} + {80793CD6-2451-4BC7-9873-6A1835673C75} = {43BF4E21-BF07-46CE-B3FE-56EB96950B38} + {97835F17-B421-406E-A622-677201E87B65} = {43BF4E21-BF07-46CE-B3FE-56EB96950B38} + {C6E86BC1-7C80-4C8A-9B97-E234180BD497} = {43BF4E21-BF07-46CE-B3FE-56EB96950B38} + {849B352B-FAE2-4605-9FA1-A3DC994DD403} = {43BF4E21-BF07-46CE-B3FE-56EB96950B38} + {E752B93D-4FF9-48A1-93FA-9AD6C4166E87} = {43BF4E21-BF07-46CE-B3FE-56EB96950B38} + {727F83C4-F547-4AC7-8BCF-4E7636C7FD1D} = {883D3BA4-2A79-4613-9BAF-8C3F76D3382F} + {8A11255F-641A-4B32-9E9A-0CA7A4657C1B} = {883D3BA4-2A79-4613-9BAF-8C3F76D3382F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3D08F1B6-4148-4B08-9176-EA967E0BFFE9} + EndGlobalSection +EndGlobal diff --git a/src/OrderOrchestratorService/Configurations/EndpointsConfiguration.cs b/src/OrderOrchestratorService/Configurations/EndpointsConfiguration.cs new file mode 100644 index 0000000..d128778 --- /dev/null +++ b/src/OrderOrchestratorService/Configurations/EndpointsConfiguration.cs @@ -0,0 +1,14 @@ +namespace OrderOrchestratorService.Configurations +{ + public class EndpointsConfiguration + { + public string? OrderStateMachineAddress { get; set; } + public string? ArchiveOrderStateMachineAddress { get; set; } + public string? CartServiceAddress { get; set; } + public string? PaymentServiceAddress { get; set; } + public string? MessageSchedulerAddress { get; set; } + public string? DeliveryServiceAddress { get; set; } + public string? FeedbackServiceAddress { get; set; } + public string? HistoryServiceAddress { get; set; } + } +} diff --git a/src/OrderOrchestratorService/Configurations/RabbitMqConfiguration.cs b/src/OrderOrchestratorService/Configurations/RabbitMqConfiguration.cs new file mode 100644 index 0000000..2aaf084 --- /dev/null +++ b/src/OrderOrchestratorService/Configurations/RabbitMqConfiguration.cs @@ -0,0 +1,10 @@ +namespace OrderOrchestratorService.Configurations +{ + public class RabbitMqConfiguration + { + public string? Hostname { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + } +} diff --git a/src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumer.cs b/src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumer.cs new file mode 100644 index 0000000..35856fd --- /dev/null +++ b/src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumer.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Threading.Tasks; +using ApiService.Contracts.MonitoringApi; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using OrderOrchestratorService.Database; + +namespace OrderOrchestratorService.Consumers +{ + public class GetAllOrdersStateConsumer : IConsumer + { + private readonly StateMachinesDbContext _dbContext; + + public GetAllOrdersStateConsumer(StateMachinesDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task Consume(ConsumeContext context) + { + var sagas = await _dbContext.OrderStates!.ToListAsync(); + + var response = sagas.ToDictionary(s => s.CorrelationId, s => s.CurrentState); + + await context.RespondAsync(new + { + States = response + }); + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumerDefinition.cs b/src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumerDefinition.cs new file mode 100644 index 0000000..99e0b23 --- /dev/null +++ b/src/OrderOrchestratorService/Consumers/GetAllOrdersStateConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace OrderOrchestratorService.Consumers +{ + public class GetAllOrdersStateConsumerDefinition : ConsumerDefinition + { + public GetAllOrdersStateConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumer.cs b/src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumer.cs new file mode 100644 index 0000000..dcf0bab --- /dev/null +++ b/src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumer.cs @@ -0,0 +1,64 @@ +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using CartService.Contracts; +using FeedbackService.Contracts; +using HistoryService.Contracts; +using MassTransit; +using Microsoft.Extensions.Logging; + +namespace OrderOrchestratorService.Consumers +{ + public class GetArchivedOrderConsumer : IConsumer + { + private readonly ILogger _logger; + private readonly IRequestClient _cartRequestClient; + private readonly IRequestClient _archiveRequestClient; + private readonly IRequestClient _feedbackRequestClient; + + public GetArchivedOrderConsumer(ILogger logger, + IRequestClient cartRequestClient, + IRequestClient archiveRequestClient, + IRequestClient feedbackRequestClient) + { + _logger = logger; + _cartRequestClient = cartRequestClient; + _archiveRequestClient = archiveRequestClient; + _feedbackRequestClient = feedbackRequestClient; + } + + public async Task Consume(ConsumeContext context) + { + var orderId = context.Message.OrderId; + + var cart = (await _cartRequestClient.GetResponse(new + { + OrderId = orderId + })).Message; + + var archive = (await _archiveRequestClient.GetResponse(new + { + OrderId = orderId + })).Message; + + var feedback = (await _feedbackRequestClient.GetResponse(new + { + OrderId = orderId + })).Message; + + await context.RespondAsync(new + { + + OrderId = orderId, + Cart = cart.CartContent, + TotalPrice = cart.TotalPrice, + IsConfirmed = archive.IsConfirmed, + SubmitDate = archive.SubmitDate, + Manager = archive.Manager, + ConfirmDate = archive.ConfirmDate, + DeliveredDate = archive.DeliveredDate, + FeedbackText = feedback.Text, + FeedbackStars = feedback.StarsAmount + }); + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumerDefinition.cs b/src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumerDefinition.cs new file mode 100644 index 0000000..66614d4 --- /dev/null +++ b/src/OrderOrchestratorService/Consumers/GetArchivedOrderConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace OrderOrchestratorService.Consumers +{ + public class GetArchivedOrderConsumerDefinition : ConsumerDefinition + { + public GetArchivedOrderConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/OrderOrchestratorService/Consumers/GetOrderStateConsumer.cs b/src/OrderOrchestratorService/Consumers/GetOrderStateConsumer.cs new file mode 100644 index 0000000..a83a906 --- /dev/null +++ b/src/OrderOrchestratorService/Consumers/GetOrderStateConsumer.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using ApiService.Contracts.MonitoringApi; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using OrderOrchestratorService.Database; + +namespace OrderOrchestratorService.Consumers +{ + public class GetOrderStateConsumer : IConsumer + { + private readonly StateMachinesDbContext _dbContext; + + public GetOrderStateConsumer(StateMachinesDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task Consume(ConsumeContext context) + { + var orderId = context.Message.OrderId; + var saga = await _dbContext.OrderStates!.FirstOrDefaultAsync(o => o.CorrelationId == orderId); + + if (saga != null) + { + await context.RespondAsync(new + { + OrderId = orderId, + State = saga.CurrentState + }); + } + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Consumers/GetOrderStateConsumerDefinition.cs b/src/OrderOrchestratorService/Consumers/GetOrderStateConsumerDefinition.cs new file mode 100644 index 0000000..9cbce2e --- /dev/null +++ b/src/OrderOrchestratorService/Consumers/GetOrderStateConsumerDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace OrderOrchestratorService.Consumers +{ + public class GetOrderStateConsumerDefinition : ConsumerDefinition + { + public GetOrderStateConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + { + consumerConfigurator.UseDelayedRedelivery(r => r.Intervals(1000, 2000, 5000, 10000, 10000)); + consumerConfigurator.UseMessageRetry((r => r.Intervals(1000, 2000, 5000, 10000, 10000))); + consumerConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/OrderOrchestratorService/Database/Configurations/CartPositionConfiguration.cs b/src/OrderOrchestratorService/Database/Configurations/CartPositionConfiguration.cs new file mode 100644 index 0000000..7edbdb2 --- /dev/null +++ b/src/OrderOrchestratorService/Database/Configurations/CartPositionConfiguration.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using OrderOrchestratorService.Database.Models; + +namespace OrderOrchestratorService.Database.Configurations +{ + public class CartPositionConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(c => c.Id).IsUnique(); + builder.HasKey(c => c.Id); + + builder.Property(c => c.Id) + .IsRequired() + .ValueGeneratedNever(); + + builder.Property(c => c.Name).IsRequired(); + builder.Property(c => c.Amount).IsRequired(); + builder.Property(c => c.Price).IsRequired(); + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Database/Configurations/OrderStateMap.cs b/src/OrderOrchestratorService/Database/Configurations/OrderStateMap.cs new file mode 100644 index 0000000..b2e932a --- /dev/null +++ b/src/OrderOrchestratorService/Database/Configurations/OrderStateMap.cs @@ -0,0 +1,25 @@ +using MassTransit.EntityFrameworkCoreIntegration.Mappings; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using OrderOrchestratorService.StateMachines.OrderStateMachine; + +namespace OrderOrchestratorService.Database.Configurations +{ + public class OrderStateMap : SagaClassMap + { + protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) + { + entity.HasIndex(c => c.CorrelationId).IsUnique(); + entity.HasKey(c => c.CorrelationId); + + entity.Property(x => x.RowVersion).IsRowVersion(); + + entity.HasMany(o => o.Cart) + .WithOne(c => c.OrderState!) + .HasForeignKey(c => c.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + entity.Navigation(c => c.Cart).AutoInclude(); + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Database/Converters/FromDtoCartPositionToDbConverter.cs b/src/OrderOrchestratorService/Database/Converters/FromDtoCartPositionToDbConverter.cs new file mode 100644 index 0000000..5e1979f --- /dev/null +++ b/src/OrderOrchestratorService/Database/Converters/FromDtoCartPositionToDbConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DtoCartPosition = Contracts.Shared.CartPosition; +using DbCartPosition = OrderOrchestratorService.Database.Models.CartPosition; + +namespace OrderOrchestratorService.Database.Converters +{ + public static class FromDtoCartPositionToDbConverter + { + public static DbCartPosition Convert(DtoCartPosition dtoCartPosition, Guid orderId) + { + return new DbCartPosition(Guid.NewGuid(), orderId, dtoCartPosition.Name, dtoCartPosition.Amount, + dtoCartPosition.Price); + } + + public static DtoCartPosition ConvertBack(DbCartPosition dbCartPosition) + { + return new DtoCartPosition() + { + Name = dbCartPosition.Name, + Amount = dbCartPosition.Amount, + Price = dbCartPosition.Price + }; + } + + public static List ConvertMany(List dtoCartPositions, Guid orderId) + { + return dtoCartPositions.Select(c => Convert(c, orderId)).ToList(); + } + + public static List ConvertBackMany(List dbCartPositions) + { + return dbCartPositions.Select(c => ConvertBack(c)).ToList(); + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Database/Models/CartPosition.cs b/src/OrderOrchestratorService/Database/Models/CartPosition.cs new file mode 100644 index 0000000..57691f1 --- /dev/null +++ b/src/OrderOrchestratorService/Database/Models/CartPosition.cs @@ -0,0 +1,33 @@ +using System; +using OrderOrchestratorService.StateMachines.OrderStateMachine; + +namespace OrderOrchestratorService.Database.Models +{ + public class CartPosition + { + public Guid Id { get; set; } + public Guid OrderId { get; set; } + public OrderState? OrderState { get; set; } + public string? Name { get; set; } + public int Amount { get; set; } + public int Price { get; set; } + + public CartPosition() + { + + } + + public CartPosition(Guid id, + Guid orderId, + string name, + int amount, + int price) + { + Id = id; + OrderId = orderId; + Name = name; + Amount = amount; + Price = price; + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Database/StateMachinesDbContext.cs b/src/OrderOrchestratorService/Database/StateMachinesDbContext.cs new file mode 100644 index 0000000..1a88e41 --- /dev/null +++ b/src/OrderOrchestratorService/Database/StateMachinesDbContext.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using MassTransit.EntityFrameworkCoreIntegration; +using MassTransit.EntityFrameworkCoreIntegration.Mappings; +using Microsoft.EntityFrameworkCore; +using OrderOrchestratorService.Database.Configurations; +using OrderOrchestratorService.Database.Models; +using OrderOrchestratorService.StateMachines.OrderStateMachine; + +namespace OrderOrchestratorService.Database +{ + public class StateMachinesDbContext : SagaDbContext + { + public DbSet? OrderStates { get; set; } + public DbSet? CartPositions { get; set; } + + public StateMachinesDbContext(DbContextOptions options) : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new CartPositionConfiguration()); + base.OnModelCreating(modelBuilder); + } + + protected override IEnumerable Configurations + { + get + { + yield return new OrderStateMap(); + } + + } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Dockerfile b/src/OrderOrchestratorService/Dockerfile new file mode 100644 index 0000000..3ee646b --- /dev/null +++ b/src/OrderOrchestratorService/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +COPY ./bin/Release/net6.0 . + +ENTRYPOINT ["dotnet", "OrderOrchestratorService.dll"] \ No newline at end of file diff --git a/src/OrderOrchestratorService/InternalContracts/FeedbackReceivingTimeoutExpired.cs b/src/OrderOrchestratorService/InternalContracts/FeedbackReceivingTimeoutExpired.cs new file mode 100644 index 0000000..76b9cbc --- /dev/null +++ b/src/OrderOrchestratorService/InternalContracts/FeedbackReceivingTimeoutExpired.cs @@ -0,0 +1,9 @@ +using System; + +namespace OrderOrchestratorService.InternalContracts +{ + public interface FeedbackReceivingTimeoutExpired + { + public Guid OrderId { get; set; } + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/Migrations/20211105110239_Initial.Designer.cs b/src/OrderOrchestratorService/Migrations/20211105110239_Initial.Designer.cs new file mode 100644 index 0000000..7f49f93 --- /dev/null +++ b/src/OrderOrchestratorService/Migrations/20211105110239_Initial.Designer.cs @@ -0,0 +1,109 @@ +// + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OrderOrchestratorService.Database; + +namespace OrderOrchestratorService.Migrations +{ + [DbContext(typeof(StateMachinesDbContext))] + [Migration("20211105110239_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("OrderOrchecstratorService.Database.Models.CartPosition", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("OrderId"); + + b.ToTable("CartPositions"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("ConfirmationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .HasColumnType("integer"); + + b.Property("DeliveryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("Manager") + .HasColumnType("text"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("SubmitDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("integer"); + + b.HasKey("CorrelationId"); + + b.HasIndex("CorrelationId") + .IsUnique(); + + b.ToTable("OrderStates"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.Database.Models.CartPosition", b => + { + b.HasOne("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", "OrderState") + .WithMany("Cart") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrderState"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", b => + { + b.Navigation("Cart"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/OrderOrchestratorService/Migrations/20211105110239_Initial.cs b/src/OrderOrchestratorService/Migrations/20211105110239_Initial.cs new file mode 100644 index 0000000..cd7f8fc --- /dev/null +++ b/src/OrderOrchestratorService/Migrations/20211105110239_Initial.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace OrderOrchestratorService.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrderStates", + columns: table => new + { + CorrelationId = table.Column(type: "uuid", nullable: false), + CurrentState = table.Column(type: "integer", nullable: false), + SubmitDate = table.Column(type: "timestamp with time zone", nullable: true), + TotalPrice = table.Column(type: "integer", nullable: false), + Manager = table.Column(type: "text", nullable: true), + IsConfirmed = table.Column(type: "boolean", nullable: false), + ConfirmationDate = table.Column(type: "timestamp with time zone", nullable: true), + DeliveryDate = table.Column(type: "timestamp with time zone", nullable: true), + RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderStates", x => x.CorrelationId); + }); + + migrationBuilder.CreateTable( + name: "CartPositions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Amount = table.Column(type: "integer", nullable: false), + Price = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CartPositions", x => x.Id); + table.ForeignKey( + name: "FK_CartPositions_OrderStates_OrderId", + column: x => x.OrderId, + principalTable: "OrderStates", + principalColumn: "CorrelationId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CartPositions_Id", + table: "CartPositions", + column: "Id", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CartPositions_OrderId", + table: "CartPositions", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderStates_CorrelationId", + table: "OrderStates", + column: "CorrelationId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CartPositions"); + + migrationBuilder.DropTable( + name: "OrderStates"); + } + } +} diff --git a/src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.Designer.cs b/src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.Designer.cs new file mode 100644 index 0000000..c20ef7e --- /dev/null +++ b/src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.Designer.cs @@ -0,0 +1,112 @@ +// + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OrderOrchestratorService.Database; + +namespace OrderOrchestratorService.Migrations +{ + [DbContext(typeof(StateMachinesDbContext))] + [Migration("20211108065608_Added_FeedbackReceivingTimeoutToken")] + partial class Added_FeedbackReceivingTimeoutToken + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("OrderOrchecstratorService.Database.Models.CartPosition", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("OrderId"); + + b.ToTable("CartPositions"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("ConfirmationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .HasColumnType("integer"); + + b.Property("DeliveryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("FeedbackReceivingTimeoutToken") + .HasColumnType("uuid"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("Manager") + .HasColumnType("text"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("SubmitDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("integer"); + + b.HasKey("CorrelationId"); + + b.HasIndex("CorrelationId") + .IsUnique(); + + b.ToTable("OrderStates"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.Database.Models.CartPosition", b => + { + b.HasOne("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", "OrderState") + .WithMany("Cart") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrderState"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", b => + { + b.Navigation("Cart"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.cs b/src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.cs new file mode 100644 index 0000000..6623348 --- /dev/null +++ b/src/OrderOrchestratorService/Migrations/20211108065608_Added_FeedbackReceivingTimeoutToken.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace OrderOrchestratorService.Migrations +{ + public partial class Added_FeedbackReceivingTimeoutToken : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FeedbackReceivingTimeoutToken", + table: "OrderStates", + type: "uuid", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FeedbackReceivingTimeoutToken", + table: "OrderStates"); + } + } +} diff --git a/src/OrderOrchestratorService/Migrations/StateMachinesDbContextModelSnapshot.cs b/src/OrderOrchestratorService/Migrations/StateMachinesDbContextModelSnapshot.cs new file mode 100644 index 0000000..e8155c3 --- /dev/null +++ b/src/OrderOrchestratorService/Migrations/StateMachinesDbContextModelSnapshot.cs @@ -0,0 +1,110 @@ +// + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OrderOrchestratorService.Database; + +namespace OrderOrchestratorService.Migrations +{ + [DbContext(typeof(StateMachinesDbContext))] + partial class StateMachinesDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("OrderOrchecstratorService.Database.Models.CartPosition", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("OrderId"); + + b.ToTable("CartPositions"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("ConfirmationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .HasColumnType("integer"); + + b.Property("DeliveryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("FeedbackReceivingTimeoutToken") + .HasColumnType("uuid"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("Manager") + .HasColumnType("text"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("SubmitDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("integer"); + + b.HasKey("CorrelationId"); + + b.HasIndex("CorrelationId") + .IsUnique(); + + b.ToTable("OrderStates"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.Database.Models.CartPosition", b => + { + b.HasOne("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", "OrderState") + .WithMany("Cart") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrderState"); + }); + + modelBuilder.Entity("OrderOrchecstratorService.StateMachines.OrderStateMachine.OrderState", b => + { + b.Navigation("Cart"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/OrderOrchestratorService/OrderOrchestratorService.csproj b/src/OrderOrchestratorService/OrderOrchestratorService.csproj new file mode 100644 index 0000000..351ebe8 --- /dev/null +++ b/src/OrderOrchestratorService/OrderOrchestratorService.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + enable + true + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/src/OrderOrchestratorService/Program.cs b/src/OrderOrchestratorService/Program.cs new file mode 100644 index 0000000..e52d122 --- /dev/null +++ b/src/OrderOrchestratorService/Program.cs @@ -0,0 +1,106 @@ +using System; +using System.Reflection; +using CartService.Contracts; +using FeedbackService.Contracts; +using HistoryService.Contracts; +using MassTransit; +using MassTransit.EntityFrameworkCoreIntegration; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OrderOrchestratorService.Configurations; +using OrderOrchestratorService.Consumers; +using OrderOrchestratorService.Database; +using OrderOrchestratorService.StateMachines.ArchivedOrderStateMachine; +using OrderOrchestratorService.StateMachines.OrderStateMachine; + +namespace OrderOrchestratorService +{ + public class Program + { + public static void Main(string[] args) + { + Console.Title = Assembly.GetExecutingAssembly().GetName().Name!; + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + + var contextOptions = new DbContextOptionsBuilder() + .UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")) + .Options; + + using (var context = new StateMachinesDbContext(contextOptions)) + { + context.Database.Migrate(); + } + + var endpointsSection = hostContext.Configuration.GetSection("EndpointsConfiguration"); + var endpointsConfig = endpointsSection.Get(); + + services.Configure(endpointsSection); + + var rabbitMqSection = hostContext.Configuration.GetSection("RabbitMqConfiguration"); + var rabbitMqConfig = rabbitMqSection.Get(); + + services.AddDbContext(builder => + { + builder.UseNpgsql(hostContext.Configuration.GetConnectionString("DefaultConnection")); + }); + + services.AddMassTransit(x => + { + x.AddSagaRepository() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.ConcurrencyMode = ConcurrencyMode.Optimistic; + }); + + x.AddSagaStateMachine(typeof(OrderStateMachineDefinition)) + .Endpoint(e => + { + e.Name = endpointsConfig.OrderStateMachineAddress; + }); + + x.AddSagaStateMachine(typeof(ArchivedOrderStateMachineDefinition)) + .InMemoryRepository() + .Endpoint(e => + { + e.Name = endpointsConfig.ArchiveOrderStateMachineAddress; + }); + + x.AddDelayedMessageScheduler(); + x.AddConsumer(typeof(GetAllOrdersStateConsumerDefinition)); + x.AddConsumer(typeof(GetOrderStateConsumerDefinition)); + //x.AddConsumer(typeof(GetArchivedOrderConsumerDefinition)); + + x.AddRequestClient(new Uri(endpointsConfig.HistoryServiceAddress!)); + x.AddRequestClient(new Uri(endpointsConfig.FeedbackServiceAddress!)); + x.AddRequestClient(new Uri(endpointsConfig.CartServiceAddress!)); + + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseBsonSerializer(); + + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + + cfg.Host(rabbitMqConfig.Hostname, rabbitMqConfig.VirtualHost, h => + { + h.Username(rabbitMqConfig.Username); + h.Password(rabbitMqConfig.Password); + }); + }); + + }).AddMassTransitHostedService(true); + }); + } +} diff --git a/src/OrderOrchestratorService/Properties/launchSettings.json b/src/OrderOrchestratorService/Properties/launchSettings.json new file mode 100644 index 0000000..8fc5ded --- /dev/null +++ b/src/OrderOrchestratorService/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "OrderOrchecstratorService": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderState.cs b/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderState.cs new file mode 100644 index 0000000..921af03 --- /dev/null +++ b/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderState.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Automatonymous; +using Contracts.Shared; + +namespace OrderOrchestratorService.StateMachines.ArchivedOrderStateMachine +{ +#nullable disable + public class ArchivedOrderState : SagaStateMachineInstance + { + public Guid CorrelationId { get; set; } + public int CurrentState { get; set; } + public int InformationStatus { get; set; } + public Guid? RequestId { get; set; } + public Uri ResponseAddress { get; set; } + + public List Cart { get; set; } + + public int TotalPrice { get; set; } + + public bool IsConfirmed { get; set; } + + public DateTimeOffset SubmitDate { get; set; } + + public string Manager { get; set; } + + public DateTimeOffset? ConfirmDate { get; set; } + + public DateTimeOffset? DeliveredDate { get; set; } + + public string FeedbackText { get; set; } + public int FeedbackStars { get; set; } + } +#nullable restore +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachine.cs b/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachine.cs new file mode 100644 index 0000000..471ee81 --- /dev/null +++ b/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachine.cs @@ -0,0 +1,189 @@ +using System; +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using Automatonymous; +using Automatonymous.Binders; +using CartService.Contracts; +using FeedbackService.Contracts; +using HistoryService.Contracts; +using MassTransit; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrderOrchestratorService.Configurations; + +namespace OrderOrchestratorService.StateMachines.ArchivedOrderStateMachine +{ + public class ArchivedOrderStateMachine : MassTransitStateMachine + { +#nullable disable + private readonly ILogger _logger; + private readonly EndpointsConfiguration _endpointsConfiguration; + + public Event ArchivedOrderRequested { get; set; } + public Event InformationCollected { get; set; } + + public State AwaitingInformation { get; set; } + + public Request ArchiveRequest { get; set; } + public Request OrderFeedbackRequest { get; set; } + public Request CartRequest { get; set; } + + public ArchivedOrderStateMachine(IOptions settings, + ILogger logger) + { + _logger = logger; + _endpointsConfiguration = settings.Value; + + InstanceState(x => x.CurrentState); + + BuildStateMachine(); + + OnUnhandledEvent(HandleUnhandledEvent); + + Initially(WhenArchivedOrderRequested()); + + During(AwaitingInformation, + WhenCartRequestCompleted(), + WhenArchiveRequestCompleted(), + WhenOrderFeedbackRequestCompleted(), + Ignore(ArchivedOrderRequested)); + + DuringAny( + When(InformationCollected) + .IfElse(context => context.ShouldBeResponded(), + responded => responded + .SendAsync(x => x.Instance.ResponseAddress, + x => x.CreateArchivedOrderResponse(), + (consumeContext, sendContext) => + sendContext.RequestId = consumeContext.Instance.RequestId), + published => published + .PublishAsync(x => x.CreateArchivedOrderResponse())) + .Finalize()); + + CompositeEvent(() => InformationCollected, + x => x.InformationStatus, + CartRequest.Completed, + ArchiveRequest.Completed, + OrderFeedbackRequest.Completed); + + SetCompletedWhenFinalized(); + } + + private Task HandleUnhandledEvent(UnhandledEventContext context) + { + _logger.LogDebug($"[{DateTime.Now}][SAGA] Ignored unhandled event: {context.Event.Name}"); + + return Task.CompletedTask; + } + + private void BuildStateMachine() + { + Event(() => ArchivedOrderRequested, x => x.CorrelateById(x => x.Message.OrderId)); + + Request(() => ArchiveRequest, r => + { + r.ServiceAddress = new Uri(_endpointsConfiguration.HistoryServiceAddress!); + }); + + Request(() => OrderFeedbackRequest, r => + { + r.ServiceAddress = new Uri(_endpointsConfiguration.FeedbackServiceAddress!); + }); + + Request(() => CartRequest, r => + { + r.ServiceAddress = new Uri(_endpointsConfiguration.CartServiceAddress!); + }); + + + } + + private EventActivities WhenArchivedOrderRequested() + { + return When(ArchivedOrderRequested) + .TransitionTo(AwaitingInformation) + .Then(x => + { + if (x.TryGetPayload(out SagaConsumeContext payload)) + { + x.Instance.RequestId = payload.RequestId; + x.Instance.ResponseAddress = payload.ResponseAddress; + } + }) + .Request(CartRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId + })) + .Request(ArchiveRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId + })) + .Request(OrderFeedbackRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId + })); + } + + private EventActivities WhenCartRequestCompleted() + { + return When(CartRequest.Completed) + .Then(x => + { + _logger.LogInformation("WhenOrderCartRequestCompleted"); + x.Instance.Cart = x.Data.CartContent; + x.Instance.TotalPrice = x.Data.TotalPrice; + }); + } + + private EventActivities WhenArchiveRequestCompleted() + { + return When(ArchiveRequest.Completed) + .Then(x => + { + _logger.LogInformation("WhenArchiveRequestCompleted"); + x.Instance.ConfirmDate = x.Data.ConfirmDate; + x.Instance.SubmitDate = x.Data.SubmitDate; + x.Instance.IsConfirmed = x.Data.IsConfirmed; + x.Instance.Manager = x.Data.Manager; + x.Instance.DeliveredDate = x.Data.DeliveredDate; + }); + } + + private EventActivities WhenOrderFeedbackRequestCompleted() + { + return When(OrderFeedbackRequest.Completed) + .Then(x => + { + _logger.LogInformation("WhenOrderFeedbackRequestCompleted"); + x.Instance.FeedbackText = x.Data.Text; + x.Instance.FeedbackStars = x.Data.StarsAmount; + }); + } + } + + public static class ArchivedOrderStateMachineExtensions + { + public static Task CreateArchivedOrderResponse(this ConsumeEventContext context) + { + return context.Init((new + { + OrderId = context.Instance.CorrelationId, + Cart = context.Instance.Cart, + TotalPrice = context.Instance.TotalPrice, + IsConfirmed = context.Instance.IsConfirmed, + SubmitDate = context.Instance.SubmitDate, + Manager = context.Instance.Manager, + ConfirmDate = context.Instance.ConfirmDate, + DeliveredDate = context.Instance.DeliveredDate, + FeedbackText = context.Instance.FeedbackText, + FeedbackStars = context.Instance.FeedbackStars, + })); + } + + public static bool ShouldBeResponded(this BehaviorContext context) + { + return context.Instance.RequestId.HasValue && context.Instance.ResponseAddress != null; + } + } +#nullable restore +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachineDefinition.cs b/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachineDefinition.cs new file mode 100644 index 0000000..59f2283 --- /dev/null +++ b/src/OrderOrchestratorService/StateMachines/ArchivedOrderStateMachine/ArchivedOrderStateMachineDefinition.cs @@ -0,0 +1,19 @@ +using GreenPipes; +using MassTransit; +using MassTransit.Definition; + +namespace OrderOrchestratorService.StateMachines.ArchivedOrderStateMachine; + +public class ArchivedOrderStateMachineDefinition : SagaDefinition +{ + public ArchivedOrderStateMachineDefinition() + { + + } + + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + { + endpointConfigurator.UseMessageRetry(r => r.Intervals(50, 100, 500, 1000)); + endpointConfigurator.UseInMemoryOutbox(); + } +} \ No newline at end of file diff --git a/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderState.cs b/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderState.cs new file mode 100644 index 0000000..09b17c3 --- /dev/null +++ b/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderState.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Automatonymous; +using OrderOrchestratorService.Database.Models; + +namespace OrderOrchestratorService.StateMachines.OrderStateMachine +{ +#nullable disable + public class OrderState : SagaStateMachineInstance + { + public Guid CorrelationId { get; set; } + + public int CurrentState { get; set; } + + public DateTimeOffset? SubmitDate { get; set; } + + public int TotalPrice { get; set; } + + public List Cart { get; set; } + + public string Manager { get; set; } + + public bool IsConfirmed { get; set; } + + public DateTimeOffset? ConfirmationDate { get; set; } + + public DateTimeOffset? DeliveryDate { get; set; } + + public byte[] RowVersion { get; set; } + public Guid? FeedbackReceivingTimeoutToken { get; set; } + } +#nullable restore +} diff --git a/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachine.cs b/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachine.cs new file mode 100644 index 0000000..264f1bb --- /dev/null +++ b/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachine.cs @@ -0,0 +1,346 @@ +using System; +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using ApiService.Contracts.UserApi; +using Automatonymous; +using Automatonymous.Binders; +using CartService.Contracts; +using DeliveryService.Contracts; +using FeedbackService.Contracts; +using HistoryService.Contracts; +using MassTransit; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrderOrchestratorService.Configurations; +using OrderOrchestratorService.Database.Converters; +using OrderOrchestratorService.InternalContracts; +using PaymentService.Contracts; + +namespace OrderOrchestratorService.StateMachines.OrderStateMachine +{ +#nullable disable + public class OrderStateMachine : MassTransitStateMachine + { + + private readonly EndpointsConfiguration _settings; + private readonly ILogger _logger; + + public State AwaitingConfirmation { get; private set; } + public State AwaitingDelivery { get; private set; } + public State AwaitingFeedback { get; private set; } + + public Event OrderSubmitted { get; private set; } + public Event OrderConfirmed { get; private set; } + public Event OrderRejected { get; private set; } + public Event OrderDelivered { get; private set; } + public Event ReceivedFeedback { get; private set; } + public Event OrderAborted { get; private set; } + + public Request CartRequest { get; private set; } + public Request MoneyReservationRequest { get; private set; } + public Request MoneyUnreservationRequest { get; private set; } + public Request AddFeedbackRequest { get; private set; } + public Request ArchiveOrderRequest { get; private set; } + + public Schedule FeedbackReceivingTimeout { get; set; } + + public OrderStateMachine(IOptions settings, ILogger logger) + { + _settings = settings.Value; + _logger = logger; + + InstanceState(x => x.CurrentState); + + BuildStateMachine(); + + OnUnhandledEvent(HandleUnhandledEvent); + + Initially(WhenOrderSubmitted()); + During(CartRequest.Pending, WhenCartReturned()); + During(MoneyReservationRequest.Pending, WhenMoneyReserved(), + WhenMoneyReservationRequestTimeoutExpired(), + WhenMoneyReservationRequestFaulted()); + + During(MoneyUnreservationRequest.Pending, WhenMoneyUnreserved()); + During(AwaitingConfirmation, WhenOrderConfirmed(), WhenOrderRejected()); + During(AwaitingDelivery, WhenOrderDelivered()); + During(AwaitingFeedback, WhenFeedbackReceived(), WhenFeedbackReceivingTimeoutExpired()); + During(AddFeedbackRequest.Pending, WhenFeedbackAdded()); + During(ArchiveOrderRequest.Pending, WhenOrderArchived()); + DuringAny(WhenOrderAborted()); + + SetCompletedWhenFinalized(); + } + + private Task HandleUnhandledEvent(UnhandledEventContext context) + { + if (context.Event.Name.Contains("TimeoutExpired")) + { + _logger.LogDebug($"[{DateTime.Now}][SAGA] Ignored unhandled event: {context.Event.Name}"); + + context.Ignore(); + } + else + context.Throw(); + + return Task.CompletedTask; + } + + private void BuildStateMachine() + { + Event(() => OrderSubmitted, x => x.CorrelateById(context => context.Message.OrderId)); + Event(() => OrderConfirmed, x => x.CorrelateById(context => context.Message.OrderId)); + Event(() => OrderRejected, x => x.CorrelateById(context => context.Message.OrderId)); + Event(() => OrderDelivered, x => x.CorrelateById(context => context.Message.OrderId)); + Event(() => ReceivedFeedback, x => x.CorrelateById(context => context.Message.OrderId)); + Event(() => OrderAborted, x => x.CorrelateById(context => context.Message.OrderId)); + + Request(() => CartRequest, r => + { + r.ServiceAddress = new Uri(_settings.CartServiceAddress); + r.Timeout = TimeSpan.Zero; + }); + + Request(() => MoneyReservationRequest, r => + { + r.ServiceAddress = new Uri(_settings.PaymentServiceAddress); + r.Timeout = TimeSpan.Zero; + }); + + Request(() => MoneyUnreservationRequest, r => + { + r.ServiceAddress = new Uri(_settings.PaymentServiceAddress); + r.Timeout = TimeSpan.Zero; + }); + + Request(() => AddFeedbackRequest, r => + { + r.ServiceAddress = new Uri(_settings.FeedbackServiceAddress); + r.Timeout = TimeSpan.Zero; + }); + + Request(() => ArchiveOrderRequest, r => + { + r.ServiceAddress = new Uri(_settings.HistoryServiceAddress); + r.Timeout = TimeSpan.Zero; + }); + + Schedule(() => FeedbackReceivingTimeout, instance => instance.FeedbackReceivingTimeoutToken, + s => + { + s.Delay = TimeSpan.FromMinutes(1); + s.Received = r => r.CorrelateById(x => x.Message.OrderId); + }); + } + + private EventActivities WhenOrderAborted() + { + return When(OrderAborted) + .Then(context => + { + _logger.LogInformation($"[{DateTime.Now}][SAGA] Order aborted. CorrelationId: {context.Instance.CorrelationId}"); + + + }) + .RespondAsync(x => x.Init(new + { + OrderId = x.Instance.CorrelationId + })) + .Finalize(); + } + + private EventActivities WhenOrderSubmitted() + { + return When(OrderSubmitted) + .Then(context => + { + _logger.LogInformation($"[{DateTime.Now}][SAGA] Order submitted. CorrelationId: {context.Instance.CorrelationId}"); + context.Instance.SubmitDate = DateTimeOffset.Now; + }) + .Request(CartRequest, x => x.Init(new { OrderId = x.Instance.CorrelationId })) + .TransitionTo(CartRequest.Pending); + } + + private EventActivities WhenCartReturned() + { + return When(CartRequest.Completed) + .Then(x => + { + x.Instance.Cart = FromDtoCartPositionToDbConverter.ConvertMany(x.Data.CartContent, x.Instance.CorrelationId); + x.Instance.TotalPrice = x.Data.TotalPrice; + _logger.LogInformation($"[{DateTime.Now}][SAGA] Cart returned. CorrelationId: {x.Instance.CorrelationId}"); + }) + .Request(MoneyReservationRequest, x => x.Init(new + { + OrderId = x.Data.OrderId, + Amount = x.Data.TotalPrice + })) + .TransitionTo(MoneyReservationRequest.Pending); + } + + private EventActivities WhenMoneyReserved() + { + return When(MoneyReservationRequest.Completed) + .Then(x => + { + _logger.LogInformation($"[{DateTime.Now}][SAGA] Money reserved. CorrelationId: {x.Instance.CorrelationId}"); + }) + .PublishAsync(x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Cart = FromDtoCartPositionToDbConverter.ConvertBackMany(x.Instance.Cart), + TotalPrice = x.Instance.TotalPrice + })) + .TransitionTo(AwaitingConfirmation); + } + + private EventActivities WhenOrderConfirmed() + { + return When(OrderConfirmed) + .Then(x => + { + _logger.LogInformation($"[{DateTime.Now}][SAGA] Order confirmed. CorrelationId: {x.Instance.CorrelationId}"); + + x.Instance.IsConfirmed = true; + x.Instance.ConfirmationDate = DateTimeOffset.Now; + x.Instance.Manager = x.Data.ConfirmManager; + }) + .SendAsync(new Uri(_settings.DeliveryServiceAddress), x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Cart = FromDtoCartPositionToDbConverter.ConvertBackMany(x.Instance.Cart) + })) + .TransitionTo(AwaitingDelivery); + } + + private EventActivities WhenOrderRejected() + { + return When(OrderRejected) + .Then(x => + { + _logger.LogInformation($"[{DateTime.Now}][SAGA] Order rejected. CorrelationId: {x.Instance.CorrelationId}"); + + x.Instance.Manager = x.Data.RejectManager; + }) + .PublishAsync(x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Reason = x.Data.Reason + })) + .Request(MoneyUnreservationRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Amount = x.Instance.TotalPrice + })) + .TransitionTo(MoneyUnreservationRequest.Pending); + } + + private EventActivities WhenOrderDelivered() + { + return When(OrderDelivered) + .Then(x => + { + x.Instance.DeliveryDate = DateTimeOffset.Now; + }) + .PublishAsync(x => x.Init(new + { + OrderId = x.Instance.CorrelationId + })) + .Schedule(FeedbackReceivingTimeout, x => x.Init(new + { + OrderId = x.Instance.CorrelationId + })) + .TransitionTo(AwaitingFeedback); + } + + private EventActivities WhenFeedbackReceived() + { + return When(ReceivedFeedback) + .Unschedule(FeedbackReceivingTimeout) + .Request(AddFeedbackRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Text = x.Data.Text, + StarsAmount = x.Data.StarsAmount + })) + .TransitionTo(AddFeedbackRequest.Pending); + } + + private EventActivities WhenFeedbackReceivingTimeoutExpired() + { + return When(FeedbackReceivingTimeout.Received) + .Then(x => _logger.LogInformation( + $"[{DateTime.Now}][SAGA] Feedback receiving timed out. CorrelationId: {x.Instance.CorrelationId}")) + .Request(ArchiveOrderRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Cart = FromDtoCartPositionToDbConverter.ConvertBackMany(x.Instance.Cart), + TotalPrice = x.Instance.TotalPrice, + IsConfirmed = x.Instance.IsConfirmed, + SubmitDate = x.Instance.SubmitDate, + Manager = x.Instance.Manager, + ConfirmDate = x.Instance.ConfirmationDate, + DeliveredDate = x.Instance.DeliveryDate + })) + .TransitionTo(ArchiveOrderRequest.Pending); + } + + private EventActivities WhenFeedbackAdded() + { + return When(AddFeedbackRequest.Completed) + .Request(ArchiveOrderRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Cart = FromDtoCartPositionToDbConverter.ConvertBackMany(x.Instance.Cart), + TotalPrice = x.Instance.TotalPrice, + IsConfirmed = x.Instance.IsConfirmed, + SubmitDate = x.Instance.SubmitDate, + Manager = x.Instance.Manager, + ConfirmDate = x.Instance.ConfirmationDate, + DeliveredDate = x.Instance.DeliveryDate + })) + .TransitionTo(ArchiveOrderRequest.Pending); + } + + private EventActivities WhenOrderArchived() + { + return When(ArchiveOrderRequest.Completed) + .Then(x => _logger.LogInformation($"[{DateTime.Now}][SAGA] Order archived. CorrelationId: {x.Instance.CorrelationId}")) + .Finalize(); + } + + private EventActivities WhenMoneyUnreserved() + { + return When(MoneyUnreservationRequest.Completed) + .Then(x => _logger.LogInformation( + $"[{DateTime.Now}][SAGA] Money unreserved. CorrelationId: {x.Instance.CorrelationId}")) + .Request(ArchiveOrderRequest, x => x.Init(new + { + OrderId = x.Instance.CorrelationId, + Cart = FromDtoCartPositionToDbConverter.ConvertBackMany(x.Instance.Cart), + TotalPrice = x.Instance.TotalPrice, + IsConfirmed = x.Instance.IsConfirmed, + SubmitDate = x.Instance.SubmitDate, + Manager = x.Instance.Manager, + ConfirmDate = x.Instance.ConfirmationDate, + DeliveredDate = x.Instance.DeliveryDate + })) + .TransitionTo(ArchiveOrderRequest.Pending); + } + + private EventActivities WhenMoneyReservationRequestTimeoutExpired() + { + return When(MoneyReservationRequest.TimeoutExpired) + .Then(x => _logger.LogWarning( + $"[{DateTime.Now}][SAGA] MoneyReservation request timed out. CorrelationId: {x.Instance.CorrelationId}")); + } + + private EventActivities WhenMoneyReservationRequestFaulted() + { + return When(MoneyReservationRequest.Faulted) + .Then(x => _logger.LogError( + $"[{DateTime.Now}][SAGA] MoneyReservation request failed. CorrelationId: {x.Instance.CorrelationId}")); + } + } +#nullable restore +} diff --git a/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachineDefinition.cs b/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachineDefinition.cs new file mode 100644 index 0000000..642dbe0 --- /dev/null +++ b/src/OrderOrchestratorService/StateMachines/OrderStateMachine/OrderStateMachineDefinition.cs @@ -0,0 +1,22 @@ +using GreenPipes; +using MassTransit; +using MassTransit.Definition; + +namespace OrderOrchestratorService.StateMachines.OrderStateMachine +{ +#nullable restore + public class OrderStateMachineDefinition : SagaDefinition + { + public OrderStateMachineDefinition() + { + + } + + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, + ISagaConfigurator sagaConfigurator) + { + endpointConfigurator.UseMessageRetry(r => r.Intervals(50, 100, 500, 1000)); + endpointConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/OrderOrchestratorService/appsettings.Development.json b/src/OrderOrchestratorService/appsettings.Development.json new file mode 100644 index 0000000..d208817 --- /dev/null +++ b/src/OrderOrchestratorService/appsettings.Development.json @@ -0,0 +1,32 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=127.0.0.1;Port=5432;Database=OrderOrchestratorDb;" + }, + + "EndpointsConfiguration": { + "OrderStateMachineAddress": "order-state-machine", + "ArchiveOrderStateMachineAddress": "archive-order-state-machine", + "CartServiceAddress": "exchange:cart-service", + "PaymentServiceAddress": "exchange:payment-service", + "DeliveryServiceAddress": "exchange:delivery-service", + "FeedbackServiceAddress": "exchange:feedback-service", + "HistoryServiceAddress": "exchange:history-service", + "MessageSchedulerAddress": "state-machine-message-scheduler" + }, + + "RabbitMqConfiguration": { + "Hostname": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } +} diff --git a/src/OrderOrchestratorService/appsettings.json b/src/OrderOrchestratorService/appsettings.json new file mode 100644 index 0000000..dcae5ad --- /dev/null +++ b/src/OrderOrchestratorService/appsettings.json @@ -0,0 +1,32 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MassTransit": "Debug" + } + }, + + "ConnectionStrings": { + "DefaultConnection": "User ID=postgres;Password=postgres;Server=database;Port=5432;Database=OrderOrchestratorDb;" + }, + + "EndpointsConfiguration": { + "OrderStateMachineAddress": "order-state-machine", + "ArchiveOrderStateMachineAddress": "archive-order-state-machine", + "CartServiceAddress": "exchange:cart-service", + "PaymentServiceAddress": "exchange:payment-service", + "DeliveryServiceAddress": "exchange:delivery-service", + "FeedbackServiceAddress": "exchange:feedback-service", + "HistoryServiceAddress": "exchange:history-service", + "MessageSchedulerAddress": "state-machine-message-scheduler" + }, + + "RabbitMqConfiguration": { + "Hostname": "rabbitmq", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } +} diff --git a/src/PaymentService/Configurations/EndpointsConfiguration.cs b/src/PaymentService/Configurations/EndpointsConfiguration.cs new file mode 100644 index 0000000..0d58041 --- /dev/null +++ b/src/PaymentService/Configurations/EndpointsConfiguration.cs @@ -0,0 +1,7 @@ +namespace PaymentService.Configurations +{ + public class EndpointsConfiguration + { + public string? PaymentServiceAddress { get; set; } + } +} \ No newline at end of file diff --git a/src/PaymentService/Configurations/RabbitMqConfiguration.cs b/src/PaymentService/Configurations/RabbitMqConfiguration.cs new file mode 100644 index 0000000..d3eec87 --- /dev/null +++ b/src/PaymentService/Configurations/RabbitMqConfiguration.cs @@ -0,0 +1,10 @@ +namespace PaymentService.Configurations +{ + public class RabbitMqConfiguration + { + public string? Hostname { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + } +} \ No newline at end of file diff --git a/src/PaymentService/Consumers/ReserveMoneyConsumer.cs b/src/PaymentService/Consumers/ReserveMoneyConsumer.cs new file mode 100644 index 0000000..6a9b379 --- /dev/null +++ b/src/PaymentService/Consumers/ReserveMoneyConsumer.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using MassTransit; +using Microsoft.Extensions.Logging; +using PaymentService.Contracts; + +namespace PaymentService.Consumers +{ + public class ReserveMoneyConsumer : IConsumer + { + private readonly ILogger _logger; + + public ReserveMoneyConsumer(ILogger logger) + { + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + //throw new System.Exception("Hello!"); + + _logger.LogInformation("[{consumerName}] Received money reservation request for order {orderId}.", + nameof(ReserveMoneyConsumer), context.Message.OrderId); + + await Task.Delay(1000); + + _logger.LogInformation("[{consumerName}] Reserved {amount} money for order {orderId}.", + nameof(ReserveMoneyConsumer), context.Message.Amount, context.Message.OrderId); + + if (context.Message.Amount <= 0) + { + await context.RespondAsync(new + { + OrderId = context.Message.OrderId, + Reason = "Can not reserve less than 1" + }); + } + else + { + await context.RespondAsync(new + { + OrderId = context.Message.OrderId, + Amount = context.Message.Amount + }); + } + } + } +} \ No newline at end of file diff --git a/src/PaymentService/Consumers/ReserveMoneyConsumerDefinition.cs b/src/PaymentService/Consumers/ReserveMoneyConsumerDefinition.cs new file mode 100644 index 0000000..ba95c58 --- /dev/null +++ b/src/PaymentService/Consumers/ReserveMoneyConsumerDefinition.cs @@ -0,0 +1,23 @@ +using GreenPipes; +using MassTransit; +using MassTransit.ConsumeConfigurators; +using MassTransit.Definition; + +namespace PaymentService.Consumers +{ + public class ReserveMoneyConsumerDefinition : ConsumerDefinition + { + public ReserveMoneyConsumerDefinition() + { + + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator) + { + endpointConfigurator.UseDelayedRedelivery(r => r.Interval(5, 1000)); + endpointConfigurator.UseMessageRetry(r => r.Interval(5, 5000)); + endpointConfigurator.UseInMemoryOutbox(); + } + } +} diff --git a/src/PaymentService/Consumers/UnreserveMoneyConsumer.cs b/src/PaymentService/Consumers/UnreserveMoneyConsumer.cs new file mode 100644 index 0000000..5f96524 --- /dev/null +++ b/src/PaymentService/Consumers/UnreserveMoneyConsumer.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using MassTransit; +using Microsoft.Extensions.Logging; +using PaymentService.Contracts; + +namespace PaymentService.Consumers +{ + public class UnreserveMoneyConsumer : IConsumer + { + private readonly ILogger _logger; + + public UnreserveMoneyConsumer(ILogger logger) + { + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + _logger.LogInformation("[{consumerName}] Received money unreservation request for order {orderId}.", + nameof(UnreserveMoneyConsumer), context.Message.OrderId); + + await Task.Delay(1000); + + _logger.LogInformation("[{consumerName}] Unreserved {amount} money for order {orderId}.", + nameof(UnreserveMoneyConsumer), context.Message.Amount, context.Message.OrderId); + + await context.RespondAsync(new + { + OrderId = context.Message.OrderId, + Amount = context.Message.Amount + }); + } + } +} \ No newline at end of file diff --git a/src/PaymentService/Dockerfile b/src/PaymentService/Dockerfile new file mode 100644 index 0000000..5347e78 --- /dev/null +++ b/src/PaymentService/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +COPY ./bin/Release/net6.0 . + +ENTRYPOINT ["dotnet", "PaymentService.dll"] \ No newline at end of file diff --git a/src/PaymentService/PaymentService.csproj b/src/PaymentService/PaymentService.csproj new file mode 100644 index 0000000..9207e78 --- /dev/null +++ b/src/PaymentService/PaymentService.csproj @@ -0,0 +1,34 @@ + + + + Exe + net6.0 + enable + true + + + + + + + + + + + + + + Always + + + + + + + + + + Always + + + diff --git a/src/PaymentService/Program.cs b/src/PaymentService/Program.cs new file mode 100644 index 0000000..03a4d6c --- /dev/null +++ b/src/PaymentService/Program.cs @@ -0,0 +1,58 @@ +using System; +using System.Reflection; +using MassTransit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using PaymentService.Configurations; +using PaymentService.Consumers; + +namespace PaymentService +{ + class Program + { + static void Main(string[] args) + { + Console.Title = Assembly.GetExecutingAssembly().GetName().Name!; + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + var endpointsConfigSection = hostContext.Configuration.GetSection("EndpointsConfiguration"); + var endpointsConfig = endpointsConfigSection.Get(); + + var rabbitMqConfigSection = hostContext.Configuration.GetSection("RabbitMqConfiguration"); + var rabbitMqConfig = rabbitMqConfigSection.Get(); + + services.AddMassTransit(c => + { + c.AddConsumer(typeof(ReserveMoneyConsumerDefinition)) + .Endpoint(configurator => + { + configurator.Name = endpointsConfig.PaymentServiceAddress; + }); + + c.AddConsumer() + .Endpoint(configurator => + { + configurator.Name = endpointsConfig.PaymentServiceAddress; + }); + + c.UsingRabbitMq((context, configurator) => + { + configurator.UseBsonSerializer(); + configurator.Host(rabbitMqConfig.Hostname, rabbitMqConfig.VirtualHost, h => + { + h.Username(rabbitMqConfig.Username); + h.Password(rabbitMqConfig.Password); + }); + + configurator.ConfigureEndpoints(context); + }); + }); + services.AddMassTransitHostedService(true); + }); + } +} \ No newline at end of file diff --git a/src/PaymentService/Properties/launchSettings.json b/src/PaymentService/Properties/launchSettings.json new file mode 100644 index 0000000..c4dcf09 --- /dev/null +++ b/src/PaymentService/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "PaymentService": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/PaymentService/appsettings.Development.json b/src/PaymentService/appsettings.Development.json new file mode 100644 index 0000000..d90d82d --- /dev/null +++ b/src/PaymentService/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "EndpointsConfiguration": { + "PaymentServiceAddress": "payment-service" + }, + "RabbitMqConfiguration": { + "Hostname": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } + } \ No newline at end of file diff --git a/src/PaymentService/appsettings.json b/src/PaymentService/appsettings.json new file mode 100644 index 0000000..98e4bd8 --- /dev/null +++ b/src/PaymentService/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "EndpointsConfiguration": { + "PaymentServiceAddress": "payment-service" + }, + "RabbitMqConfiguration": { + "Hostname": "rabbitmq", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + } +} \ No newline at end of file diff --git a/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ArchiveOrderConsumer.cs b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ArchiveOrderConsumer.cs new file mode 100644 index 0000000..69a0d63 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ArchiveOrderConsumer.cs @@ -0,0 +1,21 @@ +using HistoryService.Contracts; +using MassTransit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OrderOrchestratorService.Tests.ConsumerStubs +{ + internal class ArchiveOrderConsumer : IConsumer + { + public async Task Consume(ConsumeContext context) + { + await context.RespondAsync(new + { + OrderId = context.Message.OrderId + }); + } + } +} diff --git a/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetCartConsumer.cs b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetCartConsumer.cs new file mode 100644 index 0000000..60a4324 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetCartConsumer.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using CartService.Contracts; +using Contracts.Shared; +using MassTransit; + +namespace OrderOrchestratorService.Tests.ConsumerStubs; + +public class GetCartConsumer : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + await context.RespondAsync(new + { + + OrderId = context.Message.OrderId, + CartContent = new[] { + new CartPosition () + { + Amount = 5, + Name = "Food", + Price = 20 + } + }, + TotalPrice = 100 + }); + } +} \ No newline at end of file diff --git a/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFeedbackConsumer.cs b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFeedbackConsumer.cs new file mode 100644 index 0000000..2873c13 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFeedbackConsumer.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using FeedbackService.Contracts; +using MassTransit; + +namespace OrderOrchestratorService.Tests.ConsumerStubs; + +public class GetOrderFeedbackConsumer : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + await context.RespondAsync(new + { + + OrderId = context.Message.OrderId, + Text = default(string), + StarsAmount = default(int) + }); + } +} \ No newline at end of file diff --git a/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFromArchiveConsumer.cs b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFromArchiveConsumer.cs new file mode 100644 index 0000000..e54f283 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/GetOrderFromArchiveConsumer.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using HistoryService.Contracts; +using MassTransit; + +namespace OrderOrchestratorService.Tests.ConsumerStubs; + +public class GetOrderFromArchiveConsumer : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + await context.RespondAsync(new + { + + OrderId = context.Message.OrderId, + IsConfirmed = default(bool), + SubmitDate = default(DateTimeOffset), + Manager = default(string), + ConfirmDate = default(DateTimeOffset?), + DeliveredDate = default(DateTimeOffset?) + }); + } +} \ No newline at end of file diff --git a/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ReserveMoneyConsumer.cs b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ReserveMoneyConsumer.cs new file mode 100644 index 0000000..514bba5 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/ConsumerStubs/ReserveMoneyConsumer.cs @@ -0,0 +1,35 @@ +using MassTransit; +using PaymentService.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OrderOrchestratorService.Tests.ConsumerStubs +{ + public class ReserveMoneyConsumer : IConsumer + { + public async Task Consume(ConsumeContext context) + { + await Task.Delay(1000); + + if (context.Message.Amount <= 0) + { + await context.RespondAsync(new + { + OrderId = context.Message.OrderId, + Reason = "Can not reserve less than 1" + }); + } + else + { + await context.RespondAsync(new + { + OrderId = context.Message.OrderId, + Amount = context.Message.Amount + }); + } + } + } +} diff --git a/src/Tests/OrderOrchestratorService.Tests/GetArchivedOrderConsumerTests.cs b/src/Tests/OrderOrchestratorService.Tests/GetArchivedOrderConsumerTests.cs new file mode 100644 index 0000000..d0612d8 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/GetArchivedOrderConsumerTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using CartService.Contracts; +using FeedbackService.Contracts; +using HistoryService.Contracts; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using MassTransit.Testing; +using OrderOrchestratorService.Consumers; +using OrderOrchestratorService.Tests.ConsumerStubs; + +namespace OrderOrchestratorService.Tests +{ + public class GetArchivedOrderConsumerTests + { + [Fact] + public async Task Test() + { + var serviceCollection = new ServiceCollection() + .AddLogging() + .AddMassTransitInMemoryTestHarness(cfg => + { + cfg.AddRequestClient(); + cfg.AddRequestClient(); + cfg.AddRequestClient(); + + cfg.AddConsumer(); + cfg.AddConsumer(); + cfg.AddConsumer(); + + cfg.AddConsumer(); + cfg.AddConsumerTestHarness(); + }); + + var provider = serviceCollection.BuildServiceProvider(true); + + var harness = provider.GetRequiredService(); + + await harness.Start(); + + try + { + var orderId = Guid.NewGuid(); + + var bus = provider.GetRequiredService(); + var client = bus.CreateRequestClient(); + + var response = await client.GetResponse(new + { + OrderId = orderId + }); + + var consumerTestHarness = provider + .GetRequiredService>(); + + Assert.True(await harness.Consumed.Any()); + Assert.True(await consumerTestHarness.Consumed.Any()); + + Assert.True(await harness.Sent.Any()); + + Assert.NotNull(response); + Assert.Equal(orderId, response.Message.OrderId); + } + finally + { + await provider.DisposeAsync(); + await harness.Stop(); + } + } + } +} \ No newline at end of file diff --git a/src/Tests/OrderOrchestratorService.Tests/OrderOrchestratorService.Tests.csproj b/src/Tests/OrderOrchestratorService.Tests/OrderOrchestratorService.Tests.csproj new file mode 100644 index 0000000..8e473de --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/OrderOrchestratorService.Tests.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + enable + true + + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Tests/OrderOrchestratorService.Tests/StateMachines/OrderStateMachineTests.cs b/src/Tests/OrderOrchestratorService.Tests/StateMachines/OrderStateMachineTests.cs new file mode 100644 index 0000000..22437e1 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/StateMachines/OrderStateMachineTests.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ApiService.Contracts.ManagerApi; +using ApiService.Contracts.UserApi; +using Automatonymous.Graphing; +using Automatonymous.Visualizer; +using CartService.Contracts; +using DeliveryService.Contracts; +using MassTransit; +using MassTransit.Saga.InMemoryRepository; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using OrderOrchestratorService.Database.Models; +using OrderOrchestratorService.StateMachines.OrderStateMachine; +using OrderOrchestratorService.Tests.ConsumerStubs; +using PaymentService.Contracts; +using Xunit; +using Xunit.Abstractions; +using OrderMachine = OrderOrchestratorService.StateMachines.OrderStateMachine.OrderStateMachine; + +namespace OrderOrchestratorService.Tests.StateMachines; + +public class OrderStateMachineTests : + IClassFixture> +{ + private readonly StateMachineTestFixture _fixture; + private readonly ITestOutputHelper _output; + + public OrderStateMachineTests(StateMachineTestFixture fixture, + ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + + [Fact] + public async Task SagaShouldConsumeSubmitOrderMessage() + { + var orderId = NewId.NextGuid(); + var username = "TestUser"; + + await _fixture.TestHarness.Bus.Publish(new + { + + OrderId = orderId, + UserName = username + }); + + // Assert that endpoint consumed message + Assert.True(_fixture.TestHarness.Consumed.Select().Any()); + + // Assert that saga consumed message + Assert.True(_fixture.SagaHarness.Consumed.Select().Any()); + + // Assert that saga created + Assert.True(await _fixture.SagaHarness.Created.Any(s => s.CorrelationId == orderId), + "Saga was not created"); + } + + [Fact] + public async Task InstanceShouldTransitToGetCartStateWhenSumbitOrderMessageConsumed() + { + + var orderId = NewId.NextGuid(); + var username = "TestUser"; + + await _fixture.TestHarness.Bus.Publish(new + { + + OrderId = orderId, + UserName = username + }); + + Assert.NotNull(await _fixture.SagaHarness.Exists(orderId, _fixture.Machine.CartRequest.Pending)); + } + + [Fact] + public async Task ShouldGetCartMessageWhenSumbitOrderMessageConsumed() + { + var orderId = NewId.NextGuid(); + var username = "TestUser"; + + await _fixture.TestHarness.Bus.Publish(new + { + + OrderId = orderId, + UserName = username + }); + + Assert.True(_fixture.TestHarness.Sent + .Select() + .Any()); + } + + /* + * Below you can see an example of adding some stuff to service provider. + * This things depend on TStateMachine dependencies. + * I added a consumer stub as saga is making a request and waiting for response. + * + * Note: Adding services demands reinitialization of service provider. + * But this operation is really fast. So you should not care about it. + */ + [Fact] + public async Task ShouldMakeGetCartRequestWhenOrderSubmittedMessageConsumed() + { + await AddRequiredServices(); + + var orderId = NewId.NextGuid(); + + await _fixture.TestHarness.Bus.Publish(new + { + OrderId = orderId + }); + + var consumerHarness = _fixture.ServiceProvider + .GetRequiredService>(); + + Assert.True(consumerHarness.Consumed.Select().Any(), + "Get cart consumer did not consume message"); + + Assert.True(_fixture.SagaHarness.Consumed.Select().Any()); + } + + private async Task AddRequiredServices() + { + _fixture.ConfigureMassTransit = (cfg) => + { + /* + * If your state machine always requires service address to be specified + * you must add Endpoint to the consumer. + * The endpoint value should be the same as in your mocked options + */ + cfg.AddConsumer() + .Endpoint(c => c.Name = "cart-service"); + cfg.AddConsumerTestHarness(); + + cfg.AddConsumer() + .Endpoint(c => c.Name = "payment-service"); + cfg.AddConsumerTestHarness(); + }; + + await _fixture.DisposeAsync(); + await _fixture.InitializeAsync(); + } + + [Fact] + public async Task ShouldMakeReserveMoneyRequestWhenGetCartResponseConsumed() + { + await AddRequiredServices(); + + var orderId = NewId.NextGuid(); + + await _fixture.TestHarness.Bus.Publish(new + { + OrderId = orderId + }); + + var getCartConsumerHarness = _fixture.ServiceProvider + .GetRequiredService>(); + + Assert.True(_fixture.SagaHarness.Consumed.Select().Any(), + "Saga did not consume GetCartResponse message"); + + var consumerHarness = _fixture.ServiceProvider + .GetRequiredService>(); + + Assert.True(consumerHarness.Consumed.Select().Any()); + } + + /* + * Below I show how to add saga to storage + */ + [Fact] + public async Task ShouldProduceDeliverOrderMessageWhenConfirmOrderConsumed() + { + var orderId = NewId.NextGuid(); + + var instance = new SagaInstance(new OrderState + { + CorrelationId = orderId, + CurrentState = _fixture.ConvertStateToInt(x => x.AwaitingConfirmation), + // You need to add some car pos or converter in state machine will raise an exception + Cart = new List() { new CartPosition(NewId.NextGuid(), orderId, "Cart pos name", 5, 100) } + }); + + _fixture.Repository.Add(instance); + + await _fixture.TestHarness.Bus.Publish(new + { + OrderId = orderId, + ConfirmManager = "Manager name" + }); + + Assert.True(_fixture.SagaHarness.Consumed.Select().Any(), "Saga did not consume ConfirmOrder message"); + + + // Assert that saga sent DeliverOrder message + Assert.True(_fixture.TestHarness.Sent.Select().Any()); + + // Assert that saga transited to next state + Assert.NotNull(await _fixture.SagaHarness.Exists(orderId, _fixture.Machine.AwaitingDelivery)); + } + + /* + * Below I show how to work with system time + */ + [Fact] + public async Task ShouldMakeArchiveRequestWhenAwaitingFeedbackScheduleExpires() + { + var orderId = NewId.NextGuid(); + + var instance = new SagaInstance(new OrderState + { + CorrelationId = orderId, + CurrentState = _fixture.ConvertStateToInt(_fixture.Machine.AwaitingDelivery), + // You need to add some car pos or converter in state machine will raise an exception + Cart = new List() { new CartPosition(NewId.NextGuid(), orderId, "Cart pos name", 5, 100) } + }); + + _fixture.Repository.Add(instance); + + await _fixture.TestHarness.Bus.Publish(new + { + OrderId = orderId + }); + + Assert.NotNull(await _fixture.SagaHarness.Exists(orderId, _fixture.Machine.AwaitingFeedback)); + + await _fixture.AdvanceSystemTime(TimeSpan.FromMinutes(1)); + + // Saga transited to next step + Assert.NotNull(await _fixture.SagaHarness.Exists(orderId, _fixture.Machine.ArchiveOrderRequest.Pending)); + } + + [Fact] + public void GetDigraph() + { + var graph = _fixture.Machine.GetGraph(); + + var generator = new StateMachineGraphvizGenerator(graph); + + _output.WriteLine(generator.CreateDotFile()); + } +} \ No newline at end of file diff --git a/src/Tests/OrderOrchestratorService.Tests/StateMachines/StateMachineTestFixture.cs b/src/Tests/OrderOrchestratorService.Tests/StateMachines/StateMachineTestFixture.cs new file mode 100644 index 0000000..d251427 --- /dev/null +++ b/src/Tests/OrderOrchestratorService.Tests/StateMachines/StateMachineTestFixture.cs @@ -0,0 +1,149 @@ +using Automatonymous; +using MassTransit; +using MassTransit.ExtensionsDependencyInjectionIntegration; +using MassTransit.Saga.InMemoryRepository; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using OrderOrchestratorService.Configurations; +using Quartz; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace OrderOrchestratorService.Tests.StateMachines +{ +#nullable disable + public class StateMachineTestFixture : IAsyncLifetime + where TStateMachine : class, SagaStateMachine + where TInstance : class, SagaStateMachineInstance + { + public TStateMachine Machine; + public ServiceProvider ServiceProvider; + public IStateMachineSagaTestHarness SagaHarness; + public InMemoryTestHarness TestHarness; + public IndexedSagaDictionary Repository; + public Action ConfigureMassTransit = null; + public Action ConfigureServices = null; + + private readonly TimeSpan _testTimeout = TimeSpan.FromSeconds(5); + + private IServiceCollection _сollection; + private Task _scheduler; + + public StateMachineTestFixture() + { + + } + + public async Task InitializeAsync() + { + _сollection = new ServiceCollection() + .AddTransient(sp => GetMockedEndpoints()) + .AddTransient(sp => GetMockedLogger()) + .AddMassTransitInMemoryTestHarness(cfg => + { + + cfg.AddSagaStateMachine() + .InMemoryRepository(); + + cfg.AddSagaStateMachineTestHarness(); + + cfg.AddPublishMessageScheduler(); + + if (ConfigureMassTransit != null) + ConfigureMassTransit(cfg); + }); + + if (ConfigureServices != null) + ConfigureServices(_сollection); + + ServiceProvider = _сollection.BuildServiceProvider(true); + + TestHarness = ServiceProvider.GetRequiredService(); + TestHarness.TestTimeout = _testTimeout; + + TestHarness.OnConfigureInMemoryBus += (cfg) => + { + cfg.UseInMemoryScheduler(out _scheduler); + }; + + await TestHarness.Start(); + + SagaHarness = ServiceProvider.GetRequiredService>(); + Machine = ServiceProvider.GetRequiredService(); + Repository = ServiceProvider.GetRequiredService>(); + } + + public async Task DisposeAsync() + { + try + { + await TestHarness.Stop(); + } + finally + { + await ServiceProvider.DisposeAsync(); + RestoreDefaultQuartzSystemTime(); + } + } + + private IOptions GetMockedEndpoints() + { + var endpointsConfigMock = new Mock>(); + + endpointsConfigMock.Setup(f => f.Value).Returns(new EndpointsConfiguration() + { + ArchiveOrderStateMachineAddress = "exchange:test", + CartServiceAddress = "exchange:cart-service", + DeliveryServiceAddress = "exchange:test", + FeedbackServiceAddress = "exchange:test", + HistoryServiceAddress = "exchange:test", + MessageSchedulerAddress = "exchange:test", + OrderStateMachineAddress = "exchange:test", + PaymentServiceAddress = "exchange:payment-service" + }); + + return endpointsConfigMock.Object; + } + + private ILogger GetMockedLogger() + { + + return new Mock>().Object; + } + + public int ConvertStateToInt(State state) + { + return Machine.States.ToList().IndexOf(Machine.GetState(state.Name)) + 1; + } + + public int ConvertStateToInt(Func stateSelector) + { + var state = stateSelector(Machine); + return Machine.States.ToList().IndexOf(Machine.GetState(state.Name)) + 1; + } + + public async Task AdvanceSystemTime(TimeSpan duration) + { + var scheduler = await _scheduler.ConfigureAwait(false); + + await scheduler.Standby().ConfigureAwait(false); + + SystemTime.UtcNow = () => DateTimeOffset.UtcNow + duration; + SystemTime.Now = () => DateTimeOffset.Now + duration; + + await scheduler.Start().ConfigureAwait(false); + } + + public void RestoreDefaultQuartzSystemTime() + { + SystemTime.UtcNow = () => DateTimeOffset.UtcNow; + SystemTime.Now = () => DateTimeOffset.Now; + } + } +#nullable restore +} diff --git a/src/Tests/PaymentService.Tests/PaymentService.Tests.csproj b/src/Tests/PaymentService.Tests/PaymentService.Tests.csproj new file mode 100644 index 0000000..0f63d2c --- /dev/null +++ b/src/Tests/PaymentService.Tests/PaymentService.Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Tests/PaymentService.Tests/ReserveMoneyConsumerTests.cs b/src/Tests/PaymentService.Tests/ReserveMoneyConsumerTests.cs new file mode 100644 index 0000000..a501f21 --- /dev/null +++ b/src/Tests/PaymentService.Tests/ReserveMoneyConsumerTests.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MassTransit; +using Xunit; +using MassTransit.Testing; +using Microsoft.Extensions.Logging; +using Moq; +using PaymentService.Consumers; +using PaymentService.Contracts; + +namespace PaymentService.Tests +{ + public class ReserveMoneyConsumerTests + { + [Fact] + public async Task MoneyReservationTest() + { + var harness = new InMemoryTestHarness() + { + TestTimeout = TimeSpan.FromSeconds(2) + }; + + var consumerHarness = harness.Consumer(() => + { + return new ReserveMoneyConsumer(new Mock>().Object); + }); + + await harness.Start(); + + try + { + var orderId = Guid.NewGuid(); + var amount = 100; + + var client = await harness.ConnectRequestClient(); + + var response = await client.GetResponse(new + { + + OrderId = orderId, + Amount = amount + }); + + Assert.Equal(orderId, response.Message.OrderId); + + Assert.True(consumerHarness.Consumed.Select().Any()); + Assert.True(harness.Sent.Select().Any()); + + } + finally + { + await harness.Stop(); + } + } + + + [Theory] + [InlineData(0)] + [InlineData(-20)] + [InlineData(-1000)] + public async Task ShouldNotReserveMoneyWhenAmointIsLessThanZero(int amount) + { + var harness = new InMemoryTestHarness() + { + TestTimeout = TimeSpan.FromSeconds(2) + }; + + var consumerHarness = harness.Consumer(() => + { + return new ReserveMoneyConsumer(new Mock>().Object); + }); + + await harness.Start(); + + try + { + var orderId = Guid.NewGuid(); + + var client = await harness.ConnectRequestClient(); + + var response = await client.GetResponse(new + { + + OrderId = orderId, + Amount = amount + }); + + Assert.Equal(orderId, response.Message.OrderId); + + Assert.True(consumerHarness.Consumed.Select().Any()); + + Assert.True(harness.Sent.Select().Any()); + Assert.False(harness.Sent.Select().Any()); + + } + finally + { + await harness.Stop(); + } + } + } +} \ No newline at end of file