From e41ab6b045c3b4211c1428b0d9ed97d15e8f4b80 Mon Sep 17 00:00:00 2001 From: Houston Haynes Date: Fri, 22 Apr 2022 14:47:39 -0400 Subject: [PATCH 1/7] Initial commit --- .dockerignore | 10 + .editorconfig | 1 + .env | 12 + .gitignore | 45 ++ ChangeLog.md | 45 ++ .../PaymentService/.config/dotnet-tools.json | 12 + .../Controllers/PaymentController.cs | 33 + ExternalSystem/PaymentService/Dockerfile | 18 + .../PaymentService/PaymentService.csproj | 17 + ExternalSystem/PaymentService/Program.cs | 25 + ...PaymentServiceExternal - Web Deploy.pubxml | 31 + .../Properties/launchSettings.json | 29 + ExternalSystem/PaymentService/Startup.cs | 36 ++ .../appsettings.Development.json | 10 + .../PaymentService/appsettings.json | 15 + LICENSE | 21 + README.md | 29 + SF-architecture.md | 7 + .../ApplicationManifest.xml | 49 ++ .../InvoicePkg/Config/Settings.xml | 9 + .../InvoicePkg/ServiceManifest.xml | 44 ++ .../TripPkg/Config/Settings.xml | 9 + .../TripPkg/ServiceManifest.xml | 43 ++ .../ApplicationParameters/Cloud.xml | 7 + .../ApplicationParameters/Local.1Node.xml | 5 + .../ApplicationParameters/Local.5Node.xml | 5 + .../DuberMicroservices.sfproj | 48 ++ .../PublishProfiles/Cloud.xml | 29 + .../PublishProfiles/Local.1Node.xml | 11 + .../PublishProfiles/Local.5Node.xml | 11 + .../Scripts/Deploy-FabricApplication.ps1 | 258 ++++++++ .../Linux/DuberMicroservices/packages.config | 4 + TunningDocker.md | 11 + build-images.ps1 | 10 + deploy/cosmos/deploycosmos.json | 32 + deploy/cosmos/deploycosmos.parameters.json | 9 + deploy/create-resources.cmd | 23 + deploy/k8s/gke/default-ingress.yaml | 23 + deploy/k8s/gke/delete-resources.sh | 26 + deploy/k8s/gke/deploy.sh | 26 + deploy/k8s/gke/env-config.yaml | 20 + .../invoice-deployment-with-proxy.yaml | 62 ++ .../k8s/gke/invoice/invoice-deployment.yaml | 46 ++ deploy/k8s/gke/invoice/invoice-hpa.yaml | 12 + deploy/k8s/gke/invoice/invoice-service.yaml | 12 + .../notifications-deployment.yaml | 43 ++ .../gke/notifications/notifications-hpa.yaml | 12 + .../notifications/notifications-ingress.yaml | 17 + .../notifications/notifications-service.yaml | 12 + deploy/k8s/gke/trip/trip-deployment.yaml | 46 ++ deploy/k8s/gke/trip/trip-hpa.yaml | 12 + deploy/k8s/gke/trip/trip-service.yaml | 12 + .../website-deployment-with-proxy.yaml | 76 +++ .../k8s/gke/website/website-deployment.yaml | 60 ++ deploy/k8s/gke/website/website-hpa.yaml | 12 + deploy/k8s/gke/website/website-service.yaml | 12 + deploy/k8s/local/dashboard-adminuser.yaml | 18 + deploy/k8s/local/delete-resources.ps1 | 29 + deploy/k8s/local/deploy-local.ps1 | 56 ++ deploy/k8s/local/env-config.yaml | 18 + .../external-system/payment-deployment.yaml | 23 + .../external-system/payment-service.yaml | 12 + .../k8s/local/invoice/invoice-deployment.yaml | 43 ++ deploy/k8s/local/invoice/invoice-ingress.yaml | 13 + deploy/k8s/local/invoice/invoice-service.yaml | 12 + deploy/k8s/local/mongo/mongo-admin.yaml | 14 + deploy/k8s/local/mongo/mongo-deployment.yaml | 22 + deploy/k8s/local/mongo/mongo-service.yaml | 13 + .../local/nginx-ingress/custom-service.yaml | 23 + .../notifications-deployment.yaml | 43 ++ .../notifications/notifications-hpa.yaml | 12 + .../notifications/notifications-ingress.yaml | 15 + .../notifications/notifications-service.yaml | 12 + deploy/k8s/local/rabbit/rabbit-admin.yaml | 14 + .../k8s/local/rabbit/rabbit-deployment.yaml | 22 + deploy/k8s/local/rabbit/rabbit-service.yaml | 13 + deploy/k8s/local/redis/redis-deployment.yaml | 22 + deploy/k8s/local/redis/redis-service.yaml | 13 + deploy/k8s/local/sql-server/sql-admin.yaml | 14 + .../k8s/local/sql-server/sql-deployment.yaml | 29 + deploy/k8s/local/sql-server/sql-service.yaml | 13 + deploy/k8s/local/trip/trip-deployment.yaml | 43 ++ deploy/k8s/local/trip/trip-hpa.yaml | 12 + deploy/k8s/local/trip/trip-ingress.yaml | 13 + deploy/k8s/local/trip/trip-service.yaml | 12 + .../k8s/local/website/website-deployment.yaml | 60 ++ deploy/k8s/local/website/website-hpa.yaml | 12 + deploy/k8s/local/website/website-ingress.yaml | 13 + deploy/k8s/local/website/website-service.yaml | 12 + deploy/redis/readme.md | 31 + deploy/redis/redisdeploy.json | 42 ++ deploy/redis/redisdeploy.parameters.json | 9 + deploy/servicebus/sbusdeploy.json | 166 +++++ deploy/servicebus/sbusdeploy.parameters.json | 9 + .../LinuxContainers/gen-keyvaultcert.ps1 | 53 ++ .../LinuxContainers/servicefabricdeploy.json | 611 ++++++++++++++++++ .../servicefabricdeploy.parameters.json | 72 +++ deploy/sql/sqldeploy.json | 93 +++ deploy/sql/sqldeploy.parameters.json | 21 + docker-compose.dcproj | 18 + docker-compose.override.yml | 73 +++ docker-compose.yml | 55 ++ k8s-architecture.md | 7 + local-deployment.md | 28 + ...oservices-netcore-docker-servicefabric.sln | 355 ++++++++++ package-lock.json | 12 + publish-images.ps1 | 16 + .../InvoiceCreatedDomainEventHandler.cs | 31 + .../InvoicePaidDomainEventHandler.cs | 31 + .../Events/InvoiceCreatedIntegrationEvent.cs | 24 + .../Events/InvoicePaidIntegrationEvent.cs | 28 + .../Events/TripCancelledIntegrationEvent.cs | 25 + .../Events/TripFinishedIntegrationEvent.cs | 28 + .../TripCancelledIntegrationEventHandler.cs | 61 ++ .../TripFinishedIntegrationEventHandler.cs | 61 ++ .../Mapping/InvoiceDomainProfile.cs | 19 + .../Mapping/InvoiceEventsProfile.cs | 30 + .../Application/Model/CreateInvoiceRequest.cs | 19 + .../Application/Model/Invoice.cs | 63 ++ .../CreateInvoiceRequestValidator.cs | 14 + .../Controllers/HomeController.cs | 13 + .../Controllers/InvoiceController.cs | 155 +++++ src/Application/Duber.Invoice.API/Dockerfile | 18 + .../Duber.Invoice.API/Dockerfile.original | 20 + .../Duber.Invoice.API.csproj | 49 ++ .../ApplicationBuilderExtensions.cs | 19 + .../Extensions/ServiceCollectionExtensions.cs | 185 ++++++ .../InternalServerErrorObjectResult.cs | 14 + .../AutofacModules/MediatorModule.cs | 38 ++ .../Filters/HttpGlobalExceptionFilter.cs | 70 ++ .../Filters/ValidatorActionFilter.cs | 20 + src/Application/Duber.Invoice.API/Program.cs | 36 ++ .../Properties/launchSettings.json | 34 + src/Application/Duber.Invoice.API/Startup.cs | 118 ++++ .../appsettings.Development.json | 10 + .../Duber.Invoice.API/appsettings.json | 29 + .../TripCreatedDomainEventHandlerAsync.cs | 36 ++ .../TripUpdatedDomainEventHandlerAsync.cs | 93 +++ .../TripCancelledIntegrationEvent.cs | 28 + .../TripCreatedIntegrationEvent.cs | 37 ++ .../TripFinishedIntegrationEvent.cs | 31 + .../TripUpdatedIntegrationEvent.cs | 49 ++ .../Mapping/TripCommandsProfile.cs | 31 + .../Application/Mapping/TripDomainProfile.cs | 20 + .../Application/Mapping/TripEventsProfile.cs | 43 ++ .../Application/Model/CreateTripCommand.cs | 23 + .../Application/Model/Location.cs | 11 + .../Duber.Trip.API/Application/Model/Trip.cs | 68 ++ .../Application/Model/UpdateTripCommand.cs | 16 + .../Validations/UpdateTripCommandValidator.cs | 13 + .../Controllers/EventStoreController.cs | 62 ++ .../Controllers/HomeController.cs | 13 + .../Controllers/TripController.cs | 146 +++++ src/Application/Duber.Trip.API/Dockerfile | 18 + .../Duber.Trip.API/Duber.Trip.API.csproj | 49 ++ .../ApplicationBuilderExtensions.cs | 18 + .../Extensions/ServiceCollectionExtensions.cs | 169 +++++ .../InternalServerErrorObjectResult.cs | 14 + .../Filters/HttpGlobalExceptionFilter.cs | 69 ++ .../Filters/ValidatorActionFilter.cs | 20 + .../Repository/EventStoreRepository.cs | 34 + .../Repository/IEventStoreRepository.cs | 14 + .../Repository/IdempotencyStoreProvider.cs | 37 ++ src/Application/Duber.Trip.API/Program.cs | 36 ++ .../Properties/launchSettings.json | 29 + src/Application/Duber.Trip.API/Startup.cs | 109 ++++ .../appsettings.Development.json | 10 + .../Duber.Trip.API/appsettings.json | 28 + .../Events/TripCancelledIntegrationEvent.cs | 11 + .../Events/TripCreatedDomainEvent.cs | 11 + .../IntegrationEvents/Events/TripEventBase.cs | 18 + .../Events/TripFinishedIntegrationEvent.cs | 11 + .../Events/TripUpdatedIntegrationEvent.cs | 35 + .../TripCreatedIntegrationEventHandler.cs | 26 + .../TripFinishedIntegrationEventHandler.cs | 26 + .../TripUpdatedIntegrationEventHandler.cs | 34 + .../Duber.Trip.Notifications/Dockerfile | 21 + .../Duber.Trip.Notifications.csproj | 24 + .../ApplicationBuilderExtensions.cs | 21 + .../Extensions/ServiceCollectionExtensions.cs | 73 +++ .../NotificationsHub.cs | 12 + .../Duber.Trip.Notifications/Program.cs | 33 + .../Properties/launchSettings.json | 30 + .../Duber.Trip.Notifications/Startup.cs | 77 +++ .../appsettings.Development.json | 9 + .../Duber.Trip.Notifications/appsettings.json | 25 + .../Duber.Domain.Driver.UnitTest.csproj | 15 + .../Duber.Domain.Driver.UnitTest/UnitTest1.cs | 13 + .../Duber.Domain.Driver.csproj | 35 + .../Exceptions/DriverDomainException.cs | 18 + .../20180328053630_InitialCreate.Designer.cs | 139 ++++ .../20180328053630_InitialCreate.cs | 159 +++++ .../Migrations/DriverContextModelSnapshot.cs | 138 ++++ .../Duber.Domain.Driver/Model/Driver.cs | 103 +++ .../Duber.Domain.Driver/Model/DriverStatus.cs | 52 ++ .../Duber.Domain.Driver/Model/Vehicle.cs | 60 ++ .../Duber.Domain.Driver/Model/VehicleType.cs | 51 ++ .../Persistence/DriverContext.cs | 44 ++ .../Persistence/DriverContextSeed.cs | 80 +++ .../DriverEntityTypeConfiguration.cs | 55 ++ .../DriverStatusEntityTypeConfiguration.cs | 25 + .../VehicleEntityTypeConfiguration.cs | 45 ++ .../VehicleTypeEntityTypeConfiguration.cs | 25 + .../Repository/DriverRepository.cs | 45 ++ .../Repository/IDriverRepository.cs | 17 + .../Adapters/PaymentServiceAdapter.cs | 35 + .../Contracts/IPaymentService.cs | 10 + .../Duber.Domain.ACL/Duber.Domain.ACL.csproj | 16 + .../Duber.Domain.ACL/ThirdPartyServices.cs | 13 + .../Translators/PaymentInfoTranslator.cs | 25 + .../Duber.Domain.SharedKernel.csproj | 11 + .../Model/PaymentInfo.cs | 54 ++ .../Model/PaymentMethod.cs | 51 ++ .../Model/TripStatus.cs | 54 ++ .../Duber.Domain.Invoice.UnitTest.csproj | 15 + .../UnitTest1.cs | 13 + .../Duber.Domain.Invoice.csproj | 29 + .../Events/InvoiceCreatedDomainEvent.cs | 28 + .../Events/InvoicePaidDomainEvent.cs | 28 + .../InvoiceDomainArgumentNullException.cs | 18 + .../InvoiceDomainInvalidOperationException.cs | 18 + .../Extensions/TripInformationExtensions.cs | 18 + .../20180407204749_InitialCreate.Designer.cs | 53 ++ .../20180407204749_InitialCreate.cs | 37 ++ .../20180410015654_PaymentInfo.Designer.cs | 82 +++ .../Migrations/20180410015654_PaymentInfo.cs | 45 ++ .../InvoiceMigrationContextModelSnapshot.cs | 81 +++ .../Duber.Domain.Invoice/Model/Invoice.cs | 129 ++++ .../Model/TripInformation.cs | 46 ++ .../Persistence/IInvoiceContext.cs | 20 + .../Persistence/InvoiceContext.cs | 73 +++ .../Persistence/InvoiceMigrationContext.cs | 102 +++ .../Repository/IInvoiceRepository.cs | 19 + .../Repository/InvoiceRepository.cs | 84 +++ .../Services/IPaymentService.cs | 10 + .../Services/PaymentService.cs | 34 + .../Duber.Domain.Trip.UnitTest.csproj | 15 + .../Duber.Domain.Trip.UnitTest/UnitTest1.cs | 13 + .../Commands/CreateTripCommand.cs | 27 + .../Handlers/CreateTripCommandHandlerAsync.cs | 29 + .../Handlers/UpdateTripCommandHandlerAsync.cs | 53 ++ .../Commands/UpdateTripCommand.cs | 23 + .../Duber.Domain.Trip.csproj | 25 + .../Events/TripCreatedDomainEvent.cs | 25 + .../Events/TripUpdatedDomainEvent.cs | 39 ++ .../TripDomainArgumentNullException.cs | 20 + .../TripDomainInvalidOperationException.cs | 20 + .../Trip/Duber.Domain.Trip/Model/Location.cs | 28 + .../Trip/Duber.Domain.Trip/Model/Rating.cs | 27 + .../Trip/Duber.Domain.Trip/Model/Trip.cs | 264 ++++++++ .../Model/VehicleInformation.cs | 33 + .../Duber.Domain.User.UnitTest.csproj | 15 + .../Duber.Domain.User.UnitTest/UnitTest1.cs | 13 + .../Duber.Domain.User.csproj | 26 + .../Exceptions/UserDomainException.cs | 18 + .../20180328010407_InitialCreate.Designer.cs | 81 +++ .../20180328010407_InitialCreate.cs | 85 +++ .../Migrations/UserContextModelSnapshot.cs | 80 +++ .../User/Duber.Domain.User/Model/User.cs | 69 ++ .../PaymentMethodEntityTypeConfiguration.cs | 25 + .../UserEntityTypeConfiguration.cs | 45 ++ .../Persistence/UserContext.cs | 38 ++ .../Persistence/UserContextSeed.cs | 65 ++ .../Repository/IUserRepository.cs | 17 + .../Repository/UserRepository.cs | 44 ++ ...rastructure.Resilience.Abstractions.csproj | 11 + .../IPolicyAsyncExecutor.cs | 27 + .../IPolicySyncExecutor.cs | 19 + .../PolicyAsyncExecutor.cs | 53 ++ .../PolicySyncExecutor.cs | 52 ++ ...uber.Infrastructure.Resilience.Http.csproj | 13 + .../ResilientHttpClient.cs | 32 + ...Duber.Infrastructure.Resilience.Sql.csproj | 16 + .../ISqlPolicyBuilder.cs | 53 ++ .../Internals/SqlAsyncPolicyBuilder.cs | 154 +++++ .../Internals/SqlHandledExceptions.cs | 12 + .../Internals/SqlSyncPolicyBuilder.cs | 153 +++++ .../Policies/AsyncPolicies.cs | 231 +++++++ .../Policies/PolicyKeys.cs | 18 + .../Policies/SyncPolicies.cs | 227 +++++++ .../SqlPolicyBuilder.cs | 27 + .../Duber.Infrastructure.WebHost.csproj | 12 + .../Properties/launchSettings.json | 27 + .../WebHostExtensions.cs | 37 ++ .../Duber.Infrastructure/DDD/Entity.cs | 91 +++ .../Duber.Infrastructure/DDD/Enumeration.cs | 103 +++ .../DDD/IAggregateRoot.cs | 4 + .../Duber.Infrastructure/DDD/ValueObject.cs | 63 ++ .../Duber.Infrastructure.csproj | 19 + .../Extensions/MediatorExtensions.cs | 26 + .../Duber.Infrastructure/Http/JsonContent.cs | 13 + .../Repository.Abstractions/IRepository.cs | 9 + .../Repository.Abstractions/IUnitOfWork.cs | 11 + ...Infrastructure.EventBus.Idempotency.csproj | 15 + .../IIdempotencyStoreProvider.cs | 11 + .../IdempotentIntegrationEvent.cs | 19 + .../IdempotentIntegrationEventHandler.cs | 42 ++ .../IdempotentMessage.cs | 11 + .../ServiceCollectionExtensions.cs | 32 + .../DefaultRabbitMQPersisterConnection.cs | 121 ++++ ...er.Infrastructure.EventBus.RabbitMQ.csproj | 21 + .../EventBusRabbitMQ.cs | 277 ++++++++ .../IRabbitMQPersisterConnection.cs | 15 + .../IoC/ServiceCollectionExtensions.cs | 71 ++ .../DefaultServiceBusPersisterConnection.cs | 44 ++ ....Infrastructure.EventBus.ServiceBus.csproj | 22 + .../EventBusServiceBus.cs | 226 +++++++ .../IServiceBusPersisterConnection.cs | 12 + .../IoC/ServiceCollectionExtensions.cs | 57 ++ .../IDynamicIntegrationEventHandler.cs | 9 + .../Abstractions/IEventBus.cs | 22 + .../Abstractions/IIntegrationEventHandler.cs | 15 + .../Duber.Infrastructure.EventBus.csproj | 7 + .../Events/IntegrationEvent.cs | 16 + .../IEventBusSubscriptionsManager.cs | 33 + .../InMemoryEventBusSubscriptionsManager.cs | 162 +++++ .../SubscriptionInfo.cs | 28 + .../Events/InvoiceCreatedIntegrationEvent.cs | 24 + .../Events/InvoicePaidIntegrationEvent.cs | 33 + .../Events/TripCreatedIntegrationEvent.cs | 58 ++ .../Events/TripUpdatedIntegrationEvent.cs | 52 ++ .../InvoiceCreatedIntegrationEventHandler.cs | 51 ++ .../InvoicePaidIntegrationEventHandler.cs | 51 ++ .../TripCreatedIntegrationEventHandler.cs | 69 ++ .../TripUpdatedIntegrationEventHandler.cs | 54 ++ .../Controllers/HomeController.cs | 66 ++ .../Controllers/TripController.cs | 278 ++++++++ src/Web/Duber.WebSite/Dockerfile | 18 + src/Web/Duber.WebSite/Duber.WebSite.csproj | 64 ++ .../ApplicationBuilderExtensions.cs | 22 + .../Duber.WebSite/Extensions/Extensions.cs | 41 ++ .../Extensions/ServiceCollectionExtensions.cs | 176 +++++ .../Persistence/ReportingContext.cs | 33 + .../Repository/IReportingRepository.cs | 28 + .../Repository/ReportingRepository.cs | 88 +++ .../20180412031300_InitialCreate.Designer.cs | 81 +++ .../20180412031300_InitialCreate.cs | 56 ++ .../ReportingContextModelSnapshot.cs | 80 +++ src/Web/Duber.WebSite/Models/DriverModel.cs | 13 + .../Duber.WebSite/Models/ErrorViewModel.cs | 11 + src/Web/Duber.WebSite/Models/Trip.cs | 74 +++ .../Duber.WebSite/Models/TripApiSettings.cs | 19 + .../Duber.WebSite/Models/TripRequestModel.cs | 53 ++ src/Web/Duber.WebSite/Models/UserModel.cs | 13 + src/Web/Duber.WebSite/Program.cs | 51 ++ .../Properties/launchSettings.json | 27 + src/Web/Duber.WebSite/Startup.cs | 85 +++ src/Web/Duber.WebSite/Views/Home/About.cshtml | 7 + .../Duber.WebSite/Views/Home/Contact.cshtml | 17 + src/Web/Duber.WebSite/Views/Home/Index.cshtml | 18 + .../Duber.WebSite/Views/Shared/Error.cshtml | 22 + .../Duber.WebSite/Views/Shared/_Layout.cshtml | 79 +++ .../Shared/_ValidationScriptsPartial.cshtml | 18 + .../Views/Trip/DriverTrips.cshtml | 64 ++ src/Web/Duber.WebSite/Views/Trip/Index.cshtml | 121 ++++ .../Views/Trip/TripDetails.cshtml | 144 +++++ .../Views/Trip/TripsByDriver.cshtml | 45 ++ .../Views/Trip/TripsByUser.cshtml | 45 ++ .../Duber.WebSite/Views/Trip/UserTrips.cshtml | 64 ++ src/Web/Duber.WebSite/Views/Trip/_Map.cshtml | 13 + .../Duber.WebSite/Views/_ViewImports.cshtml | 3 + src/Web/Duber.WebSite/Views/_ViewStart.cshtml | 3 + .../appsettings.Development.json | 10 + src/Web/Duber.WebSite/appsettings.json | 51 ++ src/Web/Duber.WebSite/bundleconfig.json | 24 + src/Web/Duber.WebSite/libman.json | 33 + src/Web/Duber.WebSite/wwwroot/css/site.css | 35 + .../Duber.WebSite/wwwroot/css/site.min.css | 1 + src/Web/Duber.WebSite/wwwroot/favicon.ico | Bin 0 -> 32038 bytes .../Duber.WebSite/wwwroot/images/banner1.svg | 1 + .../Duber.WebSite/wwwroot/images/banner2.svg | 1 + .../Duber.WebSite/wwwroot/images/banner3.svg | 1 + .../Duber.WebSite/wwwroot/images/banner4.svg | 1 + .../Duber.WebSite/wwwroot/images/car-icon.png | Bin 0 -> 3893 bytes .../wwwroot/images/car-icon1.png | Bin 0 -> 1987 bytes .../wwwroot/images/car-icon3.png | Bin 0 -> 2450 bytes .../wwwroot/images/car-icon4.png | Bin 0 -> 3266 bytes .../wwwroot/images/duber_logo.png | Bin 0 -> 7222 bytes src/Web/Duber.WebSite/wwwroot/js/site.js | 167 +++++ src/Web/Duber.WebSite/wwwroot/js/site.min.js | 0 380 files changed, 16243 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .gitignore create mode 100644 ChangeLog.md create mode 100644 ExternalSystem/PaymentService/.config/dotnet-tools.json create mode 100644 ExternalSystem/PaymentService/Controllers/PaymentController.cs create mode 100644 ExternalSystem/PaymentService/Dockerfile create mode 100644 ExternalSystem/PaymentService/PaymentService.csproj create mode 100644 ExternalSystem/PaymentService/Program.cs create mode 100644 ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml create mode 100644 ExternalSystem/PaymentService/Properties/launchSettings.json create mode 100644 ExternalSystem/PaymentService/Startup.cs create mode 100644 ExternalSystem/PaymentService/appsettings.Development.json create mode 100644 ExternalSystem/PaymentService/appsettings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SF-architecture.md create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/ApplicationManifest.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/Config/Settings.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/ServiceManifest.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/Config/Settings.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/ServiceManifest.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Cloud.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.1Node.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.5Node.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj create mode 100644 ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Cloud.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.1Node.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.5Node.xml create mode 100644 ServiceFabric/Linux/DuberMicroservices/Scripts/Deploy-FabricApplication.ps1 create mode 100644 ServiceFabric/Linux/DuberMicroservices/packages.config create mode 100644 TunningDocker.md create mode 100644 build-images.ps1 create mode 100644 deploy/cosmos/deploycosmos.json create mode 100644 deploy/cosmos/deploycosmos.parameters.json create mode 100644 deploy/create-resources.cmd create mode 100644 deploy/k8s/gke/default-ingress.yaml create mode 100644 deploy/k8s/gke/delete-resources.sh create mode 100644 deploy/k8s/gke/deploy.sh create mode 100644 deploy/k8s/gke/env-config.yaml create mode 100644 deploy/k8s/gke/invoice/invoice-deployment-with-proxy.yaml create mode 100644 deploy/k8s/gke/invoice/invoice-deployment.yaml create mode 100644 deploy/k8s/gke/invoice/invoice-hpa.yaml create mode 100644 deploy/k8s/gke/invoice/invoice-service.yaml create mode 100644 deploy/k8s/gke/notifications/notifications-deployment.yaml create mode 100644 deploy/k8s/gke/notifications/notifications-hpa.yaml create mode 100644 deploy/k8s/gke/notifications/notifications-ingress.yaml create mode 100644 deploy/k8s/gke/notifications/notifications-service.yaml create mode 100644 deploy/k8s/gke/trip/trip-deployment.yaml create mode 100644 deploy/k8s/gke/trip/trip-hpa.yaml create mode 100644 deploy/k8s/gke/trip/trip-service.yaml create mode 100644 deploy/k8s/gke/website/website-deployment-with-proxy.yaml create mode 100644 deploy/k8s/gke/website/website-deployment.yaml create mode 100644 deploy/k8s/gke/website/website-hpa.yaml create mode 100644 deploy/k8s/gke/website/website-service.yaml create mode 100644 deploy/k8s/local/dashboard-adminuser.yaml create mode 100644 deploy/k8s/local/delete-resources.ps1 create mode 100644 deploy/k8s/local/deploy-local.ps1 create mode 100644 deploy/k8s/local/env-config.yaml create mode 100644 deploy/k8s/local/external-system/payment-deployment.yaml create mode 100644 deploy/k8s/local/external-system/payment-service.yaml create mode 100644 deploy/k8s/local/invoice/invoice-deployment.yaml create mode 100644 deploy/k8s/local/invoice/invoice-ingress.yaml create mode 100644 deploy/k8s/local/invoice/invoice-service.yaml create mode 100644 deploy/k8s/local/mongo/mongo-admin.yaml create mode 100644 deploy/k8s/local/mongo/mongo-deployment.yaml create mode 100644 deploy/k8s/local/mongo/mongo-service.yaml create mode 100644 deploy/k8s/local/nginx-ingress/custom-service.yaml create mode 100644 deploy/k8s/local/notifications/notifications-deployment.yaml create mode 100644 deploy/k8s/local/notifications/notifications-hpa.yaml create mode 100644 deploy/k8s/local/notifications/notifications-ingress.yaml create mode 100644 deploy/k8s/local/notifications/notifications-service.yaml create mode 100644 deploy/k8s/local/rabbit/rabbit-admin.yaml create mode 100644 deploy/k8s/local/rabbit/rabbit-deployment.yaml create mode 100644 deploy/k8s/local/rabbit/rabbit-service.yaml create mode 100644 deploy/k8s/local/redis/redis-deployment.yaml create mode 100644 deploy/k8s/local/redis/redis-service.yaml create mode 100644 deploy/k8s/local/sql-server/sql-admin.yaml create mode 100644 deploy/k8s/local/sql-server/sql-deployment.yaml create mode 100644 deploy/k8s/local/sql-server/sql-service.yaml create mode 100644 deploy/k8s/local/trip/trip-deployment.yaml create mode 100644 deploy/k8s/local/trip/trip-hpa.yaml create mode 100644 deploy/k8s/local/trip/trip-ingress.yaml create mode 100644 deploy/k8s/local/trip/trip-service.yaml create mode 100644 deploy/k8s/local/website/website-deployment.yaml create mode 100644 deploy/k8s/local/website/website-hpa.yaml create mode 100644 deploy/k8s/local/website/website-ingress.yaml create mode 100644 deploy/k8s/local/website/website-service.yaml create mode 100644 deploy/redis/readme.md create mode 100644 deploy/redis/redisdeploy.json create mode 100644 deploy/redis/redisdeploy.parameters.json create mode 100644 deploy/servicebus/sbusdeploy.json create mode 100644 deploy/servicebus/sbusdeploy.parameters.json create mode 100644 deploy/servicefabric/LinuxContainers/gen-keyvaultcert.ps1 create mode 100644 deploy/servicefabric/LinuxContainers/servicefabricdeploy.json create mode 100644 deploy/servicefabric/LinuxContainers/servicefabricdeploy.parameters.json create mode 100644 deploy/sql/sqldeploy.json create mode 100644 deploy/sql/sqldeploy.parameters.json create mode 100644 docker-compose.dcproj create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.yml create mode 100644 k8s-architecture.md create mode 100644 local-deployment.md create mode 100644 microservices-netcore-docker-servicefabric.sln create mode 100644 package-lock.json create mode 100644 publish-images.ps1 create mode 100644 src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs create mode 100644 src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs create mode 100644 src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs create mode 100644 src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs create mode 100644 src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs create mode 100644 src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs create mode 100644 src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripCancelledIntegrationEventHandler.cs create mode 100644 src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripFinishedIntegrationEventHandler.cs create mode 100644 src/Application/Duber.Invoice.API/Application/Mapping/InvoiceDomainProfile.cs create mode 100644 src/Application/Duber.Invoice.API/Application/Mapping/InvoiceEventsProfile.cs create mode 100644 src/Application/Duber.Invoice.API/Application/Model/CreateInvoiceRequest.cs create mode 100644 src/Application/Duber.Invoice.API/Application/Model/Invoice.cs create mode 100644 src/Application/Duber.Invoice.API/Application/Validations/CreateInvoiceRequestValidator.cs create mode 100644 src/Application/Duber.Invoice.API/Controllers/HomeController.cs create mode 100644 src/Application/Duber.Invoice.API/Controllers/InvoiceController.cs create mode 100644 src/Application/Duber.Invoice.API/Dockerfile create mode 100644 src/Application/Duber.Invoice.API/Dockerfile.original create mode 100644 src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj create mode 100644 src/Application/Duber.Invoice.API/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Application/Duber.Invoice.API/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Application/Duber.Invoice.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs create mode 100644 src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs create mode 100644 src/Application/Duber.Invoice.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs create mode 100644 src/Application/Duber.Invoice.API/Infrastructure/Filters/ValidatorActionFilter.cs create mode 100644 src/Application/Duber.Invoice.API/Program.cs create mode 100644 src/Application/Duber.Invoice.API/Properties/launchSettings.json create mode 100644 src/Application/Duber.Invoice.API/Startup.cs create mode 100644 src/Application/Duber.Invoice.API/appsettings.Development.json create mode 100644 src/Application/Duber.Invoice.API/appsettings.json create mode 100644 src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs create mode 100644 src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs create mode 100644 src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCancelledIntegrationEvent.cs create mode 100644 src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCreatedIntegrationEvent.cs create mode 100644 src/Application/Duber.Trip.API/Application/IntegrationEvents/TripFinishedIntegrationEvent.cs create mode 100644 src/Application/Duber.Trip.API/Application/IntegrationEvents/TripUpdatedIntegrationEvent.cs create mode 100644 src/Application/Duber.Trip.API/Application/Mapping/TripCommandsProfile.cs create mode 100644 src/Application/Duber.Trip.API/Application/Mapping/TripDomainProfile.cs create mode 100644 src/Application/Duber.Trip.API/Application/Mapping/TripEventsProfile.cs create mode 100644 src/Application/Duber.Trip.API/Application/Model/CreateTripCommand.cs create mode 100644 src/Application/Duber.Trip.API/Application/Model/Location.cs create mode 100644 src/Application/Duber.Trip.API/Application/Model/Trip.cs create mode 100644 src/Application/Duber.Trip.API/Application/Model/UpdateTripCommand.cs create mode 100644 src/Application/Duber.Trip.API/Application/Validations/UpdateTripCommandValidator.cs create mode 100644 src/Application/Duber.Trip.API/Controllers/EventStoreController.cs create mode 100644 src/Application/Duber.Trip.API/Controllers/HomeController.cs create mode 100644 src/Application/Duber.Trip.API/Controllers/TripController.cs create mode 100644 src/Application/Duber.Trip.API/Dockerfile create mode 100644 src/Application/Duber.Trip.API/Duber.Trip.API.csproj create mode 100644 src/Application/Duber.Trip.API/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Application/Duber.Trip.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs create mode 100644 src/Application/Duber.Trip.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs create mode 100644 src/Application/Duber.Trip.API/Infrastructure/Filters/ValidatorActionFilter.cs create mode 100644 src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs create mode 100644 src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs create mode 100644 src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs create mode 100644 src/Application/Duber.Trip.API/Program.cs create mode 100644 src/Application/Duber.Trip.API/Properties/launchSettings.json create mode 100644 src/Application/Duber.Trip.API/Startup.cs create mode 100644 src/Application/Duber.Trip.API/appsettings.Development.json create mode 100644 src/Application/Duber.Trip.API/appsettings.json create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCreatedDomainEvent.cs create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripEventBase.cs create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripFinishedIntegrationEventHandler.cs create mode 100644 src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs create mode 100644 src/Application/Duber.Trip.Notifications/Dockerfile create mode 100644 src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj create mode 100644 src/Application/Duber.Trip.Notifications/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Application/Duber.Trip.Notifications/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Application/Duber.Trip.Notifications/NotificationsHub.cs create mode 100644 src/Application/Duber.Trip.Notifications/Program.cs create mode 100644 src/Application/Duber.Trip.Notifications/Properties/launchSettings.json create mode 100644 src/Application/Duber.Trip.Notifications/Startup.cs create mode 100644 src/Application/Duber.Trip.Notifications/appsettings.Development.json create mode 100644 src/Application/Duber.Trip.Notifications/appsettings.json create mode 100644 src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj create mode 100644 src/Domain/Driver/Duber.Domain.Driver.UnitTest/UnitTest1.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Exceptions/DriverDomainException.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.Designer.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Migrations/DriverContextModelSnapshot.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Model/Driver.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Model/DriverStatus.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Model/Vehicle.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Model/VehicleType.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContext.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContextSeed.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverEntityTypeConfiguration.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverStatusEntityTypeConfiguration.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleEntityTypeConfiguration.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleTypeEntityTypeConfiguration.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Repository/DriverRepository.cs create mode 100644 src/Domain/Driver/Duber.Domain.Driver/Repository/IDriverRepository.cs create mode 100644 src/Domain/Duber.Domain.ACL/Adapters/PaymentServiceAdapter.cs create mode 100644 src/Domain/Duber.Domain.ACL/Contracts/IPaymentService.cs create mode 100644 src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj create mode 100644 src/Domain/Duber.Domain.ACL/ThirdPartyServices.cs create mode 100644 src/Domain/Duber.Domain.ACL/Translators/PaymentInfoTranslator.cs create mode 100644 src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj create mode 100644 src/Domain/Duber.Domain.SharedKernel/Model/PaymentInfo.cs create mode 100644 src/Domain/Duber.Domain.SharedKernel/Model/PaymentMethod.cs create mode 100644 src/Domain/Duber.Domain.SharedKernel/Model/TripStatus.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/UnitTest1.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoiceCreatedDomainEvent.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoicePaidDomainEvent.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainArgumentNullException.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainInvalidOperationException.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Extensions/TripInformationExtensions.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.Designer.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.Designer.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Migrations/InvoiceMigrationContextModelSnapshot.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Model/Invoice.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Model/TripInformation.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Persistence/IInvoiceContext.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceContext.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceMigrationContext.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Repository/IInvoiceRepository.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Repository/InvoiceRepository.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Services/IPaymentService.cs create mode 100644 src/Domain/Invoice/Duber.Domain.Invoice/Services/PaymentService.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj create mode 100644 src/Domain/Trip/Duber.Domain.Trip.UnitTest/UnitTest1.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainArgumentNullException.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainInvalidOperationException.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Model/Location.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Model/Rating.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs create mode 100644 src/Domain/Trip/Duber.Domain.Trip/Model/VehicleInformation.cs create mode 100644 src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj create mode 100644 src/Domain/User/Duber.Domain.User.UnitTest/UnitTest1.cs create mode 100644 src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj create mode 100644 src/Domain/User/Duber.Domain.User/Exceptions/UserDomainException.cs create mode 100644 src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.Designer.cs create mode 100644 src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.cs create mode 100644 src/Domain/User/Duber.Domain.User/Migrations/UserContextModelSnapshot.cs create mode 100644 src/Domain/User/Duber.Domain.User/Model/User.cs create mode 100644 src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/PaymentMethodEntityTypeConfiguration.cs create mode 100644 src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/UserEntityTypeConfiguration.cs create mode 100644 src/Domain/User/Duber.Domain.User/Persistence/UserContext.cs create mode 100644 src/Domain/User/Duber.Domain.User/Persistence/UserContextSeed.cs create mode 100644 src/Domain/User/Duber.Domain.User/Repository/IUserRepository.cs create mode 100644 src/Domain/User/Duber.Domain.User/Repository/UserRepository.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicyAsyncExecutor.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicySyncExecutor.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicyAsyncExecutor.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicySyncExecutor.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Http/ResilientHttpClient.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/ISqlPolicyBuilder.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlAsyncPolicyBuilder.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlHandledExceptions.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlSyncPolicyBuilder.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/AsyncPolicies.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/PolicyKeys.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/SyncPolicies.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.Resilience.Sql/SqlPolicyBuilder.cs create mode 100644 src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj create mode 100644 src/Infrastructure/Duber.Infrastructure.WebHost/Properties/launchSettings.json create mode 100644 src/Infrastructure/Duber.Infrastructure.WebHost/WebHostExtensions.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/DDD/Entity.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/DDD/Enumeration.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/DDD/IAggregateRoot.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/DDD/ValueObject.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj create mode 100644 src/Infrastructure/Duber.Infrastructure/Extensions/MediatorExtensions.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/Http/JsonContent.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IRepository.cs create mode 100644 src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IUnitOfWork.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IIdempotencyStoreProvider.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEvent.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEventHandler.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentMessage.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/ServiceCollectionExtensions.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/DefaultRabbitMQPersisterConnection.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IRabbitMQPersisterConnection.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IoC/ServiceCollectionExtensions.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/DefaultServiceBusPersisterConnection.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IServiceBusPersisterConnection.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IoC/ServiceCollectionExtensions.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IDynamicIntegrationEventHandler.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IIntegrationEventHandler.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Events/IntegrationEvent.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/IEventBusSubscriptionsManager.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/InMemoryEventBusSubscriptionsManager.cs create mode 100644 src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/SubscriptionInfo.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripCreatedIntegrationEvent.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoiceCreatedIntegrationEventHandler.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoicePaidIntegrationEventHandler.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs create mode 100644 src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs create mode 100644 src/Web/Duber.WebSite/Controllers/HomeController.cs create mode 100644 src/Web/Duber.WebSite/Controllers/TripController.cs create mode 100644 src/Web/Duber.WebSite/Dockerfile create mode 100644 src/Web/Duber.WebSite/Duber.WebSite.csproj create mode 100644 src/Web/Duber.WebSite/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Web/Duber.WebSite/Extensions/Extensions.cs create mode 100644 src/Web/Duber.WebSite/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Web/Duber.WebSite/Infrastructure/Persistence/ReportingContext.cs create mode 100644 src/Web/Duber.WebSite/Infrastructure/Repository/IReportingRepository.cs create mode 100644 src/Web/Duber.WebSite/Infrastructure/Repository/ReportingRepository.cs create mode 100644 src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.Designer.cs create mode 100644 src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.cs create mode 100644 src/Web/Duber.WebSite/Migrations/ReportingContextModelSnapshot.cs create mode 100644 src/Web/Duber.WebSite/Models/DriverModel.cs create mode 100644 src/Web/Duber.WebSite/Models/ErrorViewModel.cs create mode 100644 src/Web/Duber.WebSite/Models/Trip.cs create mode 100644 src/Web/Duber.WebSite/Models/TripApiSettings.cs create mode 100644 src/Web/Duber.WebSite/Models/TripRequestModel.cs create mode 100644 src/Web/Duber.WebSite/Models/UserModel.cs create mode 100644 src/Web/Duber.WebSite/Program.cs create mode 100644 src/Web/Duber.WebSite/Properties/launchSettings.json create mode 100644 src/Web/Duber.WebSite/Startup.cs create mode 100644 src/Web/Duber.WebSite/Views/Home/About.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Home/Contact.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Home/Index.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Shared/Error.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Shared/_Layout.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Shared/_ValidationScriptsPartial.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Trip/DriverTrips.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Trip/Index.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Trip/TripDetails.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Trip/TripsByDriver.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Trip/TripsByUser.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Trip/UserTrips.cshtml create mode 100644 src/Web/Duber.WebSite/Views/Trip/_Map.cshtml create mode 100644 src/Web/Duber.WebSite/Views/_ViewImports.cshtml create mode 100644 src/Web/Duber.WebSite/Views/_ViewStart.cshtml create mode 100644 src/Web/Duber.WebSite/appsettings.Development.json create mode 100644 src/Web/Duber.WebSite/appsettings.json create mode 100644 src/Web/Duber.WebSite/bundleconfig.json create mode 100644 src/Web/Duber.WebSite/libman.json create mode 100644 src/Web/Duber.WebSite/wwwroot/css/site.css create mode 100644 src/Web/Duber.WebSite/wwwroot/css/site.min.css create mode 100644 src/Web/Duber.WebSite/wwwroot/favicon.ico create mode 100644 src/Web/Duber.WebSite/wwwroot/images/banner1.svg create mode 100644 src/Web/Duber.WebSite/wwwroot/images/banner2.svg create mode 100644 src/Web/Duber.WebSite/wwwroot/images/banner3.svg create mode 100644 src/Web/Duber.WebSite/wwwroot/images/banner4.svg create mode 100644 src/Web/Duber.WebSite/wwwroot/images/car-icon.png create mode 100644 src/Web/Duber.WebSite/wwwroot/images/car-icon1.png create mode 100644 src/Web/Duber.WebSite/wwwroot/images/car-icon3.png create mode 100644 src/Web/Duber.WebSite/wwwroot/images/car-icon4.png create mode 100644 src/Web/Duber.WebSite/wwwroot/images/duber_logo.png create mode 100644 src/Web/Duber.WebSite/wwwroot/js/site.js create mode 100644 src/Web/Duber.WebSite/wwwroot/js/site.min.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..43e8ab1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.dockerignore +.env +.git +.gitignore +.vs +.vscode +docker-compose.yml +docker-compose.*.yml +*/bin +*/obj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/.editorconfig @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..c5c4c95 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# Compose supports declaring default environment variables in an environment file named .env placed in the folder docker-compose command is executed from (current working directory). +# Compose expects each line in an env file to be in VAR=VAL format. Lines beginning with # (i.e. comments) are ignored, as are blank lines. +# Note: Values present in the environment at runtime will always override those defined inside the .env file. Similarly, values passed via command-line arguments take precedence as well. + +#APP_ENVIRONMENT=Production +#SERVICE_BUS_ENABLED=True +#AZURE_INVOICE_DB=Your connection string +#AZURE_SERVICE_BUS=Your connection string +#PAYMENT_SERVICE_URL=Your Url +#AZURE_TRIP_DB=Your connection string +#AZURE_WEBSITE_DB=Your connection string +#TRIP_SERVICE_BASE_URL=Your Url diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1f349a --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/.vs/microservices-netcore-docker-servicefabric/v15/Server/sqlite3 +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ + +src/Web/Duber.WebSite/wwwroot/lib +node_modules +packages/ +src/Web/Duber.WebSite/healthchecksdb-shm +src/Web/Duber.WebSite/healthchecksdb +src/Web/Duber.WebSite/healthchecksdb-wal diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..2ed712f --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,45 @@ +## 2.1.0 +**Support to deploy on Cloud K8s cluter:** +* Adjust manifests to deploy in a cloud cluster as a cloud native solution +* Deployed, ran and tested on Google Kubernetes Engine (GKE) + +## 2.0.4 +**Health checks implementation & ConfigMap set up:** +* Add health checks to all microservices +* Add health checks UI add-on to frontend +* Upgrade EF Core to `3.0.0` +* Organize better the `Startup` files +* Set up k8s probes +* Move env variables to a common `ConfigMap` + +## 2.0.3 +**Event bus handlers idempotency:** +* Add *Duber.Infrastructure.EventBus.Idempotency* project to handle idempotency at integration events level. +* Make *TripFinishedIntegrationEvent* idempotent in order to avoid it can be paid more than once due to concurrency, retries, etc. + +## 2.0.2 +**Notifications service:** +* Create an independent service to manage the notifications in order to decouple it from the frontend and to allow a better scaling out for both, frontend and notifications service. +* Add Redis to the cluster in order for *SignalR* to work properly into the cluster. +* Disable *Sticky Sessions* in frontend's Ingress to allow a better load balancing since the notifications don't depend on the frontend anymore. The notification's Ingress is the one that has *Sticky Sessions* to manage the *SignalR* connections. + +## 2.0.1 +**Kubernetes support:** +* Enable the solution to being deployed on a local cluster +* Use an Nginx Ingress Controller to expose frontend (Trip and Invoice services optional if you want to expose the API's) +* Set up Nginx LB to use Sticky sessions in order for SignalR to work properly. + +**Frontend client dependencies:** +* Use Libman to manage the client dependencies. +* Delete static *SignalR* client dependencies, using Libman now. + +**General enhancement:** +* Refactor SignalR messaging in order to send messages only tho the connected client rather than all clients. +* Refactor RabbitMQ client in order to use named Queues and full support to async handlers. + +## 2.0.0 +* Upgrades to .Net Core 3.1 +* Makes Trip and Invoice API's RESTful +* Refactors ***Duber.Infrastructure.Resilience.Sql*** project to follow the pattern proposed [here](https://github.com/vany0114/resilience-strategy-with-polly) +* Gets rid of Restsharp dependency in order to use HttpClient. +* Upgrade Kledex package (formerly Weapsy.CQRS/OpenCQRS) diff --git a/ExternalSystem/PaymentService/.config/dotnet-tools.json b/ExternalSystem/PaymentService/.config/dotnet-tools.json new file mode 100644 index 0000000..56e0950 --- /dev/null +++ b/ExternalSystem/PaymentService/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "3.1.2", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/ExternalSystem/PaymentService/Controllers/PaymentController.cs b/ExternalSystem/PaymentService/Controllers/PaymentController.cs new file mode 100644 index 0000000..4482569 --- /dev/null +++ b/ExternalSystem/PaymentService/Controllers/PaymentController.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.AspNetCore.Mvc; + +namespace PaymentService.Controllers +{ + [Route("api/[controller]")] + public class PaymentController : Controller + { + private readonly List _paymentStatuses = new List { "Accepted", "Rejected" }; + private readonly List _cardTypes = new List { "Visa", "Master Card", "American Express" }; + + [HttpPost] + [Route("performpayment")] + public IEnumerable PerformPayment(int userId, string reference) + { + // just to add some latency + Thread.Sleep(500); + + // let's say that based on the user identification the payment system is able to retrieve the user payment information. + // the payment system returns the response in a list of string like this: payment status, card type, card number, user and reference + return new[] + { + _paymentStatuses[new Random().Next(0, 2)], + _cardTypes[new Random().Next(0, 3)], + Guid.NewGuid().ToString(), + userId.ToString(), + reference + }; + } + } +} diff --git a/ExternalSystem/PaymentService/Dockerfile b/ExternalSystem/PaymentService/Dockerfile new file mode 100644 index 0000000..855dfbf --- /dev/null +++ b/ExternalSystem/PaymentService/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY . . +WORKDIR "/src/ExternalSystem/PaymentService" +RUN dotnet restore +RUN dotnet build --no-restore -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PaymentService.dll"] \ No newline at end of file diff --git a/ExternalSystem/PaymentService/PaymentService.csproj b/ExternalSystem/PaymentService/PaymentService.csproj new file mode 100644 index 0000000..094cb5a --- /dev/null +++ b/ExternalSystem/PaymentService/PaymentService.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.1 + ..\..\docker-compose.dcproj + + + + + + + + + + + + diff --git a/ExternalSystem/PaymentService/Program.cs b/ExternalSystem/PaymentService/Program.cs new file mode 100644 index 0000000..968443c --- /dev/null +++ b/ExternalSystem/PaymentService/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace PaymentService +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml b/ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml new file mode 100644 index 0000000..0b0fe35 --- /dev/null +++ b/ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml @@ -0,0 +1,31 @@ + + + + + MSDeploy + /subscriptions/b9fa199b-d651-4969-a504-266371834927/resourcegroups/duber/providers/Microsoft.Web/sites/PaymentServiceExternal + duber + AzureWebSite + Release + Any CPU + https://paymentserviceexternal.azurewebsites.net + True + False + netcoreapp3.1 + 8504d9b8-c4e8-4172-8da3-cbd9971e3207 + false + paymentserviceexternal.scm.azurewebsites.net:443 + PaymentServiceExternal + + True + WMSVC + True + $PaymentServiceExternal + <_SavePWD>True + <_DestinationType>AzureWebSite + False + + \ No newline at end of file diff --git a/ExternalSystem/PaymentService/Properties/launchSettings.json b/ExternalSystem/PaymentService/Properties/launchSettings.json new file mode 100644 index 0000000..eeeaf56 --- /dev/null +++ b/ExternalSystem/PaymentService/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60796/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "PaymentService": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:60797/" + } + } +} diff --git a/ExternalSystem/PaymentService/Startup.cs b/ExternalSystem/PaymentService/Startup.cs new file mode 100644 index 0000000..3e24afc --- /dev/null +++ b/ExternalSystem/PaymentService/Startup.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace PaymentService +{ + 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(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} diff --git a/ExternalSystem/PaymentService/appsettings.Development.json b/ExternalSystem/PaymentService/appsettings.Development.json new file mode 100644 index 0000000..fa8ce71 --- /dev/null +++ b/ExternalSystem/PaymentService/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/ExternalSystem/PaymentService/appsettings.json b/ExternalSystem/PaymentService/appsettings.json new file mode 100644 index 0000000..26bb0ac --- /dev/null +++ b/ExternalSystem/PaymentService/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..872f93a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Geovanny Alzate Sandoval + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7baff7b --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Microservices w/ .Net Core, Docker and Service Fabric/Kubernetes + +## Prerequisites and Installation Requirements (only for development) + +1. Install Docker for [Windows](https://docs.docker.com/docker-for-windows/install/)/[Mac](https://docs.docker.com/docker-for-mac/install/). +2. Install [.NET Core SDK](https://www.microsoft.com/net/download) +3. Install [Visual Studio](https://www.visualstudio.com/downloads/) 2017 15.8 or later (Visual Studio 2019 16.4 or later recommended) or [Visual Studio Code](https://code.visualstudio.com/). +4. [Tuning Docker for better performance and Debugging](https://github.com/vany0114/microservices-dotnetcore-docker-sf-k8s/blob/master/TunningDocker.md) +5. Clone this Repo +6. Set `docker-compose` project as startup project. +7. Press F5 and that's it! + +## Architecture and Deployment +* [Local deployment w/ Kubernetes](https://github.com/vany0114/microservices-dotnetcore-docker-sf-k8s/blob/master/local-deployment.md) +* [Service Fabric Architecture and Deployment](https://github.com/vany0114/microservices-dotnetcore-docker-sf-k8s/blob/master/SF-architecture.md) +* [Kubernetes Cloud Native Architecture and Deployment](https://github.com/vany0114/microservices-dotnetcore-docker-sf-k8s/blob/master/k8s-architecture.md) + +## Screenshots +### Website +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/duber-in-action.gif) +### Trip API +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/duber-trip-api.png) +### Invoice API +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/duber-invoice-api.png) + +## Support +If you find this project helpful you can [support me](http://www.paypal.me/vany0114/3)! + +Visit my blog to view the whole posts series and to know all the details about this project. diff --git a/SF-architecture.md b/SF-architecture.md new file mode 100644 index 0000000..3ac5fc6 --- /dev/null +++ b/SF-architecture.md @@ -0,0 +1,7 @@ +# Service Fabric Architecture and Deployment + +## Architecture +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/Duber_Production_Environment_Architecture.png) + +## Deployment +Visit [my blog](http://elvanydev.com/Microservices-part4/) to see all about the deployment using a Service Fabric cluster. \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/ApplicationManifest.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/ApplicationManifest.xml new file mode 100644 index 0000000..9c1c816 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/ApplicationManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/Config/Settings.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/Config/Settings.xml new file mode 100644 index 0000000..ad84ffd --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/Config/Settings.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/ServiceManifest.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/ServiceManifest.xml new file mode 100644 index 0000000..b405989 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/InvoicePkg/ServiceManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + vany0114/duber.invoice.api:prod + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/Config/Settings.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/Config/Settings.xml new file mode 100644 index 0000000..ad84ffd --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/Config/Settings.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/ServiceManifest.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/ServiceManifest.xml new file mode 100644 index 0000000..850baf2 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationPackageRoot/TripPkg/ServiceManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + vany0114/duber.trip.api:prod + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Cloud.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Cloud.xml new file mode 100644 index 0000000..9181c33 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Cloud.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.1Node.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.1Node.xml new file mode 100644 index 0000000..6ba1e58 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.1Node.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.5Node.xml b/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.5Node.xml new file mode 100644 index 0000000..6ba1e58 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/ApplicationParameters/Local.5Node.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj b/ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj new file mode 100644 index 0000000..55ecc9d --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj @@ -0,0 +1,48 @@ + + + + + d8a868b6-7d03-4368-a52e-64f1e1b357db + 2.1 + 1.5 + 1.6.6 + v4.6.1 + + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Service Fabric Tools\Microsoft.VisualStudio.Azure.Fabric.ApplicationProject.targets + + + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Cloud.xml b/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Cloud.xml new file mode 100644 index 0000000..52b208c --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Cloud.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.1Node.xml b/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.1Node.xml new file mode 100644 index 0000000..6e1403e --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.1Node.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.5Node.xml b/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.5Node.xml new file mode 100644 index 0000000..f42d759 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/PublishProfiles/Local.5Node.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/Scripts/Deploy-FabricApplication.ps1 b/ServiceFabric/Linux/DuberMicroservices/Scripts/Deploy-FabricApplication.ps1 new file mode 100644 index 0000000..2897b10 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/Scripts/Deploy-FabricApplication.ps1 @@ -0,0 +1,258 @@ +<# +.SYNOPSIS +Deploys a Service Fabric application type to a cluster. + +.DESCRIPTION +This script deploys a Service Fabric application type to a cluster. It is invoked by Visual Studio when deploying a Service Fabric Application project. + +.NOTES +WARNING: This script file is invoked by Visual Studio. Its parameters must not be altered but its logic can be customized as necessary. + +.PARAMETER PublishProfileFile +Path to the file containing the publish profile. + +.PARAMETER ApplicationPackagePath +Path to the folder of the packaged Service Fabric application. + +.PARAMETER DeployOnly +Indicates that the Service Fabric application should not be created or upgraded after registering the application type. + +.PARAMETER ApplicationParameter +Hashtable of the Service Fabric application parameters to be used for the application. + +.PARAMETER UnregisterUnusedApplicationVersionsAfterUpgrade +Indicates whether to unregister any unused application versions that exist after an upgrade is finished. + +.PARAMETER OverrideUpgradeBehavior +Indicates the behavior used to override the upgrade settings specified by the publish profile. +'None' indicates that the upgrade settings will not be overridden. +'ForceUpgrade' indicates that an upgrade will occur with default settings, regardless of what is specified in the publish profile. +'VetoUpgrade' indicates that an upgrade will not occur, regardless of what is specified in the publish profile. + +.PARAMETER UseExistingClusterConnection +Indicates that the script should make use of an existing cluster connection that has already been established in the PowerShell session. The cluster connection parameters configured in the publish profile are ignored. + +.PARAMETER OverwriteBehavior +Overwrite Behavior if an application exists in the cluster with the same name. Available Options are Never, Always, SameAppTypeAndVersion. This setting is not applicable when upgrading an application. +'Never' will not remove the existing application. This is the default behavior. +'Always' will remove the existing application even if its Application type and Version is different from the application being created. +'SameAppTypeAndVersion' will remove the existing application only if its Application type and Version is same as the application being created. + +.PARAMETER SkipPackageValidation +Switch signaling whether the package should be validated or not before deployment. + +.PARAMETER SecurityToken +A security token for authentication to cluster management endpoints. Used for silent authentication to clusters that are protected by Azure Active Directory. + +.PARAMETER CopyPackageTimeoutSec +Timeout in seconds for copying application package to image store. + +.EXAMPLE +. Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' + +Deploy the application using the default package location for a Debug build. + +.EXAMPLE +. Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' -DoNotCreateApplication + +Deploy the application but do not create the application instance. + +.EXAMPLE +. Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' -ApplicationParameter @{CustomParameter1='MyValue'; CustomParameter2='MyValue'} + +Deploy the application by providing values for parameters that are defined in the application manifest. +#> + +Param +( + [String] + $PublishProfileFile, + + [String] + $ApplicationPackagePath, + + [Switch] + $DeployOnly, + + [Hashtable] + $ApplicationParameter, + + [Boolean] + $UnregisterUnusedApplicationVersionsAfterUpgrade, + + [String] + [ValidateSet('None', 'ForceUpgrade', 'VetoUpgrade')] + $OverrideUpgradeBehavior = 'None', + + [Switch] + $UseExistingClusterConnection, + + [String] + [ValidateSet('Never','Always','SameAppTypeAndVersion')] + $OverwriteBehavior = 'Never', + + [Switch] + $SkipPackageValidation, + + [String] + $SecurityToken, + + [int] + $CopyPackageTimeoutSec +) + +function Read-XmlElementAsHashtable +{ + Param ( + [System.Xml.XmlElement] + $Element + ) + + $hashtable = @{} + if ($Element.Attributes) + { + $Element.Attributes | + ForEach-Object { + $boolVal = $null + if ([bool]::TryParse($_.Value, [ref]$boolVal)) { + $hashtable[$_.Name] = $boolVal + } + else { + $hashtable[$_.Name] = $_.Value + } + } + } + + return $hashtable +} + +function Read-PublishProfile +{ + Param ( + [ValidateScript({Test-Path $_ -PathType Leaf})] + [String] + $PublishProfileFile + ) + + $publishProfileXml = [Xml] (Get-Content $PublishProfileFile) + $publishProfile = @{} + + $publishProfile.ClusterConnectionParameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("ClusterConnectionParameters") + $publishProfile.UpgradeDeployment = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("UpgradeDeployment") + $publishProfile.CopyPackageParameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("CopyPackageParameters") + + if ($publishProfileXml.PublishProfile.Item("UpgradeDeployment")) + { + $publishProfile.UpgradeDeployment.Parameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("UpgradeDeployment").Item("Parameters") + if ($publishProfile.UpgradeDeployment["Mode"]) + { + $publishProfile.UpgradeDeployment.Parameters[$publishProfile.UpgradeDeployment["Mode"]] = $true + } + } + + $publishProfileFolder = (Split-Path $PublishProfileFile) + $publishProfile.ApplicationParameterFile = [System.IO.Path]::Combine($PublishProfileFolder, $publishProfileXml.PublishProfile.ApplicationParameterFile.Path) + + return $publishProfile +} + +$LocalFolder = (Split-Path $MyInvocation.MyCommand.Path) + +if (!$PublishProfileFile) +{ + $PublishProfileFile = "$LocalFolder\..\PublishProfiles\Local.xml" +} + +if (!$ApplicationPackagePath) +{ + $ApplicationPackagePath = "$LocalFolder\..\pkg\Release" +} + +$ApplicationPackagePath = Resolve-Path $ApplicationPackagePath + +$publishProfile = Read-PublishProfile $PublishProfileFile + +if (-not $UseExistingClusterConnection) +{ + $ClusterConnectionParameters = $publishProfile.ClusterConnectionParameters + if ($SecurityToken) + { + $ClusterConnectionParameters["SecurityToken"] = $SecurityToken + } + + try + { + [void](Connect-ServiceFabricCluster @ClusterConnectionParameters) + } + catch [System.Fabric.FabricObjectClosedException] + { + Write-Warning "Service Fabric cluster may not be connected." + throw + } +} + +$RegKey = "HKLM:\SOFTWARE\Microsoft\Service Fabric SDK" +$ModuleFolderPath = (Get-ItemProperty -Path $RegKey -Name FabricSDKPSModulePath).FabricSDKPSModulePath +Import-Module "$ModuleFolderPath\ServiceFabricSDK.psm1" + +$IsUpgrade = ($publishProfile.UpgradeDeployment -and $publishProfile.UpgradeDeployment.Enabled -and $OverrideUpgradeBehavior -ne 'VetoUpgrade') -or $OverrideUpgradeBehavior -eq 'ForceUpgrade' + +$PublishParameters = @{ + 'ApplicationPackagePath' = $ApplicationPackagePath + 'ApplicationParameterFilePath' = $publishProfile.ApplicationParameterFile + 'ApplicationParameter' = $ApplicationParameter + 'ErrorAction' = 'Stop' +} + +if ($publishProfile.CopyPackageParameters.CopyPackageTimeoutSec) +{ + $PublishParameters['CopyPackageTimeoutSec'] = $publishProfile.CopyPackageParameters.CopyPackageTimeoutSec +} + +if ($publishProfile.CopyPackageParameters.CompressPackage) +{ + $PublishParameters['CompressPackage'] = $publishProfile.CopyPackageParameters.CompressPackage +} + +# CopyPackageTimeoutSec parameter overrides the value from the publish profile +if ($CopyPackageTimeoutSec) +{ + $PublishParameters['CopyPackageTimeoutSec'] = $CopyPackageTimeoutSec +} + +if ($IsUpgrade) +{ + $Action = "RegisterAndUpgrade" + if ($DeployOnly) + { + $Action = "Register" + } + + $UpgradeParameters = $publishProfile.UpgradeDeployment.Parameters + + if ($OverrideUpgradeBehavior -eq 'ForceUpgrade') + { + # Warning: Do not alter these upgrade parameters. It will create an inconsistency with Visual Studio's behavior. + $UpgradeParameters = @{ UnmonitoredAuto = $true; Force = $true } + } + + $PublishParameters['Action'] = $Action + $PublishParameters['UpgradeParameters'] = $UpgradeParameters + $PublishParameters['UnregisterUnusedVersions'] = $UnregisterUnusedApplicationVersionsAfterUpgrade + + Publish-UpgradedServiceFabricApplication @PublishParameters +} +else +{ + $Action = "RegisterAndCreate" + if ($DeployOnly) + { + $Action = "Register" + } + + $PublishParameters['Action'] = $Action + $PublishParameters['OverwriteBehavior'] = $OverwriteBehavior + $PublishParameters['SkipPackageValidation'] = $SkipPackageValidation + + Publish-NewServiceFabricApplication @PublishParameters +} \ No newline at end of file diff --git a/ServiceFabric/Linux/DuberMicroservices/packages.config b/ServiceFabric/Linux/DuberMicroservices/packages.config new file mode 100644 index 0000000..2b70012 --- /dev/null +++ b/ServiceFabric/Linux/DuberMicroservices/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TunningDocker.md b/TunningDocker.md new file mode 100644 index 0000000..a266488 --- /dev/null +++ b/TunningDocker.md @@ -0,0 +1,11 @@ +# Tuning Docker for better performance and Debugging + +It is important to set Docker up properly with enough memory RAM and CPU assigned to it in order to improve the performance on your local/development environment, or you will get errors when starting the containers with VS 2019 or "docker-compose up". Once Docker is installed in your machine, enter into its Settings and the Advanced menu option so you are able to adjust it to the minimum amount of memory and CPU (Memory: Around 4096MB and CPU:3) as shown in the image. + +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/docker_settings.png) + +Share drives in Docker settings, in order to deploy it as a Docker Compose application and also to debug with Visual Studio 2019 (See the below image) + +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/docker_settings_shared_drives.png) + +> Note: The first time you hit F5 it'll take a few minutes, because in addition to compile the solution, it needs to pull/download the base images (SQL for Linux Docker, ASPNET, MongoDb and RabbitMQ images, etc) and register them in the local image repo of your PC. The next time you hit F5 it'll be much faster. \ No newline at end of file diff --git a/build-images.ps1 b/build-images.ps1 new file mode 100644 index 0000000..50cf4dd --- /dev/null +++ b/build-images.ps1 @@ -0,0 +1,10 @@ +param([String]$service="all") + +if ($service -eq "all") +{ + docker-compose -f docker-compose.yml build +} +else +{ + docker-compose -f docker-compose.yml build $service +} \ No newline at end of file diff --git a/deploy/cosmos/deploycosmos.json b/deploy/cosmos/deploycosmos.json new file mode 100644 index 0000000..c892539 --- /dev/null +++ b/deploy/cosmos/deploycosmos.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "String" + } + }, + "variables": { + "name": "[parameters('name')]", + "location":"[resourceGroup().location]" + }, + "resources": [ + { + "type": "Microsoft.DocumentDb/databaseAccounts", + "kind": "MongoDB", + "name": "[variables('name')]", + "apiVersion": "2015-04-08", + "location": "[variables('location')]", + "properties": { + "databaseAccountOfferType": "Standard", + "locations": [ + { + "id": "[concat(variables('name'), '-', variables('location'))]", + "failoverPriority": 0, + "locationName": "[variables('location')]" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/deploy/cosmos/deploycosmos.parameters.json b/deploy/cosmos/deploycosmos.parameters.json new file mode 100644 index 0000000..c4fb184 --- /dev/null +++ b/deploy/cosmos/deploycosmos.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "value": "duber-tripdb" + } + } +} \ No newline at end of file diff --git a/deploy/create-resources.cmd b/deploy/create-resources.cmd new file mode 100644 index 0000000..bd27efe --- /dev/null +++ b/deploy/create-resources.cmd @@ -0,0 +1,23 @@ +@echo off +if %1.==. GOTO error +if %2.==. GOTO error +if NOT %3.==-c. GOTO deployresources +if %4.==. GOTO error +echo Creating resource group %2 in '%4' +call az group create --name %2 --location %4 +:deployresources +echo Deploying ARM template '%1.json' in resource group %2 +call az group deployment create --resource-group %2 --parameters @%1.parameters.json --template-file %1.json +GOTO end +:error +echo. +echo Usage: +echo create-resources arm-file resource-group-name [-c location] +echo arm-file: Path to ARM template WITHOUT .json extension. An parameter file with same name plus '.parameters' MUST exist in same folde +echo resource-grop-name: Name of the resource group to use or create +echo -c: If appears means that resource group must be created. If -c is specified, must use enter location +echo. +echo Examples: +echo create-resources path_and_filename testgroup (Deploys path_and_filename.json with parameters specified in path_and_filename.parameters.json file). +echo create-resources path_and_filename newgroup -c westus (Deploys path_and_filename.json (with parameters specified in path_and_filename.parameters.json file) in a NEW resource group named newgroup in the westus location) +:end diff --git a/deploy/k8s/gke/default-ingress.yaml b/deploy/k8s/gke/default-ingress.yaml new file mode 100644 index 0000000..f033740 --- /dev/null +++ b/deploy/k8s/gke/default-ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: default-ingress + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/rewrite-target: /$1 +spec: + rules: + - http: + paths: + - backend: + serviceName: trip + servicePort: 80 + path: /services/trip/(.*) + - backend: + serviceName: invoice + servicePort: 80 + path: /services/invoice/(.*) + - backend: + serviceName: frontend + servicePort: 80 + path: /(.*) \ No newline at end of file diff --git a/deploy/k8s/gke/delete-resources.sh b/deploy/k8s/gke/delete-resources.sh new file mode 100644 index 0000000..9ff547b --- /dev/null +++ b/deploy/k8s/gke/delete-resources.sh @@ -0,0 +1,26 @@ +# configmap +kubectl delete cm env-config + +# common ingress +kubectl delete ing default-ingress + +# invoice +kubectl delete deploy invoice +kubectl delete svc invoice +kubectl delete hpa invoice + +# trip +kubectl delete deploy trip +kubectl delete hpa trip +kubectl delete svc trip + +# website +kubectl delete deploy frontend +kubectl delete hpa frontend +kubectl delete svc frontend + +#notificatiosn +kubectl delete deploy notifications +kubectl delete hpa notifications +kubectl delete ing notifications +kubectl delete svc notifications \ No newline at end of file diff --git a/deploy/k8s/gke/deploy.sh b/deploy/k8s/gke/deploy.sh new file mode 100644 index 0000000..befcf81 --- /dev/null +++ b/deploy/k8s/gke/deploy.sh @@ -0,0 +1,26 @@ +# configmap +kubectl apply -f env-config.yaml + +# common ingress +kubectl apply -f default-ingress.yaml + +# invoice +kubectl apply -f invoice\invoice-deployment.yaml +kubectl apply -f invoice\invoice-service.yaml +kubectl apply -f invoice\invoice-hpa.yaml + +# trip +kubectl apply -f trip\trip-deployment.yaml +kubectl apply -f trip\trip-hpa.yaml +kubectl apply -f trip\trip-service.yaml + +# website +kubectl apply -f website\website-deployment.yaml +kubectl apply -f website\website-hpa.yaml +kubectl apply -f website\website-service.yaml + +#notificatiosn +kubectl apply -f notifications\notifications-deployment.yaml +kubectl apply -f notifications\notifications-hpa.yaml +kubectl apply -f notifications\notifications-ingress.yaml +kubectl apply -f notifications\notifications-service.yaml \ No newline at end of file diff --git a/deploy/k8s/gke/env-config.yaml b/deploy/k8s/gke/env-config.yaml new file mode 100644 index 0000000..9410cf1 --- /dev/null +++ b/deploy/k8s/gke/env-config.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: env-config +data: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__InvoiceDB: {your invoice db connection string} + ConnectionStrings__WebsiteDB: {your website db connection string} + ConnectionStrings__SignalrBackPlane: {your redis connection string} + EventStoreConfiguration__ConnectionString: {your mongo db connection string} + EventBusConnection: {your service bus connection string} + EventBusUserName: {your rabbitmq user} + EventBusPassword: {your rabbitmq password} + PaymentServiceBaseUrl: {your payment service url} + InvoiceApiSettings__BaseUrl: http://invoice + TripApiSettings__BaseUrl: http://trip + TripApiSettings__NotificationsClientUrl: http://{your load balancer ip}/notifications + TripApiSettings__NotificationsServerUrl: http://notifications + AzureServiceBusEnabled: "false" + IsDeployedOnCluster: "true" \ No newline at end of file diff --git a/deploy/k8s/gke/invoice/invoice-deployment-with-proxy.yaml b/deploy/k8s/gke/invoice/invoice-deployment-with-proxy.yaml new file mode 100644 index 0000000..bcfce2d --- /dev/null +++ b/deploy/k8s/gke/invoice/invoice-deployment-with-proxy.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: invoice +spec: + selector: + matchLabels: + app: invoice + replicas: 1 + template: + metadata: + labels: + app: invoice + spec: + containers: + - name: invoice + image: vany0114/duber.invoice.api + imagePullPolicy: Always + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + envFrom: + - configMapRef: + name: env-config + env: + - name: ReverseProxyPrefix + value: "services/invoice" + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 30 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 + - name: cloudsql-proxy + image: gcr.io/cloudsql-docker/gce-proxy:1.16 + command: ["/cloud_sql_proxy", + "-instances==tcp:3306", + "-credential_file=/secrets/cloudsql/creadentials.json"] + securityContext: + runAsUser: 2 # non-root user + allowPrivilegeEscalation: false + volumeMounts: + - name: cloudsql-instance-credentials + mountPath: /secrets/cloudsql + readOnly: true + volumes: + - name: cloudsql-instance-credentials + secret: + secretName: cloudsql-instance-credentials \ No newline at end of file diff --git a/deploy/k8s/gke/invoice/invoice-deployment.yaml b/deploy/k8s/gke/invoice/invoice-deployment.yaml new file mode 100644 index 0000000..ac90ab9 --- /dev/null +++ b/deploy/k8s/gke/invoice/invoice-deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: invoice +spec: + selector: + matchLabels: + app: invoice + replicas: 1 + template: + metadata: + labels: + app: invoice + spec: + containers: + - name: invoice + image: vany0114/duber.invoice.api + imagePullPolicy: Always + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "128Mi" + cpu: "200m" + envFrom: + - configMapRef: + name: env-config + env: + - name: ReverseProxyPrefix + value: "services/invoice" + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 30 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/gke/invoice/invoice-hpa.yaml b/deploy/k8s/gke/invoice/invoice-hpa.yaml new file mode 100644 index 0000000..99d7ea3 --- /dev/null +++ b/deploy/k8s/gke/invoice/invoice-hpa.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: invoice +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: invoice + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/deploy/k8s/gke/invoice/invoice-service.yaml b/deploy/k8s/gke/invoice/invoice-service.yaml new file mode 100644 index 0000000..4c41e3b --- /dev/null +++ b/deploy/k8s/gke/invoice/invoice-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: invoice +spec: + selector: + app: invoice + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/gke/notifications/notifications-deployment.yaml b/deploy/k8s/gke/notifications/notifications-deployment.yaml new file mode 100644 index 0000000..da99306 --- /dev/null +++ b/deploy/k8s/gke/notifications/notifications-deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notifications +spec: + selector: + matchLabels: + app: notifications + replicas: 1 + template: + metadata: + labels: + app: notifications + spec: + containers: + - name: notifications + image: vany0114/duber.trip.notifications + imagePullPolicy: Always + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "128Mi" + cpu: "200m" + envFrom: + - configMapRef: + name: env-config + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 30 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/gke/notifications/notifications-hpa.yaml b/deploy/k8s/gke/notifications/notifications-hpa.yaml new file mode 100644 index 0000000..70cc79a --- /dev/null +++ b/deploy/k8s/gke/notifications/notifications-hpa.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: notifications +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: notifications + minReplicas: 4 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/deploy/k8s/gke/notifications/notifications-ingress.yaml b/deploy/k8s/gke/notifications/notifications-ingress.yaml new file mode 100644 index 0000000..b4a0732 --- /dev/null +++ b/deploy/k8s/gke/notifications/notifications-ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: notifications + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/affinity: cookie + nginx.ingress.kubernetes.io/session-cookie-path: /notifications/ + nginx.ingress.kubernetes.io/rewrite-target: /$1 +spec: + rules: + - http: + paths: + - backend: + serviceName: notifications + servicePort: 80 + path: /notifications/(.*) \ No newline at end of file diff --git a/deploy/k8s/gke/notifications/notifications-service.yaml b/deploy/k8s/gke/notifications/notifications-service.yaml new file mode 100644 index 0000000..94b1213 --- /dev/null +++ b/deploy/k8s/gke/notifications/notifications-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: notifications +spec: + selector: + app: notifications + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/gke/trip/trip-deployment.yaml b/deploy/k8s/gke/trip/trip-deployment.yaml new file mode 100644 index 0000000..bcc612b --- /dev/null +++ b/deploy/k8s/gke/trip/trip-deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: trip +spec: + selector: + matchLabels: + app: trip + replicas: 1 + template: + metadata: + labels: + app: trip + spec: + containers: + - name: trip + image: vany0114/duber.trip.api + imagePullPolicy: Always + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "128Mi" + cpu: "200m" + envFrom: + - configMapRef: + name: env-config + env: + - name: ReverseProxyPrefix + value: "services/trip" + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 30 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/gke/trip/trip-hpa.yaml b/deploy/k8s/gke/trip/trip-hpa.yaml new file mode 100644 index 0000000..7a7703b --- /dev/null +++ b/deploy/k8s/gke/trip/trip-hpa.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: trip +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: trip + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/deploy/k8s/gke/trip/trip-service.yaml b/deploy/k8s/gke/trip/trip-service.yaml new file mode 100644 index 0000000..44960b8 --- /dev/null +++ b/deploy/k8s/gke/trip/trip-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: trip +spec: + selector: + app: trip + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/gke/website/website-deployment-with-proxy.yaml b/deploy/k8s/gke/website/website-deployment-with-proxy.yaml new file mode 100644 index 0000000..02f732e --- /dev/null +++ b/deploy/k8s/gke/website/website-deployment-with-proxy.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + selector: + matchLabels: + app: frontend + replicas: 1 + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: vany0114/duber.website + imagePullPolicy: Always + resources: + requests: + memory: "128Mi" + cpu: "200m" + limits: + memory: "256Mi" + cpu: "600m" + envFrom: + - configMapRef: + name: env-config + env: + - name: HealthChecksUI__HealthChecks__0__Name + value: "Invoice HTTP Check" + - name: HealthChecksUI__HealthChecks__0__Uri + value: $(InvoiceApiSettings__BaseUrl)/readiness + - name: HealthChecksUI__HealthChecks__1__Name + value: "Trip HTTP Check" + - name: HealthChecksUI__HealthChecks__1__Uri + value: $(TripApiSettings__BaseUrl)/readiness + - name: HealthChecksUI__HealthChecks__2__Name + value: "Notifications HTTP Check" + - name: HealthChecksUI__HealthChecks__2__Uri + value: $(TripApiSettings__NotificationsServerUrl)/readiness + - name: HealthChecksUI__HealthChecks__3__Name + value: "Frontend HTTP Check" + - name: HealthChecksUI__HealthChecks__3__Uri + value: "http://frontend/readiness" + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 60 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 + - name: cloudsql-proxy + image: gcr.io/cloudsql-docker/gce-proxy:1.16 + command: ["/cloud_sql_proxy", + "-instances==tcp:3306", + "-credential_file=/secrets/cloudsql/creadentials.json"] + securityContext: + runAsUser: 2 # non-root user + allowPrivilegeEscalation: false + volumeMounts: + - name: cloudsql-instance-credentials + mountPath: /secrets/cloudsql + readOnly: true + volumes: + - name: cloudsql-instance-credentials + secret: + secretName: cloudsql-instance-credentials \ No newline at end of file diff --git a/deploy/k8s/gke/website/website-deployment.yaml b/deploy/k8s/gke/website/website-deployment.yaml new file mode 100644 index 0000000..1b010d9 --- /dev/null +++ b/deploy/k8s/gke/website/website-deployment.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + selector: + matchLabels: + app: frontend + replicas: 1 + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: vany0114/duber.website + imagePullPolicy: Always + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "128Mi" + cpu: "200m" + envFrom: + - configMapRef: + name: env-config + env: + - name: HealthChecksUI__HealthChecks__0__Name + value: "Invoice HTTP Check" + - name: HealthChecksUI__HealthChecks__0__Uri + value: $(InvoiceApiSettings__BaseUrl)/readiness + - name: HealthChecksUI__HealthChecks__1__Name + value: "Trip HTTP Check" + - name: HealthChecksUI__HealthChecks__1__Uri + value: $(TripApiSettings__BaseUrl)/readiness + - name: HealthChecksUI__HealthChecks__2__Name + value: "Notifications HTTP Check" + - name: HealthChecksUI__HealthChecks__2__Uri + value: $(TripApiSettings__NotificationsServerUrl)/readiness + - name: HealthChecksUI__HealthChecks__3__Name + value: "Frontend HTTP Check" + - name: HealthChecksUI__HealthChecks__3__Uri + value: "http://frontend/readiness" + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 60 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/gke/website/website-hpa.yaml b/deploy/k8s/gke/website/website-hpa.yaml new file mode 100644 index 0000000..999fdab --- /dev/null +++ b/deploy/k8s/gke/website/website-hpa.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: frontend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: frontend + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/deploy/k8s/gke/website/website-service.yaml b/deploy/k8s/gke/website/website-service.yaml new file mode 100644 index 0000000..9f7074c --- /dev/null +++ b/deploy/k8s/gke/website/website-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend +spec: + selector: + app: frontend + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/dashboard-adminuser.yaml b/deploy/k8s/local/dashboard-adminuser.yaml new file mode 100644 index 0000000..0485553 --- /dev/null +++ b/deploy/k8s/local/dashboard-adminuser.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: admin-user + namespace: kubernetes-dashboard +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: admin-user +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: admin-user + namespace: kubernetes-dashboard \ No newline at end of file diff --git a/deploy/k8s/local/delete-resources.ps1 b/deploy/k8s/local/delete-resources.ps1 new file mode 100644 index 0000000..8150318 --- /dev/null +++ b/deploy/k8s/local/delete-resources.ps1 @@ -0,0 +1,29 @@ +# configmap +kubectl delete cm env-config + +# external system +kubectl delete deploy payment +kubectl delete svc payment + +# invoice +kubectl delete deploy invoice +kubectl delete ing invoice +kubectl delete svc invoice + +# trip +kubectl delete deploy trip +kubectl delete hpa trip +kubectl delete ing trip +kubectl delete svc trip + +# website +kubectl delete deploy frontend +kubectl delete hpa frontend +kubectl delete ing frontend +kubectl delete svc frontend + +#notificatiosn +kubectl delete deploy notifications +kubectl delete hpa notifications +kubectl delete ing notifications +kubectl delete svc notifications \ No newline at end of file diff --git a/deploy/k8s/local/deploy-local.ps1 b/deploy/k8s/local/deploy-local.ps1 new file mode 100644 index 0000000..174fee9 --- /dev/null +++ b/deploy/k8s/local/deploy-local.ps1 @@ -0,0 +1,56 @@ +# k8s dashboard +kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta8/aio/deploy/recommended.yaml +kubectl apply -f dashboard-adminuser.yaml + +# nginx-ingress +kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml +kubectl apply -f nginx-ingress\custom-service.yaml + +# configmap +kubectl apply -f env-config.yaml + +# mongo +kubectl apply -f mongo\mongo-admin.yaml +kubectl apply -f mongo\mongo-deployment.yaml +kubectl apply -f mongo\mongo-service.yaml + +# redis +kubectl apply -f redis\redis-deployment.yaml +kubectl apply -f redis\redis-service.yaml + +# rabbit +kubectl apply -f rabbit\rabbit-admin.yaml +kubectl apply -f rabbit\rabbit-deployment.yaml +kubectl apply -f rabbit\rabbit-service.yaml + +# sql-server +kubectl apply -f sql-server\sql-admin.yaml +kubectl apply -f sql-server\sql-deployment.yaml +kubectl apply -f sql-server\sql-service.yaml + +# external system +kubectl apply -f external-system\payment-deployment.yaml +kubectl apply -f external-system\payment-service.yaml + +# invoice +kubectl apply -f invoice\invoice-deployment.yaml +kubectl apply -f invoice\invoice-ingress.yaml +kubectl apply -f invoice\invoice-service.yaml + +# trip +kubectl apply -f trip\trip-deployment.yaml +kubectl apply -f trip\trip-hpa.yaml +kubectl apply -f trip\trip-ingress.yaml +kubectl apply -f trip\trip-service.yaml + +# website +kubectl apply -f website\website-deployment.yaml +kubectl apply -f website\website-hpa.yaml +kubectl apply -f website\website-ingress.yaml +kubectl apply -f website\website-service.yaml + +#notificatiosn +kubectl apply -f notifications\notifications-deployment.yaml +kubectl apply -f notifications\notifications-hpa.yaml +kubectl apply -f notifications\notifications-ingress.yaml +kubectl apply -f notifications\notifications-service.yaml \ No newline at end of file diff --git a/deploy/k8s/local/env-config.yaml b/deploy/k8s/local/env-config.yaml new file mode 100644 index 0000000..30f7fe6 --- /dev/null +++ b/deploy/k8s/local/env-config.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: env-config +data: + ASPNETCORE_ENVIRONMENT: Development + ConnectionStrings__InvoiceDB: Server=sql-data;Database=Duber.InvoiceDb;User Id=sa;Password=Pass@word + ConnectionStrings__WebsiteDB: Server=sql-data;Database=Duber.WebSiteDb;User Id=sa;Password=Pass@word + ConnectionStrings__SignalrBackPlane: redis + EventStoreConfiguration__ConnectionString: mongodb://nosql-data + EventBusConnection: rabbitmq + PaymentServiceBaseUrl: http://payment + InvoiceApiSettings__BaseUrl: http://invoice + TripApiSettings__BaseUrl: http://trip + TripApiSettings__NotificationsClientUrl: http://trip.notifications.local.com:81 + TripApiSettings__NotificationsServerUrl: http://notifications + AzureServiceBusEnabled: "false" + IsDeployedOnCluster: "true" \ No newline at end of file diff --git a/deploy/k8s/local/external-system/payment-deployment.yaml b/deploy/k8s/local/external-system/payment-deployment.yaml new file mode 100644 index 0000000..f358128 --- /dev/null +++ b/deploy/k8s/local/external-system/payment-deployment.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: payment +spec: + selector: + matchLabels: + app: payment + replicas: 1 + template: + metadata: + labels: + app: payment + spec: + containers: + - name: payment + image: vany0114/externalsystem.paymentservice + imagePullPolicy: Always + envFrom: + - configMapRef: + name: env-config + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/external-system/payment-service.yaml b/deploy/k8s/local/external-system/payment-service.yaml new file mode 100644 index 0000000..2e5d1e7 --- /dev/null +++ b/deploy/k8s/local/external-system/payment-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: payment +spec: + selector: + app: payment + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/invoice/invoice-deployment.yaml b/deploy/k8s/local/invoice/invoice-deployment.yaml new file mode 100644 index 0000000..7c40163 --- /dev/null +++ b/deploy/k8s/local/invoice/invoice-deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: invoice +spec: + selector: + matchLabels: + app: invoice + replicas: 2 + template: + metadata: + labels: + app: invoice + spec: + containers: + - name: invoice + image: vany0114/duber.invoice.api + imagePullPolicy: Always + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + envFrom: + - configMapRef: + name: env-config + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 30 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/invoice/invoice-ingress.yaml b/deploy/k8s/local/invoice/invoice-ingress.yaml new file mode 100644 index 0000000..28ec57a --- /dev/null +++ b/deploy/k8s/local/invoice/invoice-ingress.yaml @@ -0,0 +1,13 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: invoice +spec: + rules: + - host: invoice.local.com + http: + paths: + - path: / + backend: + serviceName: invoice + servicePort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/invoice/invoice-service.yaml b/deploy/k8s/local/invoice/invoice-service.yaml new file mode 100644 index 0000000..4c41e3b --- /dev/null +++ b/deploy/k8s/local/invoice/invoice-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: invoice +spec: + selector: + app: invoice + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/mongo/mongo-admin.yaml b/deploy/k8s/local/mongo/mongo-admin.yaml new file mode 100644 index 0000000..cbba348 --- /dev/null +++ b/deploy/k8s/local/mongo/mongo-admin.yaml @@ -0,0 +1,14 @@ +# to expose the port in order to connect with an IDE such as Studio 3T, etc + +kind: Service +apiVersion: v1 +metadata: + name: mongo-service +spec: + type: NodePort + selector: + app: nosql-data + ports: + - port: 27017 + nodePort: 31434 + name: mongo-port \ No newline at end of file diff --git a/deploy/k8s/local/mongo/mongo-deployment.yaml b/deploy/k8s/local/mongo/mongo-deployment.yaml new file mode 100644 index 0000000..e61df82 --- /dev/null +++ b/deploy/k8s/local/mongo/mongo-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nosql-data +spec: + selector: + matchLabels: + app: nosql-data + replicas: 1 + template: + metadata: + labels: + app: nosql-data + spec: + containers: + - name: nosql-data + image: mongo + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 27017 + protocol: TCP \ No newline at end of file diff --git a/deploy/k8s/local/mongo/mongo-service.yaml b/deploy/k8s/local/mongo/mongo-service.yaml new file mode 100644 index 0000000..b85acd6 --- /dev/null +++ b/deploy/k8s/local/mongo/mongo-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: nosql-data +spec: + selector: + app: nosql-data + ports: + - port: 27017 + targetPort: http + protocol: TCP + name: http + type: ClusterIP \ No newline at end of file diff --git a/deploy/k8s/local/nginx-ingress/custom-service.yaml b/deploy/k8s/local/nginx-ingress/custom-service.yaml new file mode 100644 index 0000000..ebf1b78 --- /dev/null +++ b/deploy/k8s/local/nginx-ingress/custom-service.yaml @@ -0,0 +1,23 @@ +kind: Service +apiVersion: v1 +metadata: + name: ingress-nginx + namespace: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx +spec: + externalTrafficPolicy: Local + type: LoadBalancer + selector: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + ports: + - name: http + port: 81 + protocol: TCP + targetPort: http + - name: https + port: 444 + protocol: TCP + targetPort: https \ No newline at end of file diff --git a/deploy/k8s/local/notifications/notifications-deployment.yaml b/deploy/k8s/local/notifications/notifications-deployment.yaml new file mode 100644 index 0000000..817de15 --- /dev/null +++ b/deploy/k8s/local/notifications/notifications-deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notifications +spec: + selector: + matchLabels: + app: notifications + replicas: 1 + template: + metadata: + labels: + app: notifications + spec: + containers: + - name: notifications + image: vany0114/duber.trip.notifications + imagePullPolicy: Always + resources: + requests: + memory: "128Mi" + cpu: "200m" + limits: + memory: "256Mi" + cpu: "600m" + envFrom: + - configMapRef: + name: env-config + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 30 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/notifications/notifications-hpa.yaml b/deploy/k8s/local/notifications/notifications-hpa.yaml new file mode 100644 index 0000000..70cc79a --- /dev/null +++ b/deploy/k8s/local/notifications/notifications-hpa.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: notifications +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: notifications + minReplicas: 4 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/deploy/k8s/local/notifications/notifications-ingress.yaml b/deploy/k8s/local/notifications/notifications-ingress.yaml new file mode 100644 index 0000000..d0cf545 --- /dev/null +++ b/deploy/k8s/local/notifications/notifications-ingress.yaml @@ -0,0 +1,15 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: notifications + annotations: + nginx.ingress.kubernetes.io/affinity: cookie +spec: + rules: + - host: trip.notifications.local.com + http: + paths: + - path: / + backend: + serviceName: notifications + servicePort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/notifications/notifications-service.yaml b/deploy/k8s/local/notifications/notifications-service.yaml new file mode 100644 index 0000000..94b1213 --- /dev/null +++ b/deploy/k8s/local/notifications/notifications-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: notifications +spec: + selector: + app: notifications + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/rabbit/rabbit-admin.yaml b/deploy/k8s/local/rabbit/rabbit-admin.yaml new file mode 100644 index 0000000..dfe9f27 --- /dev/null +++ b/deploy/k8s/local/rabbit/rabbit-admin.yaml @@ -0,0 +1,14 @@ +# to expose the port in order to connect to the admin dashboard. + +kind: Service +apiVersion: v1 +metadata: + name: rabbitmq-admin +spec: + type: NodePort + selector: + app: rabbitmq + ports: + - port: 15672 + nodePort: 31672 + name: rabbitmq-port \ No newline at end of file diff --git a/deploy/k8s/local/rabbit/rabbit-deployment.yaml b/deploy/k8s/local/rabbit/rabbit-deployment.yaml new file mode 100644 index 0000000..31a3b0d --- /dev/null +++ b/deploy/k8s/local/rabbit/rabbit-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rabbitmq +spec: + selector: + matchLabels: + app: rabbitmq + replicas: 1 + template: + metadata: + labels: + app: rabbitmq + spec: + containers: + - name: rabbitmq + image: rabbitmq:3-management + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 5672 + protocol: TCP \ No newline at end of file diff --git a/deploy/k8s/local/rabbit/rabbit-service.yaml b/deploy/k8s/local/rabbit/rabbit-service.yaml new file mode 100644 index 0000000..34fea70 --- /dev/null +++ b/deploy/k8s/local/rabbit/rabbit-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq +spec: + selector: + app: rabbitmq + ports: + - port: 5672 + targetPort: http + protocol: TCP + name: http + type: ClusterIP \ No newline at end of file diff --git a/deploy/k8s/local/redis/redis-deployment.yaml b/deploy/k8s/local/redis/redis-deployment.yaml new file mode 100644 index 0000000..d1d476b --- /dev/null +++ b/deploy/k8s/local/redis/redis-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis +spec: + selector: + matchLabels: + app: redis + replicas: 1 + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:alpine + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 6379 + protocol: TCP \ No newline at end of file diff --git a/deploy/k8s/local/redis/redis-service.yaml b/deploy/k8s/local/redis/redis-service.yaml new file mode 100644 index 0000000..9bbbe37 --- /dev/null +++ b/deploy/k8s/local/redis/redis-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: http + protocol: TCP + name: http + type: ClusterIP \ No newline at end of file diff --git a/deploy/k8s/local/sql-server/sql-admin.yaml b/deploy/k8s/local/sql-server/sql-admin.yaml new file mode 100644 index 0000000..a7d920b --- /dev/null +++ b/deploy/k8s/local/sql-server/sql-admin.yaml @@ -0,0 +1,14 @@ +# to expose the port in order to connect with an IDE such as SQL MS, etc + +kind: Service +apiVersion: v1 +metadata: + name: sql-service +spec: + type: NodePort + selector: + app: sql-data + ports: + - port: 1433 + nodePort: 31433 + name: sql-port \ No newline at end of file diff --git a/deploy/k8s/local/sql-server/sql-deployment.yaml b/deploy/k8s/local/sql-server/sql-deployment.yaml new file mode 100644 index 0000000..7ab9139 --- /dev/null +++ b/deploy/k8s/local/sql-server/sql-deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sql-data +spec: + selector: + matchLabels: + app: sql-data + replicas: 1 + template: + metadata: + labels: + app: sql-data + spec: + containers: + - name: sql-data + image: microsoft/mssql-server-linux:2017-latest + imagePullPolicy: IfNotPresent + env: + - name: ACCEPT_EULA + value: "Y" + - name: MSSQL_PID + value: Developer + - name: MSSQL_SA_PASSWORD + value: Pass@word + ports: + - name: http + containerPort: 1433 + protocol: TCP \ No newline at end of file diff --git a/deploy/k8s/local/sql-server/sql-service.yaml b/deploy/k8s/local/sql-server/sql-service.yaml new file mode 100644 index 0000000..d54ed55 --- /dev/null +++ b/deploy/k8s/local/sql-server/sql-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: sql-data +spec: + selector: + app: sql-data + ports: + - port: 1433 + targetPort: http + protocol: TCP + name: http + type: ClusterIP \ No newline at end of file diff --git a/deploy/k8s/local/trip/trip-deployment.yaml b/deploy/k8s/local/trip/trip-deployment.yaml new file mode 100644 index 0000000..571acf2 --- /dev/null +++ b/deploy/k8s/local/trip/trip-deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: trip +spec: + selector: + matchLabels: + app: trip + replicas: 1 + template: + metadata: + labels: + app: trip + spec: + containers: + - name: trip + image: vany0114/duber.trip.api + imagePullPolicy: Always + resources: + requests: + memory: "128Mi" + cpu: "200m" + limits: + memory: "256Mi" + cpu: "600m" + envFrom: + - configMapRef: + name: env-config + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 30 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/trip/trip-hpa.yaml b/deploy/k8s/local/trip/trip-hpa.yaml new file mode 100644 index 0000000..7a7703b --- /dev/null +++ b/deploy/k8s/local/trip/trip-hpa.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: trip +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: trip + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/deploy/k8s/local/trip/trip-ingress.yaml b/deploy/k8s/local/trip/trip-ingress.yaml new file mode 100644 index 0000000..f47dec3 --- /dev/null +++ b/deploy/k8s/local/trip/trip-ingress.yaml @@ -0,0 +1,13 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: trip +spec: + rules: + - host: trip.local.com + http: + paths: + - path: / + backend: + serviceName: trip + servicePort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/trip/trip-service.yaml b/deploy/k8s/local/trip/trip-service.yaml new file mode 100644 index 0000000..44960b8 --- /dev/null +++ b/deploy/k8s/local/trip/trip-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: trip +spec: + selector: + app: trip + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/website/website-deployment.yaml b/deploy/k8s/local/website/website-deployment.yaml new file mode 100644 index 0000000..3327b74 --- /dev/null +++ b/deploy/k8s/local/website/website-deployment.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + selector: + matchLabels: + app: frontend + replicas: 1 + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: vany0114/duber.website + imagePullPolicy: Always + resources: + requests: + memory: "128Mi" + cpu: "200m" + limits: + memory: "256Mi" + cpu: "600m" + envFrom: + - configMapRef: + name: env-config + env: + - name: HealthChecksUI__HealthChecks__0__Name + value: "Invoice HTTP Check" + - name: HealthChecksUI__HealthChecks__0__Uri + value: $(InvoiceApiSettings__BaseUrl)/readiness + - name: HealthChecksUI__HealthChecks__1__Name + value: "Trip HTTP Check" + - name: HealthChecksUI__HealthChecks__1__Uri + value: $(TripApiSettings__BaseUrl)/readiness + - name: HealthChecksUI__HealthChecks__2__Name + value: "Notifications HTTP Check" + - name: HealthChecksUI__HealthChecks__2__Uri + value: $(TripApiSettings__NotificationsServerUrl)/readiness + - name: HealthChecksUI__HealthChecks__3__Name + value: "Frontend HTTP Check" + - name: HealthChecksUI__HealthChecks__3__Uri + value: "http://frontend/readiness" + livenessProbe: + httpGet: + port: 80 + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readiness + port: 80 + initialDelaySeconds: 60 + periodSeconds: 60 + timeoutSeconds: 5 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/website/website-hpa.yaml b/deploy/k8s/local/website/website-hpa.yaml new file mode 100644 index 0000000..999fdab --- /dev/null +++ b/deploy/k8s/local/website/website-hpa.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: frontend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: frontend + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/deploy/k8s/local/website/website-ingress.yaml b/deploy/k8s/local/website/website-ingress.yaml new file mode 100644 index 0000000..ed72c9e --- /dev/null +++ b/deploy/k8s/local/website/website-ingress.yaml @@ -0,0 +1,13 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: frontend +spec: + rules: + - host: duber.local.com + http: + paths: + - path: / + backend: + serviceName: frontend + servicePort: 80 \ No newline at end of file diff --git a/deploy/k8s/local/website/website-service.yaml b/deploy/k8s/local/website/website-service.yaml new file mode 100644 index 0000000..9f7074c --- /dev/null +++ b/deploy/k8s/local/website/website-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend +spec: + selector: + app: frontend + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/deploy/redis/readme.md b/deploy/redis/readme.md new file mode 100644 index 0000000..61be1f6 --- /dev/null +++ b/deploy/redis/readme.md @@ -0,0 +1,31 @@ +# Deploying Redis Cache + +The ARM template `redisdeploy.json` and its parameter file (`redisdeploy.parameters.json`) are used to deploy following resources: + +1. One Redis Cache + +## Editing sbusdeploy.parameters.json file + +You can edit the `redisdeploy.parameters.parameters.json` file to set your values, but is not needed. The only parameter than can +be set is: + +1. `namespaceprefix` is a string that is used to create the Redis namespace. ARM script creates unique values by appending a unique string to this parameter value, so you can leave the default value. + +## Deploy the template + +Once parameter file is edited you can deploy it using [create-resources script](../readme.md). + +i. e. if you are in windows, to deploy a Redis cache in a new Azure Resource Group located in westus, go to `deploy\az` folder and type: + +``` +create-resources.cmd redis\redisdeploy newResourceGroup -c westus +``` + + + + + + + + + diff --git a/deploy/redis/redisdeploy.json b/deploy/redis/redisdeploy.json new file mode 100644 index 0000000..04b53a3 --- /dev/null +++ b/deploy/redis/redisdeploy.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "namespaceprefix": { + "type": "string", + "metadata": { + "description": "Name of the Redis namespace" + } + } + }, + "variables": { + "location": "[resourceGroup().location]", + "namespaceprefix": "[concat(parameters('namespaceprefix'), uniqueString(resourceGroup().id))]", + "sbVersion": "2016-04-01" + }, + "resources": [ + { + "type": "Microsoft.Cache/Redis", + "name": "[variables('namespaceprefix')]", + "apiVersion": "[variables('sbVersion')]", + "location": "[variables('location')]", + "scale": null, + "properties": { + "redisVersion": "3.2.7", + "sku": { + "name": "Standard", + "family": "C", + "capacity": 1 + }, + "enableNonSslPort": true, + "redisConfiguration": { + "maxclients": "1000", + "maxmemory-reserved": "50", + "maxfragmentationmemory-reserved": "50", + "maxmemory-policy": "volatile-lru", + "maxmemory-delta": "50" + } + } + } + ] +} \ No newline at end of file diff --git a/deploy/redis/redisdeploy.parameters.json b/deploy/redis/redisdeploy.parameters.json new file mode 100644 index 0000000..da66878 --- /dev/null +++ b/deploy/redis/redisdeploy.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "namespaceprefix": { + "value": "duber-redis" + } + } +} \ No newline at end of file diff --git a/deploy/servicebus/sbusdeploy.json b/deploy/servicebus/sbusdeploy.json new file mode 100644 index 0000000..d4e4baf --- /dev/null +++ b/deploy/servicebus/sbusdeploy.json @@ -0,0 +1,166 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "namespaceprefix": { + "type": "string", + "metadata": { + "description": "Name of the Service Bus namespace" + } + } + }, + "variables": { + "serviceBusTopicName": "duber_event_bus", + "TripSubscriptionName": "Trip", + "InvoiceSubscriptionName": "Invoice", + "WebSiteSubscriptionName": "WebSite", + "NotificationsSubscriptionName": "Notifications", + "location": "[resourceGroup().location]", + "sbVersion": "2015-08-01", + "defaultSASKeyName": "Root", + "namespace":"[parameters('namespaceprefix')]", + "authRuleResourceId": "[resourceId('Microsoft.ServiceBus/namespaces/topics/authorizationRules', variables('namespace'), variables('serviceBusTopicName'), variables('defaultSASKeyName'))]" + }, + "resources": [ + { + "apiVersion": "[variables('sbVersion')]", + "name": "[variables('namespace')]", + "type": "Microsoft.ServiceBus/Namespaces", + "location": "[variables('location')]", + "sku": { + "name": "Standard", + "tier": "Standard" + }, + "resources": [ + { + "apiVersion": "[variables('sbVersion')]", + "name": "[variables('serviceBusTopicName')]", + "type": "Topics", + "dependsOn": [ + "[concat('Microsoft.ServiceBus/namespaces/', variables('namespace'))]" + ], + "properties": { + "path": "[variables('serviceBusTopicName')]", + "defaultMessageTimeToLive": "14.00:00:00", + "maxSizeInMegabytes": 1024, + "requiresDuplicateDetection": false, + "enableBatchedOperations": true, + "sizeInBytes": 0, + "filteringMessagesBeforePublishing": false, + "isAnonymousAccessible": false, + "status": "Active", + "supportOrdering": false, + "autoDeleteOnIdle": "10675199.02:48:05.4775807", + "enablePartitioning": true, + "isExpress": false, + "enableSubscriptionPartitioning": false, + "enableExpress": false + }, + "resources": [ + { + "type": "AuthorizationRules", + "name": "[variables('defaultSASKeyName')]", + "apiVersion": "[variables('sbVersion')]", + "properties": { + "rights": [ + "Manage", + "Send", + "Listen" + ] + }, + "dependsOn": [ + "[variables('serviceBusTopicName')]" + ] + }, + { + "apiVersion": "[variables('sbVersion')]", + "name": "[variables('TripSubscriptionName')]", + "type": "Subscriptions", + "dependsOn": [ + "[variables('serviceBusTopicName')]" + ], + "properties": { + "lockDuration": "00:00:30", + "requiresSession": false, + "defaultMessageTimeToLive": "14.00:00:00", + "deadLetteringOnMessageExpiration": true, + "deadLetteringOnFilterEvaluationExceptions": true, + "maxDeliveryCount": 10, + "enableBatchedOperations": false, + "status": "Active", + "autoDeleteOnIdle": "10675199.02:48:05.4775807", + "entityAvailabilityStatus": "Available" + } + }, + { + "apiVersion": "[variables('sbVersion')]", + "name": "[variables('InvoiceSubscriptionName')]", + "type": "Subscriptions", + "dependsOn": [ + "[variables('serviceBusTopicName')]" + ], + "properties": { + "lockDuration": "00:00:30", + "requiresSession": false, + "defaultMessageTimeToLive": "14.00:00:00", + "deadLetteringOnMessageExpiration": true, + "deadLetteringOnFilterEvaluationExceptions": true, + "maxDeliveryCount": 10, + "enableBatchedOperations": false, + "status": "Active", + "autoDeleteOnIdle": "10675199.02:48:05.4775807", + "entityAvailabilityStatus": "Available" + } + }, + { + "apiVersion": "[variables('sbVersion')]", + "name": "[variables('WebSiteSubscriptionName')]", + "type": "Subscriptions", + "dependsOn": [ + "[variables('serviceBusTopicName')]" + ], + "properties": { + "lockDuration": "00:00:30", + "requiresSession": false, + "defaultMessageTimeToLive": "14.00:00:00", + "deadLetteringOnMessageExpiration": true, + "deadLetteringOnFilterEvaluationExceptions": true, + "maxDeliveryCount": 10, + "enableBatchedOperations": false, + "status": "Active", + "autoDeleteOnIdle": "10675199.02:48:05.4775807", + "entityAvailabilityStatus": "Available" + } + }, + { + "apiVersion": "[variables('sbVersion')]", + "name": "[variables('NotificationsSubscriptionName')]", + "type": "Subscriptions", + "dependsOn": [ + "[variables('serviceBusTopicName')]" + ], + "properties": { + "lockDuration": "00:00:30", + "requiresSession": false, + "defaultMessageTimeToLive": "14.00:00:00", + "deadLetteringOnMessageExpiration": true, + "deadLetteringOnFilterEvaluationExceptions": true, + "maxDeliveryCount": 10, + "enableBatchedOperations": false, + "status": "Active", + "autoDeleteOnIdle": "10675199.02:48:05.4775807", + "entityAvailabilityStatus": "Available" + } + } + ] + } + ] + } + ], + "outputs": { + "NamespaceConnectionString": { + "type": "string", + "value": "[listkeys(variables('authRuleResourceId'), variables('sbVersion')).primaryConnectionString]" + } + } +} \ No newline at end of file diff --git a/deploy/servicebus/sbusdeploy.parameters.json b/deploy/servicebus/sbusdeploy.parameters.json new file mode 100644 index 0000000..9e1a3c4 --- /dev/null +++ b/deploy/servicebus/sbusdeploy.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "namespaceprefix": { + "value": "dubersb" + } + } +} diff --git a/deploy/servicefabric/LinuxContainers/gen-keyvaultcert.ps1 b/deploy/servicefabric/LinuxContainers/gen-keyvaultcert.ps1 new file mode 100644 index 0000000..c6fc340 --- /dev/null +++ b/deploy/servicefabric/LinuxContainers/gen-keyvaultcert.ps1 @@ -0,0 +1,53 @@ +Param( + [parameter(Mandatory=$true)][string]$vaultName, + [parameter(Mandatory=$true)][string]$certName, + [parameter(Mandatory=$true)][string]$certPwd, + [parameter(Mandatory=$true)][string]$subjectName, + [parameter(Mandatory=$false)][string]$ValidityInMonths=12, + [parameter(Mandatory=$true)][string]$saveDir +) + + +#Log in Azure Account +Login-AzureRmAccount + + +# Create Cert in KeyVault +Write-Host "Creating certificate in Azure KeyVault..." -ForegroundColor Yellow +$policy = New-AzureKeyVaultCertificatePolicy -SubjectName $subjectName -IssuerName Self -ValidityInMonths $ValidityInMonths +Add-AzureKeyVaultCertificate -VaultName $vaultName -Name $certName -CertificatePolicy $policy + +# Downloading Certificate +Write-Host "Downloading Certificate from KeyVault..." -ForegroundColor Yellow + +$Stoploop = $false +$Retrycount = 0 + +do { + try { + + $kvSecret = Get-AzureKeyVaultSecret -VaultName $vaultName -Name $certName -ErrorAction SilentlyContinue + $kvSecretBytes = [System.Convert]::FromBase64String($kvSecret.SecretValueText) + $certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection + $certCollection.Import($kvSecretBytes,$null,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) + $protectedCertificateBytes = $certCollection.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12, $certPwd) + [System.IO.File]::WriteAllBytes($saveDir + "\" + $certName + ".pfx", $protectedCertificateBytes) + + $Stoploop = $true + Write-Host "Finished!" -ForegroundColor Yellow + } + catch { + if ($Retrycount -gt 5){ + $Stoploop = $true + Write-Host "Not possible to retrieve the certificate!" -ForegroundColor Yellow + } + else { + Start-Sleep -Seconds 20 + $Retrycount = $Retrycount + 1 + } + } +} +While ($Stoploop -eq $false) + +# Show Certificate Values +Get-AzureKeyVaultCertificate -VaultName $vaultName -Name $certName \ No newline at end of file diff --git a/deploy/servicefabric/LinuxContainers/servicefabricdeploy.json b/deploy/servicefabric/LinuxContainers/servicefabricdeploy.json new file mode 100644 index 0000000..735b8df --- /dev/null +++ b/deploy/servicefabric/LinuxContainers/servicefabricdeploy.json @@ -0,0 +1,611 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "clusterLocation": { + "type": "string", + "metadata": { + "description": "Location of the Cluster" + } + }, + "clusterName": { + "type": "string", + "defaultValue": "Cluster", + "metadata": { + "description": "Name of your cluster - Between 3 and 23 characters. Letters and numbers only" + } + }, + "nt0applicationStartPort": { + "type": "int", + "defaultValue": 20000 + }, + "nt0applicationEndPort": { + "type": "int", + "defaultValue": 30000 + }, + "nt0ephemeralStartPort": { + "type": "int", + "defaultValue": 49152 + }, + "nt0ephemeralEndPort": { + "type": "int", + "defaultValue": 65534 + }, + "nt0fabricTcpGatewayPort": { + "type": "int", + "defaultValue": 19000 + }, + "nt0fabricHttpGatewayPort": { + "type": "int", + "defaultValue": 19080 + }, + "tripApiHttpRule": { + "type": "int", + "defaultValue": 5100 + }, + "invoiceApiHttpRule": { + "type": "int", + "defaultValue": 5104 + }, + "subnet0Name": { + "type": "string", + "defaultValue": "Subnet-0" + }, + "subnet0Prefix": { + "type": "string", + "defaultValue": "10.0.0.0/24" + }, + "computeLocation": { + "type": "string" + }, + "publicIPAddressName": { + "type": "string", + "defaultValue": "PublicIP-VM" + }, + "publicIPAddressType": { + "type": "string", + "allowedValues": [ + "Dynamic" + ], + "defaultValue": "Dynamic" + }, + "vmStorageAccountContainerName": { + "type": "string", + "defaultValue": "vhds" + }, + "adminUserName": { + "type": "string", + "defaultValue": "testadm", + "metadata": { + "description": "Remote desktop user Id" + } + }, + "adminPassword": { + "type": "securestring", + "metadata": { + "description": "Remote desktop user password. Must be a strong password" + } + }, + "virtualNetworkName": { + "type": "string", + "defaultValue": "VNet" + }, + "addressPrefix": { + "type": "string", + "defaultValue": "10.0.0.0/16" + }, + "dnsName": { + "type": "string" + }, + "nicName": { + "type": "string", + "defaultValue": "NIC" + }, + "lbName": { + "type": "string", + "defaultValue": "LoadBalancer" + }, + "lbIPName": { + "type": "string", + "defaultValue": "PublicIP-LB-FE" + }, + "overProvision": { + "type": "string", + "defaultValue": "false" + }, + "vmImagePublisher": { + "type": "string", + "defaultValue": "Microsoft.Azure.ServiceFabric" + }, + "vmImageOffer": { + "type": "string", + "defaultValue": "UbuntuServer" + }, + "vmImageSku": { + "type": "string", + "defaultValue": "16.04" + }, + "vmImageVersion": { + "type": "string", + "defaultValue": "6.0.12" + }, + "storageAccountType": { + "type": "string", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS" + ], + "defaultValue": "Standard_LRS", + "metadata": { + "description": "Replication option for the VM image storage account" + } + }, + "supportLogStorageAccountType": { + "type": "string", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS" + ], + "defaultValue": "Standard_LRS", + "metadata": { + "description": "Replication option for the support log storage account" + } + }, + "supportLogStorageAccountName": { + "type": "string", + "defaultValue": "[toLower( concat('sflogs', uniqueString(resourceGroup().id),'2'))]", + "metadata": { + "description": "Name for the storage account that contains support logs from the cluster" + } + }, + "applicationDiagnosticsStorageAccountType": { + "type": "string", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS" + ], + "defaultValue": "Standard_LRS", + "metadata": { + "description": "Replication option for the application diagnostics storage account" + } + }, + "applicationDiagnosticsStorageAccountName": { + "type": "string", + "defaultValue": "[toLower(concat(uniqueString(resourceGroup().id), '3' ))]", + "metadata": { + "description": "Name for the storage account that contains application diagnostics data from the cluster" + } + }, + "nt0InstanceCount": { + "type": "int", + "defaultValue": 5, + "metadata": { + "description": "Instance count for node type" + } + }, + "vmNodeType0Name": { + "type": "string", + "defaultValue": "primary", + "maxLength": 9 + }, + "vmNodeType0Size": { + "type": "string", + "defaultValue": "Standard_D1_v2" + } + }, + "variables": { + "vmssApiVersion": "2017-03-30", + "lbApiVersion": "2015-06-15", + "vNetApiVersion": "2015-06-15", + "storageApiVersion": "2016-01-01", + "publicIPApiVersion": "2015-06-15", + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',parameters('virtualNetworkName'))]", + "subnet0Ref": "[concat(variables('vnetID'),'/subnets/',parameters('subnet0Name'))]", + "wadlogs": "", + "wadperfcounters1": "", + "wadperfcounters2": "", + "wadcfgxstart": "[concat(variables('wadlogs'),variables('wadperfcounters1'),variables('wadperfcounters2'),'')]", + "lbID0": "[resourceId('Microsoft.Network/loadBalancers', concat('LB','-', parameters('clusterName'),'-',parameters('vmNodeType0Name')))]", + "lbIPConfig0": "[concat(variables('lbID0'),'/frontendIPConfigurations/LoadBalancerIPConfig')]", + "lbPoolID0": "[concat(variables('lbID0'),'/backendAddressPools/LoadBalancerBEAddressPool')]", + "lbProbeID0": "[concat(variables('lbID0'),'/probes/FabricGatewayProbe')]", + "lbHttpProbeID0": "[concat(variables('lbID0'),'/probes/FabricHttpGatewayProbe')]", + "lbNatPoolID0": "[concat(variables('lbID0'),'/inboundNatPools/LoadBalancerBEAddressNatPool')]", + "vmStorageAccountName0": "[toLower(concat(uniqueString(resourceGroup().id), '1', '0' ))]", + "wadmetricsresourceid0": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/',resourceGroup().name ,'/providers/','Microsoft.Compute/virtualMachineScaleSets/', parameters('vmNodeType0Name'))]" + }, + "resources": [ + { + "apiVersion": "[variables('storageApiVersion')]", + "type": "Microsoft.Storage/storageAccounts", + "name": "[parameters('supportLogStorageAccountName')]", + "location": "[parameters('computeLocation')]", + "dependsOn": [], + "properties": {}, + "kind": "Storage", + "sku": { + "name": "[parameters('supportLogStorageAccountType')]" + }, + "tags": { + "resourceType": "Service Fabric", + "clusterName": "[parameters('clusterName')]" + } + }, + { + "apiVersion": "[variables('storageApiVersion')]", + "type": "Microsoft.Storage/storageAccounts", + "name": "[parameters('applicationDiagnosticsStorageAccountName')]", + "location": "[parameters('computeLocation')]", + "dependsOn": [], + "properties": {}, + "kind": "Storage", + "sku": { + "name": "[parameters('applicationDiagnosticsStorageAccountType')]" + }, + "tags": { + "resourceType": "Service Fabric", + "clusterName": "[parameters('clusterName')]" + } + }, + { + "apiVersion": "[variables('vNetApiVersion')]", + "type": "Microsoft.Network/virtualNetworks", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('computeLocation')]", + "dependsOn": [], + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('addressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[parameters('subnet0Name')]", + "properties": { + "addressPrefix": "[parameters('subnet0Prefix')]" + } + } + ] + }, + "tags": { + "resourceType": "Service Fabric", + "clusterName": "[parameters('clusterName')]" + } + }, + { + "apiVersion": "[variables('publicIPApiVersion')]", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[concat(parameters('lbIPName'),'-','0')]", + "location": "[parameters('computeLocation')]", + "properties": { + "dnsSettings": { + "domainNameLabel": "[parameters('dnsName')]" + }, + "publicIPAllocationMethod": "Dynamic" + }, + "tags": { + "resourceType": "Service Fabric", + "clusterName": "[parameters('clusterName')]" + } + }, + { + "apiVersion": "[variables('lbApiVersion')]", + "type": "Microsoft.Network/loadBalancers", + "name": "[concat('LB','-', parameters('clusterName'),'-',parameters('vmNodeType0Name'))]", + "location": "[parameters('computeLocation')]", + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/',concat(parameters('lbIPName'),'-','0'))]" + ], + "properties": { + "frontendIPConfigurations": [ + { + "name": "LoadBalancerIPConfig", + "properties": { + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses',concat(parameters('lbIPName'),'-','0'))]" + } + } + } + ], + "backendAddressPools": [ + { + "name": "LoadBalancerBEAddressPool", + "properties": {} + } + ], + "loadBalancingRules": [ + { + "name": "LBRule", + "properties": { + "backendAddressPool": { + "id": "[variables('lbPoolID0')]" + }, + "backendPort": "[parameters('nt0fabricTcpGatewayPort')]", + "enableFloatingIP": "false", + "frontendIPConfiguration": { + "id": "[variables('lbIPConfig0')]" + }, + "frontendPort": "[parameters('nt0fabricTcpGatewayPort')]", + "idleTimeoutInMinutes": "5", + "probe": { + "id": "[variables('lbProbeID0')]" + }, + "protocol": "tcp" + } + }, + { + "name": "LBHttpRule", + "properties": { + "backendAddressPool": { + "id": "[variables('lbPoolID0')]" + }, + "backendPort": "[parameters('nt0fabricHttpGatewayPort')]", + "enableFloatingIP": "false", + "frontendIPConfiguration": { + "id": "[variables('lbIPConfig0')]" + }, + "frontendPort": "[parameters('nt0fabricHttpGatewayPort')]", + "idleTimeoutInMinutes": "5", + "probe": { + "id": "[variables('lbHttpProbeID0')]" + }, + "protocol": "tcp" + } + }, + { + "name": "TripApiHttpRule", + "properties": { + "backendAddressPool": { + "id": "[variables('lbPoolID0')]" + }, + "backendPort": "[parameters('tripApiHttpRule')]", + "enableFloatingIP": "false", + "frontendIPConfiguration": { + "id": "[variables('lbIPConfig0')]" + }, + "frontendPort": "[parameters('tripApiHttpRule')]", + "idleTimeoutInMinutes": "5", + "protocol": "tcp" + } + }, + { + "name": "InvoiceApiHttpRule", + "properties": { + "backendAddressPool": { + "id": "[variables('lbPoolID0')]" + }, + "backendPort": "[parameters('invoiceApiHttpRule')]", + "enableFloatingIP": "false", + "frontendIPConfiguration": { + "id": "[variables('lbIPConfig0')]" + }, + "frontendPort": "[parameters('invoiceApiHttpRule')]", + "idleTimeoutInMinutes": "5", + "protocol": "tcp" + } + } + ], + "probes": [ + { + "name": "FabricGatewayProbe", + "properties": { + "intervalInSeconds": 5, + "numberOfProbes": 2, + "port": "[parameters('nt0fabricTcpGatewayPort')]", + "protocol": "tcp" + } + }, + { + "name": "FabricHttpGatewayProbe", + "properties": { + "intervalInSeconds": 5, + "numberOfProbes": 2, + "port": "[parameters('nt0fabricHttpGatewayPort')]", + "protocol": "tcp" + } + } + ], + "inboundNatPools": [ + { + "name": "LoadBalancerBEAddressNatPool", + "properties": { + "backendPort": "22", + "frontendIPConfiguration": { + "id": "[variables('lbIPConfig0')]" + }, + "frontendPortRangeEnd": "4500", + "frontendPortRangeStart": "3389", + "protocol": "tcp" + } + } + ] + }, + "tags": { + "resourceType": "Service Fabric", + "clusterName": "[parameters('clusterName')]" + } + }, + { + "apiVersion": "[variables('vmssApiVersion')]", + "type": "Microsoft.Compute/virtualMachineScaleSets", + "name": "[parameters('vmNodeType0Name')]", + "location": "[parameters('computeLocation')]", + "dependsOn": [ + "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]", + "[concat('Microsoft.Network/loadBalancers/', concat('LB','-', parameters('clusterName'),'-',parameters('vmNodeType0Name')))]", + "[concat('Microsoft.Storage/storageAccounts/', parameters('supportLogStorageAccountName'))]", + "[concat('Microsoft.Storage/storageAccounts/', parameters('applicationDiagnosticsStorageAccountName'))]" + ], + "properties": { + "overprovision": "[parameters('overProvision')]", + "upgradePolicy": { + "mode": "Automatic" + }, + "virtualMachineProfile": { + "extensionProfile": { + "extensions": [ + { + "name": "[concat(parameters('vmNodeType0Name'),'_ServiceFabricLinuxNode')]", + "properties": { + "type": "ServiceFabricLinuxNode", + "autoUpgradeMinorVersion": true, + "protectedSettings": { + "StorageAccountKey1": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('supportLogStorageAccountName')),'2015-05-01-preview').key1]", + "StorageAccountKey2": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('supportLogStorageAccountName')),'2015-05-01-preview').key2]" + }, + "publisher": "Microsoft.Azure.ServiceFabric", + "settings": { + "clusterEndpoint": "[reference(parameters('clusterName')).clusterEndpoint]", + "nodeTypeRef": "[parameters('vmNodeType0Name')]", + "durabilityLevel": "Bronze", + "enableParallelJobs": true, + "nicPrefixOverride": "[parameters('subnet0Prefix')]" + }, + "typeHandlerVersion": "1.0" + } + }, + { + "name": "[concat('VMDiagnosticsVmExt','_vmNodeType0Name')]", + "properties": { + "type": "LinuxDiagnostic", + "autoUpgradeMinorVersion": true, + "protectedSettings": { + "storageAccountName": "[parameters('applicationDiagnosticsStorageAccountName')]", + "storageAccountKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('applicationDiagnosticsStorageAccountName')),'2015-05-01-preview').key1]", + "storageAccountEndPoint": "https://core.windows.net/" + }, + "publisher": "Microsoft.OSTCExtensions", + "settings": { + "xmlCfg": "[base64(concat(variables('wadcfgxstart'),variables('wadmetricsresourceid0'),variables('wadcfgxend')))]", + "StorageAccount": "[parameters('applicationDiagnosticsStorageAccountName')]" + }, + "typeHandlerVersion": "2.3" + } + } + ] + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "[concat(parameters('nicName'), '-0')]", + "properties": { + "ipConfigurations": [ + { + "name": "[concat(parameters('nicName'),'-',0)]", + "properties": { + "loadBalancerBackendAddressPools": [ + { + "id": "[variables('lbPoolID0')]" + } + ], + "loadBalancerInboundNatPools": [ + { + "id": "[variables('lbNatPoolID0')]" + } + ], + "subnet": { + "id": "[variables('subnet0Ref')]" + } + } + } + ], + "primary": true + } + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('adminPassword')]", + "adminUsername": "[parameters('adminUsername')]", + "computernamePrefix": "[parameters('vmNodeType0Name')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "[parameters('vmImagePublisher')]", + "offer": "[parameters('vmImageOffer')]", + "sku": "[parameters('vmImageSku')]", + "version": "[parameters('vmImageVersion')]" + }, + "osDisk": { + "caching": "ReadOnly", + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[parameters('storageAccountType')]" + } + } + } + } + }, + "sku": { + "name": "[parameters('vmNodeType0Size')]", + "capacity": "[parameters('nt0InstanceCount')]", + "tier": "Standard" + }, + "tags": { + "resourceType": "Service Fabric", + "clusterName": "[parameters('clusterName')]" + } + }, + { + "apiVersion": "2017-07-01-preview", + "type": "Microsoft.ServiceFabric/clusters", + "name": "[parameters('clusterName')]", + "location": "[parameters('clusterLocation')]", + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', parameters('supportLogStorageAccountName'))]" + ], + "properties": { + "addonFeatures": [ + "DnsService" + ], + "clientCertificateCommonNames": [], + "clientCertificateThumbprints": [], + "clusterCodeVersion": "6.3.119.1", + "clusterState": "Default", + "diagnosticsStorageAccountConfig": { + "blobEndpoint": "[reference(concat('Microsoft.Storage/storageAccounts/', parameters('supportLogStorageAccountName')), variables('storageApiVersion')).primaryEndpoints.blob]", + "protectedAccountKeyName": "StorageAccountKey1", + "queueEndpoint": "[reference(concat('Microsoft.Storage/storageAccounts/', parameters('supportLogStorageAccountName')), variables('storageApiVersion')).primaryEndpoints.queue]", + "storageAccountName": "[parameters('supportLogStorageAccountName')]", + "tableEndpoint": "[reference(concat('Microsoft.Storage/storageAccounts/', parameters('supportLogStorageAccountName')), variables('storageApiVersion')).primaryEndpoints.table]" + }, + "fabricSettings": [], + "managementEndpoint": "[concat('http://',reference(concat(parameters('lbIPName'),'-','0')).dnsSettings.fqdn,':',parameters('nt0fabricHttpGatewayPort'))]", + "nodeTypes": [ + { + "name": "[parameters('vmNodeType0Name')]", + "applicationPorts": { + "endPort": "[parameters('nt0applicationEndPort')]", + "startPort": "[parameters('nt0applicationStartPort')]" + }, + "clientConnectionEndpointPort": "[parameters('nt0fabricTcpGatewayPort')]", + "durabilityLevel": "Bronze", + "ephemeralPorts": { + "endPort": "[parameters('nt0ephemeralEndPort')]", + "startPort": "[parameters('nt0ephemeralStartPort')]" + }, + "httpGatewayEndpointPort": "[parameters('nt0fabricHttpGatewayPort')]", + "isPrimary": true, + "vmInstanceCount": "[parameters('nt0InstanceCount')]" + } + ], + "provisioningState": "Default", + "reliabilityLevel": "Silver", + "upgradeMode": "Manual", + "vmImage": "Linux" + }, + "tags": { + "resourceType": "Service Fabric", + "clusterName": "[parameters('clusterName')]" + } + } + ], + "outputs": { + "clusterProperties": { + "value": "[reference(parameters('clusterName'))]", + "type": "object" + } + } +} \ No newline at end of file diff --git a/deploy/servicefabric/LinuxContainers/servicefabricdeploy.parameters.json b/deploy/servicefabric/LinuxContainers/servicefabricdeploy.parameters.json new file mode 100644 index 0000000..a17bdb6 --- /dev/null +++ b/deploy/servicefabric/LinuxContainers/servicefabricdeploy.parameters.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "clusterName": { + "value": "prod-duber-sflinux-cluster" + }, + "clusterLocation": { + "value": "eastus" + }, + "computeLocation": { + "value": "eastus" + }, + "adminUserName": { + "value": "duber" + }, + "adminPassword": { + "value": "lahostiaestepass123*" + }, + "nicName": { + "value": "NIC-duberonsflin" + }, + "publicIPAddressName": { + "value": "duberonsflin-PubIP" + }, + "dnsName": { + "value": "prod-duber-sflinux-cluster" + }, + "virtualNetworkName": { + "value": "VNet-duberonsflin" + }, + "lbName": { + "value": "LB-duberonsflin" + }, + "lbIPName": { + "value": "LBIP-duberonsflin" + }, + "vmImageSku": { + "value": "16.04-LTS" + }, + "vmImageVersion": { + "value": "latest" + }, + "vmImagePublisher": { + "value": "Canonical" + }, + "nt0ephemeralStartPort": { + "value": 49152 + }, + "nt0ephemeralEndPort": { + "value": 65534 + }, + "nt0applicationStartPort": { + "value": 20000 + }, + "nt0applicationEndPort": { + "value": 30000 + }, + "nt0fabricTcpGatewayPort": { + "value": 19000 + }, + "nt0fabricHttpGatewayPort": { + "value": 19080 + }, + "tripApiHttpRule": { + "value": 5103 + }, + "invoiceApiHttpRule": { + "value": 5101 + } + } +} \ No newline at end of file diff --git a/deploy/sql/sqldeploy.json b/deploy/sql/sqldeploy.json new file mode 100644 index 0000000..51b7701 --- /dev/null +++ b/deploy/sql/sqldeploy.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "sql_server": { + "type": "object" + }, + "admin": { + "type": "string" + }, + "adminpwd": { + "type": "string" + } + }, + "variables": { + "sql_server_name": "[parameters('sql_server').name]", + "admin": "[parameters('admin')]", + "adminpwd": "[parameters('adminpwd')]" + }, + "resources": [ + { + "type": "Microsoft.Sql/servers", + "name": "[variables('sql_server_name')]", + "apiVersion": "2014-04-01-preview", + "location": "[resourceGroup().location]", + "properties": { + "administratorLogin": "[variables('admin')]", + "administratorLoginPassword": "[variables('adminpwd')]", + "version": "12.0" + }, + "resources": [ + { + "type": "databases", + "name": "[parameters('sql_server').dbs.invoicedb]", + "apiVersion": "2014-04-01-preview", + "location": "[resourceGroup().location]", + "properties": { + "edition": "Standard", + "collation": "SQL_Latin1_General_CP1_CI_AS", + "maxSizeBytes": "1073741824", + "requestedServiceObjectiveName": "S1" + }, + "dependsOn": [ + "[concat('Microsoft.Sql/servers/', variables('sql_server_name'))]" + ] + }, + { + "type": "databases", + "name": "[parameters('sql_server').dbs.websitedb]", + "apiVersion": "2014-04-01-preview", + "location": "[resourceGroup().location]", + "properties": { + "edition": "Standard", + "collation": "SQL_Latin1_General_CP1_CI_AS", + "maxSizeBytes": "1073741824", + "requestedServiceObjectiveName": "S1" + }, + "dependsOn": [ + "[concat('Microsoft.Sql/servers/', variables('sql_server_name'))]" + ] + }, + { + "type": "firewallrules", + "name": "AllowAllWindowsAzureIps", + "apiVersion": "2014-04-01-preview", + "location": "[resourceGroup().location]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "0.0.0.0" + }, + "dependsOn": [ + "[concat('Microsoft.Sql/servers/', variables('sql_server_name'))]" + ] + }, + { + "type": "firewallrules", + "name": "AllConnectionsAllowed", + "apiVersion": "2014-04-01-preview", + "location": "[resourceGroup().location]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "255.255.255.255" + }, + "dependsOn": [ + "[concat('Microsoft.Sql/servers/', variables('sql_server_name'))]" + ] + } + ] + } + ], + "outputs": { + } +} diff --git a/deploy/sql/sqldeploy.parameters.json b/deploy/sql/sqldeploy.parameters.json new file mode 100644 index 0000000..544f644 --- /dev/null +++ b/deploy/sql/sqldeploy.parameters.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "sql_server": { + "value": { + "name": "dubersql", + "dbs": { + "invoicedb": "Duber.InvoiceDb", + "websitedb": "Duber.WebSiteDb" + } + } + }, + "admin": { + "value": "duber_user" + }, + "adminpwd": { + "value": "lahostiaestepass123*" + } + } +} \ No newline at end of file diff --git a/docker-compose.dcproj b/docker-compose.dcproj new file mode 100644 index 0000000..410f606 --- /dev/null +++ b/docker-compose.dcproj @@ -0,0 +1,18 @@ + + + + 2.1 + Linux + 3288a978-1584-47b8-b892-d05e7f6d1a23 + LaunchBrowser + http://localhost:32774 + duber.website + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..5b0c186 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,73 @@ +version: '3.4' + +services: + duber.invoice.api: + environment: + - ASPNETCORE_ENVIRONMENT=${APP_ENVIRONMENT:-Development} + - ConnectionStrings__InvoiceDB=${AZURE_INVOICE_DB:-Server=sql.data;Database=Duber.InvoiceDb;User Id=sa;Password=Pass@word} + - EventBusConnection=${AZURE_SERVICE_BUS:-rabbitmq} + - EventBusConnectionHC=${AZURE_SERVICE_BUS:-rabbitmq} + - PaymentServiceBaseUrl=${PAYMENT_SERVICE_URL:-http://externalsystem.payment} + - AzureServiceBusEnabled=${SERVICE_BUS_ENABLED:-False} + ports: + - "32776:80" + + duber.trip.api: + environment: + - ASPNETCORE_ENVIRONMENT=${APP_ENVIRONMENT:-Development} + - EventStoreConfiguration__ConnectionString=${AZURE_TRIP_DB:-mongodb://nosql.data} + - EventBusConnection=${AZURE_SERVICE_BUS:-rabbitmq} + - EventBusConnectionHC=${AZURE_SERVICE_BUS:-rabbitmq} + - AzureServiceBusEnabled=${SERVICE_BUS_ENABLED:-False} + ports: + - "32775:80" + + duber.website: + environment: + - ASPNETCORE_ENVIRONMENT=${APP_ENVIRONMENT:-Development} + - ConnectionStrings__WebsiteDB=${AZURE_WEBSITE_DB:-Server=sql.data;Database=Duber.WebSiteDb;User Id=sa;Password=Pass@word} + - EventBusConnection=${AZURE_SERVICE_BUS:-rabbitmq} + - EventBusConnectionHC=${AZURE_SERVICE_BUS:-rabbitmq} + - InvoiceApiSettings__BaseUrl=${INVOICE_SERVICE_BASE_URL:-http://duber.invoice.api} + - TripApiSettings__BaseUrl=${TRIP_SERVICE_BASE_URL:-http://duber.trip.api} + - TripApiSettings__NotificationsServerUrl=${NOTIFICATIONS_SERVER_URL:-http://duber.trip.notifications} + - TripApiSettings__NotificationsClientUrl=${NOTIFICATIONS_CLIENT_URL:-http://docker.for.win.localhost:32778} #docker.for.mac.localhost / docker.for.linux.localhost (you could use the .env file to set this in a common variable, then use it here, i.e: LOCAL_DOMAIN=http://docker.for.win.localhost) + - AzureServiceBusEnabled=${SERVICE_BUS_ENABLED:-False} + - HealthChecksUI__HealthChecks__0__Name=Invoice HTTP Check + - HealthChecksUI__HealthChecks__0__Uri=http://duber.invoice.api/readiness + - HealthChecksUI__HealthChecks__1__Name=Trip HTTP Check + - HealthChecksUI__HealthChecks__1__Uri=http://duber.trip.api/readiness + - HealthChecksUI__HealthChecks__2__Name=Notifications HTTP Check + - HealthChecksUI__HealthChecks__2__Uri=http://duber.trip.notifications/readiness + - HealthChecksUI__HealthChecks__3__Name=Frontend HTTP Check + - HealthChecksUI__HealthChecks__3__Uri=http://duber.website/readiness + ports: + - "32774:80" + + sql.data: + environment: + - MSSQL_SA_PASSWORD=Pass@word + - ACCEPT_EULA=Y + - MSSQL_PID=Developer + ports: + - "5433:1433" + + nosql.data: + ports: + - "27017:27017" + + externalsystem.payment: + environment: + - ASPNETCORE_ENVIRONMENT=${APP_ENVIRONMENT:-Development} + ports: + - "32777:80" + + duber.trip.notifications: + environment: + - ASPNETCORE_ENVIRONMENT=${APP_ENVIRONMENT:-Development} + - ConnectionStrings__SignalrBackPlane=${REDIS_DB:-} + - EventBusConnection=${AZURE_SERVICE_BUS:-rabbitmq} + - EventBusConnectionHC=${AZURE_SERVICE_BUS:-rabbitmq} + - AzureServiceBusEnabled=${SERVICE_BUS_ENABLED:-False} + ports: + - "32778:80" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..15ede5b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.4' + +services: + duber.invoice.api: + image: duber/invoice.api:${TAG:-latest} + build: + context: . + dockerfile: src/Application/Duber.Invoice.API/Dockerfile + depends_on: + - sql.data + - rabbitmq + + duber.trip.api: + image: duber/trip.api:${TAG:-latest} + build: + context: . + dockerfile: src/Application/Duber.Trip.API/Dockerfile + depends_on: + - nosql.data + - rabbitmq + + duber.website: + image: duber/website:${TAG:-latest} + build: + context: . + dockerfile: src/Web/Duber.WebSite/Dockerfile + depends_on: + - duber.invoice.api + - duber.trip.api + - sql.data + - rabbitmq + + sql.data: + image: microsoft/mssql-server-linux:2017-latest + + nosql.data: + image: mongo + + rabbitmq: + image: rabbitmq:3-management + ports: + - "15672:15672" + - "5672:5672" + + externalsystem.payment: + image: externalsystem/paymentservice:${TAG:-latest} + build: + context: . + dockerfile: ExternalSystem/PaymentService/Dockerfile + + duber.trip.notifications: + image: duber/trip.notifications:${TAG:-latest} + build: + context: . + dockerfile: src/Application/Duber.Trip.Notifications/Dockerfile \ No newline at end of file diff --git a/k8s-architecture.md b/k8s-architecture.md new file mode 100644 index 0000000..bbdc798 --- /dev/null +++ b/k8s-architecture.md @@ -0,0 +1,7 @@ +# Kubernetes Cloud Native Architecture and Deployment + +## Architecture +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/Duber_Kubernetes_Cloud_Environment_Architecture.png) + +## Deployment +WIP diff --git a/local-deployment.md b/local-deployment.md new file mode 100644 index 0000000..2c9b156 --- /dev/null +++ b/local-deployment.md @@ -0,0 +1,28 @@ +# Local deployment using Kubernetes + +## Prerequisites and Installation Requirements + +1. Install Docker for [Windows](https://docs.docker.com/docker-for-windows/install/)/[Mac](https://docs.docker.com/docker-for-mac/install/). +2. Make sure to check the opton *Enable Kubernetes* on your Docker settings. +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/docker-desktop-k8s.png) +3. Run `deploy-local.ps1` script (located at `\deploy\k8s\local`) to deploy the solution on your local Kubernetes cluster. +4. Add `duber.local.com` and `trip.notifications.local.com` domains to your `hosts` file. Those are the hosts using by our Ingress in order to expose the Frontend and the SignalR Trip Notifications service respectively. +``` +127.0.0.1 duber.local.com +127.0.0.1 trip.notifications.local.com +``` +> Optionally, if you want to expose the API's you have to add `invoice.local.com` and `trip.local.com` domains too. +``` +127.0.0.1 invoice.local.com +127.0.0.1 trip.local.com +``` +6. Go to http://duber.local.com:81/ and you'll see the application up and working! + +## Admin tools +To be able to access to our SQL or Mongo databases through an IDE, or to RabbitMQ' dashboard, we exposed a port through a `Node Port`: +* SQL Server: `31433`, connection example: `tcp:127.0.0.1,31433` +* Mongo: `31434`, connection example: `mongodb://0.0.0.0:31434/` +* RabbitMQ: `31672`, connection example: `http://localhost:31672/#/` + +## Architecture +![](https://github.com/vany0114/vany0114.github.io/blob/master/images/Duber_Kubernetes_Local_Environment_Architecture.png) diff --git a/microservices-netcore-docker-servicefabric.sln b/microservices-netcore-docker-servicefabric.sln new file mode 100644 index 0000000..bcfed22 --- /dev/null +++ b/microservices-netcore-docker-servicefabric.sln @@ -0,0 +1,355 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{750E2FD5-5728-4F39-AA06-7FAB071FBE51}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceFabric", "ServiceFabric", "{F647C571-A1D5-4963-B28E-05C0743AEA14}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{EFC84E79-3FFD-433D-A4E8-C1967D12A2D4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{1BAAE8F7-B691-4FF5-BC66-2ADDE6C30746}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentation", "Presentation", "{6EF062A2-3CD0-40C9-B8AE-A4FFA536ED84}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Trip", "Trip", "{386D271D-8E5B-40F7-B017-EE38215FFAA7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Invoice", "Invoice", "{084F660C-5526-4370-A383-E6BBE3846F47}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{F8C00BB1-4812-4B16-9182-3269D88858CE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{69B890C5-C137-4967-8E15-26D07F312907}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{3288A978-1584-47B8-B892-D05E7F6D1A23}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.WebSite", "src\Web\Duber.WebSite\Duber.WebSite.csproj", "{E650D054-6BD7-433C-ACFE-2D87F439EFAA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Invoice", "Invoice", "{B8BC2A87-6265-4210-9AF1-61F923C306BC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Trip", "Trip", "{A03D49FF-2B8F-4AC0-8AAC-20CBCED92CBB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.Invoice", "src\Domain\Invoice\Duber.Domain.Invoice\Duber.Domain.Invoice.csproj", "{E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.Invoice.UnitTest", "src\Domain\Invoice\Duber.Domain.Invoice.UnitTest\Duber.Domain.Invoice.UnitTest.csproj", "{F2D6F089-6EFE-4018-9A03-395F1FF2E403}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.Trip", "src\Domain\Trip\Duber.Domain.Trip\Duber.Domain.Trip.csproj", "{9558B043-C3D1-497D-A5D0-B1280DDCA177}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.Trip.UnitTest", "src\Domain\Trip\Duber.Domain.Trip.UnitTest\Duber.Domain.Trip.UnitTest.csproj", "{4C56FEA9-D054-42E4-8228-7B3BED146414}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Invoice.API", "src\Application\Duber.Invoice.API\Duber.Invoice.API.csproj", "{5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Trip.API", "src\Application\Duber.Trip.API\Duber.Trip.API.csproj", "{7ABF9C75-F835-4431-A4D9-96FE74955C57}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "User", "User", "{DA20FB57-1551-4A94-8EA4-4997F6E3B803}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Driver", "Driver", "{8ABD2A67-2001-42D1-920E-7FE07158C3A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.Driver", "src\Domain\Driver\Duber.Domain.Driver\Duber.Domain.Driver.csproj", "{13C65287-4948-40B5-B464-0032BA72D3FD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.Driver.UnitTest", "src\Domain\Driver\Duber.Domain.Driver.UnitTest\Duber.Domain.Driver.UnitTest.csproj", "{E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.User.UnitTest", "src\Domain\User\Duber.Domain.User.UnitTest\Duber.Domain.User.UnitTest.csproj", "{22802D55-11B5-4FE2-8B9A-AF22FDA509BF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.ACL", "src\Domain\Duber.Domain.ACL\Duber.Domain.ACL.csproj", "{37A21CD6-47DE-4F7B-B603-36DDDBE6B818}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EventBus", "EventBus", "{FEDB83C2-C14A-4196-8973-2A5C68CE2CBA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure.EventBus", "src\Infrastructure\EventBus\Duber.Infrastructure.EventBus\Duber.Infrastructure.EventBus.csproj", "{28784EB0-08BB-400F-BC37-DCED3E18261E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.User", "src\Domain\User\Duber.Domain.User\Duber.Domain.User.csproj", "{8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure.EventBus.RabbitMQ", "src\Infrastructure\EventBus\Duber.Infrastructure.EventBus.RabbitMQ\Duber.Infrastructure.EventBus.RabbitMQ.csproj", "{0559C080-53D5-44C7-8657-4F987F0FF8B1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure.EventBus.ServiceBus", "src\Infrastructure\EventBus\Duber.Infrastructure.EventBus.ServiceBus\Duber.Infrastructure.EventBus.ServiceBus.csproj", "{D3A735EA-FD07-4CDE-A785-DA39280509DA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Domain.SharedKernel", "src\Domain\Duber.Domain.SharedKernel\Duber.Domain.SharedKernel.csproj", "{AD8286AF-8F03-46F5-9583-620F4DBA73E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure", "src\Infrastructure\Duber.Infrastructure\Duber.Infrastructure.csproj", "{92D41C6D-E785-474A-BF69-876F7A8E6912}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure.WebHost", "src\Infrastructure\Duber.Infrastructure.WebHost\Duber.Infrastructure.WebHost.csproj", "{E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExternalSystem", "ExternalSystem", "{D6C1178A-80FB-42CE-8E58-BAA7BA56244D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService", "ExternalSystem\PaymentService\PaymentService.csproj", "{8504D9B8-C4E8-4172-8DA3-CBD9971E3207}" +EndProject +Project("{A07B5EB6-E848-4116-A8D0-A826331D98C6}") = "DuberMicroservices", "ServiceFabric\Linux\DuberMicroservices\DuberMicroservices.sfproj", "{D8A868B6-7D03-4368-A52E-64F1E1B357DB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B0A3B2AF-77E2-4255-84AF-AAC4A459BF88}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure.Resilience.Abstractions", "src\Infrastructure\Duber.Infrastructure.Resilience.Abstractions\Duber.Infrastructure.Resilience.Abstractions.csproj", "{B97EE2A9-93A1-41EE-873D-244E6D654149}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure.Resilience.Http", "src\Infrastructure\Duber.Infrastructure.Resilience.Http\Duber.Infrastructure.Resilience.Http.csproj", "{FB2CE5CF-AF46-41E0-95CC-88D8134DC324}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Infrastructure.Resilience.Sql", "src\Infrastructure\Duber.Infrastructure.Resilience.Sql\Duber.Infrastructure.Resilience.Sql.csproj", "{C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duber.Trip.Notifications", "src\Application\Duber.Trip.Notifications\Duber.Trip.Notifications.csproj", "{8AAEEC31-B3CE-4583-9B42-1447B8694611}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duber.Infrastructure.EventBus.Idempotency", "src\Infrastructure\EventBus\Duber.Infrastructure.EventBus.Idempotency\Duber.Infrastructure.EventBus.Idempotency.csproj", "{426346B6-6631-40E7-9187-8721A0C23E0E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Debug|x64.ActiveCfg = Debug|Any CPU + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Debug|x64.Build.0 = Debug|Any CPU + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Release|Any CPU.Build.0 = Release|Any CPU + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Release|x64.ActiveCfg = Release|Any CPU + {3288A978-1584-47B8-B892-D05E7F6D1A23}.Release|x64.Build.0 = Release|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Debug|x64.ActiveCfg = Debug|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Debug|x64.Build.0 = Debug|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Release|Any CPU.Build.0 = Release|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Release|x64.ActiveCfg = Release|Any CPU + {E650D054-6BD7-433C-ACFE-2D87F439EFAA}.Release|x64.Build.0 = Release|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Debug|x64.Build.0 = Debug|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Release|Any CPU.Build.0 = Release|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Release|x64.ActiveCfg = Release|Any CPU + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0}.Release|x64.Build.0 = Release|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Debug|x64.Build.0 = Debug|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Release|Any CPU.Build.0 = Release|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Release|x64.ActiveCfg = Release|Any CPU + {F2D6F089-6EFE-4018-9A03-395F1FF2E403}.Release|x64.Build.0 = Release|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Debug|x64.ActiveCfg = Debug|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Debug|x64.Build.0 = Debug|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Release|Any CPU.Build.0 = Release|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Release|x64.ActiveCfg = Release|Any CPU + {9558B043-C3D1-497D-A5D0-B1280DDCA177}.Release|x64.Build.0 = Release|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Debug|x64.Build.0 = Debug|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Release|Any CPU.Build.0 = Release|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Release|x64.ActiveCfg = Release|Any CPU + {4C56FEA9-D054-42E4-8228-7B3BED146414}.Release|x64.Build.0 = Release|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Debug|x64.Build.0 = Debug|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Release|Any CPU.Build.0 = Release|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Release|x64.ActiveCfg = Release|Any CPU + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C}.Release|x64.Build.0 = Release|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Debug|x64.ActiveCfg = Debug|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Debug|x64.Build.0 = Debug|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Release|Any CPU.Build.0 = Release|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Release|x64.ActiveCfg = Release|Any CPU + {7ABF9C75-F835-4431-A4D9-96FE74955C57}.Release|x64.Build.0 = Release|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Debug|x64.Build.0 = Debug|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Release|Any CPU.Build.0 = Release|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Release|x64.ActiveCfg = Release|Any CPU + {13C65287-4948-40B5-B464-0032BA72D3FD}.Release|x64.Build.0 = Release|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Debug|x64.ActiveCfg = Debug|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Debug|x64.Build.0 = Debug|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Release|Any CPU.Build.0 = Release|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Release|x64.ActiveCfg = Release|Any CPU + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0}.Release|x64.Build.0 = Release|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Debug|x64.ActiveCfg = Debug|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Debug|x64.Build.0 = Debug|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Release|Any CPU.Build.0 = Release|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Release|x64.ActiveCfg = Release|Any CPU + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF}.Release|x64.Build.0 = Release|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Debug|x64.ActiveCfg = Debug|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Debug|x64.Build.0 = Debug|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Release|Any CPU.Build.0 = Release|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Release|x64.ActiveCfg = Release|Any CPU + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818}.Release|x64.Build.0 = Release|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Debug|x64.ActiveCfg = Debug|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Debug|x64.Build.0 = Debug|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Release|Any CPU.Build.0 = Release|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Release|x64.ActiveCfg = Release|Any CPU + {28784EB0-08BB-400F-BC37-DCED3E18261E}.Release|x64.Build.0 = Release|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Debug|x64.Build.0 = Debug|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Release|Any CPU.Build.0 = Release|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Release|x64.ActiveCfg = Release|Any CPU + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C}.Release|x64.Build.0 = Release|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Debug|x64.Build.0 = Debug|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Release|Any CPU.Build.0 = Release|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Release|x64.ActiveCfg = Release|Any CPU + {0559C080-53D5-44C7-8657-4F987F0FF8B1}.Release|x64.Build.0 = Release|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Debug|x64.Build.0 = Debug|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Release|Any CPU.Build.0 = Release|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Release|x64.ActiveCfg = Release|Any CPU + {D3A735EA-FD07-4CDE-A785-DA39280509DA}.Release|x64.Build.0 = Release|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Debug|x64.Build.0 = Debug|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Release|Any CPU.Build.0 = Release|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Release|x64.ActiveCfg = Release|Any CPU + {AD8286AF-8F03-46F5-9583-620F4DBA73E8}.Release|x64.Build.0 = Release|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Debug|x64.ActiveCfg = Debug|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Debug|x64.Build.0 = Debug|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Release|Any CPU.Build.0 = Release|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Release|x64.ActiveCfg = Release|Any CPU + {92D41C6D-E785-474A-BF69-876F7A8E6912}.Release|x64.Build.0 = Release|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Debug|x64.Build.0 = Debug|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Release|Any CPU.Build.0 = Release|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Release|x64.ActiveCfg = Release|Any CPU + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD}.Release|x64.Build.0 = Release|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Debug|x64.ActiveCfg = Debug|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Debug|x64.Build.0 = Debug|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Release|Any CPU.Build.0 = Release|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Release|x64.ActiveCfg = Release|Any CPU + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207}.Release|x64.Build.0 = Release|Any CPU + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Debug|Any CPU.ActiveCfg = Debug|x64 + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Debug|x64.ActiveCfg = Debug|x64 + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Debug|x64.Build.0 = Debug|x64 + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Debug|x64.Deploy.0 = Debug|x64 + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Release|Any CPU.ActiveCfg = Release|x64 + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Release|x64.ActiveCfg = Release|x64 + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Release|x64.Build.0 = Release|x64 + {D8A868B6-7D03-4368-A52E-64F1E1B357DB}.Release|x64.Deploy.0 = Release|x64 + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Debug|x64.ActiveCfg = Debug|Any CPU + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Debug|x64.Build.0 = Debug|Any CPU + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Release|Any CPU.Build.0 = Release|Any CPU + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Release|x64.ActiveCfg = Release|Any CPU + {B97EE2A9-93A1-41EE-873D-244E6D654149}.Release|x64.Build.0 = Release|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Debug|x64.Build.0 = Debug|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Release|Any CPU.Build.0 = Release|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Release|x64.ActiveCfg = Release|Any CPU + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324}.Release|x64.Build.0 = Release|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Debug|x64.Build.0 = Debug|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Release|Any CPU.Build.0 = Release|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Release|x64.ActiveCfg = Release|Any CPU + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD}.Release|x64.Build.0 = Release|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Debug|x64.ActiveCfg = Debug|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Debug|x64.Build.0 = Debug|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Release|Any CPU.Build.0 = Release|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Release|x64.ActiveCfg = Release|Any CPU + {8AAEEC31-B3CE-4583-9B42-1447B8694611}.Release|x64.Build.0 = Release|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Debug|x64.ActiveCfg = Debug|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Debug|x64.Build.0 = Debug|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Release|Any CPU.Build.0 = Release|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Release|x64.ActiveCfg = Release|Any CPU + {426346B6-6631-40E7-9187-8721A0C23E0E}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EFC84E79-3FFD-433D-A4E8-C1967D12A2D4} = {F647C571-A1D5-4963-B28E-05C0743AEA14} + {1BAAE8F7-B691-4FF5-BC66-2ADDE6C30746} = {750E2FD5-5728-4F39-AA06-7FAB071FBE51} + {6EF062A2-3CD0-40C9-B8AE-A4FFA536ED84} = {750E2FD5-5728-4F39-AA06-7FAB071FBE51} + {386D271D-8E5B-40F7-B017-EE38215FFAA7} = {1BAAE8F7-B691-4FF5-BC66-2ADDE6C30746} + {084F660C-5526-4370-A383-E6BBE3846F47} = {1BAAE8F7-B691-4FF5-BC66-2ADDE6C30746} + {F8C00BB1-4812-4B16-9182-3269D88858CE} = {750E2FD5-5728-4F39-AA06-7FAB071FBE51} + {69B890C5-C137-4967-8E15-26D07F312907} = {750E2FD5-5728-4F39-AA06-7FAB071FBE51} + {E650D054-6BD7-433C-ACFE-2D87F439EFAA} = {6EF062A2-3CD0-40C9-B8AE-A4FFA536ED84} + {B8BC2A87-6265-4210-9AF1-61F923C306BC} = {F8C00BB1-4812-4B16-9182-3269D88858CE} + {A03D49FF-2B8F-4AC0-8AAC-20CBCED92CBB} = {F8C00BB1-4812-4B16-9182-3269D88858CE} + {E2BFC5BC-B5EC-4556-9E25-6CCC39C1FDD0} = {B8BC2A87-6265-4210-9AF1-61F923C306BC} + {F2D6F089-6EFE-4018-9A03-395F1FF2E403} = {B8BC2A87-6265-4210-9AF1-61F923C306BC} + {9558B043-C3D1-497D-A5D0-B1280DDCA177} = {A03D49FF-2B8F-4AC0-8AAC-20CBCED92CBB} + {4C56FEA9-D054-42E4-8228-7B3BED146414} = {A03D49FF-2B8F-4AC0-8AAC-20CBCED92CBB} + {5D0A11E3-E397-4BB0-B2F5-3D0920380F4C} = {084F660C-5526-4370-A383-E6BBE3846F47} + {7ABF9C75-F835-4431-A4D9-96FE74955C57} = {386D271D-8E5B-40F7-B017-EE38215FFAA7} + {DA20FB57-1551-4A94-8EA4-4997F6E3B803} = {F8C00BB1-4812-4B16-9182-3269D88858CE} + {8ABD2A67-2001-42D1-920E-7FE07158C3A9} = {F8C00BB1-4812-4B16-9182-3269D88858CE} + {13C65287-4948-40B5-B464-0032BA72D3FD} = {8ABD2A67-2001-42D1-920E-7FE07158C3A9} + {E840668F-B6B9-4EDA-80A0-4B71B27C1DA0} = {8ABD2A67-2001-42D1-920E-7FE07158C3A9} + {22802D55-11B5-4FE2-8B9A-AF22FDA509BF} = {DA20FB57-1551-4A94-8EA4-4997F6E3B803} + {37A21CD6-47DE-4F7B-B603-36DDDBE6B818} = {F8C00BB1-4812-4B16-9182-3269D88858CE} + {FEDB83C2-C14A-4196-8973-2A5C68CE2CBA} = {69B890C5-C137-4967-8E15-26D07F312907} + {28784EB0-08BB-400F-BC37-DCED3E18261E} = {FEDB83C2-C14A-4196-8973-2A5C68CE2CBA} + {8FBCBD46-6728-46CF-ACB9-0E5DFCB0105C} = {DA20FB57-1551-4A94-8EA4-4997F6E3B803} + {0559C080-53D5-44C7-8657-4F987F0FF8B1} = {FEDB83C2-C14A-4196-8973-2A5C68CE2CBA} + {D3A735EA-FD07-4CDE-A785-DA39280509DA} = {FEDB83C2-C14A-4196-8973-2A5C68CE2CBA} + {AD8286AF-8F03-46F5-9583-620F4DBA73E8} = {F8C00BB1-4812-4B16-9182-3269D88858CE} + {92D41C6D-E785-474A-BF69-876F7A8E6912} = {69B890C5-C137-4967-8E15-26D07F312907} + {E6AB4478-0061-4DC7-8E13-DB68A72FC4DD} = {69B890C5-C137-4967-8E15-26D07F312907} + {8504D9B8-C4E8-4172-8DA3-CBD9971E3207} = {D6C1178A-80FB-42CE-8E58-BAA7BA56244D} + {D8A868B6-7D03-4368-A52E-64F1E1B357DB} = {EFC84E79-3FFD-433D-A4E8-C1967D12A2D4} + {B97EE2A9-93A1-41EE-873D-244E6D654149} = {69B890C5-C137-4967-8E15-26D07F312907} + {FB2CE5CF-AF46-41E0-95CC-88D8134DC324} = {69B890C5-C137-4967-8E15-26D07F312907} + {C2AC0E1E-2B02-474F-9DB5-AF461A6C2FDD} = {69B890C5-C137-4967-8E15-26D07F312907} + {8AAEEC31-B3CE-4583-9B42-1447B8694611} = {386D271D-8E5B-40F7-B017-EE38215FFAA7} + {426346B6-6631-40E7-9187-8721A0C23E0E} = {FEDB83C2-C14A-4196-8973-2A5C68CE2CBA} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6B8CD1A2-251A-4CE0-94BB-12805E3492A0} + EndGlobalSection +EndGlobal diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..115d4e7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "asp.net", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@aspnet/signalr": { + "version": "1.0.0-preview3-32228", + "resolved": "https://dotnet.myget.org/F/aspnetcore-dev/npm/@aspnet/signalr/-/@aspnet/signalr-1.0.0-preview3-32228.tgz", + "integrity": "sha1-49NaFjBQV7caSdMNMz/Ckyzqeqo=" + } + } +} diff --git a/publish-images.ps1 b/publish-images.ps1 new file mode 100644 index 0000000..13676a0 --- /dev/null +++ b/publish-images.ps1 @@ -0,0 +1,16 @@ +# only for testing locally purposes, you should build and publish your images from a CI pipeline. + +docker tag duber/trip.api:latest vany0114/duber.trip.api:latest +docker push vany0114/duber.trip.api:latest + +docker tag duber/invoice.api:latest vany0114/duber.invoice.api:latest +docker push vany0114/duber.invoice.api:latest + +docker tag duber/website:latest vany0114/duber.website:latest +docker push vany0114/duber.website:latest + +docker tag externalsystem/paymentservice:latest vany0114/externalsystem.paymentservice:latest +docker push vany0114/externalsystem.paymentservice:latest + +docker tag duber/trip.notifications:latest vany0114/duber.trip.notifications:latest +docker push vany0114/duber.trip.notifications:latest \ No newline at end of file diff --git a/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs new file mode 100644 index 0000000..a3f3aa3 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs @@ -0,0 +1,31 @@ +using System; +using Duber.Domain.Invoice.Events; +using System.Threading.Tasks; +using AutoMapper; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Invoice.API.Application.IntegrationEvents.Events; +using MediatR; + +namespace Duber.Invoice.API.Application.DomainEventHandlers +{ + public class InvoiceCreatedDomainEventHandler : IAsyncNotificationHandler + { + private readonly IEventBus _eventBus; + private readonly IMapper _mapper; + + public InvoiceCreatedDomainEventHandler(IEventBus eventBus, IMapper mapper) + { + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + public async Task Handle(InvoiceCreatedDomainEvent notification) + { + // to update the query side (materialized view) + var integrationEvent = _mapper.Map(notification); + _eventBus.Publish(integrationEvent); // TODO: make an async Publish method. + + await Task.CompletedTask; + } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs new file mode 100644 index 0000000..d7f3699 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using AutoMapper; +using Duber.Domain.Invoice.Events; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Invoice.API.Application.IntegrationEvents.Events; +using MediatR; + +namespace Duber.Invoice.API.Application.DomainEventHandlers +{ + public class InvoicePaidDomainEventHandler : IAsyncNotificationHandler + { + private readonly IEventBus _eventBus; + private readonly IMapper _mapper; + + public InvoicePaidDomainEventHandler(IEventBus eventBus, IMapper mapper) + { + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + public async Task Handle(InvoicePaidDomainEvent notification) + { + // to update the query side (materialized view) + var integrationEvent = _mapper.Map(notification); + _eventBus.Publish(integrationEvent); // TODO: make an async Publish method. + + await Task.CompletedTask; + } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs new file mode 100644 index 0000000..5436f75 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs @@ -0,0 +1,24 @@ +using System; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Invoice.API.Application.IntegrationEvents.Events +{ + public class InvoiceCreatedIntegrationEvent : IntegrationEvent + { + public InvoiceCreatedIntegrationEvent(Guid invoiceId, decimal fee, decimal total, Guid tripId) + { + InvoiceId = invoiceId; + Fee = fee; + Total = total; + TripId = tripId; + } + + public Guid InvoiceId { get; } + + public Guid TripId { get; } + + public decimal Fee { get; } + + public decimal Total { get; } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs new file mode 100644 index 0000000..fe2a669 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs @@ -0,0 +1,28 @@ +using System; +using Duber.Infrastructure.EventBus.Events; +using Duber.Invoice.API.Application.Model; + +namespace Duber.Invoice.API.Application.IntegrationEvents.Events +{ + public class InvoicePaidIntegrationEvent : IntegrationEvent + { + public InvoicePaidIntegrationEvent(Guid invoiceId, PaymentStatus status, string cardNumber, string cardType, Guid tripId) + { + InvoiceId = invoiceId; + Status = status; + CardNumber = cardNumber; + CardType = cardType; + TripId = tripId; + } + + public Guid InvoiceId { get; } + + public Guid TripId { get; } + + public PaymentStatus Status { get; } + + public string CardNumber { get; } + + public string CardType { get; } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs new file mode 100644 index 0000000..78dfee0 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs @@ -0,0 +1,25 @@ +using System; +using Duber.Infrastructure.EventBus.Events; +using Duber.Invoice.API.Application.Model; + +namespace Duber.Invoice.API.Application.IntegrationEvents.Events +{ + public class TripCancelledIntegrationEvent : IntegrationEvent + { + public TripCancelledIntegrationEvent(Guid tripId, TimeSpan duration, PaymentMethod paymentMethod, int userId) + { + Duration = duration; + PaymentMethod = paymentMethod; + UserId = userId; + TripId = tripId; + } + + public Guid TripId { get; } + + public TimeSpan Duration { get; } + + public PaymentMethod PaymentMethod { get; } + + public int UserId { get; } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs new file mode 100644 index 0000000..ca7613d --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs @@ -0,0 +1,28 @@ +using System; +using Duber.Infrastructure.EventBus.Events; +using Duber.Invoice.API.Application.Model; + +namespace Duber.Invoice.API.Application.IntegrationEvents.Events +{ + public class TripFinishedIntegrationEvent : IntegrationEvent + { + public TripFinishedIntegrationEvent(Guid tripId, double distance, TimeSpan duration, PaymentMethod paymentMethod, int userId) + { + Distance = distance; + Duration = duration; + PaymentMethod = paymentMethod; + UserId = userId; + TripId = tripId; + } + + public Guid TripId { get; } + + public double Distance { get; } + + public TimeSpan Duration { get; } + + public PaymentMethod PaymentMethod { get; } + + public int UserId { get; } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripCancelledIntegrationEventHandler.cs b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripCancelledIntegrationEventHandler.cs new file mode 100644 index 0000000..1cbdb0f --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripCancelledIntegrationEventHandler.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using Duber.Domain.Invoice.Repository; +using Duber.Domain.Invoice.Services; +using Duber.Domain.SharedKernel.Model; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Invoice.API.Application.IntegrationEvents.Events; +using Microsoft.Extensions.Logging; + +namespace Duber.Invoice.API.Application.IntegrationEvents.Hnadlers +{ + public class TripCancelledIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IPaymentService _paymentService; + private readonly IInvoiceRepository _invoiceRepository; + private readonly ILogger _logger; + + public TripCancelledIntegrationEventHandler(IInvoiceRepository invoiceRepository, IPaymentService paymentService, ILogger logger) + { + _invoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); + _paymentService = paymentService ?? throw new ArgumentNullException(nameof(paymentService)); + _logger = logger; + } + + public async Task Handle(TripCancelledIntegrationEvent @event) + { + _logger.LogInformation($"Trip {@event.TripId} has been cancelled."); + + var invoice = await _invoiceRepository.GetInvoiceByTripAsync(@event.TripId); + if (invoice != null) return; + + try + { + invoice = new Domain.Invoice.Model.Invoice( + @event.PaymentMethod.Id, + @event.TripId, + @event.Duration, + 0, + TripStatus.Cancelled.Id); + + await _invoiceRepository.AddInvoiceAsync(invoice); + _logger.LogInformation($"Invoice {invoice.InvoiceId} created."); + + // integration with external payment system. + if (Equals(invoice.PaymentMethod, PaymentMethod.CreditCard) && invoice.Total > 0) + { + await _paymentService.PerformPayment(invoice, @event.UserId); + _logger.LogInformation($"Payment for invoice {invoice.InvoiceId} has been processed."); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error trying to perform the payment. Trip: {@event.TripId}", ex); + } + finally + { + _invoiceRepository.Dispose(); + } + } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripFinishedIntegrationEventHandler.cs b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripFinishedIntegrationEventHandler.cs new file mode 100644 index 0000000..723d647 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/IntegrationEvents/Hnadlers/TripFinishedIntegrationEventHandler.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using Duber.Domain.Invoice.Repository; +using Duber.Domain.Invoice.Services; +using Duber.Domain.SharedKernel.Model; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Invoice.API.Application.IntegrationEvents.Events; +using Microsoft.Extensions.Logging; + +namespace Duber.Invoice.API.Application.IntegrationEvents.Hnadlers +{ + public class TripFinishedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IPaymentService _paymentService; + private readonly IInvoiceRepository _invoiceRepository; + private readonly ILogger _logger; + + public TripFinishedIntegrationEventHandler(IInvoiceRepository invoiceRepository, IPaymentService paymentService, ILogger logger) + { + _invoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); + _paymentService = paymentService ?? throw new ArgumentNullException(nameof(paymentService)); + _logger = logger; + } + + public async Task Handle(TripFinishedIntegrationEvent @event) + { + _logger.LogInformation($"Trip {@event.TripId} has finished."); + + var invoice = await _invoiceRepository.GetInvoiceByTripAsync(@event.TripId); + if (invoice != null) return; + + try + { + invoice = new Domain.Invoice.Model.Invoice( + @event.PaymentMethod.Id, + @event.TripId, + @event.Duration, + @event.Distance, + TripStatus.Finished.Id); + + await _invoiceRepository.AddInvoiceAsync(invoice); + _logger.LogInformation($"Invoice {invoice.InvoiceId} created."); + + // integration with external payment system. + if (Equals(invoice.PaymentMethod, PaymentMethod.CreditCard) && invoice.Total > 0) + { + await _paymentService.PerformPayment(invoice, @event.UserId); + _logger.LogInformation($"Payment for invoice {invoice.InvoiceId} has been processed."); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error trying to perform the payment. Trip: {@event.TripId}", ex); + } + finally + { + _invoiceRepository.Dispose(); + } + } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/Mapping/InvoiceDomainProfile.cs b/src/Application/Duber.Invoice.API/Application/Mapping/InvoiceDomainProfile.cs new file mode 100644 index 0000000..0a13211 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/Mapping/InvoiceDomainProfile.cs @@ -0,0 +1,19 @@ +using AutoMapper; +using Duber.Domain.Invoice.Model; +using Duber.Domain.SharedKernel.Model; +using ViewModel = Duber.Invoice.API.Application.Model; + +namespace Duber.Invoice.API.Application.Mapping +{ + public class InvoiceDomainProfile : Profile + { + public InvoiceDomainProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/Mapping/InvoiceEventsProfile.cs b/src/Application/Duber.Invoice.API/Application/Mapping/InvoiceEventsProfile.cs new file mode 100644 index 0000000..1c49812 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/Mapping/InvoiceEventsProfile.cs @@ -0,0 +1,30 @@ +using AutoMapper; +using Duber.Domain.Invoice.Events; +using Duber.Invoice.API.Application.IntegrationEvents.Events; +using Duber.Invoice.API.Application.Model; + +namespace Duber.Invoice.API.Application.Mapping +{ + public class InvoiceEventsProfile : Profile + { + public InvoiceEventsProfile() + { + CreateMap() + .ConstructUsing(x => new InvoiceCreatedIntegrationEvent( + x.InvoiceId, + x.Fee, + x.Total, + x.TripId + )); + + CreateMap() + .ConstructUsing(x => new InvoicePaidIntegrationEvent( + x.InvoiceId, + (PaymentStatus)(int)x.Status, + x.CardNumber, + x.CardType, + x.TripId + )); + } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/Model/CreateInvoiceRequest.cs b/src/Application/Duber.Invoice.API/Application/Model/CreateInvoiceRequest.cs new file mode 100644 index 0000000..f0384d2 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/Model/CreateInvoiceRequest.cs @@ -0,0 +1,19 @@ +using System; + +namespace Duber.Invoice.API.Application.Model +{ + public class CreateInvoiceRequest + { + public PaymentMethod PaymentMethod { get; set; } + + public Guid TripId { get; set; } + + public TimeSpan Duration { get; set; } + + public double Distance { get; set; } + + public TripStatus TripStatus { get; set; } + + public int UserId { get; set; } + } +} diff --git a/src/Application/Duber.Invoice.API/Application/Model/Invoice.cs b/src/Application/Duber.Invoice.API/Application/Model/Invoice.cs new file mode 100644 index 0000000..38530ba --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/Model/Invoice.cs @@ -0,0 +1,63 @@ +using System; + +namespace Duber.Invoice.API.Application.Model +{ + public class Invoice + { + public decimal Fee { get; set; } + + public PaymentMethod PaymentMethod { get; set; } + + public decimal Total { get; set; } + + public Guid InvoiceId { get; set; } + + public TripInformation TripInformation { get; set; } + + public DateTime Created { get; set; } + + public PaymentInfo PaymentInfo { get; set; } + } + + public class TripInformation + { + public TimeSpan Duration { get; set; } + + public double Distance { get; set; } + + public Guid Id { get; set; } + + public TripStatus Status { get; set; } + } + + public class PaymentMethod + { + public string Name { get; set; } + + public int Id { get; set; } + } + + public class TripStatus + { + public string Name { get; set; } + + public int Id { get; set; } + } + + public class PaymentInfo + { + public int UserId { get; set; } + + public PaymentStatus Status { get; set; } + + public string CardNumber { get; set; } + + public string CardType { get; set; } + } + + public enum PaymentStatus + { + Accepted = 1, + Rejected = 2, + } +} diff --git a/src/Application/Duber.Invoice.API/Application/Validations/CreateInvoiceRequestValidator.cs b/src/Application/Duber.Invoice.API/Application/Validations/CreateInvoiceRequestValidator.cs new file mode 100644 index 0000000..25c913a --- /dev/null +++ b/src/Application/Duber.Invoice.API/Application/Validations/CreateInvoiceRequestValidator.cs @@ -0,0 +1,14 @@ +using Duber.Invoice.API.Application.Model; +using FluentValidation; + +namespace Duber.Invoice.API.Application.Validations +{ + public class CreateInvoiceRequestValidator : AbstractValidator + { + public CreateInvoiceRequestValidator() + { + RuleFor(request => request.TripId).NotEmpty().WithMessage("Trip id is required."); + RuleFor(request => request.UserId).NotEmpty().WithMessage("User id is required."); + } + } +} diff --git a/src/Application/Duber.Invoice.API/Controllers/HomeController.cs b/src/Application/Duber.Invoice.API/Controllers/HomeController.cs new file mode 100644 index 0000000..dba55d0 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Controllers/HomeController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Duber.Invoice.API.Controllers +{ + public class HomeController : Controller + { + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Invoice.API/Controllers/InvoiceController.cs b/src/Application/Duber.Invoice.API/Controllers/InvoiceController.cs new file mode 100644 index 0000000..992caf2 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Controllers/InvoiceController.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using AutoMapper; +using Duber.Domain.Invoice.Repository; +using Duber.Domain.Invoice.Services; +using Duber.Domain.SharedKernel.Model; +using Microsoft.AspNetCore.Mvc; +using ViewModel = Duber.Invoice.API.Application.Model; + +namespace Duber.Invoice.API.Controllers +{ + [Route("api/v1/[controller]")] + public class InvoiceController : Controller + { + private readonly IMapper _mapper; + private readonly IPaymentService _paymentService; + private readonly IInvoiceRepository _invoiceRepository; + + public InvoiceController(IInvoiceRepository invoiceRepository, IMapper mapper, IPaymentService paymentService) + { + _invoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _paymentService = paymentService ?? throw new ArgumentNullException(nameof(paymentService)); + } + + /// + /// Returns an invoice that matches with the specified id + /// + /// + /// Returns an invoice that matches with the specified id + /// Returns an Invoice object that matches with the specified id + [HttpGet("{invoiceId}")] + [ProducesResponseType(typeof(ViewModel.Invoice), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task GetInvoice(Guid invoiceId) + { + try + { + var invoice = await _invoiceRepository.GetInvoiceAsync(invoiceId); + var invoiceViewModel = _mapper.Map(invoice); + + if (invoiceViewModel == null) + return NotFound(); + + return Ok(invoiceViewModel); + } + finally + { + _invoiceRepository.Dispose(); + } + } + + /// + /// Returns an invoice that matches with the specified trip id + /// + /// + /// Returns an invoice that matches with the specified trip id + /// Returns an invoice that matches with the specified trip id + [HttpGet("trip/{tripId}")] + [ProducesResponseType(typeof(ViewModel.Invoice), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task GetInvoiceByTrip(Guid tripId) + { + try + { + var invoice = await _invoiceRepository.GetInvoiceByTripAsync(tripId); + var invoiceViewModel = _mapper.Map(invoice); + + if (invoiceViewModel == null) + return NotFound(); + + return Ok(invoiceViewModel); + } + finally + { + _invoiceRepository.Dispose(); + } + } + + /// + /// Returns all of the Invoices + /// + /// Returns all of the Invoices + /// Returns a list of Invoice object. + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task GetInvoices() + { + try + { + var invoices = await _invoiceRepository.GetInvoicesAsync(); + var invoicesViewModel = _mapper.Map>(invoices); + + if (invoicesViewModel == null) + return NotFound(); + + return Ok(invoicesViewModel); + } + finally + { + _invoiceRepository.Dispose(); + } + } + + /// + /// Creates a new invoice. + /// + /// + /// Returns the newly created invoice identifier. + /// Returns the newly created trip identifier. + [HttpPost] + [ProducesResponseType(typeof(Guid), (int)HttpStatusCode.Created)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task CreateInvoice([FromBody]ViewModel.CreateInvoiceRequest request) + { + try + { + // to enable idempotency. + var invoice = await _invoiceRepository.GetInvoiceByTripAsync(request.TripId); + if (invoice != null) return CreatedAtAction(nameof(GetInvoice), new { invoiceId = invoice.InvoiceId }, invoice.InvoiceId); + + invoice = new Domain.Invoice.Model.Invoice( + request.PaymentMethod.Id, + request.TripId, + request.Duration, + request.Distance, + request.TripStatus.Id); + + await _invoiceRepository.AddInvoiceAsync(invoice); + + // integration with external payment system. + if (Equals(invoice.PaymentMethod, PaymentMethod.CreditCard) && invoice.Total > 0) + { + await _paymentService.PerformPayment(invoice, request.UserId); + } + + return CreatedAtAction(nameof(GetInvoice), new { invoiceId = invoice.InvoiceId }, invoice.InvoiceId); + } + finally + { + _invoiceRepository.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Invoice.API/Dockerfile b/src/Application/Duber.Invoice.API/Dockerfile new file mode 100644 index 0000000..04c70a1 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY . . +WORKDIR "/src/src/Application/Duber.Invoice.API" +RUN dotnet restore +RUN dotnet build --no-restore -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Duber.Invoice.API.dll"] \ No newline at end of file diff --git a/src/Application/Duber.Invoice.API/Dockerfile.original b/src/Application/Duber.Invoice.API/Dockerfile.original new file mode 100644 index 0000000..fb18edf --- /dev/null +++ b/src/Application/Duber.Invoice.API/Dockerfile.original @@ -0,0 +1,20 @@ +FROM microsoft/aspnetcore:2.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM microsoft/aspnetcore-build:2.0 AS build +WORKDIR /src +COPY microservices-netcore-docker-servicefabric.sln ./ +COPY src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj src/Application/Duber.Invoice.API/ +RUN dotnet restore -nowarn:msb3202,nu1503 +COPY . . +WORKDIR /src/src/Application/Duber.Invoice.API +RUN dotnet build -c Release -o /app + +FROM build AS publish +RUN dotnet publish -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Duber.Invoice.API.dll"] diff --git a/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj new file mode 100644 index 0000000..2811158 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj @@ -0,0 +1,49 @@ + + + + netcoreapp3.1 + ..\..\..\docker-compose.dcproj + + + + bin\Debug\netcoreapp2.2\Duber.Invoice.API.xml + 1701;1702;1705;1591 + + + + true + Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Application/Duber.Invoice.API/Extensions/ApplicationBuilderExtensions.cs b/src/Application/Duber.Invoice.API/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..545ff7d --- /dev/null +++ b/src/Application/Duber.Invoice.API/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,19 @@ +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Invoice.API.Application.IntegrationEvents.Events; +using Duber.Invoice.API.Application.IntegrationEvents.Hnadlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Duber.Invoice.API.Extensions +{ + public static class ApplicationBuilderExtensions + { + public static IApplicationBuilder UseServiceBroker(this IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe(); + eventBus.Subscribe(); + return app; + } + } +} diff --git a/src/Application/Duber.Invoice.API/Extensions/ServiceCollectionExtensions.cs b/src/Application/Duber.Invoice.API/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..c89783e --- /dev/null +++ b/src/Application/Duber.Invoice.API/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,185 @@ +using Duber.Domain.ACL.Adapters; +using Duber.Domain.ACL.Contracts; +using Duber.Domain.Invoice.Persistence; +using Duber.Domain.Invoice.Repository; +using Duber.Domain.Invoice.Services; +using Duber.Infrastructure.Resilience.Abstractions; +using Duber.Infrastructure.Resilience.Http; +using Duber.Infrastructure.Resilience.Sql; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Polly; +using Polly.Extensions.Http; +using System; +using System.IO; +using System.Net.Http; +using System.Reflection; +using Duber.Infrastructure.EventBus.RabbitMQ.IoC; +using Duber.Infrastructure.EventBus.ServiceBus.IoC; +using Duber.Invoice.API.Application.IntegrationEvents.Hnadlers; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.OpenApi.Models; + +#pragma warning disable 618 + +namespace Duber.Invoice.API.Extensions +{ + public static class ServiceCollectionExtensions + { + public static string HttpResiliencePolicy = "HttpResiliencePolicy"; + + public static IServiceCollection AddPersistenceAndRepository(this IServiceCollection services, IConfiguration configuration) + { + // just to perform the migrations + services.AddDbContext(options => + { + options.UseSqlServer( + configuration["ConnectionStrings:InvoiceDB"], + sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(InvoiceMigrationContext).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), null); + }); + }); + + // Invoice repository and context configuration + services.AddTransient(provider => + { + var mediator = provider.GetService(); + var sqlExecutor = provider.GetService(); + var connectionString = configuration["ConnectionStrings:InvoiceDB"]; + return new InvoiceContext(connectionString, mediator, sqlExecutor); + }); + + services.AddTransient(); + return services; + } + + public static IServiceCollection AddResilientStrategies(this IServiceCollection services, IConfiguration configuration) + { + // Resilient SQL Executor configuration. + services.AddSingleton(sp => + { + var sqlPolicyBuilder = new SqlPolicyBuilder(); + return sqlPolicyBuilder + .UseAsyncExecutor() + .WithDefaultPolicies() + .Build(); + }); + + // Create (and register with DI) a policy registry containing some policies we want to use. + var policyRegistry = services.AddPolicyRegistry(); + policyRegistry[HttpResiliencePolicy] = GetResiliencePolicy(configuration); + + // Resilient Http Invoker onfiguration. + // Register a typed client via HttpClientFactory, set to use the policy we placed in the policy registry. + services.AddHttpClient(client => + { + client.Timeout = TimeSpan.FromSeconds(50); + }).AddPolicyHandlerFromRegistry(HttpResiliencePolicy); + + return services; + } + + public static IServiceCollection AddCustomSwagger(this IServiceCollection services) + { + // swagger configuration + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Duber.Invoice HTTP API", + Version = "v1", + Description = "The Duber Invoice Service HTTP API" + }); + + // Set the comments path for the Swagger JSON and UI. + var xmlFile = $"{Assembly.GetEntryAssembly()?.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath); + }); + + return services; + } + + public static IServiceCollection AddPaymentService(this IServiceCollection services, IConfiguration configuration) + { + // payment service configuration + services.AddTransient(); + services.AddTransient(provider => + { + var httpInvoker = provider.GetRequiredService(); + var paymentServiceBaseUrl = configuration["PaymentServiceBaseUrl"]; + return new PaymentServiceAdapter(httpInvoker, paymentServiceBaseUrl); + }); + + return services; + } + + private static IAsyncPolicy GetResiliencePolicy(IConfiguration configuration) + { + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["HttpClientRetryCount"])) + { + retryCount = int.Parse(configuration["HttpClientRetryCount"]); + } + + var exceptionsAllowedBeforeBreaking = 4; + if (!string.IsNullOrEmpty(configuration["HttpClientExceptionsAllowedBeforeBreaking"])) + { + exceptionsAllowedBeforeBreaking = int.Parse(configuration["HttpClientExceptionsAllowedBeforeBreaking"]); + } + + // Define a couple of policies which will form our resilience strategy. + var policies = HttpPolicyExtensions.HandleTransientHttpError() + .RetryAsync(retryCount) + .WrapAsync(HttpPolicyExtensions.HandleTransientHttpError() + .CircuitBreakerAsync(exceptionsAllowedBeforeBreaking, TimeSpan.FromSeconds(5))); + + return policies; + } + + public static IServiceCollection AddServiceBroker(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddServiceBus(configuration); + } + else + { + services.AddRabbitMQ(configuration); + } + + services.AddTransient(); + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + hcBuilder + .AddSqlServer( + configuration["ConnectionStrings:InvoiceDB"], + name: "InvoiceDB-check", + tags: new string[] { "invoicedb" }); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder.AddAzureServiceBusTopic(configuration, "invoice-az-servicebus-check"); + } + else + { + hcBuilder.AddRabbitMQ(configuration, "invoice-rabbitmqbus-check"); + } + + return services; + } + } +} diff --git a/src/Application/Duber.Invoice.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Application/Duber.Invoice.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs new file mode 100644 index 0000000..fdbb111 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Duber.Invoice.API.Infrastructure.ActionResults +{ + public class InternalServerErrorObjectResult : ObjectResult + { + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } + } +} diff --git a/src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs b/src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs new file mode 100644 index 0000000..1a9b247 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Reflection; +using Autofac; +using Duber.Invoice.API.Application.DomainEventHandlers; +using MediatR; + +namespace Duber.Invoice.API.Infrastructure.AutofacModules +{ + public class MediatorModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly) + .AsImplementedInterfaces(); + + // Register all the event classes (they implement IAsyncNotificationHandler) in assembly holding the Commands + builder.RegisterAssemblyTypes(typeof(InvoiceCreatedDomainEventHandler).GetTypeInfo().Assembly) + .AsClosedTypesOf(typeof(IAsyncNotificationHandler<>)); + + builder.Register(context => + { + var componentContext = context.Resolve(); + return t => { object o; return componentContext.TryResolve(t, out o) ? o : null; }; + }); + + builder.Register(context => + { + var componentContext = context.Resolve(); + + return t => + { + var resolved = (IEnumerable)componentContext.Resolve(typeof(IEnumerable<>).MakeGenericType(t)); + return resolved; + }; + }); + } + } +} diff --git a/src/Application/Duber.Invoice.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Application/Duber.Invoice.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 0000000..e73fea2 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,70 @@ +using System.Net; +using Duber.Domain.Invoice.Exceptions; +using Duber.Invoice.API.Infrastructure.ActionResults; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace Duber.Invoice.API.Infrastructure.Filters +{ + public class HttpGlobalExceptionFilter : IExceptionFilter + { + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) + { + _env = env; + _logger = logger; + } + + public void OnException(ExceptionContext context) + { + _logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(InvoiceDomainArgumentNullException) || context.Exception.GetType() == typeof(InvoiceDomainInvalidOperationException)) + { + var json = new JsonErrorResponse + { + Messages = new[] { context.Exception.Message } + }; + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + //It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new BadRequestObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An unexpected error occurred. Try it again." } + }; + + if (_env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + // It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } + } +} diff --git a/src/Application/Duber.Invoice.API/Infrastructure/Filters/ValidatorActionFilter.cs b/src/Application/Duber.Invoice.API/Infrastructure/Filters/ValidatorActionFilter.cs new file mode 100644 index 0000000..5062d48 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Infrastructure/Filters/ValidatorActionFilter.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Duber.Invoice.API.Infrastructure.Filters +{ + public class ValidatorActionFilter : IActionFilter + { + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new BadRequestObjectResult(context.ModelState); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + } +} diff --git a/src/Application/Duber.Invoice.API/Program.cs b/src/Application/Duber.Invoice.API/Program.cs new file mode 100644 index 0000000..c4352fa --- /dev/null +++ b/src/Application/Duber.Invoice.API/Program.cs @@ -0,0 +1,36 @@ +using Duber.Domain.Invoice.Persistence; +using Duber.Infrastructure.WebHost; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +#pragma warning disable 618 + +namespace Duber.Invoice.API +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args) + .MigrateDbContext((_, __) => { }) + .Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .ConfigureAppConfiguration((builderContext, config) => + { + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, builder) => + { + builder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + builder.AddConsole(); + builder.AddDebug(); + builder.AddApplicationInsights(); + }) + .Build(); + } +} diff --git a/src/Application/Duber.Invoice.API/Properties/launchSettings.json b/src/Application/Duber.Invoice.API/Properties/launchSettings.json new file mode 100644 index 0000000..a07f4a5 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Properties/launchSettings.json @@ -0,0 +1,34 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55257/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Duber.Invoice.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:55258/" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/values" + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Invoice.API/Startup.cs b/src/Application/Duber.Invoice.API/Startup.cs new file mode 100644 index 0000000..af0fd94 --- /dev/null +++ b/src/Application/Duber.Invoice.API/Startup.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using AutoMapper; +using Duber.Invoice.API.Application.Validations; +using Duber.Invoice.API.Extensions; +using Duber.Invoice.API.Infrastructure.AutofacModules; +using Duber.Invoice.API.Infrastructure.Filters; +using FluentValidation.AspNetCore; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +// ReSharper disable InconsistentNaming +// ReSharper disable AssignNullToNotNullAttribute +#pragma warning disable 618 + +namespace Duber.Invoice.API +{ + 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 IServiceProvider ConfigureServices(IServiceCollection services) + { + services + .AddAutoMapper() + .AddApplicationInsightsTelemetry(Configuration) + .AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + options.Filters.Add(typeof(ValidatorActionFilter)); + }) + .AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore) + .AddFluentValidation(x => x.RegisterValidatorsFromAssemblyContaining()); + + services.AddOptions() + .AddCors(options => + { + // be careful with this policy, in production you should only add authorized origins, methods, etc. + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + + services.AddResilientStrategies(Configuration) + .AddPersistenceAndRepository(Configuration) + .AddPaymentService(Configuration) + .AddServiceBroker(Configuration) + .AddHealthChecks(Configuration) + .AddCustomSwagger(); + + //configure autofac + var container = new ContainerBuilder(); + container.Populate(services); + container.RegisterModule(new MediatorModule()); + + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseCors("CorsPolicy") + .UseRouting() + .UseServiceBroker(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/readiness", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + + app.UseSwagger(x => + { + var reverseProxyPrefix = Configuration.GetValue("ReverseProxyPrefix"); + if (!string.IsNullOrEmpty(reverseProxyPrefix)) + { + x.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.Servers = new List + { + new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}/{reverseProxyPrefix}" } + }); + } + }) + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("./swagger/v1/swagger.json", "Duber.Invoice V1"); + c.RoutePrefix = string.Empty; + }); + } + } +} diff --git a/src/Application/Duber.Invoice.API/appsettings.Development.json b/src/Application/Duber.Invoice.API/appsettings.Development.json new file mode 100644 index 0000000..fa8ce71 --- /dev/null +++ b/src/Application/Duber.Invoice.API/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Application/Duber.Invoice.API/appsettings.json b/src/Application/Duber.Invoice.API/appsettings.json new file mode 100644 index 0000000..be6d776 --- /dev/null +++ b/src/Application/Duber.Invoice.API/appsettings.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Information" + } + } + }, + "AzureServiceBusEnabled": false, + "EventBusConnection": "", + "EventBusConnectionHC": "", + "SubscriptionClientName": "Invoice", + "EventBusRetryCount": 5, + "PaymentServiceBaseUrl": "http://localhost:32777", + "HttpClientRetryCount": 5, + "HttpClientExceptionsAllowedBeforeBreaking": 4, + "SqlClientRetryCount": 5, + "SqlClientExceptionsAllowedBeforeBreaking": 4, + "ConnectionStrings": { + "InvoiceDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Duber.InvoiceDb;User Id=sa;Password=Pass@word" + }, + "ReverseProxyPrefix": "" +} diff --git a/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs new file mode 100644 index 0000000..6f5ec9f --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using AutoMapper; +using Duber.Domain.Trip.Events; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Trip.API.Application.IntegrationEvents; +using Kledex.Events; +using Microsoft.Extensions.Logging; + +namespace Duber.Trip.API.Application.DomainEventHandlers +{ + public class TripCreatedDomainEventHandlerAsync : IEventHandlerAsync + { + private readonly IEventBus _eventBus; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public TripCreatedDomainEventHandlerAsync(IEventBus eventBus, IMapper mapper, ILogger logger) + { + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _logger = logger; + } + + public async Task HandleAsync(TripCreatedDomainEvent @event) + { + _logger.LogInformation($"Trip {@event.AggregateRootId} has been created."); + var integrationEvent = _mapper.Map(@event); + + // to update the query side (materialized view) + _eventBus.Publish(integrationEvent); // TODO: make an async Publish method. + + await Task.CompletedTask; + } + } +} diff --git a/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs new file mode 100644 index 0000000..d78792e --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using AutoMapper; +using Duber.Domain.Trip.Events; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Infrastructure.EventBus.Idempotency; +using Duber.Trip.API.Application.IntegrationEvents; +using Duber.Trip.API.Application.Model; +using Kledex.Events; +using Microsoft.Extensions.Logging; +using TripStatus = Duber.Domain.SharedKernel.Model.TripStatus; + +namespace Duber.Trip.API.Application.DomainEventHandlers +{ + public class TripUpdatedDomainEventHandlerAsync : IEventHandlerAsync + { + private readonly IEventBus _eventBus; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public TripUpdatedDomainEventHandlerAsync(IEventBus eventBus, IMapper mapper, + ILogger logger) + { + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _logger = logger; + } + + public async Task HandleAsync(TripUpdatedDomainEvent @event) + { + var integrationEvent = _mapper.Map(@event); + + // to update the query side (materialized view) + _logger.LogInformation($"Trip {@event.AggregateRootId} has been updated."); + _eventBus.Publish(integrationEvent); // TODO: make an async Publish method. + + // events for invoice microservice + if (@event.Status.Name == TripStatus.Finished.Name) + { + if (!@event.Distance.HasValue || !@event.Duration.HasValue || !@event.UserTripId.HasValue) + throw new ArgumentException( + "Distance, duration and user id are required to trigger a TripFinishedIntegrationEvent"); + + _logger.LogInformation($"Trip {@event.AggregateRootId} has finished."); + var tripFinishedIntegrationEvent = new TripFinishedIntegrationEvent( + @event.AggregateRootId, + @event.Distance.Value, + @event.Duration.Value, + new PaymentMethod {Id = @event.PaymentMethod.Id, Name = @event.PaymentMethod.Name}, + @event.UserTripId.Value, + @event.ConnectionId); + + _eventBus.Publish(new IdempotentIntegrationEvent(tripFinishedIntegrationEvent, @event.AggregateRootId.ToString())); + } + else if (@event.Status.Name == TripStatus.Cancelled.Name) + { + if (!@event.Duration.HasValue || !@event.UserTripId.HasValue) + throw new ArgumentException( + "Duration and user id are required to trigger a TripCancelledIntegrationEvent"); + + _logger.LogInformation($"Trip {@event.AggregateRootId} has been canceled."); + var tripCancelledIntegrationEvent = new TripCancelledIntegrationEvent( + @event.AggregateRootId, + @event.Duration.Value, + new PaymentMethod {Id = @event.PaymentMethod.Id, Name = @event.PaymentMethod.Name}, + @event.UserTripId.Value, + @event.ConnectionId); + + _eventBus.Publish(new IdempotentIntegrationEvent(tripCancelledIntegrationEvent, @event.AggregateRootId.ToString())); + } + + await Task.CompletedTask; + } + } + + public class TripUpdatedIdempotentEventHandler : IdempotentIntegrationEventHandler + { + private readonly ILogger _logger; + + public TripUpdatedIdempotentEventHandler(IEventBus eventBus, IIdempotencyStoreProvider storeProvider, + ILogger logger) : + base(eventBus, storeProvider) + { + _logger = logger; + } + + protected override void HandleDuplicatedRequest(TripFinishedIntegrationEvent message) + { + _logger.LogInformation($"TripFinishedIntegrationEvent was already handled. Trip {message.TripId}."); + base.HandleDuplicatedRequest(message); + } + } +} diff --git a/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCancelledIntegrationEvent.cs b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCancelledIntegrationEvent.cs new file mode 100644 index 0000000..10a2c19 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCancelledIntegrationEvent.cs @@ -0,0 +1,28 @@ +using System; +using Duber.Infrastructure.EventBus.Events; +using Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Application.IntegrationEvents +{ + public class TripCancelledIntegrationEvent : IntegrationEvent + { + public TripCancelledIntegrationEvent(Guid tripId, TimeSpan duration, PaymentMethod paymentMethod, int userId, string connectionId) + { + Duration = duration; + PaymentMethod = paymentMethod; + UserId = userId; + ConnectionId = connectionId; + TripId = tripId; + } + + public Guid TripId { get; } + + public TimeSpan Duration { get; } + + public PaymentMethod PaymentMethod { get; } + + public int UserId { get; } + + public string ConnectionId { get; } + } +} diff --git a/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCreatedIntegrationEvent.cs b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCreatedIntegrationEvent.cs new file mode 100644 index 0000000..33b338f --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripCreatedIntegrationEvent.cs @@ -0,0 +1,37 @@ +using System; +using Duber.Infrastructure.EventBus.Events; +using Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Application.IntegrationEvents +{ + public class TripCreatedIntegrationEvent : IntegrationEvent + { + public TripCreatedIntegrationEvent(Guid tripId, int userTripId, int driverId, Location from, Location to, VehicleInformation vehicleInformation, PaymentMethod paymentMethod, string connectionId) + { + UserTripId = userTripId; + DriverId = driverId; + From = from; + To = to; + VehicleInformation = vehicleInformation; + PaymentMethod = paymentMethod; + ConnectionId = connectionId; + TripId = tripId; + } + + public Guid TripId { get; } + + public int UserTripId { get; } + + public int DriverId { get; } + + public Location From { get; } + + public Location To { get; } + + public VehicleInformation VehicleInformation { get; } + + public PaymentMethod PaymentMethod { get; } + + public string ConnectionId { get; } + } +} diff --git a/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripFinishedIntegrationEvent.cs b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripFinishedIntegrationEvent.cs new file mode 100644 index 0000000..4647002 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripFinishedIntegrationEvent.cs @@ -0,0 +1,31 @@ +using System; +using Duber.Infrastructure.EventBus.Events; +using Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Application.IntegrationEvents +{ + public class TripFinishedIntegrationEvent : IntegrationEvent + { + public TripFinishedIntegrationEvent(Guid tripId, double distance, TimeSpan duration, PaymentMethod paymentMethod, int userId, string connectionId) + { + Distance = distance; + Duration = duration; + PaymentMethod = paymentMethod; + UserId = userId; + ConnectionId = connectionId; + TripId = tripId; + } + + public Guid TripId { get; } + + public double Distance { get; } + + public TimeSpan Duration { get; } + + public PaymentMethod PaymentMethod { get; } + + public int UserId { get; } + + public string ConnectionId { get; } + } +} diff --git a/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripUpdatedIntegrationEvent.cs b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripUpdatedIntegrationEvent.cs new file mode 100644 index 0000000..80e399c --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/IntegrationEvents/TripUpdatedIntegrationEvent.cs @@ -0,0 +1,49 @@ +using System; +using Duber.Infrastructure.EventBus.Events; +using Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Application.IntegrationEvents +{ + public class TripUpdatedIntegrationEvent : IntegrationEvent + { + public TripUpdatedIntegrationEvent(Guid tripId, Action action, TripStatus status, DateTime? started, DateTime? ended, Location currentLocation, double? distance, TimeSpan? duration, string connectionId) + { + Action = action; + Status = status; + Started = started; + Ended = ended; + CurrentLocation = currentLocation; + Distance = distance; + Duration = duration; + ConnectionId = connectionId; + TripId = tripId; + } + + public Guid TripId { get; } + + public Action Action { get; } + + public TripStatus Status { get; } + + public DateTime? Started { get; } + + public DateTime? Ended { get; } + + public Location CurrentLocation { get; } + + public double? Distance { get; } + + public TimeSpan? Duration { get; } + + public string ConnectionId { get; } + } + + public enum Action + { + Accepted = 1, + Started = 2, + Cancelled = 3, + FinishedEarlier = 4, + UpdatedCurrentLocation = 5 + } +} diff --git a/src/Application/Duber.Trip.API/Application/Mapping/TripCommandsProfile.cs b/src/Application/Duber.Trip.API/Application/Mapping/TripCommandsProfile.cs new file mode 100644 index 0000000..36a8195 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Mapping/TripCommandsProfile.cs @@ -0,0 +1,31 @@ +using System; +using AutoMapper; +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.Trip.Commands; +using Duber.Domain.Trip.Model; +using ViewModel = Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Application.Mapping +{ + public class TripCommandsProfile : Profile + { + public TripCommandsProfile() + { + CreateMap(); + CreateMap(); + + // we're working with viewmodels in order to don't expose our domain objects, we don't want to expose things like: AggregateRootId, UserId, Source, etc. + CreateMap() + .ForMember(dest => dest.UserTripId, opts => opts.MapFrom(src => src.UserId)); + + CreateMap() + .ForMember(dest => dest.AggregateRootId, opts => opts.MapFrom(src => src.Id)) + .AfterMap((src, dest) => dest.Id = Guid.NewGuid()); // command id must be unique, the id which comes in src.Id is the aggregate root (trip id) + + + CreateMap() + .ForMember(dest => dest.AggregateRootId, opts => opts.MapFrom(src => src.Id)) + .AfterMap((src, dest) => dest.Id = Guid.NewGuid()); // command id must be unique, the id which comes in src.Id is the aggregate root (trip id + } + } +} diff --git a/src/Application/Duber.Trip.API/Application/Mapping/TripDomainProfile.cs b/src/Application/Duber.Trip.API/Application/Mapping/TripDomainProfile.cs new file mode 100644 index 0000000..53964b7 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Mapping/TripDomainProfile.cs @@ -0,0 +1,20 @@ +using AutoMapper; +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.Trip.Model; +using ViewModel = Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Application.Mapping +{ + public class TripDomainProfile : Profile + { + public TripDomainProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/src/Application/Duber.Trip.API/Application/Mapping/TripEventsProfile.cs b/src/Application/Duber.Trip.API/Application/Mapping/TripEventsProfile.cs new file mode 100644 index 0000000..33e48c9 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Mapping/TripEventsProfile.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using Duber.Domain.Trip.Events; +using Duber.Trip.API.Application.IntegrationEvents; +using Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Application.Mapping +{ + public class TripEventsProfile : Profile + { + public TripEventsProfile() + { + CreateMap() + .ConstructUsing(x => new TripUpdatedIntegrationEvent( + x.AggregateRootId, + (IntegrationEvents.Action)(int)x.Action, + new TripStatus { Id = x.Status.Id, Name = x.Status.Name }, + x.Started, + x.Ended, + x.CurrentLocation == null ? null : new Location { Latitude = x.CurrentLocation.Latitude, Longitude = x.CurrentLocation.Longitude }, + x.Distance, + x.Duration, + x.ConnectionId + )); + + CreateMap() + .ConstructUsing(x => new TripCreatedIntegrationEvent( + x.AggregateRootId, + x.UserTripId, + x.DriverId, + new Location { Latitude = x.From.Latitude, Longitude = x.From.Longitude, Description = x.From.Description}, + new Location { Latitude = x.To.Latitude, Longitude = x.To.Longitude, Description = x.To.Description}, + new VehicleInformation + { + Brand = x.VehicleInformation.Brand, + Model = x.VehicleInformation.Model, + Plate = x.VehicleInformation.Plate + }, + new PaymentMethod { Id = x.PaymentMethod.Id, Name = x.PaymentMethod.Name }, + x.ConnectionId + )); + } + } +} diff --git a/src/Application/Duber.Trip.API/Application/Model/CreateTripCommand.cs b/src/Application/Duber.Trip.API/Application/Model/CreateTripCommand.cs new file mode 100644 index 0000000..a5a2dc8 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Model/CreateTripCommand.cs @@ -0,0 +1,23 @@ +namespace Duber.Trip.API.Application.Model +{ + public class CreateTripCommand + { + public int UserId { get; set; } + + public int DriverId { get; set; } + + public Location From { get; set; } + + public Location To { get; set; } + + public string Plate { get; set; } + + public string Brand { get; set; } + + public string Model { get; set; } + + public PaymentMethod PaymentMethod { get; set; } + + public string ConnectionId { get; set; } + } +} diff --git a/src/Application/Duber.Trip.API/Application/Model/Location.cs b/src/Application/Duber.Trip.API/Application/Model/Location.cs new file mode 100644 index 0000000..c7885f4 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Model/Location.cs @@ -0,0 +1,11 @@ +namespace Duber.Trip.API.Application.Model +{ + public class Location + { + public double Latitude { get; set; } + + public double Longitude { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Application/Duber.Trip.API/Application/Model/Trip.cs b/src/Application/Duber.Trip.API/Application/Model/Trip.cs new file mode 100644 index 0000000..43d2f94 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Model/Trip.cs @@ -0,0 +1,68 @@ +using System; + +namespace Duber.Trip.API.Application.Model +{ + public class Trip + { + public Guid Id { get; set; } + + public int UserId { get; set; } + + public int DriverId { get; set; } + + public Location From { get; set; } + + public Location To { get; set; } + + public Location CurrentLocation { get; set; } + + public DateTime Created { get; set; } + + public DateTime? Started { get; set; } + + public DateTime? End { get; set; } + + public TripStatus Status { get; set; } + + public VehicleInformation VehicleInformation { get; set; } + + public Rating Rating { get; set; } + + public TimeSpan? Duration { get; set; } + + public double Distance { get; set; } + + public PaymentMethod PaymentMethod { get; set; } + } + + public class VehicleInformation + { + public string Plate { get; set; } + + public string Brand { get; set; } + + public string Model { get; set; } + } + + public class TripStatus + { + public string Name { get; set; } + + public int Id { get; set; } + } + + public class PaymentMethod + { + public string Name { get; set; } + + public int Id { get; set; } + } + + public class Rating + { + public int Driver { get; set; } + + public int User { get; set; } + } +} + diff --git a/src/Application/Duber.Trip.API/Application/Model/UpdateTripCommand.cs b/src/Application/Duber.Trip.API/Application/Model/UpdateTripCommand.cs new file mode 100644 index 0000000..e3ca40b --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Model/UpdateTripCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace Duber.Trip.API.Application.Model +{ + public class UpdateTripCommand + { + public Guid Id { get; set; } + + public string ConnectionId { get; set; } + } + + public class UpdateCurrentLocationTripCommand : UpdateTripCommand + { + public Location CurrentLocation { get; set; } + } +} diff --git a/src/Application/Duber.Trip.API/Application/Validations/UpdateTripCommandValidator.cs b/src/Application/Duber.Trip.API/Application/Validations/UpdateTripCommandValidator.cs new file mode 100644 index 0000000..e886d46 --- /dev/null +++ b/src/Application/Duber.Trip.API/Application/Validations/UpdateTripCommandValidator.cs @@ -0,0 +1,13 @@ +using Duber.Trip.API.Application.Model; +using FluentValidation; + +namespace Duber.Trip.API.Application.Validations +{ + public class UpdateTripCommandValidator : AbstractValidator + { + public UpdateTripCommandValidator() + { + RuleFor(trip => trip.Id).NotEmpty().WithMessage("Trip id is required."); + } + } +} diff --git a/src/Application/Duber.Trip.API/Controllers/EventStoreController.cs b/src/Application/Duber.Trip.API/Controllers/EventStoreController.cs new file mode 100644 index 0000000..8e1b1d0 --- /dev/null +++ b/src/Application/Duber.Trip.API/Controllers/EventStoreController.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Duber.Trip.API.Infrastructure.Repository; +using Kledex.Store.Cosmos.Mongo.Documents; +using Microsoft.AspNetCore.Mvc; + +namespace Duber.Trip.API.Controllers +{ + [Route("api/v1/[controller]")] + public class EventStoreController : Controller + { + private readonly IEventStoreRepository _repository; + + public EventStoreController(IEventStoreRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Returns all of the Aggregates + /// + /// Returns all of the Aggregates + /// Returns a list of AggregateDocument object. + [HttpGet("aggregates")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task GetAggreagates() + { + var aggregates = await _repository.GetAggregatesAsync(); + + if (aggregates == null) + return NotFound(); + + return Ok(aggregates); + } + + /// + /// Returns all events that matches with the specified aggregate id. + /// + /// + /// Returns all events that matches with the specified aggregate id. + /// Returns a list of EventDocument object. + [HttpGet("aggregates/{aggregateId}/events")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task GetEventsByAggregate(Guid aggregateId) + { + var events = await _repository.GetEventsAsync(aggregateId); + + if (events == null) + return NotFound(); + + return Ok(events); + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Trip.API/Controllers/HomeController.cs b/src/Application/Duber.Trip.API/Controllers/HomeController.cs new file mode 100644 index 0000000..b7be265 --- /dev/null +++ b/src/Application/Duber.Trip.API/Controllers/HomeController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Duber.Trip.API.Controllers +{ + public class HomeController : Controller + { + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Trip.API/Controllers/TripController.cs b/src/Application/Duber.Trip.API/Controllers/TripController.cs new file mode 100644 index 0000000..9709a97 --- /dev/null +++ b/src/Application/Duber.Trip.API/Controllers/TripController.cs @@ -0,0 +1,146 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using AutoMapper; +using Duber.Domain.Trip.Commands; +using Kledex; +using Kledex.Domain; +using Microsoft.ApplicationInsights.AspNetCore.Extensions; +using Microsoft.AspNetCore.Mvc; +using Action = Duber.Domain.Trip.Commands.Action; +using ViewModel = Duber.Trip.API.Application.Model; + +namespace Duber.Trip.API.Controllers +{ + [Route("api/v1/[controller]")] + public class TripController : Controller + { + private readonly IDispatcher _dispatcher; + private readonly IMapper _mapper; + private readonly IRepository _repository; + + public TripController(IDispatcher dispatcher, IMapper mapper, IRepository repository) + { + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Returns a trip that matches with the specified id. + /// + /// + /// Returns a trip that matches with the specified id. + /// Returns a Trip object that matches with the specified id. + [HttpGet("{tripId}")] + [ProducesResponseType(typeof(ViewModel.Trip), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task GetTrip(Guid tripId) + { + var trip = await _repository.GetByIdAsync(tripId); + var tripViewModel = _mapper.Map(trip); + + if (tripViewModel == null) + return NotFound(); + + return Ok(tripViewModel); + } + + /// + /// Creates a new trip. + /// + /// + /// Returns the newly created trip identifier. + /// Returns the newly created trip identifier. + [HttpPost] + [ProducesResponseType(typeof(Guid), (int)HttpStatusCode.Created)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task CreateTrip([FromBody]ViewModel.CreateTripCommand command) + { + // BadRequest and InternalServerError could be throw in HttpGlobalExceptionFilter + var domainCommand = _mapper.Map(command); + domainCommand.AggregateRootId = Guid.NewGuid(); + await _dispatcher.SendAsync(domainCommand); + return Created(HttpContext.Request.GetUri().AbsoluteUri, domainCommand.AggregateRootId); + } + + /// + /// Accepts the specified trip. + /// + /// + /// + [HttpPut("accept")] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task AcceptTrip([FromBody]ViewModel.UpdateTripCommand command) + { + // BadRequest and InternalServerError could be throw in HttpGlobalExceptionFilter, and also by ValidatorActionFilter due to the UpdateTripCommandValidator. + var domainCommand = _mapper.Map(command); + domainCommand.Action = Action.Accept; + + await _dispatcher.SendAsync(domainCommand); + return Ok(); + } + + /// + /// Starts the specified trip. + /// + /// + /// + [HttpPut("start")] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task StartTrip([FromBody]ViewModel.UpdateTripCommand command) + { + // BadRequest and InternalServerError could be throw in HttpGlobalExceptionFilter, and also by ValidatorActionFilter due to the UpdateTripCommandValidator. + var domainCommand = _mapper.Map(command); + domainCommand.Action = Action.Start; + + await _dispatcher.SendAsync(domainCommand); + return Ok(); + } + + /// + /// Cancels the specified trip. + /// + /// + /// + [HttpPut("cancel")] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task CancelTrip([FromBody]ViewModel.UpdateTripCommand command) + { + // BadRequest and InternalServerError could be throw in HttpGlobalExceptionFilter, and also by ValidatorActionFilter due to the UpdateTripCommandValidator. + var domainCommand = _mapper.Map(command); + domainCommand.Action = Action.Cancel; + + await _dispatcher.SendAsync(domainCommand); + return Ok(); + } + + /// + /// Updates the current location for the specified trip. + /// + /// + /// + [HttpPut("update")] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task UpdateCurrentLocation([FromBody]ViewModel.UpdateCurrentLocationTripCommand command) + { + // BadRequest and InternalServerError could be throw in HttpGlobalExceptionFilter, and also by ValidatorActionFilter due to the UpdateTripCommandValidator. + var domainCommand = _mapper.Map(command); + domainCommand.Action = Action.UpdateCurrentLocation; + + await _dispatcher.SendAsync(domainCommand); + return Ok(); + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Trip.API/Dockerfile b/src/Application/Duber.Trip.API/Dockerfile new file mode 100644 index 0000000..5746e63 --- /dev/null +++ b/src/Application/Duber.Trip.API/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY . . +WORKDIR "/src/src/Application/Duber.Trip.API" +RUN dotnet restore +RUN dotnet build --no-restore -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Duber.Trip.API.dll"] \ No newline at end of file diff --git a/src/Application/Duber.Trip.API/Duber.Trip.API.csproj b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj new file mode 100644 index 0000000..19a38ac --- /dev/null +++ b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj @@ -0,0 +1,49 @@ + + + + netcoreapp3.1 + ..\..\..\docker-compose.dcproj + + + + bin\Debug\netcoreapp2.2\Duber.Trip.API.xml + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bin\Debug\$(TargetFramework)\$(MSBuildProjectName).xml + 1701;1702;1705;1591 + + + diff --git a/src/Application/Duber.Trip.API/Extensions/ApplicationBuilderExtensions.cs b/src/Application/Duber.Trip.API/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..bb6b3d6 --- /dev/null +++ b/src/Application/Duber.Trip.API/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,18 @@ +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Infrastructure.EventBus.Idempotency; +using Duber.Trip.API.Application.DomainEventHandlers; +using Duber.Trip.API.Application.IntegrationEvents; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Duber.Trip.API.Extensions +{ + public static class ApplicationBuilderExtensions + { + public static void UseIdempotency(this IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe, TripUpdatedIdempotentEventHandler>(); + } + } +} diff --git a/src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs b/src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d024042 --- /dev/null +++ b/src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,169 @@ +using Duber.Domain.Trip.Commands; +using Duber.Trip.API.Application.DomainEventHandlers; +using Duber.Trip.API.Infrastructure.Repository; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using AutoMapper; +using Duber.Infrastructure.EventBus.Idempotency; +using Duber.Infrastructure.EventBus.RabbitMQ.IoC; +using Duber.Infrastructure.EventBus.ServiceBus.IoC; +using Kledex; +using Kledex.Commands; +using Kledex.Configuration; +using Kledex.Domain; +using Kledex.Events; +using Kledex.Extensions; +using Kledex.Queries; +using Kledex.Store.Cosmos.Mongo.Configuration; +using Microsoft.OpenApi.Models; +using Kledex.Store.Cosmos.Mongo.Extensions; +using MongoDB.Bson.Serialization; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Duber.Trip.API.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddCQRS(this IServiceCollection services, IConfiguration configuration) + { + // Kledex only needs a type per assembly, it automatically registers the rest of the commands, events, etc. + services.Configure(configuration.GetSection("EventStoreConfiguration")); + services.AddCustomKledex(options => + { + options.PublishEvents = true; + options.SaveCommandData = true; + }, typeof(CreateTripCommand), typeof(TripCreatedDomainEventHandlerAsync)) + .AddCosmosMongoStore(__ => configuration.Get()); + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddCustomSwagger(this IServiceCollection services) + { + // swagger configuration + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Duber.Trip HTTP API", + Version = "v1", + Description = "The Duber Trip Service HTTP API" + }); + + // Set the comments path for the Swagger JSON and UI. + var xmlFile = $"{Assembly.GetEntryAssembly()?.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath); + }); + + return services; + } + + /// + /// I had to override this method since I was using automapper to map out some commands and events and Kledex internally overrides those mappings. + /// + /// + /// + /// + /// + public static IKledexServiceBuilder AddCustomKledex(this IServiceCollection services, Action setupAction, params Type[] types) + { + var typeList = types.ToList(); + typeList.Add(typeof(IDispatcher)); + + services.Scan(s => s + .FromAssembliesOf(typeList) + .AddClasses() + .AsImplementedInterfaces()); + + services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); + services.AddCustomAutoMapper(typeList); + services.Configure(setupAction); + return new KledexServiceBuilder(services); + } + + private static IServiceCollection AddCustomAutoMapper(this IServiceCollection services, List types) + { + var autoMapperConfig = new MapperConfiguration(cfg => + { + foreach (var type in types) + { + var typesToMap = type.Assembly.GetTypes() + .Where(t => t.GetTypeInfo().IsClass && !t.GetTypeInfo().IsAbstract && ( + typeof(ICommand).IsAssignableFrom(t) || + typeof(IEvent).IsAssignableFrom(t) || + typeof(IQuery<>).IsAssignableFrom(t))) + .ToList(); + + foreach (var typeToMap in typesToMap) + { + cfg.CreateMap(typeToMap, typeToMap); + cfg.AddMaps(types); + } + } + }); + + services.AddSingleton(sp => autoMapperConfig.CreateMapper()); + return services; + } + + public static IServiceCollection AddServiceBroker(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddServiceBus(configuration); + } + else + { + services.AddRabbitMQ(configuration); + } + + return services; + } + + public static IServiceCollection AddIdempotency(this IServiceCollection services) + { + services.AddTransient(); + services.RegisterIdempotentHandlers(typeof(TripUpdatedIdempotentEventHandler)); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.SetIgnoreExtraElements(true); + }); + + return services; + } + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + hcBuilder + .AddMongoDb( + configuration["EventStoreConfiguration:ConnectionString"], + mongoDatabaseName: string.Empty, + name: "TripDB-check", + tags: new string[] { "tripdb" }); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder.AddAzureServiceBusTopic(configuration, "trip-az-servicebus-check"); + } + else + { + hcBuilder.AddRabbitMQ(configuration, "trip-rabbitmqbus-check"); + } + + return services; + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Trip.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Application/Duber.Trip.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs new file mode 100644 index 0000000..3c873c0 --- /dev/null +++ b/src/Application/Duber.Trip.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Duber.Trip.API.Infrastructure.ActionResults +{ + public class InternalServerErrorObjectResult : ObjectResult + { + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } + } +} diff --git a/src/Application/Duber.Trip.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Application/Duber.Trip.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 0000000..bdfd2fa --- /dev/null +++ b/src/Application/Duber.Trip.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,69 @@ +using System.Net; +using Duber.Domain.Trip.Exceptions; +using Duber.Trip.API.Infrastructure.ActionResults; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace Duber.Trip.API.Infrastructure.Filters +{ + public class HttpGlobalExceptionFilter : IExceptionFilter + { + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) + { + _env = env; + _logger = logger; + } + + public void OnException(ExceptionContext context) + { + _logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(TripDomainArgumentNullException) || context.Exception.GetType() == typeof(TripDomainInvalidOperationException)) + { + var json = new JsonErrorResponse + { + Messages = new[] { context.Exception.Message } + }; + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + //It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new BadRequestObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An unexpected error occurred. Try it again." } + }; + + if (_env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + // It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } + } +} diff --git a/src/Application/Duber.Trip.API/Infrastructure/Filters/ValidatorActionFilter.cs b/src/Application/Duber.Trip.API/Infrastructure/Filters/ValidatorActionFilter.cs new file mode 100644 index 0000000..ad898d0 --- /dev/null +++ b/src/Application/Duber.Trip.API/Infrastructure/Filters/ValidatorActionFilter.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Duber.Trip.API.Infrastructure.Filters +{ + public class ValidatorActionFilter : IActionFilter + { + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new BadRequestObjectResult(context.ModelState); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + } +} diff --git a/src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs b/src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs new file mode 100644 index 0000000..8cd2233 --- /dev/null +++ b/src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kledex.Store.Cosmos.Mongo; +using Kledex.Store.Cosmos.Mongo.Configuration; +using Kledex.Store.Cosmos.Mongo.Documents; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +// ReSharper disable FunctionRecursiveOnAllPaths + +namespace Duber.Trip.API.Infrastructure.Repository +{ + public class EventStoreRepository : IEventStoreRepository + { + private readonly MongoDbContext _dbContext; + + public EventStoreRepository(IOptions settings) + { + _dbContext = new MongoDbContext(settings); + } + + public async Task> GetAggregatesAsync() + { + var aggregateFilter = Builders.Filter.Empty; + return await _dbContext.Aggregates.Find(aggregateFilter).ToListAsync(); + } + + public async Task> GetEventsAsync(Guid aggregateId) + { + var eventFilter = Builders.Filter.Eq("aggregateId", aggregateId.ToString()); + return await _dbContext.Events.Find(eventFilter).ToListAsync(); + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs b/src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs new file mode 100644 index 0000000..ae6c1a6 --- /dev/null +++ b/src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kledex.Store.Cosmos.Mongo.Documents; + +namespace Duber.Trip.API.Infrastructure.Repository +{ + public interface IEventStoreRepository + { + Task> GetAggregatesAsync(); + + Task> GetEventsAsync(Guid aggregateId); + } +} \ No newline at end of file diff --git a/src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs b/src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs new file mode 100644 index 0000000..92b6db1 --- /dev/null +++ b/src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Idempotency; +using Kledex.Store.Cosmos.Mongo.Configuration; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace Duber.Trip.API.Infrastructure.Repository +{ + public class IdempotencyStoreProvider : IIdempotencyStoreProvider + { + private readonly IMongoDatabase _db; + + public IdempotencyStoreProvider(IOptions settings) + { + var mongoClient = new MongoClient(settings.Value.ConnectionString); + _db = mongoClient.GetDatabase(settings.Value.DatabaseName); + } + + public async Task SaveAsync(IdempotentMessage message) + { + var exists = await ExistsAsync(message.MessageId); + if (exists) return; + + var collection = _db.GetCollection("IdempotentMessages"); + await collection.InsertOneAsync(message); + } + + public async Task ExistsAsync(string id) + { + var collection = _db.GetCollection("IdempotentMessages"); + var filter = Builders.Filter.Eq("MessageId", id); + var message = await collection.FindAsync(filter).Result.FirstOrDefaultAsync(); + + return message != null; + } + } +} diff --git a/src/Application/Duber.Trip.API/Program.cs b/src/Application/Duber.Trip.API/Program.cs new file mode 100644 index 0000000..a1e30df --- /dev/null +++ b/src/Application/Duber.Trip.API/Program.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Duber.Trip.API +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .ConfigureAppConfiguration((builderContext, config) => + { + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, builder) => + { + builder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + builder.AddConsole(); + builder.AddDebug(); + builder.AddApplicationInsights(); + }) + .Build(); + } +} diff --git a/src/Application/Duber.Trip.API/Properties/launchSettings.json b/src/Application/Duber.Trip.API/Properties/launchSettings.json new file mode 100644 index 0000000..804cb03 --- /dev/null +++ b/src/Application/Duber.Trip.API/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55269/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Duber.Trip.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:55270/" + } + } +} diff --git a/src/Application/Duber.Trip.API/Startup.cs b/src/Application/Duber.Trip.API/Startup.cs new file mode 100644 index 0000000..48532a4 --- /dev/null +++ b/src/Application/Duber.Trip.API/Startup.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Duber.Trip.API.Application.Validations; +using Duber.Trip.API.Extensions; +using Duber.Trip.API.Infrastructure.Filters; +using FluentValidation.AspNetCore; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +// ReSharper disable InconsistentNaming + +namespace Duber.Trip.API +{ + 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 IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddApplicationInsightsTelemetry(Configuration) + .AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + options.Filters.Add(typeof(ValidatorActionFilter)); + }) + .AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore) + .AddFluentValidation(x => x.RegisterValidatorsFromAssemblyContaining()); + + services.AddCQRS(Configuration) + .AddIdempotency() + .AddServiceBroker(Configuration) + .AddHealthChecks(Configuration) + .AddCustomSwagger(); + + services.AddOptions() + .AddCors(options => + { + // be careful with this policy, in production you should only add authorized origins, methods, etc. + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseCors("CorsPolicy") + .UseRouting() + .UseIdempotency(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/readiness", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + + app.UseSwagger(x => + { + var reverseProxyPrefix = Configuration.GetValue("ReverseProxyPrefix"); + if (!string.IsNullOrEmpty(reverseProxyPrefix)) + { + x.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.Servers = new List + { + new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}/{reverseProxyPrefix}" } + }); + } + }) + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("./swagger/v1/swagger.json", "Duber.Trip V1"); + c.RoutePrefix = string.Empty; + }); + } + } +} diff --git a/src/Application/Duber.Trip.API/appsettings.Development.json b/src/Application/Duber.Trip.API/appsettings.Development.json new file mode 100644 index 0000000..fa8ce71 --- /dev/null +++ b/src/Application/Duber.Trip.API/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Application/Duber.Trip.API/appsettings.json b/src/Application/Duber.Trip.API/appsettings.json new file mode 100644 index 0000000..b88623d --- /dev/null +++ b/src/Application/Duber.Trip.API/appsettings.json @@ -0,0 +1,28 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Information" + } + } + }, + "AzureServiceBusEnabled": false, + "EventBusConnection": "", + "EventBusConnectionHC": "", + "SubscriptionClientName": "Trip", + "EventStoreConfiguration": { + "ConnectionString": "mongodb://nosql.data", + "DatabaseName": "EventStore", + "AggregateCollectionName": "Aggregates", + "EventCollectionName": "Events", + "CommandCollectionName": "Commands" + }, + "EventBusRetryCount": 5, + "ReverseProxyPrefix": "" +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs new file mode 100644 index 0000000..3a3b639 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCancelledIntegrationEvent.cs @@ -0,0 +1,11 @@ +using System; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Events +{ + public class TripCancelledIntegrationEvent : TripEventBase + { + public TripCancelledIntegrationEvent(Guid tripId, string connectionId) : base(tripId, connectionId) + { + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCreatedDomainEvent.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCreatedDomainEvent.cs new file mode 100644 index 0000000..547405f --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripCreatedDomainEvent.cs @@ -0,0 +1,11 @@ +using System; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Events +{ + public class TripCreatedIntegrationEvent : TripEventBase + { + public TripCreatedIntegrationEvent(Guid tripId, string connectionId) : base(tripId, connectionId) + { + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripEventBase.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripEventBase.cs new file mode 100644 index 0000000..9a63080 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripEventBase.cs @@ -0,0 +1,18 @@ +using System; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Events +{ + public class TripEventBase : IntegrationEvent + { + public TripEventBase(Guid tripId, string connectionId) + { + TripId = tripId; + ConnectionId = connectionId; + } + + public string ConnectionId { get; } + + public Guid TripId { get; } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs new file mode 100644 index 0000000..abb6a82 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripFinishedIntegrationEvent.cs @@ -0,0 +1,11 @@ +using System; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Events +{ + public class TripFinishedIntegrationEvent : TripEventBase + { + public TripFinishedIntegrationEvent(Guid tripId, string connectionId) : base(tripId, connectionId) + { + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs new file mode 100644 index 0000000..97bd965 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs @@ -0,0 +1,35 @@ +using System; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Events +{ + public class TripUpdatedIntegrationEvent : TripEventBase + { + public TripUpdatedIntegrationEvent(Guid tripId, string connectionId, Location currentLocation, Action action) : base(tripId, connectionId) + { + CurrentLocation = currentLocation; + Action = action; + } + + public Action Action { get; } + + public Location CurrentLocation { get; } + } + + public enum Action + { + Accepted = 1, + Started = 2, + Cancelled = 3, + FinishedEarlier = 4, + UpdatedCurrentLocation = 5 + } + + public class Location + { + public double Latitude { get; set; } + + public double Longitude { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs new file mode 100644 index 0000000..aed4d4f --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Trip.Notifications.Application.IntegrationEvents.Events; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Handlers +{ + public class TripCreatedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + + public TripCreatedIntegrationEventHandler(IHubContext hubContext, ILogger logger) + { + _hubContext = hubContext; + _logger = logger; + } + + public async Task Handle(TripCreatedIntegrationEvent @event) + { + _logger.LogInformation($"Trip {@event.TripId} has been created."); + await _hubContext.Clients.Client(@event.ConnectionId).SendAsync("NotifyTrip", "Created"); + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripFinishedIntegrationEventHandler.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripFinishedIntegrationEventHandler.cs new file mode 100644 index 0000000..e2680c9 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripFinishedIntegrationEventHandler.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Trip.Notifications.Application.IntegrationEvents.Events; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Handlers +{ + public class TripFinishedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + + public TripFinishedIntegrationEventHandler(IHubContext hubContext, ILogger logger) + { + _hubContext = hubContext; + _logger = logger; + } + + public async Task Handle(TripFinishedIntegrationEvent @event) + { + _logger.LogInformation($"Trip {@event.TripId} has finished."); + await _hubContext.Clients.Client(@event.ConnectionId).SendAsync("NotifyTrip", "Finished"); + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs new file mode 100644 index 0000000..1bbe488 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Trip.Notifications.Application.IntegrationEvents.Events; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +namespace Duber.Trip.Notifications.Application.IntegrationEvents.Handlers +{ + public class TripUpdatedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + + public TripUpdatedIntegrationEventHandler(IHubContext hubContext, ILogger logger) + { + _hubContext = hubContext; + _logger = logger; + } + + public async Task Handle(TripUpdatedIntegrationEvent @event) + { + _logger.LogInformation($"Trip {@event.TripId} has been updated."); + + if (@event.Action == Action.UpdatedCurrentLocation) + { + await _hubContext.Clients.Client(@event.ConnectionId).SendAsync("UpdateCurrentPosition", @event.CurrentLocation); + } + else + { + await _hubContext.Clients.Client(@event.ConnectionId).SendAsync("NotifyTrip", @event.Action.ToString()); + } + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Dockerfile b/src/Application/Duber.Trip.Notifications/Dockerfile new file mode 100644 index 0000000..729f362 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Dockerfile @@ -0,0 +1,21 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY ["src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj", "src/Application/Duber.Trip.Notifications/"] +RUN dotnet restore "src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj" +COPY . . +WORKDIR "/src/src/Application/Duber.Trip.Notifications" +RUN dotnet build "Duber.Trip.Notifications.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Duber.Trip.Notifications.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Duber.Trip.Notifications.dll"] \ No newline at end of file diff --git a/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj b/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj new file mode 100644 index 0000000..8869f8b --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + Linux + ..\..\.. + + + + + + + + + + + + + + + + + + diff --git a/src/Application/Duber.Trip.Notifications/Extensions/ApplicationBuilderExtensions.cs b/src/Application/Duber.Trip.Notifications/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..5b4c895 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,21 @@ +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Trip.Notifications.Application.IntegrationEvents.Events; +using Duber.Trip.Notifications.Application.IntegrationEvents.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Duber.Trip.Notifications.Extensions +{ + public static class ApplicationBuilderExtensions + { + public static IApplicationBuilder UseServiceBroker(this IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + + return app; + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Extensions/ServiceCollectionExtensions.cs b/src/Application/Duber.Trip.Notifications/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..c1bb23b --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,73 @@ +using Duber.Infrastructure.EventBus.RabbitMQ.IoC; +using Duber.Infrastructure.EventBus.ServiceBus.IoC; +using Duber.Trip.Notifications.Application.IntegrationEvents.Handlers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Duber.Trip.Notifications.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddServiceBroker(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddServiceBus(configuration); + } + else + { + services.AddRabbitMQ(configuration); + } + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddSignalR(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("IsDeployedOnCluster")) + { + services + .AddSignalR() + .AddRedis(configuration.GetConnectionString("SignalrBackPlane")); + } + else + { + services.AddSignalR(); + } + + return services; + } + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + if (configuration.GetValue("IsDeployedOnCluster")) + { + hcBuilder + .AddRedis( + configuration.GetConnectionString("SignalrBackPlane"), + name: "SignalrBackPlane-check", + tags: new string[] { "backplane" }); + } + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder.AddAzureServiceBusTopic(configuration, "notifications-az-servicebus-check"); + } + else + { + hcBuilder.AddRabbitMQ(configuration, "notifications-rabbitmqbus-check"); + } + + return services; + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/NotificationsHub.cs b/src/Application/Duber.Trip.Notifications/NotificationsHub.cs new file mode 100644 index 0000000..2199f1d --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/NotificationsHub.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Duber.Trip.Notifications +{ + public class NotificationsHub : Hub + { + public string GetConnectionId() + { + return Context.ConnectionId; + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Program.cs b/src/Application/Duber.Trip.Notifications/Program.cs new file mode 100644 index 0000000..f6821ad --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Program.cs @@ -0,0 +1,33 @@ +using System.IO; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace Duber.Trip.Notifications +{ + public class Program + { + public static void Main(string[] args) + { + var configuration = GetConfiguration(); + CreateHostBuilder(configuration, args).Run(); + } + + public static IWebHost CreateHostBuilder(IConfiguration configuration, string[] args) => + WebHost.CreateDefaultBuilder(args) + .CaptureStartupErrors(false) + .UseStartup() + .UseConfiguration(configuration) + .Build(); + + private static IConfiguration GetConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + return builder.Build(); + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/Properties/launchSettings.json b/src/Application/Duber.Trip.Notifications/Properties/launchSettings.json new file mode 100644 index 0000000..97a53a6 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:32778", + "sslPort": 0 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Duber.Trip.Notifications": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:32778" + } + } +} \ No newline at end of file diff --git a/src/Application/Duber.Trip.Notifications/Startup.cs b/src/Application/Duber.Trip.Notifications/Startup.cs new file mode 100644 index 0000000..a0b8b36 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/Startup.cs @@ -0,0 +1,77 @@ +using System; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Duber.Trip.Notifications.Extensions; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Duber.Trip.Notifications +{ + 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 IServiceProvider ConfigureServices(IServiceCollection services) + { + services + .AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .SetIsOriginAllowed((host) => true) + .AllowCredentials()); + }); + + services.AddSignalR(Configuration) + .AddServiceBroker(Configuration) + .AddHealthChecks(Configuration); + + var container = new ContainerBuilder(); + container.Populate(services); + + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseCors("CorsPolicy") + .UseRouting() + .UseServiceBroker(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHub("/hub/notification", + options => options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransports.All); + + endpoints.MapHealthChecks("/readiness", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/appsettings.Development.json b/src/Application/Duber.Trip.Notifications/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Application/Duber.Trip.Notifications/appsettings.json b/src/Application/Duber.Trip.Notifications/appsettings.json new file mode 100644 index 0000000..c4e16a7 --- /dev/null +++ b/src/Application/Duber.Trip.Notifications/appsettings.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Information" + } + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "SignalrBackPlane": "" + }, + "AzureServiceBusEnabled": false, + "EventBusConnection": "", + "EventBusConnectionHC": "", + "SubscriptionClientName": "Notifications", + "EventBusRetryCount": 5, + "IsDeployedOnCluster": false +} diff --git a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj new file mode 100644 index 0000000..e1d6178 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + diff --git a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/UnitTest1.cs b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/UnitTest1.cs new file mode 100644 index 0000000..8a790f6 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/UnitTest1.cs @@ -0,0 +1,13 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Duber.Domain.Driver.UnitTest +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj b/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj new file mode 100644 index 0000000..4c339e2 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Domain/Driver/Duber.Domain.Driver/Exceptions/DriverDomainException.cs b/src/Domain/Driver/Duber.Domain.Driver/Exceptions/DriverDomainException.cs new file mode 100644 index 0000000..c2a9450 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Exceptions/DriverDomainException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Duber.Domain.Driver.Exceptions +{ + public class DriverDomainException : Exception + { + public DriverDomainException() + { } + + public DriverDomainException(string message) + : base(message) + { } + + public DriverDomainException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.Designer.cs b/src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.Designer.cs new file mode 100644 index 0000000..af380d6 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.Designer.cs @@ -0,0 +1,139 @@ +// +using Duber.Domain.Driver.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace Duber.Domain.Driver.Migrations +{ + [DbContext(typeof(DriverContext))] + [Migration("20180328053630_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("Relational:Sequence:Driver.driverseq", "'driverseq', 'Driver', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("Relational:Sequence:Driver.vehicleseq", "'vehicleseq', 'Driver', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "driverseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "Driver") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Email") + .IsRequired(); + + b.Property("Name") + .IsRequired(); + + b.Property("PhoneNumber"); + + b.Property("Rating"); + + b.Property("StatusId"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("StatusId"); + + b.ToTable("Drivers","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.DriverStatus", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("DriverStatuses","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "vehicleseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "Driver") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Active"); + + b.Property("Brand") + .IsRequired(); + + b.Property("DriverId"); + + b.Property("Model") + .IsRequired(); + + b.Property("Plate") + .IsRequired(); + + b.Property("TypeId"); + + b.HasKey("Id"); + + b.HasIndex("DriverId"); + + b.HasIndex("TypeId"); + + b.ToTable("Vehicles","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.VehicleType", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("VehicleTypes","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Driver", b => + { + b.HasOne("Duber.Domain.Driver.Model.DriverStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Vehicle", b => + { + b.HasOne("Duber.Domain.Driver.Model.Driver") + .WithMany("Vehicles") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Duber.Domain.Driver.Model.VehicleType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.cs b/src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.cs new file mode 100644 index 0000000..5ca0fe8 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Migrations/20180328053630_InitialCreate.cs @@ -0,0 +1,159 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Duber.Domain.Driver.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Driver"); + + migrationBuilder.CreateSequence( + name: "driverseq", + schema: "Driver", + incrementBy: 10); + + migrationBuilder.CreateSequence( + name: "vehicleseq", + schema: "Driver", + incrementBy: 10); + + migrationBuilder.CreateTable( + name: "DriverStatuses", + schema: "Driver", + columns: table => new + { + Id = table.Column(nullable: false, defaultValue: 1), + Name = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DriverStatuses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "VehicleTypes", + schema: "Driver", + columns: table => new + { + Id = table.Column(nullable: false, defaultValue: 1), + Name = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VehicleTypes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Drivers", + schema: "Driver", + columns: table => new + { + Id = table.Column(nullable: false), + Email = table.Column(nullable: false), + Name = table.Column(nullable: false), + PhoneNumber = table.Column(nullable: true), + Rating = table.Column(nullable: false), + StatusId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Drivers", x => x.Id); + table.ForeignKey( + name: "FK_Drivers_DriverStatuses_StatusId", + column: x => x.StatusId, + principalSchema: "Driver", + principalTable: "DriverStatuses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Vehicles", + schema: "Driver", + columns: table => new + { + Id = table.Column(nullable: false), + Active = table.Column(nullable: false), + Brand = table.Column(nullable: false), + DriverId = table.Column(nullable: false), + Model = table.Column(nullable: false), + Plate = table.Column(nullable: false), + TypeId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Vehicles", x => x.Id); + table.ForeignKey( + name: "FK_Vehicles_Drivers_DriverId", + column: x => x.DriverId, + principalSchema: "Driver", + principalTable: "Drivers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Vehicles_VehicleTypes_TypeId", + column: x => x.TypeId, + principalSchema: "Driver", + principalTable: "VehicleTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_Email", + schema: "Driver", + table: "Drivers", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_StatusId", + schema: "Driver", + table: "Drivers", + column: "StatusId"); + + migrationBuilder.CreateIndex( + name: "IX_Vehicles_DriverId", + schema: "Driver", + table: "Vehicles", + column: "DriverId"); + + migrationBuilder.CreateIndex( + name: "IX_Vehicles_TypeId", + schema: "Driver", + table: "Vehicles", + column: "TypeId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Vehicles", + schema: "Driver"); + + migrationBuilder.DropTable( + name: "Drivers", + schema: "Driver"); + + migrationBuilder.DropTable( + name: "VehicleTypes", + schema: "Driver"); + + migrationBuilder.DropTable( + name: "DriverStatuses", + schema: "Driver"); + + migrationBuilder.DropSequence( + name: "driverseq", + schema: "Driver"); + + migrationBuilder.DropSequence( + name: "vehicleseq", + schema: "Driver"); + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Migrations/DriverContextModelSnapshot.cs b/src/Domain/Driver/Duber.Domain.Driver/Migrations/DriverContextModelSnapshot.cs new file mode 100644 index 0000000..fade812 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Migrations/DriverContextModelSnapshot.cs @@ -0,0 +1,138 @@ +// +using Duber.Domain.Driver.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace Duber.Domain.Driver.Migrations +{ + [DbContext(typeof(DriverContext))] + partial class DriverContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("Relational:Sequence:Driver.driverseq", "'driverseq', 'Driver', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("Relational:Sequence:Driver.vehicleseq", "'vehicleseq', 'Driver', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "driverseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "Driver") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Email") + .IsRequired(); + + b.Property("Name") + .IsRequired(); + + b.Property("PhoneNumber"); + + b.Property("Rating"); + + b.Property("StatusId"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("StatusId"); + + b.ToTable("Drivers","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.DriverStatus", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("DriverStatuses","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "vehicleseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "Driver") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Active"); + + b.Property("Brand") + .IsRequired(); + + b.Property("DriverId"); + + b.Property("Model") + .IsRequired(); + + b.Property("Plate") + .IsRequired(); + + b.Property("TypeId"); + + b.HasKey("Id"); + + b.HasIndex("DriverId"); + + b.HasIndex("TypeId"); + + b.ToTable("Vehicles","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.VehicleType", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("VehicleTypes","Driver"); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Driver", b => + { + b.HasOne("Duber.Domain.Driver.Model.DriverStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Duber.Domain.Driver.Model.Vehicle", b => + { + b.HasOne("Duber.Domain.Driver.Model.Driver") + .WithMany("Vehicles") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Duber.Domain.Driver.Model.VehicleType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Model/Driver.cs b/src/Domain/Driver/Duber.Domain.Driver/Model/Driver.cs new file mode 100644 index 0000000..9bec6e0 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Model/Driver.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Domain.Driver.Exceptions; +using Duber.Infrastructure.DDD; +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable ConvertToAutoProperty +// ReSharper disable UnusedAutoPropertyAccessor.Local +// ReSharper disable NotAccessedField.Local + +namespace Duber.Domain.Driver.Model +{ + public class Driver : Entity, IAggregateRoot + { + private string _name; + private string _email; + private string _phoneNumber; + private int _rating; + private Vehicle _currentVehicle; + private int _statusId; + + // Using a private collection field, better for DDD Aggregate's encapsulation + // so _vehicles cannot be added from "outside the AggregateRoot" directly to the collection, + // but only through the method AddVehicle() which includes behaviour. + private readonly List _vehicles; + + public string Name => _name; + + public string Email => _email; + + public string PhoneNumber => _phoneNumber; + + public int Rating => _rating; + + public Vehicle CurrentVehicle => GetCurrentVehicle(); + + // EF navigation property + public DriverStatus Status { get; private set; } + + // Using List<>.AsReadOnly() + // This will create a read only wrapper around the private list so is protected against "external updates". + // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance) + //https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx + public IReadOnlyCollection Vehicles => _vehicles; + + protected Driver() + { + _vehicles = new List(); + } + + public Driver( + string name, + string email, + int rating, + string vehiclePlate, + string vehicleBrand, + string vehicleModel, + VehicleType vehicleType, + string phoneNumber = null) : this() + { + if (rating < 0 || rating > 5) throw new DriverDomainException("Driver rating should be between 1 and 5"); + + _name = !string.IsNullOrWhiteSpace(name) ? name : throw new DriverDomainException(nameof(name)); + _email = !string.IsNullOrWhiteSpace(email) ? email : throw new DriverDomainException(nameof(email)); + _phoneNumber = phoneNumber; + _rating = rating; + _statusId = DriverStatus.Active.Id; + + _currentVehicle = new Vehicle(vehiclePlate, vehicleBrand, vehicleModel, vehicleType); + _vehicles.Add(_currentVehicle); + } + + public void AddVehicle(string vehiclePlate, string vehicleBrand, string vehicleModel, VehicleType vehicleType) + { + if (CurrentVehicle == null) + throw new DriverDomainException($"Driver {_name} doesn't have an active vehicle."); + + _currentVehicle.Inactivate(); + var newVehicle = new Vehicle(vehiclePlate, vehicleBrand, vehicleModel, vehicleType); + _vehicles.Add(newVehicle); + _currentVehicle = newVehicle; + } + + public void Inactivate() + { + // business rules here + _statusId = DriverStatus.Inactive.Id; + } + + private Vehicle GetCurrentVehicle() + { + try + { + _currentVehicle = _currentVehicle ?? _vehicles.SingleOrDefault(x => x.Active); + return _currentVehicle; + } + catch (InvalidOperationException) + { + throw new DriverDomainException("There are more than one active vehicles."); + } + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Model/DriverStatus.cs b/src/Domain/Driver/Duber.Domain.Driver/Model/DriverStatus.cs new file mode 100644 index 0000000..07f7a4e --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Model/DriverStatus.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Infrastructure.DDD; + +namespace Duber.Domain.Driver.Model +{ + public class DriverStatus : Enumeration + { + public static DriverStatus Available = new DriverStatus(1, nameof(Available)); + public static DriverStatus Busy = new DriverStatus(2, nameof(Busy)); + public static DriverStatus Inactive = new DriverStatus(3, nameof(Inactive)); + public static DriverStatus Active = new DriverStatus(4, nameof(Active)); + + protected DriverStatus() { } + + public DriverStatus(int id, string name) + : base(id, name) + { + } + + public static IEnumerable List() + { + return new[] { Available, Busy, Inactive, Active }; + } + + public static DriverStatus FromName(string name) + { + var state = List() + .SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)); + + if (state == null) + { + throw new ArgumentException($"Possible values for DriverStatus: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + + public static DriverStatus From(int id) + { + var state = List().SingleOrDefault(s => s.Id == id); + + if (state == null) + { + throw new ArgumentException($"Possible values for DriverStatus: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Model/Vehicle.cs b/src/Domain/Driver/Duber.Domain.Driver/Model/Vehicle.cs new file mode 100644 index 0000000..73cc6b7 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Model/Vehicle.cs @@ -0,0 +1,60 @@ +using Duber.Domain.Driver.Exceptions; +using Duber.Infrastructure.DDD; +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable ConvertToAutoProperty +// ReSharper disable UnusedAutoPropertyAccessor.Local +// ReSharper disable NotAccessedField.Local + +namespace Duber.Domain.Driver.Model +{ + public class Vehicle : Entity + { + private string _plate; + private string _brand; + private string _model; + private bool _active; + private int _typeId; + + public string Plate => _plate; + + public string Brand => _brand; + + public string Model => _model; + + public bool Active => _active; + + // EF navigation property + public VehicleType Type { get; private set; } + + protected Vehicle() + { + } + + // contructor is internal due to doesn't make sense create a vehicle itself without a driver (in the Duber business context) + // so the only way to create vehicles is throught the Driver aggregate root. + internal Vehicle(string plate, string brand, string model, VehicleType type) + { + _plate = !string.IsNullOrWhiteSpace(plate) ? plate : throw new DriverDomainException(nameof(plate)); + _brand = !string.IsNullOrWhiteSpace(brand) ? brand : throw new DriverDomainException(nameof(brand)); + _model = !string.IsNullOrWhiteSpace(model) ? model : throw new DriverDomainException(nameof(model)); + _active = true; + _typeId = type.Id; + } + + public void Inactivate() + { + if (!_active) + throw new DriverDomainException($"The vehicule {_plate} is already inactive"); + + _active = false; + } + + public void Activate() + { + if (!_active) + throw new DriverDomainException($"The vehicule {_plate} is already active"); + + _active = true; + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Model/VehicleType.cs b/src/Domain/Driver/Duber.Domain.Driver/Model/VehicleType.cs new file mode 100644 index 0000000..ef800ac --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Model/VehicleType.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Infrastructure.DDD; + +namespace Duber.Domain.Driver.Model +{ + public class VehicleType : Enumeration + { + public static VehicleType Car = new VehicleType(1, nameof(Car)); + public static VehicleType Bike = new VehicleType(2, nameof(Bike)); + public static VehicleType TuckTuck = new VehicleType(3, "Tuck Tuck"); + + protected VehicleType() { } + + public VehicleType(int id, string name) + : base(id, name) + { + } + + public static IEnumerable List() + { + return new[] { Car, Bike, TuckTuck }; + } + + public static VehicleType FromName(string name) + { + var state = List() + .SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)); + + if (state == null) + { + throw new ArgumentException($"Possible values for VehicleType: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + + public static VehicleType From(int id) + { + var state = List().SingleOrDefault(s => s.Id == id); + + if (state == null) + { + throw new ArgumentException($"Possible values for VehicleType: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContext.cs b/src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContext.cs new file mode 100644 index 0000000..2cdecf0 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContext.cs @@ -0,0 +1,44 @@ +using Duber.Domain.Driver.Model; +using Duber.Domain.Driver.Persistence.EntityConfigurations; +using Duber.Infrastructure.Repository.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Duber.Domain.Driver.Persistence +{ + public class DriverContext : DbContext, IUnitOfWork + { + // ReSharper disable once InconsistentNaming + public const string DEFAULT_SCHEMA = "Driver"; + + public DriverContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new VehicleTypeEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new DriverStatusEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new VehicleEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new DriverEntityTypeConfiguration()); + } + + public DbSet Drivers { get; set; } + + public DbSet DriverStatuses { get; set; } + + public DbSet Vehicles { get; set; } + + public DbSet VehicleTypes { get; set; } + } + + // in order to migration creation works. + public class DriverContextDesignFactory : IDesignTimeDbContextFactory + { + public DriverContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Initial Catalog=Duber.WebSiteDb;Integrated Security=true"); + + return new DriverContext(optionsBuilder.Options); + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContextSeed.cs b/src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContextSeed.cs new file mode 100644 index 0000000..5630d04 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Persistence/DriverContextSeed.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duber.Domain.Driver.Model; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Polly; + +namespace Duber.Domain.Driver.Persistence +{ + public class DriverContextSeed + { + public async Task SeedAsync(DriverContext context, ILogger logger) + { + var policy = CreatePolicy(logger, nameof(DriverContextSeed)); + + await policy.ExecuteAsync(async () => + { + using (context) + { + if (!context.DriverStatuses.Any()) + { + context.DriverStatuses.AddRange(GetPreconfiguredDriverStatuses()); + await context.SaveChangesAsync(); + } + + if (!context.VehicleTypes.Any()) + { + context.VehicleTypes.AddRange(GetPreconfiguredVehicleTypes()); + await context.SaveChangesAsync(); + } + + if (!context.Drivers.Any()) + { + context.Drivers.AddRange(GetPreconfiguredDrivers()); + await context.SaveChangesAsync(); + } + } + }); + } + + private static IEnumerable GetPreconfiguredDrivers() => new List + { + new Model.Driver("The Transporter", "Jason.Statham@hotmail.com", 3, "YUI 789", "Audi", "2002", VehicleType.Car), + new Model.Driver("Toreto", "toreto@gmail.com", 4, "BNE 456", "Ford", "2009", VehicleType.Car), + new Model.Driver("Jackie Chan", "jackie@gmail.com", 5, "EQM 197", "Cat", "2015", VehicleType.TuckTuck), + new Model.Driver("Robert De Niro", "deniro@hotmail.com", 5, "GXU 713", "BMW", "2013", VehicleType.Car), + new Model.Driver("Valentino Rossi", "vrossi@hotmail.com", 4, "GPX 570", "Yamaha", "2017", VehicleType.Bike) + }; + + private static IEnumerable GetPreconfiguredVehicleTypes() => new List + { + VehicleType.Car, + VehicleType.Bike, + VehicleType.TuckTuck + }; + + private static IEnumerable GetPreconfiguredDriverStatuses() => new List + { + DriverStatus.Available, + DriverStatus.Busy, + DriverStatus.Inactive, + DriverStatus.Active + }; + + private static AsyncPolicy CreatePolicy(ILogger logger, string prefix, int retries = 3) + { + return Policy.Handle(). + WaitAndRetryAsync( + retryCount: retries, + sleepDurationProvider: retry => TimeSpan.FromSeconds(5), + onRetry: (exception, timeSpan, retry, ctx) => + { + logger.LogTrace($"[{prefix}] Exception {exception.GetType().Name} with message ${exception.Message} detected on attempt {retry} of {retries}"); + } + ); + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverEntityTypeConfiguration.cs b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverEntityTypeConfiguration.cs new file mode 100644 index 0000000..7922929 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverEntityTypeConfiguration.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Duber.Domain.Driver.Persistence.EntityConfigurations +{ + internal class DriverEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Drivers", DriverContext.DEFAULT_SCHEMA); + + builder.HasKey(b => b.Id); + + builder.Ignore(b => b.DomainEvents); + + builder.Ignore(b => b.CurrentVehicle); + + builder.Property(b => b.Id) + .UseHiLo("driverseq", DriverContext.DEFAULT_SCHEMA); + + builder.Property(b => b.Name) + .IsRequired(); + + builder.Property(b => b.PhoneNumber) + .IsRequired(false); + + builder.Property(b => b.Rating) + .IsRequired(); + + builder.Property(b => b.Email) + .IsRequired(); + + builder.HasIndex(x => x.Email) + .IsUnique(); + + builder + .Property("_statusId") + .UsePropertyAccessMode(PropertyAccessMode.Field) + .HasColumnName("StatusId") + .IsRequired(); + + builder.HasOne(p => p.Status) + .WithMany() + .HasForeignKey("_statusId"); + + builder.HasMany(b => b.Vehicles) + .WithOne() + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Cascade); + + var navigation = builder.Metadata.FindNavigation(nameof(Model.Driver.Vehicles)); + navigation.SetPropertyAccessMode(PropertyAccessMode.Field); + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverStatusEntityTypeConfiguration.cs b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverStatusEntityTypeConfiguration.cs new file mode 100644 index 0000000..86dd029 --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/DriverStatusEntityTypeConfiguration.cs @@ -0,0 +1,25 @@ +using Duber.Domain.Driver.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Duber.Domain.Driver.Persistence.EntityConfigurations +{ + internal class DriverStatusEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("DriverStatuses", DriverContext.DEFAULT_SCHEMA); + + builder.HasKey(ct => ct.Id); + + builder.Property(ct => ct.Id) + .HasDefaultValue(1) + .ValueGeneratedNever() + .IsRequired(); + + builder.Property(ct => ct.Name) + .HasMaxLength(200) + .IsRequired(); + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleEntityTypeConfiguration.cs b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleEntityTypeConfiguration.cs new file mode 100644 index 0000000..bd82f8f --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleEntityTypeConfiguration.cs @@ -0,0 +1,45 @@ +using Duber.Domain.Driver.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Duber.Domain.Driver.Persistence.EntityConfigurations +{ + internal class VehicleEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Vehicles", DriverContext.DEFAULT_SCHEMA); + + builder.HasKey(b => b.Id); + + builder.Ignore(b => b.DomainEvents); + + builder.Property(b => b.Id) + .UseHiLo("vehicleseq", DriverContext.DEFAULT_SCHEMA); + + builder.Property(b => b.Active) + .IsRequired(); + + builder.Property(b => b.Brand) + .IsRequired(); + + builder.Property(b => b.Model) + .IsRequired(); + + builder.Property(b => b.Plate) + .IsRequired(); + + builder + .Property("_typeId") + .UsePropertyAccessMode(PropertyAccessMode.Field) + .HasColumnName("TypeId") + .IsRequired(); + + builder.Property("DriverId").IsRequired(); + + builder.HasOne(p => p.Type) + .WithMany() + .HasForeignKey("_typeId"); + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleTypeEntityTypeConfiguration.cs b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleTypeEntityTypeConfiguration.cs new file mode 100644 index 0000000..0178dbe --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Persistence/EntityConfigurations/VehicleTypeEntityTypeConfiguration.cs @@ -0,0 +1,25 @@ +using Duber.Domain.Driver.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Duber.Domain.Driver.Persistence.EntityConfigurations +{ + internal class VehicleTypeEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("VehicleTypes", DriverContext.DEFAULT_SCHEMA); + + builder.HasKey(ct => ct.Id); + + builder.Property(ct => ct.Id) + .HasDefaultValue(1) + .ValueGeneratedNever() + .IsRequired(); + + builder.Property(ct => ct.Name) + .HasMaxLength(200) + .IsRequired(); + } + } +} diff --git a/src/Domain/Driver/Duber.Domain.Driver/Repository/DriverRepository.cs b/src/Domain/Driver/Duber.Domain.Driver/Repository/DriverRepository.cs new file mode 100644 index 0000000..305ce1d --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Repository/DriverRepository.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duber.Domain.Driver.Persistence; +using Duber.Infrastructure.Repository.Abstractions; +using Microsoft.EntityFrameworkCore; + +namespace Duber.Domain.Driver.Repository +{ + public class DriverRepository : IDriverRepository + { + private readonly DriverContext _context; + + public DriverRepository(DriverContext context) + { + _context = context; + } + + public IUnitOfWork UnitOfWork => _context; + + public async Task> GetDriversAsync() + { + return await _context.Drivers + .Include(x => x.Status) + .Include(x => x.Vehicles) + .ThenInclude(x => x.Type) + .ToListAsync(); + } + + public async Task GetDriverAsync(int driverId) + { + return await _context.Drivers.SingleOrDefaultAsync(x => x.Id == driverId); + } + + public Model.Driver GetDriver(int driverId) + { + return _context.Drivers.SingleOrDefault(x => x.Id == driverId); + } + + public void Update(Model.Driver driver) + { + _context.Entry(driver).State = EntityState.Modified; + } + } +} \ No newline at end of file diff --git a/src/Domain/Driver/Duber.Domain.Driver/Repository/IDriverRepository.cs b/src/Domain/Driver/Duber.Domain.Driver/Repository/IDriverRepository.cs new file mode 100644 index 0000000..75d598e --- /dev/null +++ b/src/Domain/Driver/Duber.Domain.Driver/Repository/IDriverRepository.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Duber.Infrastructure.Repository.Abstractions; + +namespace Duber.Domain.Driver.Repository +{ + public interface IDriverRepository : IRepository + { + Model.Driver GetDriver(int driverId); + + Task> GetDriversAsync(); + + Task GetDriverAsync(int driverId); + + void Update(Model.Driver driver); + } +} \ No newline at end of file diff --git a/src/Domain/Duber.Domain.ACL/Adapters/PaymentServiceAdapter.cs b/src/Domain/Duber.Domain.ACL/Adapters/PaymentServiceAdapter.cs new file mode 100644 index 0000000..d128369 --- /dev/null +++ b/src/Domain/Duber.Domain.ACL/Adapters/PaymentServiceAdapter.cs @@ -0,0 +1,35 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Duber.Domain.ACL.Contracts; +using Duber.Domain.ACL.Translators; +using Duber.Domain.SharedKernel.Model; +using Duber.Infrastructure.Resilience.Http; + +namespace Duber.Domain.ACL.Adapters +{ + public class PaymentServiceAdapter : IPaymentServiceAdapter + { + private readonly ResilientHttpClient _httpClient; + private readonly string _paymentServiceBaseUrl; + + public PaymentServiceAdapter(ResilientHttpClient httpClient, string paymentServiceBaseUrl) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _paymentServiceBaseUrl = !string.IsNullOrWhiteSpace(paymentServiceBaseUrl) ? paymentServiceBaseUrl : throw new ArgumentNullException(nameof(paymentServiceBaseUrl)); + } + + public async Task ProcessPaymentAsync(int userId, string reference) + { + var uri = new Uri( + new Uri(_paymentServiceBaseUrl), + string.Format(ThirdPartyServices.Payment.PerformPayment(), userId, reference)); + + var request = new HttpRequestMessage(HttpMethod.Post, uri); + var response = await _httpClient.SendAsync(request); + + response.EnsureSuccessStatusCode(); + return PaymentInfoTranslator.Translate(await response.Content.ReadAsStringAsync()); + } + } +} \ No newline at end of file diff --git a/src/Domain/Duber.Domain.ACL/Contracts/IPaymentService.cs b/src/Domain/Duber.Domain.ACL/Contracts/IPaymentService.cs new file mode 100644 index 0000000..551927a --- /dev/null +++ b/src/Domain/Duber.Domain.ACL/Contracts/IPaymentService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Duber.Domain.SharedKernel.Model; + +namespace Duber.Domain.ACL.Contracts +{ + public interface IPaymentServiceAdapter + { + Task ProcessPaymentAsync(int userId, string reference); + } +} \ No newline at end of file diff --git a/src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj b/src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj new file mode 100644 index 0000000..86e4070 --- /dev/null +++ b/src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + diff --git a/src/Domain/Duber.Domain.ACL/ThirdPartyServices.cs b/src/Domain/Duber.Domain.ACL/ThirdPartyServices.cs new file mode 100644 index 0000000..936d786 --- /dev/null +++ b/src/Domain/Duber.Domain.ACL/ThirdPartyServices.cs @@ -0,0 +1,13 @@ +namespace Duber.Domain.ACL +{ + public static class ThirdPartyServices + { + public static class Payment + { + public static string PerformPayment() + { + return "/api/payment/performpayment?userId={0}&reference={1}"; + } + } + } +} diff --git a/src/Domain/Duber.Domain.ACL/Translators/PaymentInfoTranslator.cs b/src/Domain/Duber.Domain.ACL/Translators/PaymentInfoTranslator.cs new file mode 100644 index 0000000..86f8f94 --- /dev/null +++ b/src/Domain/Duber.Domain.ACL/Translators/PaymentInfoTranslator.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Duber.Domain.SharedKernel.Model; +using Newtonsoft.Json; + +namespace Duber.Domain.ACL.Translators +{ + public class PaymentInfoTranslator + { + public static PaymentInfo Translate(string responseContent) + { + var paymentInfoList = JsonConvert.DeserializeObject>(responseContent); + if (paymentInfoList.Count != 5) + throw new InvalidOperationException("The payment service response is not consistent."); + + return new PaymentInfo( + int.Parse(paymentInfoList[3]), + Enum.Parse(paymentInfoList[0]), + paymentInfoList[2], + paymentInfoList[1] + ); + } + } +} diff --git a/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj b/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj new file mode 100644 index 0000000..95a2eaa --- /dev/null +++ b/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/src/Domain/Duber.Domain.SharedKernel/Model/PaymentInfo.cs b/src/Domain/Duber.Domain.SharedKernel/Model/PaymentInfo.cs new file mode 100644 index 0000000..b1064dc --- /dev/null +++ b/src/Domain/Duber.Domain.SharedKernel/Model/PaymentInfo.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Duber.Infrastructure.DDD; +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable ConvertToAutoProperty +#pragma warning disable 649 + +namespace Duber.Domain.SharedKernel.Model +{ + public class PaymentInfo : ValueObject + { + private PaymentStatus _status; + private Guid _invoiceId; + private int _userId; + private string _cardNumber; + private string _cardType; + + public PaymentInfo(int userId, PaymentStatus status, string cardNumber, string cardType) + { + if (userId == default(int)) throw new ArgumentNullException(nameof(userId)); + + _userId = userId; + _status = status; + _cardNumber = cardNumber ?? throw new ArgumentNullException(nameof(cardNumber)); + _cardType = cardType ?? throw new ArgumentNullException(nameof(cardType)); + } + + // Just to EF creates the one to one relationship. (need only in the migrations) + public Guid InvoiceId => _invoiceId; + + public int UserId => _userId; + + public PaymentStatus Status => _status; + + public string CardNumber => _cardNumber; + + public string CardType => _cardType; + + protected override IEnumerable GetAtomicValues() + { + yield return UserId; + yield return Status; + yield return CardNumber; + yield return CardType; + yield return InvoiceId; + } + } + + public enum PaymentStatus + { + Accepted = 1, + Rejected = 2, + } +} diff --git a/src/Domain/Duber.Domain.SharedKernel/Model/PaymentMethod.cs b/src/Domain/Duber.Domain.SharedKernel/Model/PaymentMethod.cs new file mode 100644 index 0000000..8cec046 --- /dev/null +++ b/src/Domain/Duber.Domain.SharedKernel/Model/PaymentMethod.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Infrastructure.DDD; + +namespace Duber.Domain.SharedKernel.Model +{ + public class PaymentMethod : Enumeration + { + public static PaymentMethod Cash = new PaymentMethod(1, "Cash"); + public static PaymentMethod CreditCard = new PaymentMethod(2, "Credit Card"); + + protected PaymentMethod() { } + + public PaymentMethod(int id, string name) + : base(id, name) + { + + } + + public static IEnumerable List() + { + return new[] { Cash, CreditCard }; + } + + public static PaymentMethod FromName(string name) + { + var state = List() + .SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)); + + if (state == null) + { + throw new ArgumentException($"Possible values for PaymentMethod: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + + public static PaymentMethod From(int id) + { + var state = List().SingleOrDefault(s => s.Id == id); + + if (state == null) + { + throw new ArgumentException($"Possible values for PaymentMethod: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + } +} diff --git a/src/Domain/Duber.Domain.SharedKernel/Model/TripStatus.cs b/src/Domain/Duber.Domain.SharedKernel/Model/TripStatus.cs new file mode 100644 index 0000000..f795c62 --- /dev/null +++ b/src/Domain/Duber.Domain.SharedKernel/Model/TripStatus.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Infrastructure.DDD; + +namespace Duber.Domain.SharedKernel.Model +{ + public class TripStatus : Enumeration + { + public static TripStatus Created = new TripStatus(0, nameof(Created)); + public static TripStatus Accepted = new TripStatus(1, nameof(Accepted)); + public static TripStatus Cancelled = new TripStatus(2, nameof(Cancelled)); + public static TripStatus OnTheWay = new TripStatus(3, nameof(OnTheWay)); + public static TripStatus InCourse = new TripStatus(4, nameof(InCourse)); + public static TripStatus Finished = new TripStatus(5, nameof(Finished)); + + protected TripStatus() { } + + public TripStatus(int id, string name) + : base(id, name) + { + } + + public static IEnumerable List() + { + return new[] { Created, Accepted, Cancelled, OnTheWay, InCourse, Finished }; + } + + public static TripStatus FromName(string name) + { + var state = List() + .SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)); + + if (state == null) + { + throw new ArgumentException($"Possible values for TripStatus: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + + public static TripStatus From(int id) + { + var state = List().SingleOrDefault(s => s.Id == id); + + if (state == null) + { + throw new ArgumentException($"Possible values for TripStatus: {string.Join(",", List().Select(s => s.Name))}"); + } + + return state; + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj new file mode 100644 index 0000000..e1d6178 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/UnitTest1.cs b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/UnitTest1.cs new file mode 100644 index 0000000..27c6a23 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/UnitTest1.cs @@ -0,0 +1,13 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Duber.Domain.Invoice.UnitTest +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj new file mode 100644 index 0000000..1a7db45 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoiceCreatedDomainEvent.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoiceCreatedDomainEvent.cs new file mode 100644 index 0000000..3ca30ec --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoiceCreatedDomainEvent.cs @@ -0,0 +1,28 @@ +using Duber.Infrastructure.EventBus.Events; +using System; +using MediatR; + +namespace Duber.Domain.Invoice.Events +{ + public class InvoiceCreatedDomainEvent : INotification + { + public InvoiceCreatedDomainEvent(Guid invoiceId, decimal fee, decimal total, bool paidWithCreditCard, Guid tripId) + { + InvoiceId = invoiceId; + Fee = fee; + Total = total; + PaidWithCreditCard = paidWithCreditCard; + TripId = tripId; + } + + public Guid InvoiceId { get; } + + public Guid TripId { get; } + + public decimal Fee { get; } + + public decimal Total { get; } + + public bool PaidWithCreditCard { get; } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoicePaidDomainEvent.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoicePaidDomainEvent.cs new file mode 100644 index 0000000..9a59a81 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Events/InvoicePaidDomainEvent.cs @@ -0,0 +1,28 @@ +using System; +using Duber.Domain.SharedKernel.Model; +using MediatR; + +namespace Duber.Domain.Invoice.Events +{ + public class InvoicePaidDomainEvent : INotification + { + public InvoicePaidDomainEvent(Guid invoiceId, PaymentStatus status, string cardNumber, string cardType, Guid tripId) + { + InvoiceId = invoiceId; + Status = status; + CardNumber = cardNumber; + CardType = cardType; + TripId = tripId; + } + + public Guid InvoiceId { get; } + + public Guid TripId { get; } + + public PaymentStatus Status { get; } + + public string CardNumber { get; } + + public string CardType { get; } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainArgumentNullException.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainArgumentNullException.cs new file mode 100644 index 0000000..c66661a --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainArgumentNullException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Duber.Domain.Invoice.Exceptions +{ + public class InvoiceDomainArgumentNullException : ArgumentNullException + { + public InvoiceDomainArgumentNullException() + { } + + public InvoiceDomainArgumentNullException(string message) + : base(message) + { } + + public InvoiceDomainArgumentNullException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainInvalidOperationException.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainInvalidOperationException.cs new file mode 100644 index 0000000..51b018d --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Exceptions/InvoiceDomainInvalidOperationException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Duber.Domain.Invoice.Exceptions +{ + public class InvoiceDomainInvalidOperationException : InvalidOperationException + { + public InvoiceDomainInvalidOperationException() + { } + + public InvoiceDomainInvalidOperationException(string message) + : base(message) + { } + + public InvoiceDomainInvalidOperationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Extensions/TripInformationExtensions.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Extensions/TripInformationExtensions.cs new file mode 100644 index 0000000..0cb920e --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Extensions/TripInformationExtensions.cs @@ -0,0 +1,18 @@ +using System; +using Duber.Domain.Invoice.Model; + +namespace Duber.Domain.Invoice.Extensions +{ + public static class TripInformationExtensions + { + public static double DistanceToKilometers(this TripInformation tripInformation) + { + return tripInformation.Distance / 1000; + } + + public static double DurationToMinutes(this TripInformation tripInformation) + { + return tripInformation.Duration.TotalMinutes; + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.Designer.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.Designer.cs new file mode 100644 index 0000000..9f332da --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.Designer.cs @@ -0,0 +1,53 @@ +// +using Duber.Domain.Invoice.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; +#pragma warning disable 618 + +namespace Duber.Domain.Invoice.Migrations +{ + [DbContext(typeof(InvoiceMigrationContext))] + [Migration("20180407204749_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.Domain.Invoice.Model.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd(); + + b.Property("Created"); + + b.Property("Distance"); + + b.Property("Duration"); + + b.Property("Fee"); + + b.Property("PaymentMethodId"); + + b.Property("Total"); + + b.Property("TripId"); + + b.Property("TripStatusId"); + + b.HasKey("InvoiceId"); + + b.ToTable("Invoices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.cs new file mode 100644 index 0000000..1f6b2cc --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180407204749_InitialCreate.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Duber.Domain.Invoice.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Invoices", + columns: table => new + { + InvoiceId = table.Column(nullable: false), + Created = table.Column(nullable: false), + Distance = table.Column(nullable: false), + Duration = table.Column(nullable: false), + Fee = table.Column(nullable: false), + PaymentMethodId = table.Column(nullable: false), + Total = table.Column(nullable: false), + TripId = table.Column(nullable: false), + TripStatusId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Invoices", x => x.InvoiceId); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Invoices"); + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.Designer.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.Designer.cs new file mode 100644 index 0000000..c9872b7 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.Designer.cs @@ -0,0 +1,82 @@ +// +using Duber.Domain.Invoice.Model; +using Duber.Domain.Invoice.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; +#pragma warning disable 618 + +namespace Duber.Domain.Invoice.Migrations +{ + [DbContext(typeof(InvoiceMigrationContext))] + [Migration("20180410015654_PaymentInfo")] + partial class PaymentInfo + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.Domain.Invoice.Model.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd(); + + b.Property("Created"); + + b.Property("Distance"); + + b.Property("Duration"); + + b.Property("Fee"); + + b.Property("PaymentMethodId"); + + b.Property("Total"); + + b.Property("TripId"); + + b.Property("TripStatusId"); + + b.HasKey("InvoiceId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Duber.Domain.Invoice.Model.PaymentInfo", b => + { + b.Property("Status"); + + b.Property("CardNumber"); + + b.Property("CardType"); + + b.Property("InvoiceId"); + + b.Property("UserId"); + + b.HasKey("Status", "CardNumber", "CardType", "InvoiceId", "UserId"); + + b.HasIndex("InvoiceId") + .IsUnique(); + + b.ToTable("PaymentsInfo"); + }); + + modelBuilder.Entity("Duber.Domain.Invoice.Model.PaymentInfo", b => + { + b.HasOne("Duber.Domain.Invoice.Model.Invoice") + .WithOne("PaymentInfo") + .HasForeignKey("Duber.Domain.Invoice.Model.PaymentInfo", "InvoiceId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.cs new file mode 100644 index 0000000..cc588e8 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/20180410015654_PaymentInfo.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Duber.Domain.Invoice.Migrations +{ + public partial class PaymentInfo : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PaymentsInfo", + columns: table => new + { + Status = table.Column(nullable: false), + CardNumber = table.Column(nullable: false), + CardType = table.Column(nullable: false), + InvoiceId = table.Column(nullable: false), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PaymentsInfo", x => new { x.Status, x.CardNumber, x.CardType, x.InvoiceId, x.UserId }); + table.ForeignKey( + name: "FK_PaymentsInfo_Invoices_InvoiceId", + column: x => x.InvoiceId, + principalTable: "Invoices", + principalColumn: "InvoiceId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PaymentsInfo_InvoiceId", + table: "PaymentsInfo", + column: "InvoiceId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PaymentsInfo"); + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/InvoiceMigrationContextModelSnapshot.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/InvoiceMigrationContextModelSnapshot.cs new file mode 100644 index 0000000..0c31159 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Migrations/InvoiceMigrationContextModelSnapshot.cs @@ -0,0 +1,81 @@ +// +using Duber.Domain.Invoice.Model; +using Duber.Domain.Invoice.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; +#pragma warning disable 618 + +namespace Duber.Domain.Invoice.Migrations +{ + [DbContext(typeof(InvoiceMigrationContext))] + partial class InvoiceMigrationContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.Domain.Invoice.Model.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd(); + + b.Property("Created"); + + b.Property("Distance"); + + b.Property("Duration"); + + b.Property("Fee"); + + b.Property("PaymentMethodId"); + + b.Property("Total"); + + b.Property("TripId"); + + b.Property("TripStatusId"); + + b.HasKey("InvoiceId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Duber.Domain.Invoice.Model.PaymentInfo", b => + { + b.Property("Status"); + + b.Property("CardNumber"); + + b.Property("CardType"); + + b.Property("InvoiceId"); + + b.Property("UserId"); + + b.HasKey("Status", "CardNumber", "CardType", "InvoiceId", "UserId"); + + b.HasIndex("InvoiceId") + .IsUnique(); + + b.ToTable("PaymentsInfo"); + }); + + modelBuilder.Entity("Duber.Domain.Invoice.Model.PaymentInfo", b => + { + b.HasOne("Duber.Domain.Invoice.Model.Invoice") + .WithOne("PaymentInfo") + .HasForeignKey("Duber.Domain.Invoice.Model.PaymentInfo", "InvoiceId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Model/Invoice.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Model/Invoice.cs new file mode 100644 index 0000000..dc431c7 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Model/Invoice.cs @@ -0,0 +1,129 @@ +using System; +using Duber.Domain.Invoice.Events; +using Duber.Domain.Invoice.Exceptions; +using Duber.Domain.Invoice.Extensions; +using Duber.Domain.SharedKernel.Model; +using Duber.Infrastructure.DDD; +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable ConvertToAutoProperty +// ReSharper disable ConvertToAutoPropertyWithPrivateSetter +// ReSharper disable FieldCanBeMadeReadOnly.Local + +namespace Duber.Domain.Invoice.Model +{ + public class Invoice : Entity, IAggregateRoot + { + private Guid _invoiceId; + private decimal _fee = 1; + private decimal _total; + private TripInformation _tripInformation; + private PaymentMethod _paymentMethod; + private DateTime _created; + private PaymentInfo _paymentInfo; + + public decimal Fee => _fee; + + public PaymentMethod PaymentMethod => _paymentMethod; + + public decimal Total => _total; + + public Guid InvoiceId => _invoiceId; + + public TripInformation TripInformation => _tripInformation; + + public DateTime Created => _created; + + public PaymentInfo PaymentInfo => _paymentInfo; + + // to Dapper mapping. + protected Invoice(Guid invoiceId, decimal fee, decimal total, int paymentMethodId, Guid tripId, double distance, + TimeSpan duration, DateTime created, int tripStatusId, int status, string cardNumber, string cardType, int userId) + { + _invoiceId = invoiceId; + _created = created; + _tripInformation = new TripInformation(tripId, duration, distance, tripStatusId); + _fee = fee; + _paymentMethod = PaymentMethod.From(paymentMethodId); + _total = total; + + if (userId != default(int)) + _paymentInfo = new PaymentInfo(userId, (PaymentStatus)status, cardNumber, cardType); + } + + public Invoice(int paymentMethodId, Guid tripId, TimeSpan duration, double distance, int tripStatusId) + { + if (paymentMethodId == default(int)) throw new InvoiceDomainArgumentNullException(nameof(paymentMethodId)); + + _invoiceId = Guid.NewGuid(); + _created = DateTime.UtcNow; + _paymentMethod = PaymentMethod.From(paymentMethodId); + _tripInformation = new TripInformation(tripId, duration, distance, tripStatusId); + GetFee(); + GetTotal(); + + AddDomainEvent(new InvoiceCreatedDomainEvent(_invoiceId, _fee, _total, Equals(_paymentMethod, PaymentMethod.CreditCard), _tripInformation.Id)); + } + + public void ProcessPayment(PaymentInfo paymentInfo) + { + if (!Equals(_paymentMethod, PaymentMethod.CreditCard)) + throw new InvoiceDomainInvalidOperationException("Invalid payment method to process."); + + if (_total == 0) + throw new InvoiceDomainInvalidOperationException("This invoice doesn't have any charges."); + + _paymentInfo = paymentInfo; + AddDomainEvent(new InvoicePaidDomainEvent(_invoiceId, _paymentInfo.Status, _paymentInfo.CardNumber, _paymentInfo.CardType, _tripInformation.Id)); + } + + private void GetFee() + { + // let's say there is this bussines rule to get the fee. + if (Equals(_tripInformation.Status, TripStatus.Cancelled)) + { + _fee = 4; + } + else if (_tripInformation.DistanceToKilometers() < 5) + { + _fee = 3; + } + else if (_tripInformation.DurationToMinutes() < 15) + { + _fee = 2; + } + } + + private void GetTotal() + { + // let's say there is formula to get the total. + // a strategy pattern could be a good call to calculate de total based on the trip status. + if (Equals(_tripInformation.Status, TripStatus.Cancelled)) + { + // if the user cancels the trip after 5 minutes, it charges a value proportional to the minutes. + if (_tripInformation.DurationToMinutes() > 5) + { + _total = _fee + (decimal)_tripInformation.DurationToMinutes(); + } + else if (_tripInformation.DurationToMinutes() > 2 && _tripInformation.DurationToMinutes() <= 5) + { + // if the user cancels the trip between the 2nd and 5th minute, it charges a fixed value. + _fee = 0; + _total = 2; + } + else + { + // if the user cancels the trip before 2 minutes it doesn't charge anything + _fee = 0; + _total = 0; + } + } + else + { + _total = (decimal)(_tripInformation.DistanceToKilometers() * _tripInformation.DurationToMinutes() + (double)_fee); + + if (_total <= 0) + throw new InvoiceDomainInvalidOperationException("There was an error calculating the invoice total"); + } + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Model/TripInformation.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Model/TripInformation.cs new file mode 100644 index 0000000..f59210a --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Model/TripInformation.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Duber.Domain.Invoice.Exceptions; +using Duber.Domain.SharedKernel.Model; +using Duber.Infrastructure.DDD; +// ReSharper disable UnusedMember.Local + +namespace Duber.Domain.Invoice.Model +{ + public class TripInformation : ValueObject + { + public TimeSpan Duration { get; } + + public double Distance { get; } + + public Guid Id { get; } + + public TripStatus Status { get; } + + internal TripInformation(Guid id, TimeSpan duration, double distance, int statusId) + { + if (statusId == default(int)) throw new InvoiceDomainArgumentNullException(nameof(statusId)); + if (duration == default(TimeSpan)) throw new InvoiceDomainArgumentNullException(nameof(duration)); + + Id = id; + Duration = duration; + Status = TripStatus.From(statusId); + + if (!Equals(Status, TripStatus.Finished) && !Equals(Status, TripStatus.Cancelled)) + throw new InvoiceDomainInvalidOperationException("Invalid trip status to create an invoice"); + + if (distance <= 0 && !Equals(Status, TripStatus.Cancelled)) + throw new InvoiceDomainArgumentNullException(nameof(distance)); + + Distance = Equals(Status, TripStatus.Cancelled) ? 0 : distance; + } + + protected override IEnumerable GetAtomicValues() + { + yield return Duration; + yield return Distance; + yield return Id; + yield return Status; + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/IInvoiceContext.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/IInvoiceContext.cs new file mode 100644 index 0000000..589e598 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/IInvoiceContext.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; +using Duber.Infrastructure.DDD; + +namespace Duber.Domain.Invoice.Persistence +{ + public interface IInvoiceContext : IDisposable + { + Task ExecuteAsync(T entity, string sql, object parameters = null, int? timeOut = null, CommandType? commandType = null) + where T : Entity, IAggregateRoot; + + Task> QueryAsync(string sql, object parameters = null, int? timeOut = null, CommandType? commandType = null) + where T : Entity, IAggregateRoot; + + Task QuerySingleAsync(string sql, object parameters = null, int? timeOut = null, CommandType? commandType = null) + where T : Entity, IAggregateRoot; + } +} \ No newline at end of file diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceContext.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceContext.cs new file mode 100644 index 0000000..deaac61 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceContext.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Dapper; +using Duber.Infrastructure.DDD; +using Duber.Infrastructure.Extensions; +using Duber.Infrastructure.Resilience.Abstractions; +using MediatR; + +namespace Duber.Domain.Invoice.Persistence +{ + public class InvoiceContext : IInvoiceContext + { + private readonly string _connectionString; + private IDbConnection _connection; + private readonly IMediator _mediator; + private readonly IPolicyAsyncExecutor _resilientSqlExecutor; + + public InvoiceContext(string connectionString, IMediator mediator, IPolicyAsyncExecutor resilientSqlExecutor) + { + if (string.IsNullOrWhiteSpace(connectionString)) + throw new ArgumentException(nameof(connectionString)); + + _connectionString = connectionString; + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _resilientSqlExecutor = resilientSqlExecutor ?? throw new ArgumentNullException(nameof(resilientSqlExecutor)); + } + + public async Task ExecuteAsync(T entity, string sql, object parameters = null, int? timeOut = null, CommandType? commandType = null) + where T : Entity, IAggregateRoot + { + _connection = GetOpenConnection(); + var result = await _resilientSqlExecutor.ExecuteAsync(async () => await _connection.ExecuteAsync(sql, parameters, null, timeOut, commandType)); + + // ensures that all events are dispatched after the entity is saved successfully. + await _mediator.DispatchDomainEventsAsync(entity); + return result; + } + + public async Task> QueryAsync(string sql, object parameters = null, int? timeOut = null, CommandType? commandType = null) + where T : Entity, IAggregateRoot + { + _connection = GetOpenConnection(); + return await _resilientSqlExecutor.ExecuteAsync(async () => await _connection.QueryAsync(sql, parameters, null, timeOut, commandType)); + } + + public async Task QuerySingleAsync(string sql, object parameters = null, int? timeOut = null, CommandType? commandType = null) where T : Entity, IAggregateRoot + { + _connection = GetOpenConnection(); + return await _resilientSqlExecutor.ExecuteAsync(async () => await _connection.QuerySingleOrDefaultAsync(sql, parameters, null, timeOut, commandType)); + } + + private IDbConnection GetOpenConnection() + { + if (_connection == null) + { + return new SqlConnection(_connectionString); + } + + if (_connection.State == ConnectionState.Closed) + _connection.Open(); + + return _connection; + } + + public void Dispose() + { + _connection?.Dispose(); + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceMigrationContext.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceMigrationContext.cs new file mode 100644 index 0000000..118b44a --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Persistence/InvoiceMigrationContext.cs @@ -0,0 +1,102 @@ +using System; +using Duber.Domain.SharedKernel.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Duber.Domain.Invoice.Persistence +{ + /// + /// This context is only to create and run the migrations. Just to example purposes and, avoids that you have to deal with running scripts before to execute the solution. + /// You must use + /// + [Obsolete("This context is only to creates and runs the migrations. Just to example purposes and, avoids that you have to deal running scripts before to execute the solution.")] + public class InvoiceMigrationContext : DbContext + { + public InvoiceMigrationContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new PaymentInfoEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new InvoiceEntityTypeConfiguration()); + } + + public DbSet Invoices { get; set; } + + public DbSet PaymentsInfo { get; set; } + } + + [Obsolete] + public class UserContextDesignFactory : IDesignTimeDbContextFactory + { + public InvoiceMigrationContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Initial Catalog=Duber.InvoiceDb;Integrated Security=true"); + + return new InvoiceMigrationContext(optionsBuilder.Options); + } + } + + internal class InvoiceEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Invoices"); + + builder.HasKey(o => o.InvoiceId); + + builder.Ignore(b => b.DomainEvents); + + builder.Ignore(b => b.Id); + + builder.Property(x => x.Fee) + .IsRequired(); + + builder.Property(x => x.Total) + .IsRequired(); + + builder.Property(x => x.Created) + .IsRequired(); + + builder.Property("PaymentMethodId").IsRequired(); + + builder.Property("TripStatusId").IsRequired(); + + builder.Property("TripId").IsRequired(); + + builder.Property("Distance").IsRequired(); + + builder.Property("Duration").IsRequired(); + + builder.HasOne(a => a.PaymentInfo) + .WithOne() + .HasForeignKey(b => b.InvoiceId); + } + } + + internal class PaymentInfoEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("PaymentsInfo"); + + builder.HasKey(o => new { o.Status, o.CardNumber, o.CardType, o.InvoiceId, o.UserId }); + + builder.Property(x => x.Status) + .IsRequired(); + + builder.Property(x => x.CardNumber) + .IsRequired(); + + builder.Property(x => x.CardType) + .IsRequired(); + + builder.Property(x => x.InvoiceId) + .IsRequired(); + + builder.Property(x => x.UserId) + .IsRequired(); + } + } +} diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Repository/IInvoiceRepository.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Repository/IInvoiceRepository.cs new file mode 100644 index 0000000..d85b956 --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Repository/IInvoiceRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Duber.Domain.Invoice.Repository +{ + public interface IInvoiceRepository : IDisposable + { + Task GetInvoiceAsync(Guid id); + + Task GetInvoiceByTripAsync(Guid tripId); + + Task> GetInvoicesAsync(); + + Task AddInvoiceAsync(Model.Invoice invoice); + + Task AddPaymentInfo(Model.Invoice invoice); + } +} \ No newline at end of file diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Repository/InvoiceRepository.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Repository/InvoiceRepository.cs new file mode 100644 index 0000000..6a5129a --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Repository/InvoiceRepository.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Duber.Domain.Invoice.Persistence; + +namespace Duber.Domain.Invoice.Repository +{ + public class InvoiceRepository : IInvoiceRepository + { + private readonly IInvoiceContext _context; + + public InvoiceRepository(IInvoiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task GetInvoiceAsync(Guid id) + { + return await _context.QuerySingleAsync( + "Select i.InvoiceId, i.Fee, i.Total, i.PaymentMethodId, i.TripId, i.Distance, i.Duration, i.Created, i.TripStatusId, p.Status, p.CardNumber, p.CardType, p.UserId " + + "From Invoices i LEFT JOIN" + + " PaymentsInfo p ON p.InvoiceId = i.InvoiceId " + + "Where i.InvoiceId = @InvoiceId", + new { InvoiceId = id }); + } + + public async Task GetInvoiceByTripAsync(Guid tripId) + { + return await _context.QuerySingleAsync( + "Select i.InvoiceId, i.Fee, i.Total, i.PaymentMethodId, i.TripId, i.Distance, i.Duration, i.Created, i.TripStatusId, p.Status, p.CardNumber, p.CardType, p.UserId " + + "From Invoices i LEFT JOIN" + + " PaymentsInfo p ON p.InvoiceId = i.InvoiceId " + + "Where i.TripId = @TripId", + new { TripId = tripId }); + } + + public async Task> GetInvoicesAsync() + { + return await _context.QueryAsync( + "Select i.InvoiceId, i.Fee, i.Total, i.PaymentMethodId, i.TripId, i.Distance, i.Duration, i.Created, i.TripStatusId, p.Status, p.CardNumber, p.CardType, p.UserId " + + "From Invoices i LEFT JOIN" + + " PaymentsInfo p ON p.InvoiceId = i.InvoiceId "); + } + + public async Task AddInvoiceAsync(Model.Invoice invoice) + { + return await _context.ExecuteAsync( + invoice, + "Insert Into Invoices(InvoiceId, Fee, Total, PaymentMethodId, Distance, Duration, Created, TripId, TripStatusId) Values(@InvoiceId, @Fee, @Total, @PaymentMethodId, @Distance, @Duration, @Created, @TripId, @TripStatusId)", + new + { + invoice.InvoiceId, + invoice.Fee, + invoice.Total, + PaymentMethodId = invoice.PaymentMethod.Id, + invoice.TripInformation.Distance, + invoice.TripInformation.Duration, + invoice.Created, + TripId = invoice.TripInformation.Id, + TripStatusId = invoice.TripInformation.Status.Id + }); + } + + public async Task AddPaymentInfo(Model.Invoice invoice) + { + return await _context.ExecuteAsync( + invoice, + "Insert Into PaymentsInfo(Status, CardNumber, CardType, InvoiceId, UserId) Values(@Status, @CardNumber, @CardType, @InvoiceId, @UserId)", + new + { + invoice.PaymentInfo.Status, + invoice.PaymentInfo.CardNumber, + invoice.PaymentInfo.CardType, + invoice.InvoiceId, + invoice.PaymentInfo.UserId, + }); + } + + public void Dispose() + { + _context?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Services/IPaymentService.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Services/IPaymentService.cs new file mode 100644 index 0000000..45b967a --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Services/IPaymentService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Duber.Domain.SharedKernel.Model; + +namespace Duber.Domain.Invoice.Services +{ + public interface IPaymentService + { + Task PerformPayment(Model.Invoice invoice, int userId); + } +} \ No newline at end of file diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Services/PaymentService.cs b/src/Domain/Invoice/Duber.Domain.Invoice/Services/PaymentService.cs new file mode 100644 index 0000000..0597fde --- /dev/null +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Services/PaymentService.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Duber.Domain.ACL.Contracts; +using Duber.Domain.Invoice.Exceptions; +using Duber.Domain.Invoice.Repository; + +namespace Duber.Domain.Invoice.Services +{ + public class PaymentService : IPaymentService + { + private readonly IInvoiceRepository _invoiceRepository; + private readonly IPaymentServiceAdapter _paymentServiceAdapter; + + public PaymentService(IPaymentServiceAdapter paymentServiceAdapter, IInvoiceRepository invoiceRepository) + { + _paymentServiceAdapter = paymentServiceAdapter ?? throw new InvoiceDomainArgumentNullException(nameof(paymentServiceAdapter)); + _invoiceRepository = invoiceRepository ?? throw new InvoiceDomainArgumentNullException(nameof(invoiceRepository)); + } + + public async Task PerformPayment(Model.Invoice invoice, int userId) + { + try + { + var paymentInfo = await _paymentServiceAdapter.ProcessPaymentAsync(userId, invoice.InvoiceId.ToString()); + invoice.ProcessPayment(paymentInfo); + await _invoiceRepository.AddPaymentInfo(invoice); + } + finally + { + _invoiceRepository.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj new file mode 100644 index 0000000..e1d6178 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + diff --git a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/UnitTest1.cs b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/UnitTest1.cs new file mode 100644 index 0000000..83ef1b8 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/UnitTest1.cs @@ -0,0 +1,13 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Duber.Domain.Trip.UnitTest +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs new file mode 100644 index 0000000..1120adc --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs @@ -0,0 +1,27 @@ +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.Trip.Model; +using Kledex.Domain; + +namespace Duber.Domain.Trip.Commands +{ + public class CreateTripCommand : DomainCommand + { + public int UserTripId { get; set; } + + public int DriverId { get; set; } + + public Location From { get; set; } + + public Location To { get; set; } + + public string Plate { get; set; } + + public string Brand { get; set; } + + public string Model { get; set; } + + public PaymentMethod PaymentMethod { get; set; } + + public string ConnectionId { get; set; } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs new file mode 100644 index 0000000..ccd333e --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Kledex.Commands; + +namespace Duber.Domain.Trip.Commands.Handlers +{ + public class CreateTripCommandHandlerAsync : ICommandHandlerAsync + { + public async Task HandleAsync(CreateTripCommand command) + { + var trip = new Model.Trip( + command.AggregateRootId, + command.UserTripId, + command.DriverId, + command.From, + command.To, + command.PaymentMethod, + command.Plate, + command.Brand, + command.Model, + command.ConnectionId); + + await Task.CompletedTask; + return new CommandResponse + { + Events = trip.Events + }; + } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs new file mode 100644 index 0000000..ec690f2 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using Duber.Domain.Trip.Exceptions; +using Kledex.Commands; +using Kledex.Domain; + +namespace Duber.Domain.Trip.Commands.Handlers +{ + public class UpdateTripCommandHandlerAsync : ICommandHandlerAsync + { + private readonly IRepository _repository; + + public UpdateTripCommandHandlerAsync(IRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(UpdateTripCommand command) + { + var trip = await _repository.GetByIdAsync(command.AggregateRootId); + + if (trip == null) + throw new TripDomainInvalidOperationException("Trip not found."); + + // TODO: consider creating a separate command/handler for each action to avoid this code smell. + switch (command.Action) + { + case Action.Accept: + trip.Accept(); + break; + case Action.Start: + trip.Start(); + break; + case Action.Cancel: + trip.Cancel(); + break; + case Action.FinishEarlier: + trip.FinishEarlier(); + break; + case Action.UpdateCurrentLocation: + trip.SetCurrentLocation(command.CurrentLocation); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return new CommandResponse + { + Events = trip.Events + }; + } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs new file mode 100644 index 0000000..c0d8040 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs @@ -0,0 +1,23 @@ +using Duber.Domain.Trip.Model; +using Kledex.Domain; + +namespace Duber.Domain.Trip.Commands +{ + public class UpdateTripCommand : DomainCommand + { + public Action Action { get; set; } + + public Location CurrentLocation { get; set; } + + public string ConnectionId { get; set; } + } + + public enum Action + { + Accept = 1, + Start = 2, + Cancel = 3, + FinishEarlier = 4, + UpdateCurrentLocation = 5 + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj new file mode 100644 index 0000000..8f0dee4 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs b/src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs new file mode 100644 index 0000000..908ddbf --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs @@ -0,0 +1,25 @@ +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.Trip.Model; +using Kledex.Domain; + +namespace Duber.Domain.Trip.Events +{ + public class TripCreatedDomainEvent : DomainEvent + { + public int UserTripId { get; set; } + + public int DriverId { get; set; } + + public Location From { get; set; } + + public Location To { get; set; } + + public VehicleInformation VehicleInformation { get; set; } + + public PaymentMethod PaymentMethod { get; set; } + + public TripStatus Status { get; set; } + + public string ConnectionId { get; set; } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs b/src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs new file mode 100644 index 0000000..4eaa11b --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs @@ -0,0 +1,39 @@ +using System; +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.Trip.Model; +using Kledex.Domain; + +namespace Duber.Domain.Trip.Events +{ + public class TripUpdatedDomainEvent : DomainEvent + { + public Action Action { get; set; } + + public TripStatus Status { get; set; } + + public DateTime? Started { get; set; } + + public DateTime? Ended { get; set; } + + public Location CurrentLocation { get; set; } + + public double? Distance { get; set; } + + public TimeSpan? Duration { get; set; } + + public PaymentMethod PaymentMethod { get; set; } + + public int? UserTripId { get; set; } + + public string ConnectionId { get; set; } + } + + public enum Action + { + Accepted = 1, + Started = 2, + Cancelled = 3, + FinishedEarlier = 4, + UpdatedCurrentLocation = 5 + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainArgumentNullException.cs b/src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainArgumentNullException.cs new file mode 100644 index 0000000..7fc93ec --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainArgumentNullException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Duber.Domain.Trip.Exceptions +{ + public class TripDomainArgumentNullException : ArgumentNullException + { + public TripDomainArgumentNullException() + { } + + public TripDomainArgumentNullException(string message) + : base(message) + { } + + public TripDomainArgumentNullException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainInvalidOperationException.cs b/src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainInvalidOperationException.cs new file mode 100644 index 0000000..026b00c --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Exceptions/TripDomainInvalidOperationException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Duber.Domain.Trip.Exceptions +{ + public class TripDomainInvalidOperationException : InvalidOperationException + { + public TripDomainInvalidOperationException() + { } + + public TripDomainInvalidOperationException(string message) + : base(message) + { } + + public TripDomainInvalidOperationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Model/Location.cs b/src/Domain/Trip/Duber.Domain.Trip/Model/Location.cs new file mode 100644 index 0000000..7829485 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Model/Location.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Duber.Infrastructure.DDD; +// ReSharper disable UnusedMember.Local + +namespace Duber.Domain.Trip.Model +{ + public class Location : ValueObject + { + public double Latitude { get; private set; } + + public double Longitude { get; private set; } + + public string Description { get; private set; } + + public Location(double latitude, double longitude, string description) + { + Latitude = latitude; + Longitude = longitude; + Description = description; + } + + protected override IEnumerable GetAtomicValues() + { + yield return Latitude; + yield return Longitude; + } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Model/Rating.cs b/src/Domain/Trip/Duber.Domain.Trip/Model/Rating.cs new file mode 100644 index 0000000..90f9737 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Model/Rating.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Duber.Infrastructure.DDD; +// ReSharper disable UnusedMember.Local + +namespace Duber.Domain.Trip.Model +{ + public class Rating : ValueObject + { + public int Driver { get; private set; } + + public int User { get; private set; } + + private Rating() { } + + public Rating(int driver, int user) + { + Driver = driver; + User = user; + } + + protected override IEnumerable GetAtomicValues() + { + yield return Driver; + yield return User; + } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs b/src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs new file mode 100644 index 0000000..69a4750 --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs @@ -0,0 +1,264 @@ +using System; +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.Trip.Events; +using Duber.Domain.Trip.Exceptions; +using GeoCoordinatePortable; +using Kledex.Domain; +using Action = Duber.Domain.Trip.Events.Action; +// ReSharper disable ConvertToAutoProperty +// ReSharper disable UnusedAutoPropertyAccessor.Local +// ReSharper disable UnusedMember.Local + +namespace Duber.Domain.Trip.Model +{ + public class Trip : AggregateRoot + { + private int _userId; + private int _driverId; + private Location _from; + private Location _to; + private Location _currentLocation; + private DateTime _create; + private DateTime? _start; + private DateTime? _end; + private TripStatus _status; + private VehicleInformation _vehicleInformation; + private Rating _rating; + private PaymentMethod _paymentMethod; + private string _connectionId; + + public string ConnectionId => _connectionId; + + public int UserId => _userId; + + public int DriverId => _driverId; + + public Location From => _from; + + public Location To => _to; + + public Location CurrentLocation => _currentLocation; + + public DateTime? Created => _create; + + public DateTime? Started => _start; + + public DateTime? End => _end; + + public TripStatus Status => _status; + + public VehicleInformation VehicleInformation => _vehicleInformation; + + public Rating Rating => _rating; + + public PaymentMethod PaymentMethod => _paymentMethod; + + public TimeSpan? Duration => GetDuration(); + + public double Distance => GetDistance(); + + // public empty constructor is required for Kledex + public Trip() + { + } + + public Trip(Guid id, int userId, int driverId, Location from, Location to, PaymentMethod paymentMethod, string plate, string brand, string model, string connectionId) : base() + { + if (id == Guid.Empty) throw new TripDomainArgumentNullException(nameof(id)); + if (userId <= 0) throw new TripDomainArgumentNullException(nameof(userId)); + if (driverId <= 0) throw new TripDomainArgumentNullException(nameof(driverId)); + if (string.IsNullOrWhiteSpace(plate)) throw new TripDomainArgumentNullException(nameof(plate)); + if (string.IsNullOrWhiteSpace(brand)) throw new TripDomainArgumentNullException(nameof(brand)); + if (string.IsNullOrWhiteSpace(model)) throw new TripDomainArgumentNullException(nameof(model)); + if (string.IsNullOrWhiteSpace(connectionId)) throw new TripDomainArgumentNullException(nameof(connectionId)); + if (from == null) throw new TripDomainArgumentNullException(nameof(from)); + if (to == null) throw new TripDomainArgumentNullException(nameof(to)); + + if (Equals(from, to)) throw new TripDomainInvalidOperationException("Destination and origin can't be the same."); + + Id = id; + _paymentMethod = paymentMethod ?? throw new TripDomainArgumentNullException(nameof(paymentMethod)); + _create = DateTime.UtcNow; + _status = TripStatus.Created; + _userId = userId; + _driverId = driverId; + _from = from; + _to = to; + _vehicleInformation = new VehicleInformation(plate, brand, model); + _connectionId = connectionId; + + AddEvent(new TripCreatedDomainEvent + { + AggregateRootId = Id, + VehicleInformation = _vehicleInformation, + UserTripId = _userId, + DriverId = _driverId, + From = _from, + To = _to, + PaymentMethod = _paymentMethod, + TimeStamp = _create, + Status = _status, + ConnectionId = _connectionId + }); + } + + public void Accept() + { + if (!Equals(_status, TripStatus.Created)) + throw new TripDomainInvalidOperationException($"Invalid trip status to accept the trip. Current status: {_status.Name}"); + + _status = TripStatus.Accepted; + AddEvent(new TripUpdatedDomainEvent + { + AggregateRootId = Id, + Action = Action.Accepted, + Status = _status, + ConnectionId = _connectionId + }); + } + + public void Start() + { + if (!Equals(_status, TripStatus.Accepted)) + throw new TripDomainInvalidOperationException($"Before to start the trip, it should be accepted. Current status: {_status.Name}"); + + _start = DateTime.UtcNow; + + // we're assuming that the driver already picked up the user. + _status = TripStatus.InCourse; + + AddEvent(new TripUpdatedDomainEvent + { + AggregateRootId = Id, + Action = Action.Started, + Status = _status, + Started = _start, + ConnectionId = _connectionId + }); + } + + public void FinishEarlier() + { + if (!Equals(_status, TripStatus.InCourse)) + throw new TripDomainInvalidOperationException($"Invalid trip status to finish the trip. Current status: {_status.Name}"); + + _end = DateTime.UtcNow; + _status = TripStatus.Finished; + _to = _currentLocation; + + AddEvent(new TripUpdatedDomainEvent + { + AggregateRootId = Id, + Action = Action.FinishedEarlier, + Status = _status, + Started = _start, + Ended = _end, + Duration = GetDuration(), + Distance = GetDistance(), + PaymentMethod = _paymentMethod, + UserTripId = _userId, + ConnectionId = _connectionId + }); + } + + public void Cancel() + { + if (!Equals(_status, TripStatus.Created) || !Equals(_status, TripStatus.Accepted)) + throw new TripDomainInvalidOperationException($"Invalid trip status to cancel the trip. Current status: {_status.Name}"); + + _end = DateTime.UtcNow; + _status = TripStatus.Cancelled; + + //let's say there is a business rule that says when cancelling the rating is 2 for both user an driver. + _rating = new Rating(2, 2); + + AddEvent(new TripUpdatedDomainEvent + { + AggregateRootId = Id, + Action = Action.Cancelled, + Status = _status, + Started = _start, + Ended = _end, + PaymentMethod = _paymentMethod, + Duration = GetDuration(), + UserTripId = _userId, + ConnectionId = _connectionId + }); + } + + public void SetCurrentLocation(Location currentLocation) + { + if (!Equals(_status, TripStatus.InCourse)) + throw new TripDomainInvalidOperationException($"Invalid trip status to set the current location. Current status: {_status.Name}"); + + _currentLocation = currentLocation ?? throw new TripDomainArgumentNullException(nameof(currentLocation)); + + // TODO: handle a tolerance range to determine if current location is the destination + if (Equals(currentLocation, _to)) + { + _end = DateTime.UtcNow; + _status = TripStatus.Finished; + } + + AddEvent(new TripUpdatedDomainEvent + { + AggregateRootId = Id, + Action = Action.UpdatedCurrentLocation, + Status = _status, + Started = _start, + Ended = _end, + CurrentLocation = currentLocation, + Duration = GetDuration(), + Distance = GetDistance(), + PaymentMethod = _paymentMethod, + UserTripId = _userId, + ConnectionId = _connectionId + }); + } + + private TimeSpan? GetDuration() + { + TimeSpan? duration = null; + if (_start != null) + { + duration = _end?.Subtract(_start.Value); + } + + return duration; + } + + private double GetDistance() + { + if (_from == null || _to == null) + return 0; + + var from = new GeoCoordinate(_from.Latitude, _from.Longitude); + var to = new GeoCoordinate(_to.Latitude, _to.Longitude); + return from.GetDistanceTo(to); + } + + // Applies events after load an object from event store. + private void Apply(TripCreatedDomainEvent @event) + { + Id = @event.AggregateRootId; + _status = @event.Status; + _create = @event.TimeStamp; + _driverId = @event.DriverId; + _from = @event.From; + _to = @event.To; + _userId = @event.UserTripId; + _vehicleInformation = @event.VehicleInformation; + _paymentMethod = @event.PaymentMethod; + _connectionId = @event.ConnectionId; + } + + private void Apply(TripUpdatedDomainEvent @event) + { + _start = @event.Started; + _end = @event.Ended; + _status = @event.Status; + _currentLocation = @event.CurrentLocation; + _connectionId = @event.ConnectionId; + } + } +} diff --git a/src/Domain/Trip/Duber.Domain.Trip/Model/VehicleInformation.cs b/src/Domain/Trip/Duber.Domain.Trip/Model/VehicleInformation.cs new file mode 100644 index 0000000..e5e799b --- /dev/null +++ b/src/Domain/Trip/Duber.Domain.Trip/Model/VehicleInformation.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Duber.Infrastructure.DDD; +// ReSharper disable BuiltInTypeReferenceStyle +// ReSharper disable UnusedMember.Local + +namespace Duber.Domain.Trip.Model +{ + public class VehicleInformation : ValueObject + { + public String Plate { get; private set; } + + public String Brand { get; private set; } + + public String Model { get; private set; } + + private VehicleInformation() { } + + public VehicleInformation(string plate, string brand, string model) + { + Plate = plate; + Brand = brand; + Model = model; + } + + protected override IEnumerable GetAtomicValues() + { + yield return Plate; + yield return Brand; + yield return Model; + } + } +} diff --git a/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj new file mode 100644 index 0000000..e1d6178 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + diff --git a/src/Domain/User/Duber.Domain.User.UnitTest/UnitTest1.cs b/src/Domain/User/Duber.Domain.User.UnitTest/UnitTest1.cs new file mode 100644 index 0000000..eb79e2b --- /dev/null +++ b/src/Domain/User/Duber.Domain.User.UnitTest/UnitTest1.cs @@ -0,0 +1,13 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Duber.Domain.User.UnitTest +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj b/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj new file mode 100644 index 0000000..86dc407 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Domain/User/Duber.Domain.User/Exceptions/UserDomainException.cs b/src/Domain/User/Duber.Domain.User/Exceptions/UserDomainException.cs new file mode 100644 index 0000000..015cd3d --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Exceptions/UserDomainException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Duber.Domain.User.Exceptions +{ + public class UserDomainException : Exception + { + public UserDomainException() + { } + + public UserDomainException(string message) + : base(message) + { } + + public UserDomainException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.Designer.cs b/src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.Designer.cs new file mode 100644 index 0000000..4a57dac --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.Designer.cs @@ -0,0 +1,81 @@ +// +using Duber.Domain.User.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace Duber.Domain.User.Migrations +{ + [DbContext(typeof(UserContext))] + [Migration("20180328010407_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("Relational:Sequence:User.userseq", "'userseq', 'User', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.Domain.User.Model.PaymentMethod", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("PaymentMethods","User"); + }); + + modelBuilder.Entity("Duber.Domain.User.Model.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "userseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "User") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Email") + .IsRequired(); + + b.Property("Name") + .IsRequired(); + + b.Property("NumberPhone"); + + b.Property("PaymentMethodId"); + + b.Property("Rating") + .ValueGeneratedOnAdd() + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("PaymentMethodId"); + + b.ToTable("Users","User"); + }); + + modelBuilder.Entity("Duber.Domain.User.Model.User", b => + { + b.HasOne("Duber.Domain.User.Model.PaymentMethod", "PaymentMethod") + .WithMany() + .HasForeignKey("PaymentMethodId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.cs b/src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.cs new file mode 100644 index 0000000..f3c7b81 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Migrations/20180328010407_InitialCreate.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Duber.Domain.User.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "User"); + + migrationBuilder.CreateSequence( + name: "userseq", + schema: "User", + incrementBy: 10); + + migrationBuilder.CreateTable( + name: "PaymentMethods", + schema: "User", + columns: table => new + { + Id = table.Column(nullable: false, defaultValue: 1), + Name = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PaymentMethods", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + schema: "User", + columns: table => new + { + Id = table.Column(nullable: false), + Email = table.Column(nullable: false), + Name = table.Column(nullable: false), + NumberPhone = table.Column(nullable: true), + PaymentMethodId = table.Column(nullable: false), + Rating = table.Column(nullable: false, defaultValue: 0) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_PaymentMethods_PaymentMethodId", + column: x => x.PaymentMethodId, + principalSchema: "User", + principalTable: "PaymentMethods", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + schema: "User", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_PaymentMethodId", + schema: "User", + table: "Users", + column: "PaymentMethodId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users", + schema: "User"); + + migrationBuilder.DropTable( + name: "PaymentMethods", + schema: "User"); + + migrationBuilder.DropSequence( + name: "userseq", + schema: "User"); + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Migrations/UserContextModelSnapshot.cs b/src/Domain/User/Duber.Domain.User/Migrations/UserContextModelSnapshot.cs new file mode 100644 index 0000000..57787f3 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Migrations/UserContextModelSnapshot.cs @@ -0,0 +1,80 @@ +// +using Duber.Domain.User.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace Duber.Domain.User.Migrations +{ + [DbContext(typeof(UserContext))] + partial class UserContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("Relational:Sequence:User.userseq", "'userseq', 'User', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.Domain.User.Model.PaymentMethod", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("PaymentMethods","User"); + }); + + modelBuilder.Entity("Duber.Domain.User.Model.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "userseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "User") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Email") + .IsRequired(); + + b.Property("Name") + .IsRequired(); + + b.Property("NumberPhone"); + + b.Property("PaymentMethodId"); + + b.Property("Rating") + .ValueGeneratedOnAdd() + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("PaymentMethodId"); + + b.ToTable("Users","User"); + }); + + modelBuilder.Entity("Duber.Domain.User.Model.User", b => + { + b.HasOne("Duber.Domain.User.Model.PaymentMethod", "PaymentMethod") + .WithMany() + .HasForeignKey("PaymentMethodId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Model/User.cs b/src/Domain/User/Duber.Domain.User/Model/User.cs new file mode 100644 index 0000000..ca72e18 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Model/User.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.User.Exceptions; +using Duber.Infrastructure.DDD; +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable ConvertToAutoProperty +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace Duber.Domain.User.Model +{ + public class User : Entity, IAggregateRoot + { + private string _name; + private string _numberPhone; + private string _email; + private int _rating; + private int _paymentMethodId; + + public string Name => _name; + + public string Email => _email; + + public string NumberPhone => _numberPhone; + + public int Rating => _rating; + + // EF navigation property + public PaymentMethod PaymentMethod { get; private set; } + + protected User() + { + } + + public User(string name, string email, int rating, PaymentMethod paymentMethod, string numberPhone = null) + { + if (rating < 0 || rating > 5) throw new UserDomainException("User rating should be between 1 and 5"); + + _name = !string.IsNullOrWhiteSpace(name) ? name : throw new UserDomainException(nameof(name)); + _email = !string.IsNullOrWhiteSpace(email) ? email : throw new UserDomainException(nameof(email)); + _numberPhone = numberPhone; + _rating = rating; + _paymentMethodId = paymentMethod.Id; + } + + public void CalculateRating(List historicRatings) + { + // this is just an example, to show that here is where you perform the business validations and define the object behavior. + // note: historicRatings shouldn't be a parameter, just to example purposes. It should be a value object (list) inside of this aggregate + if (historicRatings.Count == 0) + { + _rating = 0; + } + else + { + _rating = historicRatings.Sum() / historicRatings.Count; + } + } + + public void ChangePaymentMethod(PaymentMethod newMethod) + { + if (_paymentMethodId == newMethod.Id) + throw new InvalidOperationException($"The user already has the {PaymentMethod.Name} payment method."); + + _paymentMethodId = newMethod.Id; + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/PaymentMethodEntityTypeConfiguration.cs b/src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/PaymentMethodEntityTypeConfiguration.cs new file mode 100644 index 0000000..7570d30 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/PaymentMethodEntityTypeConfiguration.cs @@ -0,0 +1,25 @@ +using Duber.Domain.SharedKernel.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Duber.Domain.User.Persistence.EntityConfigurations +{ + internal class PaymentMethodEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("PaymentMethods", UserContext.DEFAULT_SCHEMA); + + builder.HasKey(ct => ct.Id); + + builder.Property(ct => ct.Id) + .HasDefaultValue(1) + .ValueGeneratedNever() + .IsRequired(); + + builder.Property(ct => ct.Name) + .HasMaxLength(200) + .IsRequired(); + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/UserEntityTypeConfiguration.cs b/src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/UserEntityTypeConfiguration.cs new file mode 100644 index 0000000..51d51ed --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Persistence/EntityConfigurations/UserEntityTypeConfiguration.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Duber.Domain.User.Persistence.EntityConfigurations +{ + internal class UserEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Users", UserContext.DEFAULT_SCHEMA); + + builder.HasKey(o => o.Id); + builder.Property(o => o.Id) + .UseHiLo("userseq", UserContext.DEFAULT_SCHEMA); + + builder.Ignore(b => b.DomainEvents); + + builder.Property(x => x.Name) + .IsRequired(); + + builder.Property(x => x.Email) + .IsRequired(); + + builder + .Property("_paymentMethodId") + .UsePropertyAccessMode(PropertyAccessMode.Field) + .HasColumnName("PaymentMethodId") + .IsRequired(); + + builder.HasIndex(x => x.Email) + .IsUnique(); + + builder.HasOne(p => p.PaymentMethod) + .WithMany() + .HasForeignKey("_paymentMethodId"); + + builder.Property(x =>x.NumberPhone) + .IsRequired(false); + + builder.Property(x => x.Rating) + .HasDefaultValue(0) + .IsRequired(); + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Persistence/UserContext.cs b/src/Domain/User/Duber.Domain.User/Persistence/UserContext.cs new file mode 100644 index 0000000..489ffc2 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Persistence/UserContext.cs @@ -0,0 +1,38 @@ +using Duber.Domain.SharedKernel.Model; +using Duber.Domain.User.Persistence.EntityConfigurations; +using Duber.Infrastructure.Repository.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Duber.Domain.User.Persistence +{ + public class UserContext : DbContext, IUnitOfWork + { + // ReSharper disable once InconsistentNaming + public const string DEFAULT_SCHEMA = "User"; + + public UserContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new UserEntityTypeConfiguration()); + } + + public DbSet Users { get; set; } + + public DbSet PaymentMethods { get; set; } + } + + // in order to migration creation works. + public class UserContextDesignFactory : IDesignTimeDbContextFactory + { + public UserContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Initial Catalog=Duber.WebSiteDb;Integrated Security=true"); + + return new UserContext(optionsBuilder.Options); + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Persistence/UserContextSeed.cs b/src/Domain/User/Duber.Domain.User/Persistence/UserContextSeed.cs new file mode 100644 index 0000000..0997235 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Persistence/UserContextSeed.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duber.Domain.SharedKernel.Model; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Polly; + +namespace Duber.Domain.User.Persistence +{ + public class UserContextSeed + { + public async Task SeedAsync(UserContext context, ILogger logger) + { + var policy = CreatePolicy(logger, nameof(UserContextSeed)); + + await policy.ExecuteAsync(async () => + { + using (context) + { + if (!context.PaymentMethods.Any()) + { + context.PaymentMethods.AddRange(GetPreconfiguredPaymentMethods()); + await context.SaveChangesAsync(); + } + + if (!context.Users.Any()) + { + context.Users.AddRange(GetPreconfiguredUsers()); + await context.SaveChangesAsync(); + } + } + }); + } + + private static IEnumerable GetPreconfiguredUsers() => new List + { + new Model.User("James Hedfield", "jamesh@metallica.com", 5, PaymentMethod.CreditCard, "1234567890"), + new Model.User("Rob Haldford", "robh@judaspriest.com", 5, PaymentMethod.CreditCard, "7894561230"), + new Model.User("Jimi Hendrix", "heyjoe@latinchat.com", 5, PaymentMethod.Cash, "7894561230"), + new Model.User("Steve Vai", "stevevay@hotmail.com", 5, PaymentMethod.CreditCard, "4567891230"), + new Model.User("Joe Satriani", "jsatriani@gmail.com", 5, PaymentMethod.Cash, "7418529630"), + }; + + private static IEnumerable GetPreconfiguredPaymentMethods() => new List() + { + PaymentMethod.CreditCard, + PaymentMethod.Cash, + }; + + private static AsyncPolicy CreatePolicy(ILogger logger, string prefix, int retries = 3) + { + return Policy.Handle(). + WaitAndRetryAsync( + retryCount: retries, + sleepDurationProvider: retry => TimeSpan.FromSeconds(5), + onRetry: (exception, timeSpan, retry, ctx) => + { + logger.LogTrace($"[{prefix}] Exception {exception.GetType().Name} with message ${exception.Message} detected on attempt {retry} of {retries}"); + } + ); + } + } +} diff --git a/src/Domain/User/Duber.Domain.User/Repository/IUserRepository.cs b/src/Domain/User/Duber.Domain.User/Repository/IUserRepository.cs new file mode 100644 index 0000000..49ce7b5 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Repository/IUserRepository.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Duber.Infrastructure.Repository.Abstractions; + +namespace Duber.Domain.User.Repository +{ + public interface IUserRepository : IRepository + { + Model.User GetUser(int userId); + + Task> GetUsersAsync(); + + Task GetUserAsync(int userId); + + void Update(Model.User user); + } +} diff --git a/src/Domain/User/Duber.Domain.User/Repository/UserRepository.cs b/src/Domain/User/Duber.Domain.User/Repository/UserRepository.cs new file mode 100644 index 0000000..ff7f934 --- /dev/null +++ b/src/Domain/User/Duber.Domain.User/Repository/UserRepository.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duber.Domain.User.Persistence; +using Duber.Infrastructure.Repository.Abstractions; +using Microsoft.EntityFrameworkCore; + +namespace Duber.Domain.User.Repository +{ + public class UserRepository : IUserRepository + { + private readonly UserContext _context; + + public UserRepository(UserContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public IUnitOfWork UnitOfWork => _context; + + public async Task> GetUsersAsync() + { + return await _context.Users + .Include(x => x.PaymentMethod) + .ToListAsync(); + } + + public async Task GetUserAsync(int userId) + { + return await _context.Users.SingleOrDefaultAsync(x => x.Id == userId); + } + + public Model.User GetUser(int userId) + { + return _context.Users.SingleOrDefault(x => x.Id == userId); + } + + public void Update(Model.User user) + { + _context.Entry(user).State = EntityState.Modified; + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj new file mode 100644 index 0000000..418b4b6 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicyAsyncExecutor.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicyAsyncExecutor.cs new file mode 100644 index 0000000..54aaa8d --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicyAsyncExecutor.cs @@ -0,0 +1,27 @@ +using Polly; +using Polly.Registry; +using System; +using System.Threading.Tasks; + +namespace Duber.Infrastructure.Resilience.Abstractions +{ + /// + /// Polly doesn't support async void methods. So you can't pass an Action. + /// http://www.thepollyproject.org/2017/06/09/polly-and-synchronous-versus-asynchronous-policies/ + /// Returns a Task in order to avoid execution of void methods asynchronously, which causes unexpected out-of-sequence execution of policy hooks and continuing policy actions, and a risk of unobserved exceptions. + /// https://msdn.microsoft.com/en-us/magazine/jj991977.aspx + /// https://github.com/App-vNext/Polly/issues/107#issuecomment-218835218 + /// + public interface IPolicyAsyncExecutor + { + PolicyRegistry PolicyRegistry { get; set; } + + Task ExecuteAsync(Func> action); + + Task ExecuteAsync(Func> action, Context context); + + Task ExecuteAsync(Func action); + + Task ExecuteAsync(Func action, Context context); + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicySyncExecutor.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicySyncExecutor.cs new file mode 100644 index 0000000..8697c26 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/IPolicySyncExecutor.cs @@ -0,0 +1,19 @@ +using Polly; +using Polly.Registry; +using System; + +namespace Duber.Infrastructure.Resilience.Abstractions +{ + public interface IPolicySyncExecutor + { + PolicyRegistry PolicyRegistry { get; set; } + + T Execute(Func action); + + T Execute(Func action, Context context); + + void Execute(Action action); + + void Execute(Action action, Context context); + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicyAsyncExecutor.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicyAsyncExecutor.cs new file mode 100644 index 0000000..63a3421 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicyAsyncExecutor.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Polly; +using Polly.Registry; + +namespace Duber.Infrastructure.Resilience.Abstractions +{ + /// + /// Executes the action applying all the policies defined in the wrapper + /// + public class PolicyAsyncExecutor : IPolicyAsyncExecutor + { + private readonly IEnumerable _asyncPolicies; + + public PolicyRegistry PolicyRegistry { get; set; } + + public PolicyAsyncExecutor(IEnumerable policies) + { + _asyncPolicies = policies ?? throw new ArgumentNullException(nameof(policies)); + + PolicyRegistry = new PolicyRegistry + { + [nameof(PolicyAsyncExecutor)] = Policy.WrapAsync(_asyncPolicies.ToArray()) + }; + } + + public async Task ExecuteAsync(Func> action) + { + var policy = PolicyRegistry.Get(nameof(PolicyAsyncExecutor)); + return await policy.ExecuteAsync(action); + } + + public async Task ExecuteAsync(Func action) + { + var policy = PolicyRegistry.Get(nameof(PolicyAsyncExecutor)); + await policy.ExecuteAsync(action); + } + + public async Task ExecuteAsync(Func> action, Context context) + { + var policy = PolicyRegistry.Get(nameof(PolicyAsyncExecutor)); + return await policy.ExecuteAsync(action, context); + } + + public async Task ExecuteAsync(Func action, Context context) + { + var policy = PolicyRegistry.Get(nameof(PolicyAsyncExecutor)); + await policy.ExecuteAsync(action, context); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicySyncExecutor.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicySyncExecutor.cs new file mode 100644 index 0000000..54b8667 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/PolicySyncExecutor.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Polly; +using Polly.Registry; + +namespace Duber.Infrastructure.Resilience.Abstractions +{ + /// + /// Executes the action applying all the policies defined in the wrapper + /// + public class PolicySyncExecutor : IPolicySyncExecutor + { + private readonly IEnumerable _syncPolicies; + + public PolicyRegistry PolicyRegistry { get; set; } + + public PolicySyncExecutor(IEnumerable policies) + { + _syncPolicies = policies ?? throw new ArgumentNullException(nameof(policies)); + + PolicyRegistry = new PolicyRegistry + { + [nameof(PolicySyncExecutor)] = Policy.Wrap(_syncPolicies.ToArray()) + }; + } + + public T Execute(Func action) + { + var policy = PolicyRegistry.Get(nameof(PolicySyncExecutor)); + return policy.Execute(action); + } + + public void Execute(Action action) + { + var policy = PolicyRegistry.Get(nameof(PolicySyncExecutor)); + policy.Execute(action); + } + + public T Execute(Func action, Context context) + { + var policy = PolicyRegistry.Get(nameof(PolicySyncExecutor)); + return policy.Execute(action, context); + } + + public void Execute(Action action, Context context) + { + var policy = PolicyRegistry.Get(nameof(PolicySyncExecutor)); + policy.Execute(action, context); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj new file mode 100644 index 0000000..9ffb2b0 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + + + + + + + + diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Http/ResilientHttpClient.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/ResilientHttpClient.cs new file mode 100644 index 0000000..8a3e7b6 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/ResilientHttpClient.cs @@ -0,0 +1,32 @@ +using Polly; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Duber.Infrastructure.Resilience.Http +{ + public class ResilientHttpClient + { + private readonly HttpClient _client; + + public ResilientHttpClient(HttpClient client) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + } + + public async Task SendAsync(HttpRequestMessage message, Context context) + { + // We attach the Polly context to the HttpRequestMessage using an extension method provided by HttpClientFactory. + message.SetPolicyExecutionContext(context); + + // Make the request using the client configured by HttpClientFactory, which embeds the Polly and Simmy policies. + return await _client.SendAsync(message); + } + + public async Task SendAsync(HttpRequestMessage message) + { + // Make the request using the client configured by HttpClientFactory, which embeds the Polly and Simmy policies. + return await _client.SendAsync(message); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj new file mode 100644 index 0000000..a7d22ce --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/ISqlPolicyBuilder.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/ISqlPolicyBuilder.cs new file mode 100644 index 0000000..629935f --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/ISqlPolicyBuilder.cs @@ -0,0 +1,53 @@ +using Duber.Infrastructure.Resilience.Abstractions; +using Polly.Registry; +using System; +using System.Threading.Tasks; + +namespace Duber.Infrastructure.Resilience.Sql +{ + public interface ISqlAsyncPolicyBuilder + { + ISqlAsyncPolicyBuilder WithDefaultPolicies(); + + ISqlAsyncPolicyBuilder WithFallback(Func> action); + + ISqlAsyncPolicyBuilder WithFallback(Func action); + + ISqlAsyncPolicyBuilder WithOverallTimeout(TimeSpan timeout); + + ISqlAsyncPolicyBuilder WithTimeoutPerRetry(TimeSpan timeout); + + ISqlAsyncPolicyBuilder WithOverallAndTimeoutPerRetry(TimeSpan overallTimeout, TimeSpan timeoutPerRetry); + + ISqlAsyncPolicyBuilder WithTransientErrors(int retryCount = 4); + + ISqlAsyncPolicyBuilder WithTransaction(); + + ISqlAsyncPolicyBuilder WithCircuitBreaker(int exceptionsAllowedBeforeBreaking = 3); + + IPolicyAsyncExecutor Build(); + } + + public interface ISqlSyncPolicyBuilder + { + ISqlSyncPolicyBuilder WithDefaultPolicies(); + + ISqlSyncPolicyBuilder WithFallback(Func action); + + ISqlSyncPolicyBuilder WithFallback(Action action); + + ISqlSyncPolicyBuilder WithOverallTimeout(TimeSpan timeout); + + ISqlSyncPolicyBuilder WithTimeoutPerRetry(TimeSpan timeout); + + ISqlSyncPolicyBuilder WithOverallAndTimeoutPerRetry(TimeSpan overallTimeout, TimeSpan timeoutPerRetry); + + ISqlSyncPolicyBuilder WithTransientErrors(int retryCount = 4); + + ISqlSyncPolicyBuilder WithTransaction(); + + ISqlSyncPolicyBuilder WithCircuitBreaker(int exceptionsAllowedBeforeBreaking = 3); + + IPolicySyncExecutor Build(); + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlAsyncPolicyBuilder.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlAsyncPolicyBuilder.cs new file mode 100644 index 0000000..5b5ad86 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlAsyncPolicyBuilder.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duber.Infrastructure.Resilience.Abstractions; +using Duber.Infrastructure.Resilience.Sql.Policies; +using Polly; +using Polly.Registry; + +namespace Duber.Infrastructure.Resilience.Sql.Internals +{ + /// + /// The order which is inserted the policies into the list matters. + /// https://github.com/App-vNext/Polly/wiki/PolicyWrap#usage-recommendations + /// + internal class SqlAsyncPolicyBuilder : ISqlAsyncPolicyBuilder + { + private List _policies; + private const int RetryCount = 5; + private readonly bool _sharedPolicies; + private const int ExceptionsAllowedBeforeBreaking = 3; + private readonly TimeSpan _overallTimeout = GetTimeout(); + private static readonly PolicyRegistry _sharedPoliciesRegistry = new PolicyRegistry(); + + public SqlAsyncPolicyBuilder(bool sharedPolicies = false) + { + _sharedPolicies = sharedPolicies; + _policies = new List(); + } + + public ISqlAsyncPolicyBuilder WithDefaultPolicies() + { + _policies.Add(AsyncPolicies.GetTimeOutPolicy(_overallTimeout, PolicyKeys.SqlOverallTimeoutAsyncPolicy)); + _policies.Add(AsyncPolicies.GetCommonTransientErrorsPolicies(RetryCount)); + _policies.AddRange(AsyncPolicies.GetCircuitBreakerPolicies(ExceptionsAllowedBeforeBreaking)); + return this; + } + + public ISqlAsyncPolicyBuilder WithTransientErrors(int retryCount) + { + _policies.Add(AsyncPolicies.GetCommonTransientErrorsPolicies(RetryCount)); + return this; + } + + public ISqlAsyncPolicyBuilder WithTransaction() + { + _policies.Add(AsyncPolicies.GetTransactionPolicy(RetryCount)); + return this; + } + + public ISqlAsyncPolicyBuilder WithCircuitBreaker(int exceptionsAllowedBeforeBreaking) + { + _policies.AddRange(AsyncPolicies.GetCircuitBreakerPolicies(ExceptionsAllowedBeforeBreaking)); + return this; + } + + public ISqlAsyncPolicyBuilder WithFallback(Func> action) + { + _policies.Add(AsyncPolicies.GetFallbackPolicy(action)); + return this; + } + + public ISqlAsyncPolicyBuilder WithFallback(Func action) + { + _policies.Add(AsyncPolicies.GetFallbackPolicy(action)); + return this; + } + + public ISqlAsyncPolicyBuilder WithOverallTimeout(TimeSpan timeout) + { + _policies.Add(AsyncPolicies.GetTimeOutPolicy(timeout, PolicyKeys.SqlOverallTimeoutAsyncPolicy)); + return this; + } + + public ISqlAsyncPolicyBuilder WithTimeoutPerRetry(TimeSpan timeout) + { + _policies.Add(AsyncPolicies.GetTimeOutPolicy(timeout, PolicyKeys.SqlTimeoutPerRetryAsyncPolicy)); + return this; + } + + public ISqlAsyncPolicyBuilder WithOverallAndTimeoutPerRetry(TimeSpan overallTimeout, TimeSpan timeoutPerRetry) + { + _policies.Add(AsyncPolicies.GetTimeOutPolicy(_overallTimeout, PolicyKeys.SqlOverallTimeoutAsyncPolicy)); + _policies.Add(AsyncPolicies.GetTimeOutPolicy(timeoutPerRetry, PolicyKeys.SqlTimeoutPerRetryAsyncPolicy)); + return this; + } + + public IPolicyAsyncExecutor Build() + { + if (!_policies.Any()) + throw new InvalidOperationException("There are no policies to execute."); + + // to prevent consumers uses WithDefaultPolicies together with other methods. + var duplicatedPolicies = _policies.GroupBy(x => x.PolicyKey) + .Where(g => g.Count() > 1) + .Select(y => y.Key) + .ToList(); + + if (duplicatedPolicies.Any()) + throw new InvalidOperationException("There are duplicated policies. When you use WithDefaultPolicies method, you can't use either WithTransientErrors. WithCircuitBreaker or WithOverallTimeout methods at the same time, because those policies are already included."); + + // if there is timeout per retry but there's not retry policy. + var retryPolicyNames = new[] { PolicyKeys.SqlCommonTransientErrorsAsyncPolicy, PolicyKeys.SqlTransactionAsyncPolicy }; + var retryPolicies = _policies.Where(x => retryPolicyNames.Contains(x.PolicyKey)); + var timeoutPerRetryPolicy = _policies.Where(x => x.PolicyKey == PolicyKeys.SqlTimeoutPerRetryAsyncPolicy); + + if (timeoutPerRetryPolicy.Any() && !retryPolicies.Any()) + throw new InvalidOperationException("You're trying to use Timeout per retries but you don't have Retry policies configured."); + + // The order of policies into the list is important (not mandatory) in order to get a consistent resilience strategy. + // That's why I named the policies alphabetically. + // https://github.com/App-vNext/Polly/wiki/PolicyWrap#usage-recommendations + _policies = _policies.OrderBy(x => x.PolicyKey).ToList(); + + if (_sharedPolicies) + { + for (var index = 0; index < _policies.Count; index++) + { + var policy = _policies[index]; + _sharedPoliciesRegistry.TryGet(policy.PolicyKey, out IAsyncPolicy sharedPolicy); + + if (sharedPolicy == null) + { + _sharedPoliciesRegistry.Add(policy.PolicyKey, policy); + } + else + { + // replaces new policy to the one already exists into register + _policies[index] = sharedPolicy; + } + } + } + + return new PolicyAsyncExecutor(_policies); + } + + /// + /// Gets the timeout based on retries and exponential back-off + /// + private static TimeSpan GetTimeout() + { + var retry = 1; + var delay = TimeSpan.Zero; + while (retry <= RetryCount) + { + delay += TimeSpan.FromSeconds(Math.Pow(2, retry)); + retry++; + } + + // plus an arbitrary max time the operation could take + return delay + TimeSpan.FromSeconds(10); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlHandledExceptions.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlHandledExceptions.cs new file mode 100644 index 0000000..67da2fe --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlHandledExceptions.cs @@ -0,0 +1,12 @@ +namespace Duber.Infrastructure.Resilience.Sql.Internals +{ + internal enum SqlHandledExceptions + { + DatabaseNotCurrentlyAvailable = 40613, + ErrorProcessingRequest = 40197, + ServiceCurrentlyBusy = 40501, + NotEnoughResources = 49918, + SessionTerminatedLongTransaction = 40549, + SessionTerminatedToManyLocks = 40550 + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlSyncPolicyBuilder.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlSyncPolicyBuilder.cs new file mode 100644 index 0000000..adf279a --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Internals/SqlSyncPolicyBuilder.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Infrastructure.Resilience.Abstractions; +using Duber.Infrastructure.Resilience.Sql.Policies; +using Polly; +using Polly.Registry; + +namespace Duber.Infrastructure.Resilience.Sql +{ + /// + /// The order which is inserted the policies into the list matters. + /// https://github.com/App-vNext/Polly/wiki/PolicyWrap#usage-recommendations + /// + internal class SqlSyncPolicyBuilder : ISqlSyncPolicyBuilder + { + private List _policies; + private const int RetryCount = 5; + private readonly bool _sharedPolicies; + private const int ExceptionsAllowedBeforeBreaking = 3; + private readonly TimeSpan _overallTimeout = GetTimeout(); + private static readonly PolicyRegistry _registry = new PolicyRegistry(); + + public SqlSyncPolicyBuilder(bool sharePolicies = false) + { + _sharedPolicies = sharePolicies; + _policies = new List(); + } + + public ISqlSyncPolicyBuilder WithDefaultPolicies() + { + _policies.Add(SyncPolicies.GetTimeOutPolicy(_overallTimeout, PolicyKeys.SqlOverallTimeoutSyncPolicy)); + _policies.Add(SyncPolicies.GetCommonTransientErrorsPolicies(RetryCount)); + _policies.AddRange(SyncPolicies.GetCircuitBreakerPolicies(ExceptionsAllowedBeforeBreaking)); + return this; + } + + public ISqlSyncPolicyBuilder WithTransientErrors(int retryCount) + { + _policies.Add(SyncPolicies.GetCommonTransientErrorsPolicies(RetryCount)); + return this; + } + + public ISqlSyncPolicyBuilder WithTransaction() + { + _policies.Add(SyncPolicies.GetTransactionPolicy(RetryCount)); + return this; + } + + public ISqlSyncPolicyBuilder WithCircuitBreaker(int exceptionsAllowedBeforeBreaking) + { + _policies.AddRange(SyncPolicies.GetCircuitBreakerPolicies(ExceptionsAllowedBeforeBreaking)); + return this; + } + + public ISqlSyncPolicyBuilder WithFallback(Func action) + { + _policies.Add(SyncPolicies.GetFallbackPolicy(action)); + return this; + } + + public ISqlSyncPolicyBuilder WithFallback(Action action) + { + _policies.Add(SyncPolicies.GetFallbackPolicy(action)); + return this; + } + + public ISqlSyncPolicyBuilder WithOverallTimeout(TimeSpan timeout) + { + _policies.Add(SyncPolicies.GetTimeOutPolicy(timeout, PolicyKeys.SqlOverallTimeoutSyncPolicy)); + return this; + } + + public ISqlSyncPolicyBuilder WithTimeoutPerRetry(TimeSpan timeout) + { + _policies.Add(SyncPolicies.GetTimeOutPolicy(timeout, PolicyKeys.SqlTimeoutPerRetrySyncPolicy)); + return this; + } + + public ISqlSyncPolicyBuilder WithOverallAndTimeoutPerRetry(TimeSpan overallTimeout, TimeSpan timeoutPerRetry) + { + _policies.Add(SyncPolicies.GetTimeOutPolicy(_overallTimeout, PolicyKeys.SqlOverallTimeoutSyncPolicy)); + _policies.Add(SyncPolicies.GetTimeOutPolicy(timeoutPerRetry, PolicyKeys.SqlTimeoutPerRetrySyncPolicy)); + return this; + } + + public IPolicySyncExecutor Build() + { + if (!_policies.Any()) + throw new InvalidOperationException("There are no policies to execute."); + + // to prevent consumers uses WithDefaultPolicies together with other methods. + var duplicatedPolicies = _policies.GroupBy(x => x.PolicyKey) + .Where(g => g.Count() > 1) + .Select(y => y.Key) + .ToList(); + + if (duplicatedPolicies.Any()) + throw new InvalidOperationException("There are duplicated policies. When you use WithDefaultPolicies method, you can't use either WithTransientErrors. WithCircuitBreaker or WithOverallTimeout methods at the same time, because those policies are already included."); + + // if there is timeout per retry but there's not retry policy. + var retryPolicyNames = new[] { PolicyKeys.SqlCommonTransientErrorsSyncPolicy, PolicyKeys.SqlTransactionSyncPolicy }; + var retryPolicies = _policies.Where(x => retryPolicyNames.Contains(x.PolicyKey)); + var timeoutPerRetryPolicy = _policies.Where(x => x.PolicyKey == PolicyKeys.SqlTimeoutPerRetrySyncPolicy); + + if (timeoutPerRetryPolicy.Any() && !retryPolicies.Any()) + throw new InvalidOperationException("You're trying to use Timeout per retries but you don't have Retry policies configured."); + + // The order of policies into the list is important (not mandatory) in order to get a consistent resilience strategy. + // That's why I named the policies alphabetically. + // https://github.com/App-vNext/Polly/wiki/PolicyWrap#usage-recommendations + _policies = _policies.OrderBy(x => x.PolicyKey).ToList(); + + if (_sharedPolicies) + { + for (var index = 0; index < _policies.Count; index++) + { + var policy = _policies[index]; + _registry.TryGet(policy.PolicyKey, out ISyncPolicy sharedPolicy); + + if (sharedPolicy == null) + { + _registry.Add(policy.PolicyKey, policy); + } + else + { + // replaces new policy to the one already exists into register + _policies[index] = sharedPolicy; + } + } + } + + return new PolicySyncExecutor(_policies); + } + + /// + /// Gets the timeout based on retries and exponential back-off + /// + private static TimeSpan GetTimeout() + { + var retry = 1; + var delay = TimeSpan.Zero; + while (retry <= RetryCount) + { + delay += TimeSpan.FromSeconds(Math.Pow(2, retry)); + retry++; + } + + // plus an arbitrary max time the operation could take + return delay + TimeSpan.FromSeconds(10); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/AsyncPolicies.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/AsyncPolicies.cs new file mode 100644 index 0000000..b5eed12 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/AsyncPolicies.cs @@ -0,0 +1,231 @@ +using System; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; +using Duber.Infrastructure.Resilience.Sql.Internals; +using log4net; +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Duber.Infrastructure.Resilience.Sql.Policies +{ + /// + /// Sql error codes for Azure Sql. + /// https://docs.microsoft.com/en-us/azure/sql-database/sql-database-develop-error-messages + /// + internal class AsyncPolicies + { + private static readonly ILog Log = LogManager.GetLogger(typeof(AsyncPolicies)); + + private static readonly int[] SqlTransientErrors = + { + (int)SqlHandledExceptions.DatabaseNotCurrentlyAvailable, + (int)SqlHandledExceptions.ErrorProcessingRequest, + (int)SqlHandledExceptions.ServiceCurrentlyBusy, + (int)SqlHandledExceptions.NotEnoughResources + }; + + private static readonly int[] SqlTransactionErrors = + { + (int)SqlHandledExceptions.SessionTerminatedLongTransaction, + (int)SqlHandledExceptions.SessionTerminatedToManyLocks + }; + + /// + /// Gets a Retry policy for the most common transient error in Azure Sql. + /// + public static IAsyncPolicy GetCommonTransientErrorsPolicies(int retryCount) => + Policy + .Handle(ex => SqlTransientErrors.Contains(ex.Number)) + .WaitAndRetryAsync( + // number of retries + retryCount, + // exponential back-off + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retries, context) => + { + if (retryCount != retries) + return; + + // only log if the final retry fails + var msg = $"#Polly #WaitAndRetryAsync Retry {retries}" + + $"of {context.PolicyKey} " + + $"due to: {exception}."; + Log.Error(msg, exception); + }) + .WithPolicyKey(PolicyKeys.SqlCommonTransientErrorsAsyncPolicy); + + /// + /// Gets a Retry policy for the most common transaction errors in Azure Sql. + /// + public static IAsyncPolicy GetTransactionPolicy(int retryCount) => + Policy + .Handle(ex => SqlTransactionErrors.Contains(ex.Number)) + .WaitAndRetryAsync( + // number of retries + retryCount, + // exponential back-off + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retries, context) => + { + if (retryCount != retries) + return; + + // only log if the final retry fails + var msg = $"#Polly #WaitAndRetryAsync Retry {retries}" + + $"of {context.PolicyKey} " + + $"due to: {exception}."; + Log.Error(msg, exception); + }) + .WithPolicyKey(PolicyKeys.SqlTransactionAsyncPolicy); + + /// + /// Gets the circuit breaker policies for Transient and Transaction errors. + /// + /// + /// The circuit-breaker will break after N consecutive actions executed through the policy have thrown 'a' handled exception - any of the exceptions handled by the policy. + /// So, we might want the circuit ONLY breaks after throws consecutively the SAME exception N times, that's why I'm defining separate circuit-breaker policies. + /// + /// + /// Circuit broken = 3 consecutive SqlException(40613), and not, circuit broken = 3 SqlException(40613 or 40197 or 40501...) + /// https://github.com/App-vNext/Polly/issues/490 + /// + public static IAsyncPolicy[] GetCircuitBreakerPolicies(int exceptionsAllowedBeforeBreaking) + => new IAsyncPolicy[] + { + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.DatabaseNotCurrentlyAvailable) + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F1.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.ErrorProcessingRequest) + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F2.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.ServiceCurrentlyBusy) + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F3.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.NotEnoughResources) + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F4.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.SessionTerminatedLongTransaction) + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F5.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.SessionTerminatedToManyLocks) + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F6.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}") + }; + + /// + /// Gets a pessimistic timeout policy in order to cancel the process which doesn't even honor cancellation. + /// https://github.com/App-vNext/Polly/wiki/Timeout#pessimistic-timeout + /// + public static IAsyncPolicy GetTimeOutPolicy(TimeSpan timeout, string policyName) => + Policy + .TimeoutAsync( + timeout, + TimeoutStrategy.Pessimistic) + .WithPolicyKey(policyName); + + /// + /// Handles a fallback policy for SqlException, TimeoutRejectedException and BrokenCircuitException exceptions. It means, if the process fails for one of those reasons, the fallback method is executed instead. + /// + public static IAsyncPolicy GetFallbackPolicy(Func> action) => + Policy + .Handle(ex => SqlTransientErrors.Contains(ex.Number)) + .Or(ex => SqlTransactionErrors.Contains(ex.Number)) + .Or() + .Or() + .FallbackAsync(cancellationToken => action(), + ex => + { + var msg = $"#Polly #FallbackAsync Fallback method used due to: {ex}"; + Log.Error(msg, ex); + return Task.CompletedTask; + }) + .WithPolicyKey(PolicyKeys.SqlFallbackAsyncPolicy); + + /// + /// Handles a fallback policy for SqlException, TimeoutRejectedException and BrokenCircuitException exceptions. It means, if the process fails for one of those reasons, the fallback method is executed instead. + /// + public static IAsyncPolicy GetFallbackPolicy(Func action) => + Policy + .Handle(ex => SqlTransientErrors.Contains(ex.Number)) + .Or(ex => SqlTransactionErrors.Contains(ex.Number)) + .Or() + .Or() + .FallbackAsync(cancellationToken => action(), + ex => + { + var msg = $"#Polly #FallbackAsync Fallback method used due to: {ex}"; + Log.Error(msg, ex); + return Task.CompletedTask; + }) + .WithPolicyKey(PolicyKeys.SqlFallbackAsyncPolicy); + + private static void OnHalfOpen() + { + Log.Warn("#Polly #CircuitBreakerAsync Half-open: Next call is a trial"); + } + + private static void OnReset() + { + // on circuit closed + Log.Warn("#Polly #CircuitBreakerAsync Circuit breaker reset"); + } + + private static void OnBreak(Exception exception, TimeSpan duration) + { + // on circuit opened + Log.Warn("#Polly #CircuitBreakerAsync Circuit breaker opened", exception); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/PolicyKeys.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/PolicyKeys.cs new file mode 100644 index 0000000..3c942fb --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/PolicyKeys.cs @@ -0,0 +1,18 @@ +namespace Duber.Infrastructure.Resilience.Sql.Policies +{ + internal static class PolicyKeys + { + public const string SqlFallbackAsyncPolicy = "A.SqlFallbackAsyncPolicy"; + public const string SqlFallbackSyncPolicy = "A.SqlFallbackSyncPolicy"; + public const string SqlOverallTimeoutSyncPolicy = "B.SqlOverallTimeoutSyncPolicy"; + public const string SqlOverallTimeoutAsyncPolicy = "B.SqlOverallTimeoutAsyncPolicy"; + public const string SqlCommonTransientErrorsSyncPolicy = "C.SqlCommonTransientErrorsSyncPolicy"; + public const string SqlCommonTransientErrorsAsyncPolicy = "C.SqlCommonTransientErrorsAsyncPolicy"; + public const string SqlTransactionAsyncPolicy = "D.SqlTransactionAsyncPolicy"; + public const string SqlTransactionSyncPolicy = "D.SqlTransactionSyncPolicy"; + public const string SqlTimeoutPerRetrySyncPolicy = "E.SqlTimeoutPerRetrySyncPolicy"; + public const string SqlTimeoutPerRetryAsyncPolicy = "E.SqlTimeoutPerRetryAsyncPolicy"; + public const string SqlCircuitBreakerAsyncPolicy = "SqlCircuitBreakerAsyncPolicy"; + public const string SqlCircuitBreakerSyncPolicy = "SqlCircuitBreakerSyncPolicy"; + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/SyncPolicies.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/SyncPolicies.cs new file mode 100644 index 0000000..83c425f --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Policies/SyncPolicies.cs @@ -0,0 +1,227 @@ +using System; +using System.Data.SqlClient; +using System.Linq; +using Duber.Infrastructure.Resilience.Sql.Internals; +using log4net; +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Duber.Infrastructure.Resilience.Sql.Policies +{ + /// + /// Sql error codes for Azure Sql. + /// https://docs.microsoft.com/en-us/azure/sql-database/sql-database-develop-error-messages + /// + internal class SyncPolicies + { + private static readonly ILog Log = LogManager.GetLogger(typeof(SyncPolicies)); + private static readonly int[] SqlTransientErrors = + { + (int)SqlHandledExceptions.DatabaseNotCurrentlyAvailable, + (int)SqlHandledExceptions.ErrorProcessingRequest, + (int)SqlHandledExceptions.ServiceCurrentlyBusy, + (int)SqlHandledExceptions.NotEnoughResources + }; + + private static readonly int[] SqlTransactionErrors = + { + (int)SqlHandledExceptions.SessionTerminatedLongTransaction, + (int)SqlHandledExceptions.SessionTerminatedToManyLocks + }; + + /// + /// Gets a Retry policy for the most common transient error in Azure Sql. + /// + public static ISyncPolicy GetCommonTransientErrorsPolicies(int retryCount) => + Policy + .Handle(ex => SqlTransientErrors.Contains(ex.Number)) + .WaitAndRetry( + // number of retries + retryCount, + // exponential back-off + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retries, context) => + { + if (retryCount != retries) + return; + + // only log if the final retry fails + var msg = $"#Polly #WaitAndRetry Retry {retries}" + + $"of {context.PolicyKey} " + + $"due to: {exception}."; + Log.Error(msg, exception); + }) + .WithPolicyKey(PolicyKeys.SqlCommonTransientErrorsSyncPolicy); + + /// + /// Gets a Retry policy for the most common transaction errors in Azure Sql. + /// + public static ISyncPolicy GetTransactionPolicy(int retryCount) => + Policy + .Handle(ex => SqlTransactionErrors.Contains(ex.Number)) + .WaitAndRetry( + // number of retries + retryCount, + // exponential back-off + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retries, context) => + { + if (retryCount != retries) + return; + + // only log if the final retry fails + var msg = $"#Polly #WaitAndRetrySync Retry {retries}" + + $"of {context.PolicyKey} " + + $"due to: {exception}."; + Log.Error(msg, exception); + }) + .WithPolicyKey(PolicyKeys.SqlTransactionSyncPolicy); + + /// + /// Gets the circuit breaker policies for Transient and Transaction errors. + /// + /// + /// The circuit-breaker will break after N consecutive actions executed through the policy have thrown 'a' handled exception - any of the exceptions handled by the policy. + /// So, we might want the circuit ONLY breaks after throws consecutively the SAME exception N times, that's why I'm defining separate circuit-breaker policies. + /// + /// + /// Circuit broken = 3 consecutive SqlException(40613), and not, circuit broken = 3 SqlException(40613 or 40197 or 40501...) + /// https://github.com/App-vNext/Polly/issues/490 + /// + public static ISyncPolicy[] GetCircuitBreakerPolicies(int exceptionsAllowedBeforeBreaking) + => new ISyncPolicy[] + { + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.DatabaseNotCurrentlyAvailable) + .CircuitBreaker( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F1.{PolicyKeys.SqlCircuitBreakerSyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.ErrorProcessingRequest) + .CircuitBreaker( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F2.{PolicyKeys.SqlCircuitBreakerSyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.ServiceCurrentlyBusy) + .CircuitBreaker( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F3.{PolicyKeys.SqlCircuitBreakerSyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.NotEnoughResources) + .CircuitBreaker( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F4.{PolicyKeys.SqlCircuitBreakerSyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.SessionTerminatedLongTransaction) + .CircuitBreaker( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F5.{PolicyKeys.SqlCircuitBreakerSyncPolicy}"), + Policy + .Handle(ex => ex.Number == (int)SqlHandledExceptions.SessionTerminatedToManyLocks) + .CircuitBreaker( + // number of exceptions before breaking circuit + exceptionsAllowedBeforeBreaking, + // time circuit opened before retry + TimeSpan.FromSeconds(30), + OnBreak, + OnReset, + OnHalfOpen) + .WithPolicyKey($"F6.{PolicyKeys.SqlCircuitBreakerSyncPolicy}") + }; + + /// + /// Gets a pessimistic timeout policy in order to cancel the process which doesn't even honor cancellation. + /// https://github.com/App-vNext/Polly/wiki/Timeout#pessimistic-timeout + /// + public static ISyncPolicy GetTimeOutPolicy(TimeSpan timeout, string policyName) => + Policy + .Timeout( + timeout, + TimeoutStrategy.Pessimistic) + .WithPolicyKey(policyName); + + /// + /// Handles a fallback policy for SqlException, TimeoutRejectedException and BrokenCircuitException exceptions. It means, if the process fails for one of those reasons, the fallback method is executed instead. + /// + public static ISyncPolicy GetFallbackPolicy(Func action) => + Policy + .Handle(ex => SqlTransientErrors.Contains(ex.Number)) + .Or(ex => SqlTransactionErrors.Contains(ex.Number)) + .Or() + .Or() + .Fallback(() => action(), + ex => + { + var msg = $"#Polly #Fallback Fallback method used due to: {ex}"; + Log.Error(msg, ex); + }) + .WithPolicyKey(PolicyKeys.SqlFallbackSyncPolicy); + + /// + /// Handles a fallback policy for SqlException, TimeoutRejectedException and BrokenCircuitException exceptions. It means, if the process fails for one of those reasons, the fallback method is executed instead. + /// + public static ISyncPolicy GetFallbackPolicy(Action action) => + Policy + .Handle(ex => SqlTransientErrors.Contains(ex.Number)) + .Or(ex => SqlTransactionErrors.Contains(ex.Number)) + .Or() + .Or() + .Fallback(action, + ex => + { + var msg = $"#Polly #Fallback Fallback method used due to: {ex}"; + Log.Error(msg, ex); + }) + .WithPolicyKey(PolicyKeys.SqlFallbackSyncPolicy); + + private static void OnHalfOpen() + { + Log.Warn("#Polly #CircuitBreakerSync Half-open: Next call is a trial"); + } + + private static void OnReset() + { + // on circuit closed + Log.Warn("#Polly #CircuitBreakerSync Circuit breaker reset"); + } + + private static void OnBreak(Exception exception, TimeSpan duration) + { + // on circuit opened + Log.Warn("#Polly #CircuitBreakerSync Circuit breaker opened", exception); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/SqlPolicyBuilder.cs b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/SqlPolicyBuilder.cs new file mode 100644 index 0000000..3c98931 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/SqlPolicyBuilder.cs @@ -0,0 +1,27 @@ +using Duber.Infrastructure.Resilience.Sql.Internals; + +namespace Duber.Infrastructure.Resilience.Sql +{ + public class SqlPolicyBuilder + { + public ISqlAsyncPolicyBuilder UseAsyncExecutor() + { + return new SqlAsyncPolicyBuilder(); + } + + public ISqlAsyncPolicyBuilder UseAsyncExecutorWithSharedPolicies() + { + return new SqlAsyncPolicyBuilder(true); + } + + public ISqlSyncPolicyBuilder UseSyncExecutor() + { + return new SqlSyncPolicyBuilder(); + } + + public ISqlSyncPolicyBuilder UseSyncExecutorWithSharedPolicies() + { + return new SqlSyncPolicyBuilder(true); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj b/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj new file mode 100644 index 0000000..47076f3 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + + + + + + + + diff --git a/src/Infrastructure/Duber.Infrastructure.WebHost/Properties/launchSettings.json b/src/Infrastructure/Duber.Infrastructure.WebHost/Properties/launchSettings.json new file mode 100644 index 0000000..ee21a64 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.WebHost/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62189/", + "sslPort": 44306 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Duber.Infrastructure.WebHost": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/Duber.Infrastructure.WebHost/WebHostExtensions.cs b/src/Infrastructure/Duber.Infrastructure.WebHost/WebHostExtensions.cs new file mode 100644 index 0000000..d1be75c --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure.WebHost/WebHostExtensions.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Hosting; +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Duber.Infrastructure.WebHost +{ + public static class IWebHostExtensions + { + public static IWebHost MigrateDbContext(this IWebHost webHost, Action seeder) where TContext : DbContext + { + using (var scope = webHost.Services.CreateScope()) + { + var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); + var context = services.GetService(); + + try + { + logger.LogInformation($"Migrating database associated with context {typeof(TContext).Name}"); + + context.Database.Migrate(); + seeder(context, services); + + logger.LogInformation($"Migrated database associated with context {typeof(TContext).Name}"); + } + catch (Exception ex) + { + logger.LogError(ex, $"An error occurred while migrating the database used on context {typeof(TContext).Name}"); + } + } + + return webHost; + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure/DDD/Entity.cs b/src/Infrastructure/Duber.Infrastructure/DDD/Entity.cs new file mode 100644 index 0000000..3dedf85 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/DDD/Entity.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using MediatR; + +namespace Duber.Infrastructure.DDD +{ + public abstract class Entity + { + + int? _requestedHashCode; + int _Id; + + private List _domainEvents; + + public virtual int Id + { + get + { + return _Id; + } + protected set + { + _Id = value; + } + } + + public List DomainEvents => _domainEvents; + public void AddDomainEvent(INotification eventItem) + { + _domainEvents = _domainEvents ?? new List(); + _domainEvents.Add(eventItem); + } + + public void RemoveDomainEvent(INotification eventItem) + { + if (_domainEvents is null) return; + _domainEvents.Remove(eventItem); + } + + public bool IsTransient() + { + return this.Id == default(Int32); + } + + public override bool Equals(object obj) + { + if (obj == null || !(obj is Entity)) + return false; + + if (Object.ReferenceEquals(this, obj)) + return true; + + if (this.GetType() != obj.GetType()) + return false; + + Entity item = (Entity)obj; + + if (item.IsTransient() || this.IsTransient()) + return false; + else + return item.Id == this.Id; + } + + public override int GetHashCode() + { + if (!IsTransient()) + { + if (!_requestedHashCode.HasValue) + _requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) + + return _requestedHashCode.Value; + } + else + return base.GetHashCode(); + + } + + public static bool operator ==(Entity left, Entity right) + { + if (Object.Equals(left, null)) + return (Object.Equals(right, null)) ? true : false; + else + return left.Equals(right); + } + + public static bool operator !=(Entity left, Entity right) + { + return !(left == right); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure/DDD/Enumeration.cs b/src/Infrastructure/Duber.Infrastructure/DDD/Enumeration.cs new file mode 100644 index 0000000..d4e6f5e --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/DDD/Enumeration.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Duber.Infrastructure.DDD +{ + public abstract class Enumeration : IComparable + { + public string Name { get; private set; } + + public int Id { get; private set; } + + protected Enumeration() + { + } + + protected Enumeration(int id, string name) + { + Id = id; + Name = name; + } + + public override string ToString() + { + return Name; + } + + public static IEnumerable GetAll() where T : Enumeration, new() + { + var type = typeof(T); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + + foreach (var info in fields) + { + var instance = new T(); + var locatedValue = info.GetValue(instance) as T; + + if (locatedValue != null) + { + yield return locatedValue; + } + } + } + + public override bool Equals(object obj) + { + var otherValue = obj as Enumeration; + + if (otherValue == null) + { + return false; + } + + var typeMatches = GetType().Equals(obj.GetType()); + var valueMatches = Id.Equals(otherValue.Id); + + return typeMatches && valueMatches; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) + { + var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id); + return absoluteDifference; + } + + public static T FromValue(int value) where T : Enumeration, new() + { + var matchingItem = Parse(value, "value", item => item.Id == value); + return matchingItem; + } + + public static T FromDisplayName(string displayName) where T : Enumeration, new() + { + var matchingItem = Parse(displayName, "display name", item => item.Name == displayName); + return matchingItem; + } + + private static T Parse(K value, string description, Func predicate) where T : Enumeration, new() + { + var matchingItem = GetAll().FirstOrDefault(predicate); + + if (matchingItem == null) + { + var message = string.Format("'{0}' is not a valid {1} in {2}", value, description, typeof(T)); + + throw new InvalidOperationException(message); + } + + return matchingItem; + } + + public int CompareTo(object other) + { + return Id.CompareTo(((Enumeration)other).Id); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure/DDD/IAggregateRoot.cs b/src/Infrastructure/Duber.Infrastructure/DDD/IAggregateRoot.cs new file mode 100644 index 0000000..9ac65e9 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/DDD/IAggregateRoot.cs @@ -0,0 +1,4 @@ +namespace Duber.Infrastructure.DDD +{ + public interface IAggregateRoot { } +} diff --git a/src/Infrastructure/Duber.Infrastructure/DDD/ValueObject.cs b/src/Infrastructure/Duber.Infrastructure/DDD/ValueObject.cs new file mode 100644 index 0000000..8d1c93e --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/DDD/ValueObject.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Duber.Infrastructure.DDD +{ + public abstract class ValueObject + { + protected static bool EqualOperator(ValueObject left, ValueObject right) + { + if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) + { + return false; + } + return ReferenceEquals(left, null) || left.Equals(right); + } + + + protected static bool NotEqualOperator(ValueObject left, ValueObject right) + { + return !(EqualOperator(left, right)); + } + + + protected abstract IEnumerable GetAtomicValues(); + + + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != GetType()) + { + return false; + } + ValueObject other = (ValueObject)obj; + IEnumerator thisValues = GetAtomicValues().GetEnumerator(); + IEnumerator otherValues = other.GetAtomicValues().GetEnumerator(); + while (thisValues.MoveNext() && otherValues.MoveNext()) + { + if (ReferenceEquals(thisValues.Current, null) ^ ReferenceEquals(otherValues.Current, null)) + { + return false; + } + if (thisValues.Current != null && !thisValues.Current.Equals(otherValues.Current)) + { + return false; + } + } + return !thisValues.MoveNext() && !otherValues.MoveNext(); + } + + + public override int GetHashCode() + { + return GetAtomicValues() + .Select(x => x != null ? x.GetHashCode() : 0) + .Aggregate((x, y) => x ^ y); + } + + public ValueObject GetCopy() + { + return this.MemberwiseClone() as ValueObject; + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj b/src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj new file mode 100644 index 0000000..bc16783 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/src/Infrastructure/Duber.Infrastructure/Extensions/MediatorExtensions.cs b/src/Infrastructure/Duber.Infrastructure/Extensions/MediatorExtensions.cs new file mode 100644 index 0000000..cea1429 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/Extensions/MediatorExtensions.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Threading.Tasks; +using Duber.Infrastructure.DDD; +using MediatR; + +namespace Duber.Infrastructure.Extensions +{ + public static class MediatorExtensions + { + public static async Task DispatchDomainEventsAsync(this IMediator mediator, Entity entity) + { + var domainEvents = entity.DomainEvents?.ToList(); + if (domainEvents == null || domainEvents.Count == 0) + return; + + entity.DomainEvents.Clear(); + var tasks = domainEvents + .Select(async domainEvent => + { + await mediator.Publish(domainEvent); + }); + + await Task.WhenAll(tasks); + } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure/Http/JsonContent.cs b/src/Infrastructure/Duber.Infrastructure/Http/JsonContent.cs new file mode 100644 index 0000000..d37a52e --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/Http/JsonContent.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System.Net.Http; +using System.Text; + +namespace Duber.Infrastructure.Http +{ + public class JsonContent : StringContent + { + public JsonContent(object obj) : + base(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json") + { } + } +} \ No newline at end of file diff --git a/src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IRepository.cs b/src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IRepository.cs new file mode 100644 index 0000000..249a204 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IRepository.cs @@ -0,0 +1,9 @@ +using Duber.Infrastructure.DDD; + +namespace Duber.Infrastructure.Repository.Abstractions +{ + public interface IRepository where T : IAggregateRoot + { + IUnitOfWork UnitOfWork { get; } + } +} diff --git a/src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IUnitOfWork.cs b/src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IUnitOfWork.cs new file mode 100644 index 0000000..ad86240 --- /dev/null +++ b/src/Infrastructure/Duber.Infrastructure/Repository.Abstractions/IUnitOfWork.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Duber.Infrastructure.Repository.Abstractions +{ + public interface IUnitOfWork : IDisposable + { + Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj new file mode 100644 index 0000000..6942467 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + + + + + + + + + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IIdempotencyStoreProvider.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IIdempotencyStoreProvider.cs new file mode 100644 index 0000000..5603f1b --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IIdempotencyStoreProvider.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Duber.Infrastructure.EventBus.Idempotency +{ + public interface IIdempotencyStoreProvider + { + Task SaveAsync(IdempotentMessage message); + + Task ExistsAsync(string id); + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEvent.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEvent.cs new file mode 100644 index 0000000..5d678f2 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEvent.cs @@ -0,0 +1,19 @@ +using System; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Infrastructure.EventBus.Idempotency +{ + public class IdempotentIntegrationEvent : IntegrationEvent + where T : IntegrationEvent + { + public T Event { get; } + + public string MessageId { get; } + + public IdempotentIntegrationEvent(T @event, string messageId) + { + Event = @event; + MessageId = messageId; + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEventHandler.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEventHandler.cs new file mode 100644 index 0000000..23967ff --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentIntegrationEventHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Infrastructure.EventBus.Idempotency +{ + /// + /// This handler acts as an envelop to ensure idepotency to the given IntegrationEvent + /// + /// IntegrationEvent + public class IdempotentIntegrationEventHandler : IIntegrationEventHandler> + where T : IntegrationEvent + { + private readonly IEventBus _eventBus; + private readonly IIdempotencyStoreProvider _storeProvider; + + public IdempotentIntegrationEventHandler(IEventBus eventBus, IIdempotencyStoreProvider storeProvider) + { + _eventBus = eventBus; + _storeProvider = storeProvider; + } + + protected virtual void HandleDuplicatedRequest(T message) + { + } + + public async Task Handle(IdempotentIntegrationEvent message) + { + var alreadyExists = await _storeProvider.ExistsAsync(message.MessageId); + if (alreadyExists) + { + HandleDuplicatedRequest(message.Event); + } + else + { + await _storeProvider.SaveAsync(new IdempotentMessage{ MessageId = message.MessageId, Name = typeof(T).Name, Time = DateTime.UtcNow }); + _eventBus.Publish(message.Event); + } + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentMessage.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentMessage.cs new file mode 100644 index 0000000..b61499e --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/IdempotentMessage.cs @@ -0,0 +1,11 @@ +using System; + +namespace Duber.Infrastructure.EventBus.Idempotency +{ + public class IdempotentMessage + { + public string MessageId { get; set; } + public string Name { get; set; } + public DateTime Time { get; set; } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/ServiceCollectionExtensions.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d3fd786 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/ServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Duber.Infrastructure.EventBus.Abstractions; +using System.Linq; +using System; + +namespace Duber.Infrastructure.EventBus.Idempotency +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection RegisterDefaultIdempotentHandler(this IServiceCollection services) + { + services.AddTransient(typeof(IIntegrationEventHandler<>), typeof(IdempotentIntegrationEventHandler<>)); + return services; + } + + public static IServiceCollection RegisterIdempotentHandlers(this IServiceCollection services, Type assemblyType) + { + RegisterDefaultIdempotentHandler(services); + + assemblyType.Assembly + .GetTypes() + .Where(item => item.GetInterfaces().Where(i => i.IsGenericType).Any(i => i.GetGenericTypeDefinition() == typeof(IIntegrationEventHandler<>)) && !item.IsAbstract && !item.IsInterface) + .ToList() + .ForEach(assignedTypes => + { + services.AddTransient(assignedTypes); + }); + + return services; + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/DefaultRabbitMQPersisterConnection.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/DefaultRabbitMQPersisterConnection.cs new file mode 100644 index 0000000..e113f60 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/DefaultRabbitMQPersisterConnection.cs @@ -0,0 +1,121 @@ +using Microsoft.Extensions.Logging; +using Polly; +using Polly.Retry; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; +using System; +using System.IO; +using System.Net.Sockets; + +namespace Duber.Infrastructure.EventBus.RabbitMQ +{ + public class DefaultRabbitMQPersistentConnection + : IRabbitMQPersistentConnection + { + private readonly IConnectionFactory _connectionFactory; + private readonly ILogger _logger; + private readonly int _retryCount; + private IConnection _connection; + private bool _disposed; + private readonly object _syncRoot = new object(); + + public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger logger, int retryCount = 5) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _retryCount = retryCount; + } + + public bool IsConnected => _connection != null && _connection.IsOpen && !_disposed; + + public IModel CreateModel() + { + if (!IsConnected) + { + throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); + } + + return _connection.CreateModel(); + } + + public void Dispose() + { + if (_disposed) return; + + _disposed = true; + + try + { + _connection.Dispose(); + } + catch (IOException ex) + { + _logger.LogCritical(ex.ToString()); + } + } + + public bool TryConnect() + { + _logger.LogInformation("RabbitMQ Client is trying to connect"); + + lock (_syncRoot) + { + var policy = Policy.Handle() + .Or() + .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + } + ); + + policy.Execute(() => + { + _connection = _connectionFactory.CreateConnection(); + }); + + if (IsConnected) + { + _connection.ConnectionShutdown += OnConnectionShutdown; + _connection.CallbackException += OnCallbackException; + _connection.ConnectionBlocked += OnConnectionBlocked; + + _logger.LogInformation($"RabbitMQ persistent connection acquired a connection {_connection.Endpoint.HostName} and is subscribed to failure events"); + + return true; + } + + _logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); + + return false; + } + } + + private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); + + TryConnect(); + } + + private void OnCallbackException(object sender, CallbackExceptionEventArgs e) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); + + TryConnect(); + } + + private void OnConnectionShutdown(object sender, ShutdownEventArgs reason) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); + + TryConnect(); + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj new file mode 100644 index 0000000..7d3494e --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs new file mode 100644 index 0000000..4c6be2c --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs @@ -0,0 +1,277 @@ +using System; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Autofac; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Infrastructure.EventBus.Events; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Polly; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; +using IModel = RabbitMQ.Client.IModel; + +namespace Duber.Infrastructure.EventBus.RabbitMQ +{ + public class EventBusRabbitMQ : IEventBus, IDisposable + { + private const string BROKER_NAME = "duber_event_bus"; + private readonly IRabbitMQPersistentConnection _persistentConnection; + private readonly ILogger _logger; + private readonly IEventBusSubscriptionsManager _subsManager; + private readonly ILifetimeScope _autofac; + private readonly string AUTOFAC_SCOPE_NAME = "duber_event_bus"; + private readonly int _retryCount; + private IModel _consumerChannel; + private string _queueName; + + public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger logger, + ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5) + { + _persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); + _queueName = queueName; + _consumerChannel = CreateConsumerChannel(); + _autofac = autofac; + _retryCount = retryCount; + _subsManager.OnEventRemoved += SubsManager_OnEventRemoved; + } + + private void SubsManager_OnEventRemoved(object sender, string eventName) + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + using (var channel = _persistentConnection.CreateModel()) + { + channel.QueueUnbind(queue: _queueName, + exchange: BROKER_NAME, + routingKey: eventName); + + if (_subsManager.IsEmpty) + { + _queueName = string.Empty; + _consumerChannel.Close(); + } + } + } + + public void Publish(IntegrationEvent @event) + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + var policy = Policy.Handle() + .Or() + .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + }); + + using (var channel = _persistentConnection.CreateModel()) + { + var eventName = @event.GetType().Name; + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); + + var message = JsonConvert.SerializeObject(@event); + var body = Encoding.UTF8.GetBytes(message); + + // to avoid lossing messages + var properties = channel.CreateBasicProperties(); + properties.DeliveryMode = 2; // persistent + properties.Expiration = "60000"; + + policy.Execute(() => + { + channel.BasicPublish( + exchange: BROKER_NAME, + routingKey: eventName, + mandatory: true, + basicProperties: properties, + body: body); + }); + } + } + + public void SubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler + { + DoInternalSubscription(eventName); + _subsManager.AddDynamicSubscription(eventName); + StartBasicConsume(); + } + + public void Subscribe() + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = _subsManager.GetEventKey(); + DoInternalSubscription(eventName); + _subsManager.AddSubscription(); + StartBasicConsume(); + } + + private void DoInternalSubscription(string eventName) + { + var containsKey = _subsManager.HasSubscriptionsForEvent(eventName); + if (!containsKey) + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + using (var channel = _persistentConnection.CreateModel()) + { + channel.QueueBind(queue: _queueName, + exchange: BROKER_NAME, + routingKey: eventName); + } + } + } + + public void Unsubscribe() + where TH : IIntegrationEventHandler + where T : IntegrationEvent + { + _subsManager.RemoveSubscription(); + } + + public void UnsubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler + { + _subsManager.RemoveDynamicSubscription(eventName); + } + + public void Dispose() + { + _consumerChannel?.Dispose(); + _subsManager.Clear(); + } + + private void StartBasicConsume() + { + _logger.LogTrace("Starting RabbitMQ basic consume"); + + if (_consumerChannel != null) + { + var consumer = new AsyncEventingBasicConsumer(_consumerChannel); + + consumer.Received += Consumer_Received; + + _consumerChannel.BasicConsume( + queue: _queueName, + autoAck: false, + consumer: consumer); + } + else + { + _logger.LogError("StartBasicConsume can't call on _consumerChannel == null"); + } + } + + private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs) + { + var eventName = eventArgs.RoutingKey; + var message = Encoding.UTF8.GetString(eventArgs.Body); + + try + { + var policy = Policy.Handle() + .Or() + .WaitAndRetryAsync(_retryCount, retryAttempt => TimeSpan.FromSeconds(1), + (ex, time) => { _logger.LogWarning(ex.ToString()); }); + + await policy.ExecuteAsync(async () => await ProcessEvent(eventName, message)); + + // to avoid losing messages + _consumerChannel.BasicAck(deliveryTag: eventArgs.DeliveryTag, multiple: false); + } + catch (Exception ex) + { + // consider using a Dead Letter Exchange for undelivered messages. + _logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message); + } + } + + private IModel CreateConsumerChannel() + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + _logger.LogTrace("Creating RabbitMQ consumer channel"); + + var channel = _persistentConnection.CreateModel(); + + channel.ExchangeDeclare(exchange: BROKER_NAME, + type: "direct"); + + channel.QueueDeclare(queue: _queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null); + + channel.CallbackException += (sender, ea) => + { + _logger.LogWarning(ea.Exception, "Recreating RabbitMQ consumer channel"); + + _consumerChannel.Dispose(); + _consumerChannel = CreateConsumerChannel(); + StartBasicConsume(); + }; + + return channel; + } + + private async Task ProcessEvent(string eventName, string message) + { + _logger.LogTrace("Processing RabbitMQ event: {EventName}", eventName); + + if (_subsManager.HasSubscriptionsForEvent(eventName)) + { + using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) + { + var subscriptions = _subsManager.GetHandlersForEvent(eventName); + foreach (var subscription in subscriptions) + { + if (subscription.IsDynamic) + { + var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; + if (handler == null) continue; + dynamic eventData = JObject.Parse(message); + + await Task.Yield(); + await handler.Handle(eventData); + } + else + { + var handler = scope.ResolveOptional(subscription.HandlerType); + if (handler == null) continue; + var eventType = _subsManager.GetEventTypeByName(eventName); + var integrationEvent = JsonConvert.DeserializeObject(message, eventType); + var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); + + await Task.Yield(); + await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); + } + } + } + } + else + { + _logger.LogWarning("No subscription for RabbitMQ event: {EventName}", eventName); + } + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IRabbitMQPersisterConnection.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IRabbitMQPersisterConnection.cs new file mode 100644 index 0000000..cee7782 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IRabbitMQPersisterConnection.cs @@ -0,0 +1,15 @@ +using System; +using RabbitMQ.Client; + +namespace Duber.Infrastructure.EventBus.RabbitMQ +{ + public interface IRabbitMQPersistentConnection + : IDisposable + { + bool IsConnected { get; } + + bool TryConnect(); + + IModel CreateModel(); + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IoC/ServiceCollectionExtensions.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IoC/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..effa72f --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/IoC/ServiceCollectionExtensions.cs @@ -0,0 +1,71 @@ +using Autofac; +using Duber.Infrastructure.EventBus.Abstractions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; + +namespace Duber.Infrastructure.EventBus.RabbitMQ.IoC +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddRabbitMQ(this IServiceCollection services, IConfiguration configuration) + { + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + var connectionFactory = new ConnectionFactory() + { + HostName = configuration["EventBusConnection"], + DispatchConsumersAsync = true, + AutomaticRecoveryEnabled = true + }; + + if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) + { + connectionFactory.UserName = configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) + { + connectionFactory.Password = configuration["EventBusPassword"]; + } + + services.AddSingleton(connectionFactory); + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var factory = sp.GetRequiredService(); + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + + services.AddSingleton(sp => + { + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + var subscriptionClientName = configuration["SubscriptionClientName"]; + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + + services.AddSingleton(); + return services; + } + + public static IHealthChecksBuilder AddRabbitMQ(this IHealthChecksBuilder healthChecksBuilder, IConfiguration configuration, string name = "rabbitmqbus-check") + { + healthChecksBuilder + .AddRabbitMQ((sp) => sp.GetRequiredService().CreateConnection(), + name: name, + tags: new string[] { "rabbitmqbus" }); + + return healthChecksBuilder; + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/DefaultServiceBusPersisterConnection.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/DefaultServiceBusPersisterConnection.cs new file mode 100644 index 0000000..99750bf --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/DefaultServiceBusPersisterConnection.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.Azure.ServiceBus; +using Microsoft.Extensions.Logging; + +namespace Duber.Infrastructure.EventBus.ServiceBus +{ + public class DefaultServiceBusPersisterConnection :IServiceBusPersisterConnection + { + private readonly ILogger _logger; + private readonly ServiceBusConnectionStringBuilder _serviceBusConnectionStringBuilder; + private ITopicClient _topicClient; + + bool _disposed; + + public DefaultServiceBusPersisterConnection(ServiceBusConnectionStringBuilder serviceBusConnectionStringBuilder, + ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _serviceBusConnectionStringBuilder = serviceBusConnectionStringBuilder ?? + throw new ArgumentNullException(nameof(serviceBusConnectionStringBuilder)); + _topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default); + } + + public ServiceBusConnectionStringBuilder ServiceBusConnectionStringBuilder => _serviceBusConnectionStringBuilder; + + public ITopicClient CreateModel() + { + if(_topicClient.IsClosedOrClosing) + { + _topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default); + } + + return _topicClient; + } + + public void Dispose() + { + if (_disposed) return; + + _disposed = true; + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj new file mode 100644 index 0000000..f47b0f8 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs new file mode 100644 index 0000000..5feb638 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs @@ -0,0 +1,226 @@ +using System; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Autofac; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Infrastructure.EventBus.Events; +using Microsoft.Azure.ServiceBus; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Polly; + +namespace Duber.Infrastructure.EventBus.ServiceBus +{ + public class EventBusServiceBus : IEventBus + { + private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection; + private readonly ILogger _logger; + private readonly IEventBusSubscriptionsManager _subsManager; + private readonly SubscriptionClient _subscriptionClient; + private readonly ILifetimeScope _autofac; + private readonly string AUTOFAC_SCOPE_NAME = "duber_event_bus"; + private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; + private readonly int _retryCount; + + public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, + ILogger logger, IEventBusSubscriptionsManager subsManager, string subscriptionClientName, + ILifetimeScope autofac, int retryCount = 5) + { + _serviceBusPersisterConnection = serviceBusPersisterConnection; + _logger = logger; + _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); + + _subscriptionClient = new SubscriptionClient(serviceBusPersisterConnection.ServiceBusConnectionStringBuilder, subscriptionClientName); + _autofac = autofac; + _retryCount = retryCount; + + RemoveDefaultRule(); + RegisterSubscriptionClientMessageHandler(); + } + + public void Publish(IntegrationEvent @event) + { + var eventName = @event.GetType().Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); + var jsonMessage = JsonConvert.SerializeObject(@event); + var body = Encoding.UTF8.GetBytes(jsonMessage); + + var message = new Message + { + MessageId = Guid.NewGuid().ToString(), + Body = body, + Label = eventName, + }; + + var topicClient = _serviceBusPersisterConnection.CreateModel(); + + var policy = Policy.Handle() + .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + }); + + policy.Execute(() => + { + topicClient.SendAsync(message) + .GetAwaiter() + .GetResult(); + }); + } + + public void SubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler + { + _subsManager.AddDynamicSubscription(eventName); + } + + public void Subscribe() + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = GetEventName(); + + var containsKey = _subsManager.HasSubscriptionsForEvent(); + if (!containsKey) + { + try + { + _subscriptionClient.AddRuleAsync(new RuleDescription + { + Filter = new CorrelationFilter { Label = eventName }, + Name = eventName + }).GetAwaiter().GetResult(); + } + catch(ServiceBusException ex) + { + _logger.LogInformation($"The messaging entity {eventName} already exists.", ex); + } + } + + _subsManager.AddSubscription(); + } + + private string GetEventName() + { + var type = typeof(T); + var eventName = type.Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); + if (type.GetGenericArguments().Length > 0) + { + eventName = eventName.Remove(eventName.IndexOf('`')); + eventName += type.GetGenericArguments()[0].Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); + } + + return eventName; + } + + public void Unsubscribe() + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); + + try + { + _subscriptionClient + .RemoveRuleAsync(eventName) + .GetAwaiter() + .GetResult(); + } + catch (MessagingEntityNotFoundException) + { + _logger.LogInformation($"The messaging entity {eventName} Could not be found."); + } + + _subsManager.RemoveSubscription(); + } + + public void UnsubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler + { + _subsManager.RemoveDynamicSubscription(eventName); + } + + public void Dispose() + { + _subsManager.Clear(); + } + + private void RegisterSubscriptionClientMessageHandler() + { + _subscriptionClient.RegisterMessageHandler( + async (message, token) => + { + var eventName = $"{message.Label}{INTEGRATION_EVENT_SUFFIX}"; + var messageData = Encoding.UTF8.GetString(message.Body); + + var policy = Policy.Handle() + .Or() + .WaitAndRetryAsync(_retryCount, retryAttempt => TimeSpan.FromSeconds(1), + (ex, time) => { _logger.LogWarning(ex.ToString()); }); + + await policy.ExecuteAsync(async () => await ProcessEvent(eventName, messageData)); + + // Complete the message so that it is not received again. + await _subscriptionClient.CompleteAsync(message.SystemProperties.LockToken); + }, + new MessageHandlerOptions(ExceptionReceivedHandler) { MaxConcurrentCalls = 10, AutoComplete = false }); + } + + private static Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs) + { + Console.WriteLine($"Message handler encountered an exception {exceptionReceivedEventArgs.Exception}."); + var context = exceptionReceivedEventArgs.ExceptionReceivedContext; + Console.WriteLine("Exception context for troubleshooting:"); + Console.WriteLine($"- Endpoint: {context.Endpoint}"); + Console.WriteLine($"- Entity Path: {context.EntityPath}"); + Console.WriteLine($"- Executing Action: {context.Action}"); + return Task.CompletedTask; + } + + private async Task ProcessEvent(string eventName, string message) + { + if (_subsManager.HasSubscriptionsForEvent(eventName)) + { + using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) + { + var subscriptions = _subsManager.GetHandlersForEvent(eventName); + foreach (var subscription in subscriptions) + { + if (subscription.IsDynamic) + { + var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; + if (handler == null) continue; + dynamic eventData = JObject.Parse(message); + await handler.Handle(eventData); + } + else + { + var handler = scope.ResolveOptional(subscription.HandlerType); + if (handler == null) continue; + var eventType = _subsManager.GetEventTypeByName(eventName); + var integrationEvent = JsonConvert.DeserializeObject(message, eventType); + var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); + await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); + } + } + } + } + } + + private void RemoveDefaultRule() + { + try + { + _subscriptionClient + .RemoveRuleAsync(RuleDescription.DefaultRuleName) + .GetAwaiter() + .GetResult(); + } + catch (MessagingEntityNotFoundException) + { + _logger.LogInformation($"The messaging entity { RuleDescription.DefaultRuleName } Could not be found."); + } + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IServiceBusPersisterConnection.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IServiceBusPersisterConnection.cs new file mode 100644 index 0000000..54c5417 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IServiceBusPersisterConnection.cs @@ -0,0 +1,12 @@ +using System; +using Microsoft.Azure.ServiceBus; + +namespace Duber.Infrastructure.EventBus.ServiceBus +{ + public interface IServiceBusPersisterConnection : IDisposable + { + ServiceBusConnectionStringBuilder ServiceBusConnectionStringBuilder { get; } + + ITopicClient CreateModel(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IoC/ServiceCollectionExtensions.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IoC/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..6cc578e --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/IoC/ServiceCollectionExtensions.cs @@ -0,0 +1,57 @@ +using Autofac; +using Duber.Infrastructure.EventBus.Abstractions; +using Microsoft.Azure.ServiceBus; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Duber.Infrastructure.EventBus.ServiceBus.IoC +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddServiceBus(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var serviceBusConnectionString = configuration["EventBusConnection"]; + var serviceBusConnection = new ServiceBusConnectionStringBuilder(serviceBusConnectionString); + + return new DefaultServiceBusPersisterConnection(serviceBusConnection, logger); + }); + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + var subscriptionClientName = configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubcriptionsManager, subscriptionClientName, iLifetimeScope, retryCount); + }); + + services.AddSingleton(); + return services; + } + + public static IHealthChecksBuilder AddAzureServiceBusTopic(this IHealthChecksBuilder healthChecksBuilder, IConfiguration configuration, string name = "az-servicebus-check") + { + healthChecksBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnectionHC"], // this connection string can't contains the EntityPath (topic name) + topicName: "duber_event_bus", + name, + tags: new string[] { "az-servicebus" }); + + return healthChecksBuilder; + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IDynamicIntegrationEventHandler.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IDynamicIntegrationEventHandler.cs new file mode 100644 index 0000000..9ffa88a --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IDynamicIntegrationEventHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Duber.Infrastructure.EventBus.Abstractions +{ + public interface IDynamicIntegrationEventHandler + { + Task Handle(dynamic eventData); + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs new file mode 100644 index 0000000..f51b7e4 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs @@ -0,0 +1,22 @@ +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Infrastructure.EventBus.Abstractions +{ + public interface IEventBus + { + void Subscribe() + where T : IntegrationEvent + where TH : IIntegrationEventHandler; + void SubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler; + + void UnsubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler; + + void Unsubscribe() + where TH : IIntegrationEventHandler + where T : IntegrationEvent; + + void Publish(IntegrationEvent @event); + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IIntegrationEventHandler.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IIntegrationEventHandler.cs new file mode 100644 index 0000000..1a3a7f4 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IIntegrationEventHandler.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Infrastructure.EventBus.Abstractions +{ + public interface IIntegrationEventHandler : IIntegrationEventHandler + where TIntegrationEvent: IntegrationEvent + { + Task Handle(TIntegrationEvent @event); + } + + public interface IIntegrationEventHandler + { + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj new file mode 100644 index 0000000..7c4ae4c --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Events/IntegrationEvent.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Events/IntegrationEvent.cs new file mode 100644 index 0000000..7d5af52 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Events/IntegrationEvent.cs @@ -0,0 +1,16 @@ +using System; + +namespace Duber.Infrastructure.EventBus.Events +{ + public class IntegrationEvent + { + public IntegrationEvent() + { + Id = Guid.NewGuid(); + CreationDate = DateTime.UtcNow; + } + + public Guid Id { get; } + public DateTime CreationDate { get; } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/IEventBusSubscriptionsManager.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/IEventBusSubscriptionsManager.cs new file mode 100644 index 0000000..1c71019 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/IEventBusSubscriptionsManager.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Infrastructure.EventBus +{ + public interface IEventBusSubscriptionsManager + { + bool IsEmpty { get; } + event EventHandler OnEventRemoved; + void AddDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler; + + void AddSubscription() + where T : IntegrationEvent + where TH : IIntegrationEventHandler; + + void RemoveSubscription() + where TH : IIntegrationEventHandler + where T : IntegrationEvent; + void RemoveDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler; + + bool HasSubscriptionsForEvent() where T : IntegrationEvent; + bool HasSubscriptionsForEvent(string eventName); + Type GetEventTypeByName(string eventName); + void Clear(); + IEnumerable GetHandlersForEvent() where T : IntegrationEvent; + IEnumerable GetHandlersForEvent(string eventName); + string GetEventKey(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/InMemoryEventBusSubscriptionsManager.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/InMemoryEventBusSubscriptionsManager.cs new file mode 100644 index 0000000..400888e --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/InMemoryEventBusSubscriptionsManager.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.Infrastructure.EventBus +{ + public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager + { + + + private readonly Dictionary> _handlers; + private readonly List _eventTypes; + + public event EventHandler OnEventRemoved; + + public InMemoryEventBusSubscriptionsManager() + { + _handlers = new Dictionary>(); + _eventTypes = new List(); + } + + public bool IsEmpty => !_handlers.Keys.Any(); + public void Clear() => _handlers.Clear(); + + public void AddDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler + { + DoAddSubscription(typeof(TH), eventName, isDynamic: true); + } + + public void AddSubscription() + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = GetEventKey(); + + DoAddSubscription(typeof(TH), eventName, isDynamic: false); + + if (!_eventTypes.Contains(typeof(T))) + { + _eventTypes.Add(typeof(T)); + } + } + + private void DoAddSubscription(Type handlerType, string eventName, bool isDynamic) + { + if (!HasSubscriptionsForEvent(eventName)) + { + _handlers.Add(eventName, new List()); + } + + if (_handlers[eventName].Any(s => s.HandlerType == handlerType)) + { + throw new ArgumentException( + $"Handler Type {handlerType.Name} already registered for '{eventName}'", nameof(handlerType)); + } + + if (isDynamic) + { + _handlers[eventName].Add(SubscriptionInfo.Dynamic(handlerType)); + } + else + { + _handlers[eventName].Add(SubscriptionInfo.Typed(handlerType)); + } + } + + + public void RemoveDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler + { + var handlerToRemove = FindDynamicSubscriptionToRemove(eventName); + DoRemoveHandler(eventName, handlerToRemove); + } + + + public void RemoveSubscription() + where TH : IIntegrationEventHandler + where T : IntegrationEvent + { + var handlerToRemove = FindSubscriptionToRemove(); + var eventName = GetEventKey(); + DoRemoveHandler(eventName, handlerToRemove); + } + + + private void DoRemoveHandler(string eventName, SubscriptionInfo subsToRemove) + { + if (subsToRemove != null) + { + _handlers[eventName].Remove(subsToRemove); + if (!_handlers[eventName].Any()) + { + _handlers.Remove(eventName); + var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); + if (eventType != null) + { + _eventTypes.Remove(eventType); + } + RaiseOnEventRemoved(eventName); + } + + } + } + + public IEnumerable GetHandlersForEvent() where T : IntegrationEvent + { + var key = GetEventKey(); + return GetHandlersForEvent(key); + } + public IEnumerable GetHandlersForEvent(string eventName) => _handlers[eventName]; + + private void RaiseOnEventRemoved(string eventName) + { + var handler = OnEventRemoved; + handler?.Invoke(this, eventName); + } + + + private SubscriptionInfo FindDynamicSubscriptionToRemove(string eventName) + where TH : IDynamicIntegrationEventHandler + { + return DoFindSubscriptionToRemove(eventName, typeof(TH)); + } + + + private SubscriptionInfo FindSubscriptionToRemove() + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = GetEventKey(); + return DoFindSubscriptionToRemove(eventName, typeof(TH)); + } + + private SubscriptionInfo DoFindSubscriptionToRemove(string eventName, Type handlerType) + { + if (!HasSubscriptionsForEvent(eventName)) + { + return null; + } + + return _handlers[eventName].SingleOrDefault(s => s.HandlerType == handlerType); + + } + + public bool HasSubscriptionsForEvent() where T : IntegrationEvent + { + var key = GetEventKey(); + return HasSubscriptionsForEvent(key); + } + public bool HasSubscriptionsForEvent(string eventName) => _handlers.ContainsKey(eventName); + + public Type GetEventTypeByName(string eventName) => _eventTypes.SingleOrDefault(t => t.Name == eventName); + + public string GetEventKey() + { + return typeof(T).Name; + } + } +} diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/SubscriptionInfo.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/SubscriptionInfo.cs new file mode 100644 index 0000000..4167089 --- /dev/null +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/SubscriptionInfo.cs @@ -0,0 +1,28 @@ +using System; + +namespace Duber.Infrastructure.EventBus +{ + public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager + { + public class SubscriptionInfo + { + public bool IsDynamic { get; } + public Type HandlerType{ get; } + + private SubscriptionInfo(bool isDynamic, Type handlerType) + { + IsDynamic = isDynamic; + HandlerType = handlerType; + } + + public static SubscriptionInfo Dynamic(Type handlerType) + { + return new SubscriptionInfo(true, handlerType); + } + public static SubscriptionInfo Typed(Type handlerType) + { + return new SubscriptionInfo(false, handlerType); + } + } + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs new file mode 100644 index 0000000..2d00032 --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoiceCreatedIntegrationEvent.cs @@ -0,0 +1,24 @@ +using System; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.WebSite.Application.IntegrationEvents.Events +{ + public class InvoiceCreatedIntegrationEvent : IntegrationEvent + { + public InvoiceCreatedIntegrationEvent(Guid invoiceId, decimal fee, decimal total, Guid tripId) + { + InvoiceId = invoiceId; + Fee = fee; + Total = total; + TripId = tripId; + } + + public Guid TripId { get; } + + public Guid InvoiceId { get; } + + public decimal Fee { get; } + + public decimal Total { get; } + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs new file mode 100644 index 0000000..ba080fa --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/InvoicePaidIntegrationEvent.cs @@ -0,0 +1,33 @@ +using System; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.WebSite.Application.IntegrationEvents.Events +{ + public class InvoicePaidIntegrationEvent : IntegrationEvent + { + public InvoicePaidIntegrationEvent(Guid invoiceId, PaymentStatus status, string cardNumber, string cardType, Guid tripId) + { + InvoiceId = invoiceId; + Status = status; + CardNumber = cardNumber; + CardType = cardType; + TripId = tripId; + } + + public Guid InvoiceId { get; } + + public Guid TripId { get; } + + public PaymentStatus Status { get; } + + public string CardNumber { get; } + + public string CardType { get; } + } + + public enum PaymentStatus + { + Accepted = 1, + Rejected = 2, + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripCreatedIntegrationEvent.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripCreatedIntegrationEvent.cs new file mode 100644 index 0000000..57dfe6c --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripCreatedIntegrationEvent.cs @@ -0,0 +1,58 @@ +using System; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.WebSite.Application.IntegrationEvents.Events +{ + public class TripCreatedIntegrationEvent : IntegrationEvent + { + public TripCreatedIntegrationEvent(Guid tripId, int userTripId, int driverId, Location from, Location to, VehicleInformation vehicleInformation, PaymentMethod paymentMethod) + { + UserTripId = userTripId; + DriverId = driverId; + From = from; + To = to; + VehicleInformation = vehicleInformation; + PaymentMethod = paymentMethod; + TripId = tripId; + } + + public Guid TripId { get; } + + public int UserTripId { get; } + + public int DriverId { get; } + + public Location From { get; } + + public Location To { get; } + + public VehicleInformation VehicleInformation { get; } + + public PaymentMethod PaymentMethod { get; } + } + + public class VehicleInformation + { + public string Plate { get; set; } + + public string Brand { get; set; } + + public string Model { get; set; } + } + + public class PaymentMethod + { + public string Name { get; set; } + + public int Id { get; set; } + } + + public class Location + { + public double Latitude { get; set; } + + public double Longitude { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs new file mode 100644 index 0000000..03d874a --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Events/TripUpdatedIntegrationEvent.cs @@ -0,0 +1,52 @@ +using System; +using Duber.Infrastructure.EventBus.Events; + +namespace Duber.WebSite.Application.IntegrationEvents.Events +{ + public class TripUpdatedIntegrationEvent : IntegrationEvent + { + public TripUpdatedIntegrationEvent(Guid tripId, Action action, TripStatus status, DateTime? started, DateTime? ended, Location currentLocation, double? distance, TimeSpan? duration) + { + Action = action; + Status = status; + Started = started; + Ended = ended; + CurrentLocation = currentLocation; + Distance = distance; + Duration = duration; + TripId = tripId; + } + + public Guid TripId { get; } + + public Action Action { get; } + + public TripStatus Status { get; } + + public DateTime? Started { get; } + + public DateTime? Ended { get; } + + public Location CurrentLocation { get; } + + public double? Distance { get; } + + public TimeSpan? Duration { get; } + } + + public enum Action + { + Accepted = 1, + Started = 2, + Cancelled = 3, + FinishedEarlier = 4, + UpdatedCurrentLocation = 5 + } + + public class TripStatus + { + public string Name { get; set; } + + public int Id { get; set; } + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoiceCreatedIntegrationEventHandler.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoiceCreatedIntegrationEventHandler.cs new file mode 100644 index 0000000..221b12e --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoiceCreatedIntegrationEventHandler.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.WebSite.Application.IntegrationEvents.Events; +using Duber.WebSite.Infrastructure.Repository; +using Microsoft.Extensions.Logging; + +namespace Duber.WebSite.Application.IntegrationEvents.Handlers +{ + public class InvoiceCreatedIntegrationEventHandler: IIntegrationEventHandler + { + private readonly IReportingRepository _reportingRepository; + private readonly ILogger _logger; + + public InvoiceCreatedIntegrationEventHandler(IReportingRepository reportingRepository, ILogger logger) + { + _reportingRepository = reportingRepository ?? throw new ArgumentNullException(nameof(reportingRepository)); + _logger = logger; + } + + public async Task Handle(InvoiceCreatedIntegrationEvent @event) + { + _logger.LogInformation("InvoiceCreatedIntegrationEvent handled"); + var trip = await _reportingRepository.GetTripAsync(@event.TripId); + + // we throw an exception in order to don't send the Acknowledgement to the service bus, probably the consumer read + // this message before that the created one. + if (trip == null) + throw new InvalidOperationException($"The trip {@event.TripId} doesn't exist. Error trying to update the materialized view."); + + _logger.LogInformation($"Invoice {@event.InvoiceId} for Trip {@event.TripId} has been created."); + + trip.InvoiceId = @event.InvoiceId; + trip.Fee = @event.Fee; + trip.Fare = @event.Total - @event.Fee; + + try + { + await _reportingRepository.UpdateTripAsync(trip); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error trying to update the Trip: {@event.TripId}", ex); + } + finally + { + _reportingRepository.Dispose(); + } + } + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoicePaidIntegrationEventHandler.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoicePaidIntegrationEventHandler.cs new file mode 100644 index 0000000..28f5881 --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/InvoicePaidIntegrationEventHandler.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.WebSite.Application.IntegrationEvents.Events; +using Duber.WebSite.Infrastructure.Repository; +using Microsoft.Extensions.Logging; + +namespace Duber.WebSite.Application.IntegrationEvents.Handlers +{ + public class InvoicePaidIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IReportingRepository _reportingRepository; + private readonly ILogger _logger; + + public InvoicePaidIntegrationEventHandler(IReportingRepository reportingRepository, ILogger logger) + { + _reportingRepository = reportingRepository ?? throw new ArgumentNullException(nameof(reportingRepository)); + _logger = logger; + } + + public async Task Handle(InvoicePaidIntegrationEvent @event) + { + _logger.LogInformation("InvoicePaidIntegrationEvent handled"); + var trip = await _reportingRepository.GetTripAsync(@event.TripId); + + // we throw an exception in order to don't send the Acknowledgement to the service bus, probably the consumer read + // this message before that the created one. + if (trip == null) + throw new InvalidOperationException($"The trip {@event.TripId} doesn't exist. Error trying to update the materialized view."); + + _logger.LogInformation($"Invoice {@event.InvoiceId} for Trip {@event.TripId} has been paid."); + + trip.CardNumber = @event.CardNumber; + trip.CardType = @event.CardType; + trip.PaymentStatus = @event.Status == PaymentStatus.Accepted ? nameof(PaymentStatus.Accepted) : nameof(PaymentStatus.Rejected); + + try + { + await _reportingRepository.UpdateTripAsync(trip); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error trying to update the Trip: {@event.TripId}", ex); + } + finally + { + _reportingRepository.Dispose(); + } + } + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs new file mode 100644 index 0000000..6e25135 --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripCreatedIntegrationEventHandler.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using Duber.Domain.Driver.Repository; +using Duber.Domain.User.Repository; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.WebSite.Application.IntegrationEvents.Events; +using Duber.WebSite.Infrastructure.Repository; +using Duber.WebSite.Models; +using Microsoft.Extensions.Logging; + +namespace Duber.WebSite.Application.IntegrationEvents.Handlers +{ + public class TripCreatedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IReportingRepository _reportingRepository; + private readonly IDriverRepository _driverRepository; + private readonly IUserRepository _userRepository; + private readonly ILogger _logger; + + public TripCreatedIntegrationEventHandler(IReportingRepository reportingRepository, IDriverRepository driverRepository, IUserRepository userRepository, ILogger logger) + { + _reportingRepository = reportingRepository ?? throw new ArgumentNullException(nameof(reportingRepository)); + _driverRepository = driverRepository ?? throw new ArgumentNullException(nameof(driverRepository)); + _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); + _logger = logger; + } + + public async Task Handle(TripCreatedIntegrationEvent @event) + { + _logger.LogInformation("TripCreatedIntegrationEvent handled"); + var existingTrip = await _reportingRepository.GetTripAsync(@event.TripId); + if (existingTrip != null) return; + + _logger.LogInformation($"Trip {@event.TripId} has been created."); + var driver = await _driverRepository.GetDriverAsync(@event.DriverId); + var user = await _userRepository.GetUserAsync(@event.UserTripId); + + var newTrip = new Trip + { + Id = @event.TripId, + Created = @event.CreationDate, + PaymentMethod = @event.PaymentMethod.Name, + Status = "Created", + Model = @event.VehicleInformation.Model, + Brand = @event.VehicleInformation.Brand, + Plate = @event.VehicleInformation.Plate, + DriverId = @event.DriverId, + DriverName = driver.Name, + From = @event.From.Description, + To = @event.To.Description, + UserId = @event.UserTripId, + UserName = user.Name + }; + + try + { + await _reportingRepository.AddTripAsync(newTrip); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error trying to create the Trip: {@event.TripId}", ex); + } + finally + { + _reportingRepository.Dispose(); + } + } + } +} diff --git a/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs new file mode 100644 index 0000000..d8a782c --- /dev/null +++ b/src/Web/Duber.WebSite/Application/IntegrationEvents/Handlers/TripUpdatedIntegrationEventHandler.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.WebSite.Application.IntegrationEvents.Events; +using Duber.WebSite.Infrastructure.Repository; +using Microsoft.Extensions.Logging; + +namespace Duber.WebSite.Application.IntegrationEvents.Handlers +{ + public class TripUpdatedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IReportingRepository _reportingRepository; + private readonly ILogger _logger; + + public TripUpdatedIntegrationEventHandler(IReportingRepository reportingRepository, ILogger logger) + { + _reportingRepository = reportingRepository ?? throw new ArgumentNullException(nameof(reportingRepository)); + _logger = logger; + } + + public async Task Handle(TripUpdatedIntegrationEvent @event) + { + _logger.LogInformation("TripUpdatedIntegrationEvent handled"); + var trip = await _reportingRepository.GetTripAsync(@event.TripId); + + // we throw an exception in order to don't send the Acknowledgement to the service bus, probably the consumer read the + // updated message before that the created one. + if (trip == null) + throw new InvalidOperationException($"The trip {@event.TripId} doesn't exist. Error trying to update the materialized view."); + + _logger.LogInformation($"Trip {@event.TripId} has been updated. Status: {@event.Status}"); + if (trip.Status == "Finished") return; + + trip.Distance = @event.Distance; + trip.Duration = @event.Duration; + trip.Status = @event.Status.Name; + trip.Started = @event.Started; + trip.Ended = @event.Ended; + + try + { + await _reportingRepository.UpdateTripAsync(trip); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error trying to update the Trip: {@event.TripId} Trip status: {trip.Status}", ex); + } + finally + { + _reportingRepository.Dispose(); + } + } + } +} diff --git a/src/Web/Duber.WebSite/Controllers/HomeController.cs b/src/Web/Duber.WebSite/Controllers/HomeController.cs new file mode 100644 index 0000000..079459b --- /dev/null +++ b/src/Web/Duber.WebSite/Controllers/HomeController.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Duber.Domain.Driver.Model; +using Duber.Domain.Driver.Repository; +using Duber.Domain.User.Model; +using Duber.Domain.User.Repository; +using Microsoft.AspNetCore.Mvc; +using Duber.WebSite.Models; + +namespace Duber.WebSite.Controllers +{ + public class HomeController : Controller + { + private readonly IUserRepository _userRepository; + private readonly IDriverRepository _driverRepository; + + public HomeController(IUserRepository userRepository, IDriverRepository driverRepository) + { + _userRepository = userRepository; + _driverRepository = driverRepository; + } + + public IActionResult Index() + { + // users test + //var users = _userRepository.GetUsersAsync().Result; + //var user = users[0]; + //user.ChangePaymentMethod(PaymentMethod.Cash); + //_userRepository.Update(user); + //var result =_userRepository.UnitOfWork.SaveChangesAsync().Result; + //var paymentMethod = user.PaymentMethod; + + // drivers test + //var drivers = _driverRepository.GetDriversAsync().Result; + //var driver = drivers[0]; + //driver.AddVehicle("TWN 741", "Lexus", "2018", VehicleType.Car); + //_driverRepository.Update(driver); + //var result = _driverRepository.UnitOfWork.SaveChangesAsync().Result; + //var currentVehicle = driver.CurrentVehicle; + + return View(); + } + + public IActionResult About() + { + ViewData["Message"] = "Your application description page."; + + return View(); + } + + public IActionResult Contact() + { + ViewData["Message"] = "Your contact page."; + + return View(); + } + + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } +} diff --git a/src/Web/Duber.WebSite/Controllers/TripController.cs b/src/Web/Duber.WebSite/Controllers/TripController.cs new file mode 100644 index 0000000..a2703f3 --- /dev/null +++ b/src/Web/Duber.WebSite/Controllers/TripController.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Duber.Domain.Driver.Model; +using Duber.Domain.Driver.Repository; +using Duber.Domain.User.Model; +using Duber.Domain.User.Repository; +using Duber.Infrastructure.Http; +using Duber.Infrastructure.Resilience.Http; +using Duber.WebSite.Extensions; +using Duber.WebSite.Infrastructure.Repository; +using Duber.WebSite.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +// ReSharper disable ForCanBeConvertedToForeach + +namespace Duber.WebSite.Controllers +{ + public class TripController : Controller + { + private readonly IMemoryCache _cache; + private readonly IUserRepository _userRepository; + private readonly ResilientHttpClient _httpClient; + private readonly IDriverRepository _driverRepository; + private readonly IReportingRepository _reportingRepository; + private readonly IOptions _tripApiSettings; + private readonly Dictionary _originsAndDestinations; + + public TripController(IUserRepository userRepository, + IDriverRepository driverRepository, + IMemoryCache cache, + ResilientHttpClient httpClient, + IOptions tripApiSettings, + IReportingRepository reportingRepository) + { + _userRepository = userRepository; + _driverRepository = driverRepository; + _cache = cache; + _httpClient = httpClient; + _tripApiSettings = tripApiSettings; + _reportingRepository = reportingRepository; + + _originsAndDestinations = new Dictionary + { + { + new SelectListItem { Text = "Poblado's Park" }, + new LocationModel { Latitude = 6.210292869847029, Longitude = -75.57115852832794, Description = "Poblado's Park" } + }, + { + new SelectListItem { Text = "Lleras Park" }, + new LocationModel { Latitude = 6.2087793817882515, Longitude = -75.56776275426228, Description = "Lleras Park" } + }, + { + new SelectListItem { Text = "Sabaneta Park" }, + new LocationModel { Latitude = 6.151584634798451, Longitude = -75.61546325683594, Description = "Sabaneta Park" } + }, + { + new SelectListItem { Text = "The executive bar" }, + new LocationModel { Latitude = 6.252063572976704, Longitude = -75.56599313040351, Description = "The executive bar" } + }, + }; + } + + public async Task Index() + { + var drivers = await GetDrivers(); + var users = await GetUsers(); + + var model = new TripRequestModel + { + Drivers = drivers.ToSelectList(), + Users = users.ToSelectList(), + User = users.FirstOrDefault()?.Id.ToString(), + Driver = drivers.FirstOrDefault()?.Id.ToString(), + Origins = _originsAndDestinations.Keys.Select(x => x).ToList(), + Destinations = _originsAndDestinations.Keys.Select(x => x).ToList(), + From = _originsAndDestinations.Keys.Select(x => x).ToList().FirstOrDefault()?.Text, + To = _originsAndDestinations.Keys.Select(x => x).ToList().LastOrDefault()?.Text, + Places = _originsAndDestinations.Values.Select(x => x).ToList() + }; + + return View(model); + } + + [HttpPost] + public async Task SimulateTrip(TripRequestModel model) + { + if (!ModelState.IsValid) + return BadRequest(ModelState.AllErrors()); + + var tripID = await CreateTrip(model); + await AcceptOrStartTrip(_tripApiSettings.Value.AcceptUrl, tripID, model.ConnectionId); + await AcceptOrStartTrip(_tripApiSettings.Value.StartUrl, tripID, model.ConnectionId); + + for (var index = 0; index < model.Directions.Count; index += 5) + { + var direction = model.Directions[index]; + if (index + 5 >= model.Directions.Count) + direction = _originsAndDestinations.Values.SingleOrDefault(x => x.Description == model.To); + + await UpdateTripLocation(tripID, direction, model.ConnectionId); + } + + return Ok(); + } + + public async Task TripsByUser() + { + var users = await GetUsers(); + var usersModel = users.Select(x => new UserModel + { + Id = x.Id, + Email = x.Email, + Name = x.Name, + NumberPhone = x.NumberPhone + }); + + return View(usersModel.ToList()); + } + + public async Task TripsByUserId(int id) + { + try + { + var trips = await _reportingRepository.GetTripsByUserAsync(id); + return View("UserTrips", trips.ToList()); + } + finally + { + _reportingRepository.Dispose(); + } + } + + public async Task TripById(Guid id) + { + try + { + var trip = await _reportingRepository.GetTripAsync(id); + return View("TripDetails", trip); + } + finally + { + _reportingRepository.Dispose(); + } + } + + public async Task TripsByDriver() + { + var drivers = await GetDrivers(); + var driversModel = drivers.Select(x => new DriverModel() + { + Id = x.Id, + Email = x.Email, + Name = x.Name, + NumberPhone = x.PhoneNumber + }); + + return View(driversModel.ToList()); + } + + public async Task TripsByDriverId(int id) + { + try + { + var trips = await _reportingRepository.GetTripsByDriverAsync(id); + return View("DriverTrips", trips.ToList()); + } + finally + { + _reportingRepository.Dispose(); + } + } + + public async Task Test() + { + var model = new TripRequestModel + { + Driver = (await GetDrivers()).FirstOrDefault()?.Id.ToString(), + User = (await GetUsers()).FirstOrDefault()?.Id.ToString(), + From = "Poblado's Park", + To = "Sabaneta Park" + }; + + await CreateTrip(model); + return Ok(); + } + + private async Task> GetDrivers() + { + var drivers = await GetDataFromCache("drivers", () => _driverRepository.GetDriversAsync()); + return drivers; + } + + private async Task> GetUsers() + { + var users = await GetDataFromCache("users", () => _userRepository.GetUsersAsync()); + return users; + } + + private async Task GetDataFromCache(string cacheKey, Func> action) + { + if (!_cache.TryGetValue(cacheKey, out T result)) + { + result = await action(); + + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromMinutes(5)); + + _cache.Set(cacheKey, result, cacheEntryOptions); + } + + return result; + } + + private async Task CreateTrip(TripRequestModel model) + { + var drivers = await GetDrivers(); + var users = await GetUsers(); + var driverInfo = drivers.SingleOrDefault(x => x.Id == int.Parse(model.Driver)); + var userInfo = users.SingleOrDefault(x => x.Id == int.Parse(model.User)); + + var uri = new Uri(new Uri(_tripApiSettings.Value.BaseUrl), _tripApiSettings.Value.CreateUrl); + var request = new HttpRequestMessage(HttpMethod.Post, uri) + { + Content = new JsonContent(new + { + userId = int.Parse(model.User), + driverId = int.Parse(model.Driver), + from = _originsAndDestinations.Values.SingleOrDefault(x => x.Description == model.From), + to = _originsAndDestinations.Values.SingleOrDefault(x => x.Description == model.To), + plate = driverInfo?.CurrentVehicle.Plate, + brand = driverInfo?.CurrentVehicle.Brand, + model = driverInfo?.CurrentVehicle.Model, + paymentMethod = userInfo?.PaymentMethod, + connectionId = model.ConnectionId + }) + }; + + var response = await _httpClient.SendAsync(request); + + response.EnsureSuccessStatusCode(); + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + } + + private async Task AcceptOrStartTrip(string action, Guid tripId, string connectionId) + { + var uri = new Uri(new Uri(_tripApiSettings.Value.BaseUrl), action); + var request = new HttpRequestMessage(HttpMethod.Put, uri) + { + Content = new JsonContent(new { id = tripId.ToString(), connectionId }) + }; + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + private async Task UpdateTripLocation(Guid tripId, LocationModel location, string connectionId) + { + var uri = new Uri(new Uri(_tripApiSettings.Value.BaseUrl), _tripApiSettings.Value.UpdateCurrentLocationUrl); + var request = new HttpRequestMessage(HttpMethod.Put, uri) + { + Content = new JsonContent(new + { + id = tripId, + currentLocation = new { latitude = location.Latitude, longitude = location.Longitude, connectionId } + }) + }; + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + } +} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Dockerfile b/src/Web/Duber.WebSite/Dockerfile new file mode 100644 index 0000000..5fff343 --- /dev/null +++ b/src/Web/Duber.WebSite/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY . . +WORKDIR "/src/src/Web/Duber.WebSite" +RUN dotnet restore +RUN dotnet build --no-restore -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Duber.WebSite.dll"] \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Duber.WebSite.csproj b/src/Web/Duber.WebSite/Duber.WebSite.csproj new file mode 100644 index 0000000..a22caf0 --- /dev/null +++ b/src/Web/Duber.WebSite/Duber.WebSite.csproj @@ -0,0 +1,64 @@ + + + + netcoreapp3.1 + ..\..\..\docker-compose.dcproj + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Web/Duber.WebSite/Extensions/ApplicationBuilderExtensions.cs b/src/Web/Duber.WebSite/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..65666d6 --- /dev/null +++ b/src/Web/Duber.WebSite/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,22 @@ +using Duber.Infrastructure.EventBus.Abstractions; +using Duber.WebSite.Application.IntegrationEvents.Events; +using Duber.WebSite.Application.IntegrationEvents.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Duber.WebSite.Extensions +{ + public static class ApplicationBuilderExtensions + { + public static IApplicationBuilder UseServiceBroker(this IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + + return app; + } + } +} diff --git a/src/Web/Duber.WebSite/Extensions/Extensions.cs b/src/Web/Duber.WebSite/Extensions/Extensions.cs new file mode 100644 index 0000000..8a78161 --- /dev/null +++ b/src/Web/Duber.WebSite/Extensions/Extensions.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using Duber.Domain.Driver.Model; +using Duber.Domain.User.Model; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Duber.WebSite.Extensions +{ + public static class DomainExtensions + { + public static List ToSelectList(this IList list) + { + return list.Select(x => new SelectListItem { Value = x.Id.ToString(), Text = x.Name }).ToList(); + } + + public static List ToSelectList(this IList list) + { + return list.Select(x => new SelectListItem { Value = x.Id.ToString(), Text = x.Name }).ToList(); + } + } + + public static class MvcExtensions + { + public static IEnumerable AllErrors(this ModelStateDictionary modelState) + { + var result = new List(); + var erroneousFields = modelState.Where(ms => ms.Value.Errors.Any()) + .Select(x => new { x.Value.Errors }); + + foreach (var erroneousField in erroneousFields) + { + var fieldErrors = erroneousField.Errors + .Select(error => error.ErrorMessage); + result.AddRange(fieldErrors); + } + + return result; + } + } +} diff --git a/src/Web/Duber.WebSite/Extensions/ServiceCollectionExtensions.cs b/src/Web/Duber.WebSite/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..fe0825f --- /dev/null +++ b/src/Web/Duber.WebSite/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,176 @@ +using Duber.Domain.Driver.Persistence; +using Duber.Domain.Driver.Repository; +using Duber.Domain.User.Persistence; +using Duber.Domain.User.Repository; +using Duber.Infrastructure.Resilience.Abstractions; +using Duber.Infrastructure.Resilience.Http; +using Duber.Infrastructure.Resilience.Sql; +using Duber.WebSite.Infrastructure.Persistence; +using Duber.WebSite.Infrastructure.Repository; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Polly; +using Polly.Extensions.Http; +using System; +using System.Net.Http; +using System.Reflection; +using Duber.Infrastructure.EventBus.RabbitMQ.IoC; +using Duber.Infrastructure.EventBus.ServiceBus.IoC; +using Duber.WebSite.Application.IntegrationEvents.Handlers; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Duber.WebSite.Extensions +{ + public static class ServiceCollectionExtensions + { + public static string ResiliencePolicy = "ResiliencePolicy"; + + public static IServiceCollection AddPersistenceAndRepositories(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(options => + { + options.UseSqlServer( + configuration["ConnectionStrings:WebsiteDB"], + sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(UserContext).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), null); + }); + }); + + services.AddDbContext(options => + { + options.UseSqlServer( + configuration["ConnectionStrings:WebsiteDB"], + sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(DriverContext).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), null); + }); + }); + + services.AddDbContext(options => + { + options.UseSqlServer( + configuration["ConnectionStrings:WebsiteDB"], + sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(ReportingContext).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), null); + }); + }); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddResilientStrategies(this IServiceCollection services, IConfiguration configuration) + { + // Resilient Async SQL Executor configuration. + services.AddSingleton(sp => + { + var sqlPolicyBuilder = new SqlPolicyBuilder(); + return sqlPolicyBuilder + .UseAsyncExecutor() + .WithDefaultPolicies() + .Build(); + }); + + // Resilient Sync SQL Executor configuration. + services.AddSingleton(sp => + { + var sqlPolicyBuilder = new SqlPolicyBuilder(); + return sqlPolicyBuilder + .UseSyncExecutor() + .WithDefaultPolicies() + .Build(); + }); + + // Create (and register with DI) a policy registry containing some policies we want to use. + var policyRegistry = services.AddPolicyRegistry(); + policyRegistry[ResiliencePolicy] = GetHttpResiliencePolicy(configuration); + + // Resilient Http Invoker onfiguration. + // Register a typed client via HttpClientFactory, set to use the policy we placed in the policy registry. + services.AddHttpClient(client => + { + client.Timeout = TimeSpan.FromSeconds(50); + }).AddPolicyHandlerFromRegistry(ResiliencePolicy); + + return services; + } + + private static IAsyncPolicy GetHttpResiliencePolicy(IConfiguration configuration) + { + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["HttpClientRetryCount"])) + { + retryCount = int.Parse(configuration["HttpClientRetryCount"]); + } + + var exceptionsAllowedBeforeBreaking = 4; + if (!string.IsNullOrEmpty(configuration["HttpClientExceptionsAllowedBeforeBreaking"])) + { + exceptionsAllowedBeforeBreaking = int.Parse(configuration["HttpClientExceptionsAllowedBeforeBreaking"]); + } + + // Define a couple of policies which will form our resilience strategy. + var policies = HttpPolicyExtensions.HandleTransientHttpError() + .RetryAsync(retryCount) + .WrapAsync(HttpPolicyExtensions.HandleTransientHttpError() + .CircuitBreakerAsync(exceptionsAllowedBeforeBreaking, TimeSpan.FromSeconds(5))); + + return policies; + } + + public static IServiceCollection AddServiceBroker(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddServiceBus(configuration); + } + else + { + services.AddRabbitMQ(configuration); + } + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddUrlGroup(new Uri($"{configuration["InvoiceApiSettings:BaseUrl"]}/readiness"), name: "invoice-service-check", tags: new string[] { "invoice-service" }) + .AddUrlGroup(new Uri($"{configuration["TripApiSettings:BaseUrl"]}/readiness"), name: "trip-service-check", tags: new string[] { "trip-service" }) + .AddUrlGroup(new Uri($"{configuration["TripApiSettings:NotificationsServerUrl"]}/readiness"), name: "notifications-service-check", tags: new string[] { "notifications-service" }) + .AddSqlServer( + configuration["ConnectionStrings:WebsiteDB"], + name: "WebsiteDB-check", + tags: new string[] { "websitedb" }); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder.AddAzureServiceBusTopic(configuration, "website-az-servicebus-check"); + } + else + { + hcBuilder.AddRabbitMQ(configuration, "website-rabbitmqbus-check"); + } + + services.AddHealthChecksUI(); + return services; + } + } +} diff --git a/src/Web/Duber.WebSite/Infrastructure/Persistence/ReportingContext.cs b/src/Web/Duber.WebSite/Infrastructure/Persistence/ReportingContext.cs new file mode 100644 index 0000000..65fb876 --- /dev/null +++ b/src/Web/Duber.WebSite/Infrastructure/Persistence/ReportingContext.cs @@ -0,0 +1,33 @@ +using Duber.WebSite.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Duber.WebSite.Infrastructure.Persistence +{ + public class ReportingContext : DbContext + { + // ReSharper disable once InconsistentNaming + private const string DEFAULT_SCHEMA = "Reporting"; + + public ReportingContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema(DEFAULT_SCHEMA); + base.OnModelCreating(modelBuilder); + } + + public DbSet Trips { get; set; } + } + + public class ReportingContextDesignFactory : IDesignTimeDbContextFactory + { + public ReportingContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Initial Catalog=Duber.WebSiteDb;Integrated Security=true"); + + return new ReportingContext(optionsBuilder.Options); + } + } +} diff --git a/src/Web/Duber.WebSite/Infrastructure/Repository/IReportingRepository.cs b/src/Web/Duber.WebSite/Infrastructure/Repository/IReportingRepository.cs new file mode 100644 index 0000000..5afd158 --- /dev/null +++ b/src/Web/Duber.WebSite/Infrastructure/Repository/IReportingRepository.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Duber.WebSite.Models; + +namespace Duber.WebSite.Infrastructure.Repository +{ + public interface IReportingRepository : IDisposable + { + Trip GetTrip(Guid tripId); + + void AddTrip(Trip trip); + + void UpdateTrip(Trip trip); + + Task AddTripAsync(Trip trip); + + Task UpdateTripAsync(Trip trip); + + Task> GetTripsAsync(); + + Task GetTripAsync(Guid tripId); + + Task> GetTripsByUserAsync(int userId); + + Task> GetTripsByDriverAsync(int driverid); + } +} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Infrastructure/Repository/ReportingRepository.cs b/src/Web/Duber.WebSite/Infrastructure/Repository/ReportingRepository.cs new file mode 100644 index 0000000..47cb95b --- /dev/null +++ b/src/Web/Duber.WebSite/Infrastructure/Repository/ReportingRepository.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duber.Infrastructure.Resilience.Abstractions; +using Duber.WebSite.Infrastructure.Persistence; +using Duber.WebSite.Models; +using Microsoft.EntityFrameworkCore; + +namespace Duber.WebSite.Infrastructure.Repository +{ + public class ReportingRepository : IReportingRepository + { + private readonly ReportingContext _reportingContext; + private readonly IPolicyAsyncExecutor _resilientAsyncSqlExecutor; + private readonly IPolicySyncExecutor _resilientSyncSqlExecutor; + + public ReportingRepository(ReportingContext reportingContext, IPolicyAsyncExecutor resilientAsyncSqlExecutor, IPolicySyncExecutor resilientSyncSqlExecutor) + { + _reportingContext = reportingContext ?? throw new ArgumentNullException(nameof(reportingContext)); + _resilientAsyncSqlExecutor = resilientAsyncSqlExecutor ?? throw new ArgumentNullException(nameof(resilientAsyncSqlExecutor)); + _resilientSyncSqlExecutor = resilientSyncSqlExecutor ?? throw new ArgumentNullException(nameof(resilientSyncSqlExecutor)); + } + + public async Task AddTripAsync(Trip trip) + { + _reportingContext.Trips.Add(trip); + await _resilientAsyncSqlExecutor.ExecuteAsync(async () => await _reportingContext.SaveChangesAsync()); + } + + public void AddTrip(Trip trip) + { + _reportingContext.Trips.Add(trip); + _resilientSyncSqlExecutor.Execute(() => _reportingContext.SaveChanges()); + } + + public void UpdateTrip(Trip trip) + { + _reportingContext.Attach(trip); + _resilientSyncSqlExecutor.Execute(() => _reportingContext.SaveChanges()); + } + + public async Task UpdateTripAsync(Trip trip) + { + _reportingContext.Attach(trip); + await _resilientAsyncSqlExecutor.ExecuteAsync(async () => await _reportingContext.SaveChangesAsync()); + } + + public async Task> GetTripsAsync() + { + return await _resilientAsyncSqlExecutor.ExecuteAsync(async () => await _reportingContext.Trips.ToListAsync()); + } + + public async Task GetTripAsync(Guid tripId) + { + return await _resilientAsyncSqlExecutor.ExecuteAsync(async () => + await _reportingContext.Trips.SingleOrDefaultAsync(x => x.Id == tripId)); + } + + public Trip GetTrip(Guid tripId) + { + return _resilientSyncSqlExecutor.Execute(() => _reportingContext.Trips.SingleOrDefault(x => x.Id == tripId)); + } + + public async Task> GetTripsByUserAsync(int userId) + { + return await _resilientAsyncSqlExecutor.ExecuteAsync(async () => + await _reportingContext.Trips + .Where(x => x.UserId == userId) + .OrderByDescending(x => x.Created) + .ToListAsync()); + } + + public async Task> GetTripsByDriverAsync(int driverid) + { + return await _resilientAsyncSqlExecutor.ExecuteAsync(async () => + await _reportingContext.Trips + .Where(x => x.DriverId == driverid) + .OrderByDescending(x => x.Created) + .ToListAsync()); + } + + public void Dispose() + { + _reportingContext?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.Designer.cs b/src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.Designer.cs new file mode 100644 index 0000000..8844def --- /dev/null +++ b/src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.Designer.cs @@ -0,0 +1,81 @@ +// +using Duber.WebSite.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace Duber.WebSite.Migrations +{ + [DbContext(typeof(ReportingContext))] + [Migration("20180412031300_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Reporting") + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.WebSite.Models.Trip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Brand"); + + b.Property("CardNumber"); + + b.Property("CardType"); + + b.Property("Created"); + + b.Property("Distance"); + + b.Property("DriverId"); + + b.Property("DriverName"); + + b.Property("Duration"); + + b.Property("Ended"); + + b.Property("Fare"); + + b.Property("Fee"); + + b.Property("From"); + + b.Property("InvoiceId"); + + b.Property("Model"); + + b.Property("PaymentMethod"); + + b.Property("PaymentStatus"); + + b.Property("Plate"); + + b.Property("Started"); + + b.Property("Status"); + + b.Property("To"); + + b.Property("UserId"); + + b.Property("UserName"); + + b.HasKey("Id"); + + b.ToTable("Trips"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.cs b/src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.cs new file mode 100644 index 0000000..c71b3fd --- /dev/null +++ b/src/Web/Duber.WebSite/Migrations/20180412031300_InitialCreate.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Duber.WebSite.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Reporting"); + + migrationBuilder.CreateTable( + name: "Trips", + schema: "Reporting", + columns: table => new + { + Id = table.Column(nullable: false), + Brand = table.Column(nullable: true), + CardNumber = table.Column(nullable: true), + CardType = table.Column(nullable: true), + Created = table.Column(nullable: false), + Distance = table.Column(nullable: true), + DriverId = table.Column(nullable: false), + DriverName = table.Column(nullable: true), + Duration = table.Column(nullable: true), + Ended = table.Column(nullable: true), + Fare = table.Column(nullable: true), + Fee = table.Column(nullable: true), + From = table.Column(nullable: true), + InvoiceId = table.Column(nullable: true), + Model = table.Column(nullable: true), + PaymentMethod = table.Column(nullable: true), + PaymentStatus = table.Column(nullable: true), + Plate = table.Column(nullable: true), + Started = table.Column(nullable: true), + Status = table.Column(nullable: true), + To = table.Column(nullable: true), + UserId = table.Column(nullable: false), + UserName = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Trips", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Trips", + schema: "Reporting"); + } + } +} diff --git a/src/Web/Duber.WebSite/Migrations/ReportingContextModelSnapshot.cs b/src/Web/Duber.WebSite/Migrations/ReportingContextModelSnapshot.cs new file mode 100644 index 0000000..dbde4f4 --- /dev/null +++ b/src/Web/Duber.WebSite/Migrations/ReportingContextModelSnapshot.cs @@ -0,0 +1,80 @@ +// +using Duber.WebSite.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace Duber.WebSite.Migrations +{ + [DbContext(typeof(ReportingContext))] + partial class ReportingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Reporting") + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Duber.WebSite.Models.Trip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Brand"); + + b.Property("CardNumber"); + + b.Property("CardType"); + + b.Property("Created"); + + b.Property("Distance"); + + b.Property("DriverId"); + + b.Property("DriverName"); + + b.Property("Duration"); + + b.Property("Ended"); + + b.Property("Fare"); + + b.Property("Fee"); + + b.Property("From"); + + b.Property("InvoiceId"); + + b.Property("Model"); + + b.Property("PaymentMethod"); + + b.Property("PaymentStatus"); + + b.Property("Plate"); + + b.Property("Started"); + + b.Property("Status"); + + b.Property("To"); + + b.Property("UserId"); + + b.Property("UserName"); + + b.HasKey("Id"); + + b.ToTable("Trips"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Duber.WebSite/Models/DriverModel.cs b/src/Web/Duber.WebSite/Models/DriverModel.cs new file mode 100644 index 0000000..50019b5 --- /dev/null +++ b/src/Web/Duber.WebSite/Models/DriverModel.cs @@ -0,0 +1,13 @@ +namespace Duber.WebSite.Models +{ + public class DriverModel + { + public int Id { get; set; } + + public string Name { get; set; } + + public string NumberPhone { get; set; } + + public string Email { get; set; } + } +} diff --git a/src/Web/Duber.WebSite/Models/ErrorViewModel.cs b/src/Web/Duber.WebSite/Models/ErrorViewModel.cs new file mode 100644 index 0000000..ab32408 --- /dev/null +++ b/src/Web/Duber.WebSite/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace Duber.WebSite.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Models/Trip.cs b/src/Web/Duber.WebSite/Models/Trip.cs new file mode 100644 index 0000000..1662661 --- /dev/null +++ b/src/Web/Duber.WebSite/Models/Trip.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Threading.Tasks; + +namespace Duber.WebSite.Models +{ + public class Trip + { + // trip information + public Guid Id { get; set; } + + public string Status { get; set; } + + public DateTime Created { get; set; } + + public DateTime? Started { get; set; } + + public DateTime? Ended { get; set; } + + public double? Distance { get; set; } + + public TimeSpan? Duration { get; set; } + + public string From { get; set; } + + public string To { get; set; } + + public string PaymentMethod { get; set; } + + // user information + public int UserId { get; set; } + + [Display(Name = "User")] + public string UserName { get; set; } + + // driver information + public int DriverId { get; set; } + + [Display(Name = "Driver")] + public string DriverName { get; set; } + + // vehicle information + public string Plate { get; set; } + + public string Brand { get; set; } + + public string Model { get; set; } + + // invoice information + public Guid? InvoiceId { get; set; } + + [Display(Name = "Booking Fee")] + public decimal? Fee { get; set; } + + [Display(Name = "Trip Fare")] + public decimal? Fare { get; set; } + + [NotMapped] + public decimal? Total => Fee + Fare; + + // payment information + [Display(Name = "Status")] + public string PaymentStatus { get; set; } + + [Display(Name = "Credit Card Number")] + public string CardNumber { get; set; } + + [Display(Name = "Type")] + public string CardType { get; set; } + } +} diff --git a/src/Web/Duber.WebSite/Models/TripApiSettings.cs b/src/Web/Duber.WebSite/Models/TripApiSettings.cs new file mode 100644 index 0000000..cd92935 --- /dev/null +++ b/src/Web/Duber.WebSite/Models/TripApiSettings.cs @@ -0,0 +1,19 @@ +namespace Duber.WebSite.Models +{ + public class TripApiSettings + { + public string BaseUrl { get; set; } + + public string CreateUrl { get; set; } + + public string AcceptUrl { get; set; } + + public string StartUrl { get; set; } + + public string CancelUrl { get; set; } + + public string UpdateCurrentLocationUrl { get; set; } + + public string NotificationsClientUrl { get; set; } + } +} diff --git a/src/Web/Duber.WebSite/Models/TripRequestModel.cs b/src/Web/Duber.WebSite/Models/TripRequestModel.cs new file mode 100644 index 0000000..724fb08 --- /dev/null +++ b/src/Web/Duber.WebSite/Models/TripRequestModel.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Duber.WebSite.Models +{ + public class TripRequestModel : IValidatableObject + { + [Required] + public string User { get; set; } + + [Required] + public string Driver { get; set; } + + public List Users { get; set; } + + public List Drivers { get; set; } + + public List Origins { get; set; } + + public List Destinations { get; set; } + + public List Places { get; set; } + + public List Directions { get; set; } + + [Required] + public string From { get; set; } + + [Required] + public string To { get; set; } + + [Required] + public string ConnectionId { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (From == To) + { + yield return new ValidationResult("The origin can't be the same to destination", new[] { "From" }); + } + } + } + + public class LocationModel + { + public double Latitude { get; set; } + + public double Longitude { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Web/Duber.WebSite/Models/UserModel.cs b/src/Web/Duber.WebSite/Models/UserModel.cs new file mode 100644 index 0000000..b4538ff --- /dev/null +++ b/src/Web/Duber.WebSite/Models/UserModel.cs @@ -0,0 +1,13 @@ +namespace Duber.WebSite.Models +{ + public class UserModel + { + public int Id { get; set; } + + public string Name { get; set; } + + public string NumberPhone { get; set; } + + public string Email { get; set; } + } +} diff --git a/src/Web/Duber.WebSite/Program.cs b/src/Web/Duber.WebSite/Program.cs new file mode 100644 index 0000000..de7901d --- /dev/null +++ b/src/Web/Duber.WebSite/Program.cs @@ -0,0 +1,51 @@ +using Duber.Domain.Driver.Persistence; +using Duber.Domain.User.Persistence; +using Duber.Infrastructure.WebHost; +using Duber.WebSite.Infrastructure.Persistence; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Duber.WebSite +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args) + .MigrateDbContext((context, services) => + { + var logger = services.GetService>(); + new UserContextSeed() + .SeedAsync(context, logger) + .Wait(); + }) + .MigrateDbContext((context, services) => + { + var logger = services.GetService>(); + new DriverContextSeed() + .SeedAsync(context, logger) + .Wait(); + }) + .MigrateDbContext((_, __) => { }) + .Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .ConfigureAppConfiguration((builderContext, config) => + { + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, builder) => + { + builder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + builder.AddConsole(); + builder.AddDebug(); + }) + .Build(); + } +} diff --git a/src/Web/Duber.WebSite/Properties/launchSettings.json b/src/Web/Duber.WebSite/Properties/launchSettings.json new file mode 100644 index 0000000..8450def --- /dev/null +++ b/src/Web/Duber.WebSite/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:32774/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Duber.WebSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:32774/" + } + } +} diff --git a/src/Web/Duber.WebSite/Startup.cs b/src/Web/Duber.WebSite/Startup.cs new file mode 100644 index 0000000..e13a8a3 --- /dev/null +++ b/src/Web/Duber.WebSite/Startup.cs @@ -0,0 +1,85 @@ +using System; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Duber.WebSite.Extensions; +using Duber.WebSite.Models; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +// ReSharper disable InconsistentNaming +// ReSharper disable ArgumentsStyleLiteral +// ReSharper disable AssignNullToNotNullAttribute + +namespace Duber.WebSite +{ + 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 IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddMemoryCache() + .Configure(x => x.ValueCountLimit = 2048) + .AddApplicationInsightsTelemetry(Configuration) + .AddMvc(); + + services.Configure(Configuration.GetSection("TripApiSettings")) + .AddResilientStrategies(Configuration) + .AddPersistenceAndRepositories(Configuration) + .AddServiceBroker(Configuration) + .AddHealthChecks(Configuration); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseBrowserLink(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } + + app.UseStaticFiles() + .UseRouting() + .UseServiceBroker(); + + app.UseHealthChecksUI(config => config.UIPath = "/hc-ui"); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + + endpoints.MapHealthChecks("/readiness", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + } + } +} diff --git a/src/Web/Duber.WebSite/Views/Home/About.cshtml b/src/Web/Duber.WebSite/Views/Home/About.cshtml new file mode 100644 index 0000000..3674e37 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Home/About.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"]

+

@ViewData["Message"]

+ +

Use this area to provide additional information.

diff --git a/src/Web/Duber.WebSite/Views/Home/Contact.cshtml b/src/Web/Duber.WebSite/Views/Home/Contact.cshtml new file mode 100644 index 0000000..a11a186 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Home/Contact.cshtml @@ -0,0 +1,17 @@ +@{ + ViewData["Title"] = "Contact"; +} +

@ViewData["Title"]

+

@ViewData["Message"]

+ +
+ One Microsoft Way
+ Redmond, WA 98052-6399
+ P: + 425.555.0100 +
+ +
+ Support: Support@example.com
+ Marketing: Marketing@example.com +
diff --git a/src/Web/Duber.WebSite/Views/Home/Index.cshtml b/src/Web/Duber.WebSite/Views/Home/Index.cshtml new file mode 100644 index 0000000..6c55af6 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Home/Index.cshtml @@ -0,0 +1,18 @@ +@{ + ViewData["Title"] = "Home Page"; +} + +
+ ASP.NET +
+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus eu faucibus justo. Vivamus sagittis ultricies nisi et placerat. Donec a orci rhoncus, accumsan felis ut, scelerisque ipsum. Quisque imperdiet diam sollicitudin maximus vehicula. Nulla luctus erat id erat lobortis, in sagittis leo luctus. Etiam fringilla lorem risus, at eleifend tellus vestibulum et. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque et felis non libero molestie tempus et et lectus. Vivamus imperdiet velit eget dolor varius, id tincidunt enim malesuada. Suspendisse quis lectus at leo ultricies accumsan vel sit amet lacus. Praesent tellus orci, pretium et diam et, malesuada commodo nibh. Morbi ornare ipsum tincidunt felis cursus, ut vulputate enim lacinia. Phasellus imperdiet vulputate efficitur. Aliquam accumsan quam posuere nisl sodales gravida. +

+

+ Mauris ac tellus a sem euismod auctor. In ipsum purus, dictum sit amet consectetur quis, porttitor at enim. Sed semper pharetra erat, quis ullamcorper mauris. Duis posuere enim id ornare ultricies. Mauris a orci ut risus facilisis accumsan at dictum mauris. Nulla facilisi. Fusce vel neque ut velit convallis ornare. Vestibulum aliquam felis in nisl semper, eget iaculis ligula efficitur. Curabitur sit amet dui eu orci congue porttitor sed id mauris. Fusce efficitur lorem a arcu rhoncus, ac aliquam mi mollis. Pellentesque commodo rutrum fringilla. Nunc vitae dolor scelerisque, ultrices turpis sed, condimentum tortor. +

+
+
diff --git a/src/Web/Duber.WebSite/Views/Shared/Error.cshtml b/src/Web/Duber.WebSite/Views/Shared/Error.cshtml new file mode 100644 index 0000000..ec2ea6b --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Shared/Error.cshtml @@ -0,0 +1,22 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/src/Web/Duber.WebSite/Views/Shared/_Layout.cshtml b/src/Web/Duber.WebSite/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..e814dc9 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Shared/_Layout.cshtml @@ -0,0 +1,79 @@ + + + + + + @ViewData["Title"] - Duber.WebSite + + + + + + + + + + + + +
+ @RenderBody() +
+
+

© 2018 - Duber.WebSite

+
+
+ + + + + + + + + + + + + + + + + + + + + @RenderSection("Scripts", required: false) + + diff --git a/src/Web/Duber.WebSite/Views/Shared/_ValidationScriptsPartial.cshtml b/src/Web/Duber.WebSite/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..a699aaf --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/src/Web/Duber.WebSite/Views/Trip/DriverTrips.cshtml b/src/Web/Duber.WebSite/Views/Trip/DriverTrips.cshtml new file mode 100644 index 0000000..137af28 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Trip/DriverTrips.cshtml @@ -0,0 +1,64 @@ +@model List + +@{ + ViewData["Title"] = "Driver Trips"; +} + +

Driver Trips

+

@Model.FirstOrDefault()?.DriverName

+ +
+ + + + + + + + + + + + + + @for (var i = 0; i < Model.Count(); i++) + { + + + + + + + + + + } + +
+ Date + + From + + To + + User + + Status + + Total +
+ @Html.DisplayFor(m => m[i].Created) + + @Html.DisplayFor(m => m[i].From) + + @Html.DisplayFor(m => m[i].To) + + @Html.DisplayFor(m => m[i].UserName) + + @Html.DisplayFor(m => m[i].Status) + + @Html.DisplayFor(m => m[i].Total) + + @Html.ActionLink("Details", "TripById", new { id = Model[i].Id }) +
+
diff --git a/src/Web/Duber.WebSite/Views/Trip/Index.cshtml b/src/Web/Duber.WebSite/Views/Trip/Index.cshtml new file mode 100644 index 0000000..7d698a3 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Trip/Index.cshtml @@ -0,0 +1,121 @@ +@using Microsoft.AspNetCore.Html +@using Microsoft.Extensions.Options +@using Newtonsoft.Json +@model TripRequestModel + +@{ + ViewBag.Title = "Simulate a Trip"; +} + +

Simulate a Trip

+ + + + + +
+
Configure your trip
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+
+
+ +
+
Track your trip
+
+ @{ + await Html.RenderPartialAsync("_Map"); + } +
+
+ +@inject IOptions settings + +@section Scripts { + +} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Views/Trip/TripDetails.cshtml b/src/Web/Duber.WebSite/Views/Trip/TripDetails.cshtml new file mode 100644 index 0000000..44b6c2e --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Trip/TripDetails.cshtml @@ -0,0 +1,144 @@ +@model Trip +@{ + ViewData["Title"] = "Trip Details"; +} + +

Trip Details

+ +@if (@Model.Status == "Cancelled") +{ +
+ The Trip was @Model.Status +
+} + +
+

Trip Information

+
+
+
+ +
+
+ @Model.UserName +
+
+ +
+
+ @Model.DriverName + (@string.Format("{0}-{1} {2}", Model.Brand, Model.Model, Model.Plate)) +
+
+
+
+ +
+
+ @Model.Started +
+
+ +
+
+ @Model.Ended +
+
+
+
+ +
+
+ @Model.From +
+
+ +
+
+ @Model.To +
+
+
+
+ +
+
+ @string.Format("{0}Km", Math.Round((decimal)(Model.Distance / 1000), 2)) +
+
+ +
+
+ @string.Format("{0:%h} hours, {0:%m} min, {0:%s} secs", Model.Duration.Value) +
+
+
+
+ +
+

Invoice Information

+
+
+
+ +
+
+ @Model.Fare.Value.ToString("N2") +
+
+
+
+ +
+
+ @Model.Fare.Value.ToString("N2") +
+
+
+
+ +
+
+ @Model.Fee.Value.ToString("N2") +
+
+
+
+
+ +
+
+ @Model.Total.Value.ToString("N2") +
+
+
+
+ +@if (Model.PaymentMethod == "Credit Card") +{ +
+

Payment Information

+
+
+
+ +
+
+ @Model.PaymentStatus +
+
+ +
+
+ @string.Concat(new string('*', 10), Model.CardNumber.Substring(Model.CardNumber.Length - 4)) +
+
+ +
+
+ @Model.CardType +
+
+
+
+} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Views/Trip/TripsByDriver.cshtml b/src/Web/Duber.WebSite/Views/Trip/TripsByDriver.cshtml new file mode 100644 index 0000000..5529cb5 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Trip/TripsByDriver.cshtml @@ -0,0 +1,45 @@ +@model List + +@{ + ViewBag.Title = "Trips by Driver"; +} + +

Trips by Driver

+ +
+ + + + + + + + + + + @for (var i = 0; i < Model.Count(); i++) + { + + + + + + + } + +
+ Name + + Email + + Number Phone +
+ @Html.DisplayFor(m => m[i].Name) + + @Html.DisplayFor(m => m[i].Email) + + @Html.DisplayFor(m => m[i].NumberPhone) + + @Html.ActionLink("Trips", "TripsByDriverId", new { id = Model[i].Id }) +
+
\ No newline at end of file diff --git a/src/Web/Duber.WebSite/Views/Trip/TripsByUser.cshtml b/src/Web/Duber.WebSite/Views/Trip/TripsByUser.cshtml new file mode 100644 index 0000000..e3551ff --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Trip/TripsByUser.cshtml @@ -0,0 +1,45 @@ +@model List + +@{ + ViewBag.Title = "Trips by User"; +} + +

Trips by User

+ +
+ + + + + + + + + + + @for (var i = 0; i < Model.Count(); i++) + { + + + + + + + } + +
+ Name + + Email + + Number Phone +
+ @Html.DisplayFor(m => m[i].Name) + + @Html.DisplayFor(m => m[i].Email) + + @Html.DisplayFor(m => m[i].NumberPhone) + + @Html.ActionLink("Trips", "TripsByUserId", new {id = Model[i].Id}) +
+
\ No newline at end of file diff --git a/src/Web/Duber.WebSite/Views/Trip/UserTrips.cshtml b/src/Web/Duber.WebSite/Views/Trip/UserTrips.cshtml new file mode 100644 index 0000000..408b7f7 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Trip/UserTrips.cshtml @@ -0,0 +1,64 @@ +@model List + +@{ + ViewData["Title"] = "User Trips"; +} + +

User Trips

+

@Model.FirstOrDefault()?.UserName

+ +
+ + + + + + + + + + + + + + @for (var i = 0; i < Model.Count(); i++) + { + + + + + + + + + + } + +
+ Date + + From + + To + + Driver + + Status + + Total +
+ @Html.DisplayFor(m => m[i].Created) + + @Html.DisplayFor(m => m[i].From) + + @Html.DisplayFor(m => m[i].To) + + @Html.DisplayFor(m => m[i].DriverName) + + @Html.DisplayFor(m => m[i].Status) + + @Html.DisplayFor(m => m[i].Total) + + @Html.ActionLink("Details", "TripById", new { id = Model[i].Id }) +
+
diff --git a/src/Web/Duber.WebSite/Views/Trip/_Map.cshtml b/src/Web/Duber.WebSite/Views/Trip/_Map.cshtml new file mode 100644 index 0000000..592fd43 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/Trip/_Map.cshtml @@ -0,0 +1,13 @@ +@using Microsoft.AspNetCore.Html +@using Newtonsoft.Json + + +
+ + \ No newline at end of file diff --git a/src/Web/Duber.WebSite/Views/_ViewImports.cshtml b/src/Web/Duber.WebSite/Views/_ViewImports.cshtml new file mode 100644 index 0000000..50e8705 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using Duber.WebSite +@using Duber.WebSite.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Web/Duber.WebSite/Views/_ViewStart.cshtml b/src/Web/Duber.WebSite/Views/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/src/Web/Duber.WebSite/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Web/Duber.WebSite/appsettings.Development.json b/src/Web/Duber.WebSite/appsettings.Development.json new file mode 100644 index 0000000..fa8ce71 --- /dev/null +++ b/src/Web/Duber.WebSite/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Web/Duber.WebSite/appsettings.json b/src/Web/Duber.WebSite/appsettings.json new file mode 100644 index 0000000..c6e0d5a --- /dev/null +++ b/src/Web/Duber.WebSite/appsettings.json @@ -0,0 +1,51 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "AzureServiceBusEnabled": false, + "EventBusConnection": "", + "EventBusConnectionHC": "", + "SubscriptionClientName": "WebSite", + "EventBusRetryCount": 5, + "HttpClientRetryCount": 5, + "HttpClientExceptionsAllowedBeforeBreaking": 4, + "TripApiSettings": { + "BaseUrl": "http://localhost:32775", + "CreateUrl": "/api/v1/trip", + "AcceptUrl": "/api/v1/trip/accept", + "StartUrl": "/api/v1/trip/start", + "CancelUrl": "/api/v1/trip/cancel", + "UpdateCurrentLocationUrl": "/api/v1/trip/update", + "NotificationsClientUrl": "http://localhost:32778", + "NotificationsServerUrl": "http://localhost:32778" + }, + "InvoiceApiSettings": { + "BaseUrl": "http://localhost:32776" + }, + "ConnectionStrings": { + "WebsiteDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Duber.WebSiteDb;User Id=sa;Password=Pass@word" + }, + "HealthChecks-UI": { + "HealthChecks": [ + { + "Name": "Invoice HTTP Check", + "Uri": "http://localhost:32776/readiness" + }, + { + "Name": "Trip HTTP Check", + "Uri": "http://localhost:32775/readiness" + }, + { + "Name": "Notifications HTTP Check", + "Uri": "http://localhost:32778/readiness" + }, + { + "Name": "Frontend HTTP Check", + "Uri": "http://localhost:32774/readiness" + } + ] + } +} diff --git a/src/Web/Duber.WebSite/bundleconfig.json b/src/Web/Duber.WebSite/bundleconfig.json new file mode 100644 index 0000000..6d3f9a5 --- /dev/null +++ b/src/Web/Duber.WebSite/bundleconfig.json @@ -0,0 +1,24 @@ +// Configure bundling and minification for the project. +// More info at https://go.microsoft.com/fwlink/?LinkId=808241 +[ + { + "outputFileName": "wwwroot/css/site.min.css", + // An array of relative input file paths. Globbing patterns supported + "inputFiles": [ + "wwwroot/css/site.css" + ] + }, + { + "outputFileName": "wwwroot/js/site.min.js", + "inputFiles": [ + "wwwroot/js/site.js" + ], + // Optionally specify minification options + "minify": { + "enabled": true, + "renameLocals": true + }, + // Optionally generate .map file + "sourceMap": false + } +] diff --git a/src/Web/Duber.WebSite/libman.json b/src/Web/Duber.WebSite/libman.json new file mode 100644 index 0000000..190c8e5 --- /dev/null +++ b/src/Web/Duber.WebSite/libman.json @@ -0,0 +1,33 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "jquery@2.2.0", + "destination": "wwwroot/lib/jquery/" + }, + { + "provider": "jsdelivr", + "library": "jquery-validation@1.14.0", + "destination": "wwwroot/lib/jquery-validation/" + }, + { + "library": "jquery-validation-unobtrusive@3.2.6", + "destination": "wwwroot/lib/jquery-validation-unobtrusive/" + }, + { + "library": "lodash.js@4.17.5", + "destination": "wwwroot/lib/lodash.js/" + }, + { + "provider": "jsdelivr", + "library": "bootstrap@3.3.7", + "destination": "wwwroot/lib/bootstrap/" + }, + { + "provider": "unpkg", + "library": "@microsoft/signalr@3.1.2", + "destination": "wwwroot/lib/signalr" + } + ] +} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/wwwroot/css/site.css b/src/Web/Duber.WebSite/wwwroot/css/site.css new file mode 100644 index 0000000..6d0f6e4 --- /dev/null +++ b/src/Web/Duber.WebSite/wwwroot/css/site.css @@ -0,0 +1,35 @@ +body { + padding-top: 50px; + padding-bottom: 20px; +} + +/* Wrapping element */ +/* Set some basic padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + +/* Carousel */ +.carousel-caption p { + font-size: 20px; + line-height: 1.4; +} + +/* Make .svg files in the carousel display properly in older browsers */ +.carousel-inner .item img[src$=".svg"] { + width: 100%; +} + +/* QR code generator */ +#qrCode { + margin: 15px; +} + +/* Hide/rearrange for smaller screens */ +@media screen and (max-width: 767px) { + /* Hide captions */ + .carousel-caption { + display: none; + } +} diff --git a/src/Web/Duber.WebSite/wwwroot/css/site.min.css b/src/Web/Duber.WebSite/wwwroot/css/site.min.css new file mode 100644 index 0000000..5e93e30 --- /dev/null +++ b/src/Web/Duber.WebSite/wwwroot/css/site.min.css @@ -0,0 +1 @@ +body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} \ No newline at end of file diff --git a/src/Web/Duber.WebSite/wwwroot/favicon.ico b/src/Web/Duber.WebSite/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch literal 32038 zcmeHwX>eTEbtY7aYbrGrkNjgie?1jXjZ#zP%3n{}GObKv$BxI7Sl;Bwl5E+Qtj&t8 z*p|m4DO#HoJC-FyvNnp8NP<{Na0LMnTtO21(rBP}?EAiNjWgeO?z`{3ZoURUQlV2d zY1Pqv{m|X_oO91|?^z!6@@~od!@OH>&BN;>c@O+yUfy5w>LccTKJJ&`-k<%M^Zvi( z<$dKp=jCnNX5Qa+M_%6g|IEv~4R84q9|7E=|Ho(Wz3f-0wPjaRL;W*N^>q%^KGRr7 zxbjSORb_c&eO;oV_DZ7ua!sPH=0c+W;`vzJ#j~-x3uj};50#vqo*0w4!LUqs*UCh9 zvy2S%$#8$K4EOa&e@~aBS65_hc~Mpu=454VT2^KzWqEpBA=ME|O;1cn?8p<+{MKJf zbK#@1wzL44m$k(?85=Obido7=C|xWKe%66$z)NrzRwR>?hK?_bbwT z@Da?lBrBL}Zemo1@!9pYRau&!ld17h{f+UV0sY(R{ET$PBB|-=Nr@l-nY6w8HEAw* zRMIQU`24Jl_IFEPcS=_HdrOP5yf81z_?@M>83Vv65$QFr9nPg(wr`Ke8 zaY4ogdnMA*F7a4Q1_uXadTLUpCk;$ZPRRJ^sMOch;rlbvUGc1R9=u;dr9YANbQ<4Z z#P|Cp9BP$FXNPolgyr1XGt$^lFPF}rmBF5rj1Kh5%dforrP8W}_qJL$2qMBS-#%-|s#BPZBSETsn_EBYcr(W5dq( z@f%}C|iN7)YN`^)h7R?Cg}Do*w-!zwZb9=BMp%Wsh@nb22hA zA{`wa8Q;yz6S)zfo%sl08^GF`9csI9BlGnEy#0^Y3b);M+n<(}6jziM7nhe57a1rj zC@(2ISYBL^UtWChKzVWgf%4LW2Tqg_^7jMw`C$KvU+mcakFjV(BGAW9g%CzSyM;Df z143=mq0oxaK-H;o>F3~zJ<(3-j&?|QBn)WJfP#JR zRuA;`N?L83wQt78QIA$(Z)lGQY9r^SFal;LB^qi`8%8@y+mwcGsf~nv)bBy2S7z~9 z=;X@Gglk)^jpbNz?1;`!J3QUfAOp4U$Uxm5>92iT`mek#$>s`)M>;e4{#%HAAcb^8_Ax%ersk|}# z0bd;ZPu|2}18KtvmIo8`1@H~@2ejwo(5rFS`Z4&O{$$+ch2hC0=06Jh`@p+p8LZzY z&2M~8T6X^*X?yQ$3N5EzRv$(FtSxhW>>ABUyp!{484f8(%C1_y)3D%Qgfl_!sz`LTXOjR&L!zPA0qH_iNS!tY{!^2WfD%uT}P zI<~&?@&))5&hPPHVRl9);TPO>@UI2d!^ksb!$9T96V(F){puTsn(}qt_WXNw4VvHj zf;6A_XCvE`Z@}E-IOaG0rs>K>^=Sr&OgT_p;F@v0VCN0Y$r|Lw1?Wjt`AKK~RT*kJ z2>QPuVgLNcF+XKno;WBv$yj@d_WFJbl*#*V_Cwzo@%3n5%z4g21G*PVZ)wM5$A{klYozmGlB zT@u2+s}=f}25%IA!yNcXUr!!1)z(Nqbhojg0lv@7@0UlvUMT)*r;M$d0-t)Z?B1@qQk()o!4fqvfr_I0r7 zy1(NdkHEj#Yu{K>T#We#b#FD=c1XhS{hdTh9+8gy-vkcdkk*QS@y(xxEMb1w6z<^~ zYcETGfB#ibR#ql0EiD;PR$L&Vrh2uRv5t_$;NxC;>7_S5_OXxsi8udY3BUUdi55Sk zcyKM+PQ9YMA%D1kH1q48OFG(Gbl=FmV;yk8o>k%0$rJ8%-IYsHclnYuTskkaiCGkUlkMY~mx&K}XRlKIW;odWIeuKjtbc^8bBOTqK zjj(ot`_j?A6y_h%vxE9o*ntx#PGrnK7AljD_r58ylE*oy@{IY%+mA^!|2vW_`>`aC{#3`#3;D_$^S^cM zRcF+uTO2sICledvFgNMU@A%M)%8JbSLq{dD|2|2Sg8vvh_uV6*Q?F&rKaV{v_qz&y z`f;stIb?Cb2!Cg7CG91Bhu@D@RaIrq-+o+T2fwFu#|j>lD6ZS9-t^5cx>p|?flqUA z;Cgs#V)O#`Aw4$Kr)L5?|7f4izl!;n0jux}tEW$&&YBXz9o{+~HhoiYDJ`w5BVTl&ARya=M7zdy$FEe}iGBur8XE>rhLj&_yDk5D4n2GJZ07u7%zyAfNtOLn;)M?h*Py-Xtql5aJOtL4U8e|!t? z((sc6&OJXrPdVef^wZV&x=Z&~uA7^ix8rly^rEj?#d&~pQ{HN8Yq|fZ#*bXn-26P^ z5!)xRzYO9{u6vx5@q_{FE4#7BipS#{&J7*>y}lTyV94}dfE%Yk>@@pDe&F7J09(-0|wuI|$of-MRfK51#t@t2+U|*s=W; z!Y&t{dS%!4VEEi$efA!#<<7&04?kB}Soprd8*jYv;-Qj~h~4v>{XX~kjF+@Z7<t?^|i z#>_ag2i-CRAM8Ret^rZt*^K?`G|o>1o(mLkewxyA)38k93`<~4VFI?5VB!kBh%NNU zxb8K(^-MU1ImWQxG~nFB-Un;6n{lQz_FfsW9^H$Xcn{;+W^ZcG$0qLM#eNV=vGE@# z1~k&!h4@T|IiI<47@pS|i?Qcl=XZJL#$JKve;booMqDUYY{(xcdj6STDE=n?;fsS1 ze`h~Q{CT$K{+{t+#*I1=&&-UU8M&}AwAxD-rMa=e!{0gQXP@6azBq9(ji11uJF%@5 zCvV`#*?;ZguQ7o|nH%bm*s&jLej#@B35gy32ZAE0`Pz@#j6R&kN5w{O4~1rhDoU zEBdU)%Nl?8zi|DR((u|gg~r$aLYmGMyK%FO*qLvwxK5+cn*`;O`16c!&&XT{$j~5k zXb^fbh1GT-CI*Nj{-?r7HNg=e3E{6rxuluPXY z5Nm8ktc$o4-^SO0|Es_sp!A$8GVwOX+%)cH<;=u#R#nz;7QsHl;J@a{5NUAmAHq4D zIU5@jT!h?kUp|g~iN*!>jM6K!W5ar0v~fWrSHK@})@6Lh#h)C6F6@)&-+C3(zO! z8+kV|B7LctM3DpI*~EYo>vCj>_?x&H;>y0*vKwE0?vi$CLt zfSJB##P|M2dEUDBPKW=9cY-F;L;h3Fs4E2ERdN#NSL7ctAC z?-}_a{*L@GA7JHJudxtDVA{K5Yh*k(%#x4W7w+^ zcb-+ofbT5ieG+@QG2lx&7!MyE2JWDP@$k`M;0`*d+oQmJ2A^de!3c53HFcfW_Wtv< zKghQ;*FifmI}kE4dc@1y-u;@qs|V75Z^|Q0l0?teobTE8tGl@EB?k#q_wUjypJ*R zyEI=DJ^Z+d*&}B_xoWvs27LtH7972qqMxVFcX9}c&JbeNCXUZM0`nQIkf&C}&skSt z^9fw@b^Hb)!^hE2IJq~~GktG#ZWwWG<`@V&ckVR&r=JAO4YniJewVcG`HF;59}=bf zLyz0uxf6MhuSyH#-^!ZbHxYl^mmBVrx) zyrb8sQ*qBd_WXm9c~Of$&ZP$b^)<~0%nt#7y$1Jg$e}WCK>TeUB{P>|b1FAB?%K7>;XiOfd}JQ`|IP#Vf%kVy zXa4;XFZ+>n;F>uX&3|4zqWK2u3c<>q;tzjsb1;d{u;L$-hq3qe@82(ob<3qom#%`+ z;vzYAs7TIMl_O75BXu|r`Qhc4UT*vN$3Oo0kAC!{f2#HexDy|qUpgTF;k{o6|L>7l z=?`=*LXaow1o;oNNLXsGTrvC)$R&{m=94Tf+2iTT3Y_Or z-!;^0a{kyWtO4vksG_3cyc7HQ0~detf0+2+qxq(e1NS251N}w5iTSrM)`0p8rem!j zZ56hGD=pHI*B+dd)2B`%|9f0goozCSeXPw3 z+58k~sI02Yz#lOneJzYcG)EB0|F+ggC6D|B`6}d0khAK-gz7U3EGT|M_9$ZINqZjwf>P zJCZ=ogSoE`=yV5YXrcTQZx@Un(64*AlLiyxWnCJ9I<5Nc*eK6eV1Mk}ci0*NrJ=t| zCXuJG`#7GBbPceFtFEpl{(lTm`LX=B_!H+& z>$*Hf}}y zkt@nLXFG9%v**s{z&{H4e?aqp%&l#oU8lxUxk2o%K+?aAe6jLojA& z_|J0<-%u^<;NT*%4)n2-OdqfctSl6iCHE?W_Q2zpJken#_xUJlidzs249H=b#g z?}L4-Tnp6)t_5X?_$v)vz`s9@^BME2X@w<>sKZ3=B{%*B$T5Nj%6!-Hr;I!Scj`lH z&2dHFlOISwWJ&S2vf~@I4i~(0*T%OFiuX|eD*nd2utS4$1_JM?zmp>a#CsVy6Er^z zeNNZZDE?R3pM?>~e?H_N`C`hy%m4jb;6L#8=a7l>3eJS2LGgEUxsau-Yh9l~o7=Yh z2mYg3`m5*3Ik|lKQf~euzZlCWzaN&=vHuHtOwK!2@W6)hqq$Zm|7`Nmu%9^F6UH?+ z@2ii+=iJ;ZzhiUKu$QB()nKk3FooI>Jr_IjzY6=qxYy;&mvi7BlQ?t4kRjIhb|2q? zd^K~{-^cxjVSj?!Xs=Da5IHmFzRj!Kzh~b!?`P7c&T9s77VLYB?8_?F zauM^)p;qFG!9PHLfIsnt43UnmV?Wn?Ki7aXSosgq;f?MYUuSIYwOn(5vWhb{f%$pn z4ySN-z}_%7|B);A@PA5k*7kkdr4xZ@s{e9j+9w;*RFm;XPDQwx%~;8iBzSKTIGKO z{53ZZU*OLr@S5=k;?CM^i#zkxs3Sj%z0U`L%q`qM+tP zX$aL;*^g$7UyM2Go+_4A+f)IQcy^G$h2E zb?nT$XlgTEFJI8GN6NQf%-eVn9mPilRqUbT$pN-|;FEjq@Ao&TxpZg=mEgBHB zU@grU;&sfmqlO=6|G3sU;7t8rbK$?X0y_v9$^{X`m4jZ_BR|B|@?ZCLSPPEzz`w1n zP5nA;4(kQFKm%$enjkkBxM%Y}2si&d|62L)U(dCzCGn56HN+i#6|nV-TGIo0;W;`( zW-y=1KF4dp$$mC_|6}pbb>IHoKQeZajXQB>jVR?u`R>%l1o54?6NnS*arpVopdEF; zeC5J3*M0p`*8lif;!irrcjC?(uExejsi~>4wKYwstGY^N@KY}TujLx`S=Cu+T=!dx zKWlPm->I**E{A*q-Z^FFT5$G%7Ij0_*Mo4-y6~RmyTzUB&lfae(WZfO>um}mnsDXPEbau-!13!!xd!qh*{C)6&bz0j1I{>y$D-S)b*)JMCPk!=~KL&6Ngin0p6MCOxF2L_R9t8N!$2Wpced<#`y!F;w zKTi5V_kX&X09wAIJ#anfg9Dhn0s7(C6Nj3S-mVn(i|C6ZAVq0$hE)874co};g z^hR7pe4lU$P;*ggYc4o&UTQC%liCXooIfkI3TNaBV%t~FRr}yHu7kjQ2J*3;e%;iW zvDVCh8=G80KAeyhCuY2LjrC!Od1rvF7h}zszxGV)&!)6ChP5WAjv-zQAMNJIG!JHS zwl?pLxC-V5II#(hQ`l)ZAp&M0xd4%cxmco*MIk?{BD=BK`1vpc}D39|XlV z{c&0oGdDa~TL2FT4lh=~1NL5O-P~0?V2#ie`v^CnANfGUM!b4F=JkCwd7Q`c8Na2q zJGQQk^?6w}Vg9-{|2047((lAV84uN%sK!N2?V(!_1{{v6rdgZl56f0zDMQ+q)jKzzu^ztsVken;=DjAh6G`Cw`Q4G+BjS+n*=KI~^K{W=%t zbD-rN)O4|*Q~@<#@1Vx$E!0W9`B~IZeFn87sHMXD>$M%|Bh93rdGf1lKoX3K651t&nhsl= zXxG|%@8}Bbrlp_u#t*DZX<}_0Yb{A9*1Pd_)LtqNwy6xT4pZrOY{s?N4)pPwT(i#y zT%`lRi8U#Ken4fw>H+N`{f#FF?ZxFlLZg7z7#cr4X>id z{9kUD`d2=w_Zlb{^c`5IOxWCZ1k<0T1D1Z31IU0Q2edsZ1K0xv$pQVYq2KEp&#v#Z z?{m@Lin;*Str(C2sfF^L>{R3cjY`~#)m>Wm$Y|1fzeS0-$(Q^z@} zEO*vlb-^XK9>w&Ef^=Zzo-1AFSP#9zb~X5_+){$(eB4K z8gtW+nl{q+CTh+>v(gWrsP^DB*ge(~Q$AGxJ-eYc1isti%$%nM<_&Ev?%|??PK`$p z{f-PM{Ym8k<$$)(F9)tqzFJ?h&Dk@D?Dt{4CHKJWLs8$zy6+(R)pr@0ur)xY{=uXFFzH_> z-F^tN1y(2hG8V)GpDg%wW0Px_ep~nIjD~*HCSxDi0y`H!`V*~RHs^uQsb1*bK1qGpmd zB1m`Cjw0`nLBF2|umz+a#2X$c?Lj;M?Lj;MUp*d>7j~ayNAyj@SLpeH`)BgRH}byy zyQSat!;U{@O(<<2fp&oQkIy$z`_CQ-)O@RN;QD9T4y|wIJ^%U#(BF%=`i49}j!D-) zkOwPSJaG03SMkE~BzW}b_v>LA&y)EEYO6sbdnTX*$>UF|JhZ&^MSb4}Tgbne_4n+C zwI8U4i~PI>7a3{kVa8|))*%C0|K+bIbmV~a`|G#+`TU#g zXW;bWIcWsQi9c4X*RUDpIfyoPY)2bI-r9)xulm1CJDkQd6u+f)_N=w1ElgEBjprPF z3o?Ly0RVeY_{3~fPVckRMxe2lM8hj!B8F)JO z!`AP6>u>5Y&3o9t0QxBpNE=lJx#NyIbp1gD zzUYBIPYHIv9ngk-Zt~<)62^1Zs1LLYMh@_tP^I7EX-9)Ed0^@y{k65Gp0KRcTmMWw zU|+)qx{#q0SL+4q?Q`i0>COIIF8a0Cf&C`hbMj?LmG9K&iW-?PJt*u)38tTXAP>@R zZL6uH^!RYNq$p>PKz7f-zvg>OKXcZ8h!%Vo@{VUZp|+iUD_xb(N~G|6c#oQK^nHZU zKg#F6<)+`rf~k*Xjjye+syV{bwU2glMMMs-^ss4`bYaVroXzn`YQUd__UlZL_mLs z(vO}k!~(mi|L+(5&;>r<;|OHnbXBE78LruP;{yBxZ6y7K3)nMo-{6PCI7gQi6+rF_ zkPod!Z8n}q46ykrlQS|hVB(}(2Kf7BCZ>Vc;V>ccbk2~NGaf6wGQH@W9&?Zt3v(h*P4xDrN>ex7+jH*+Qg z%^jH$&+*!v{sQ!xkWN4+>|b}qGvEd6ANzgqoVy5Qfws}ef2QqF{iiR5{pT}PS&yjo z>lron#va-p=v;m>WB+XVz|o;UJFdjo5_!RRD|6W{4}A2a#bZv)gS_`b|KsSH)Sd_JIr%<%n06TX&t{&!H#{)?4W9hlJ`R1>FyugOh3=D_{einr zu(Wf`qTkvED+gEULO0I*Hs%f;&=`=X4;N8Ovf28x$A*11`dmfy2=$+PNqX>XcG`h% zJY&A6@&)*WT^rC(Caj}2+|X|6cICm5h0OK0cGB_!wEKFZJU)OQ+TZ1q2bTx9hxnq& z$9ee|f9|0M^)#E&Pr4)f?o&DMM4w>Ksb{hF(0|wh+5_{vPow{V%TFzU2za&gjttNi zIyR9qA56dX52Qbv2aY^g`U7R43-p`#sO1A=KS2aKgfR+Yu^bQ*i-qu z%0mP;Ap)B~zZgO9lG^`325gOf?iUHF{~7jyGC)3L(eL(SQ70VzR~wLN18tnx(Cz2~ zctBl1kI)wAe+cxWHw*NW-d;=pd+>+wd$a@GBju*wFvabSaPtHiT!o#QFC+wBVwYo3s=y;z1jM+M=Fj!FZM>UzpL-eZzOT( zhmZmEfWa=%KE#V3-ZK5#v!Hzd{zc^{ctF~- z>DT-U`}5!fk$aj24`#uGdB7r`>oX5tU|d*b|N3V1lXmv%MGrvE(dXG)^-J*LA>$LE z7kut4`zE)v{@Op|(|@i#c>tM!12FQh?}PfA0`Bp%=%*RiXVzLDXnXtE@4B)5uR}a> zbNU}q+712pIrM`k^odG8dKtG$zwHmQI^c}tfjx5?egx3!e%JRm_64e+>`Ra1IRfLb z1KQ`SxmH{cZfyVS5m(&`{V}Y4j6J{b17`h6KWqZ&hfc(oR zxM%w!$F(mKy05kY&lco3%zvLCxBW+t*rxO+i=qGMvobx0-<7`VUu)ka`){=ew+Ovt zg%52_{&UbkUA8aJPWsk)gYWV4`dnxI%s?7^fGpq{ZQuu=VH{-t7w~K%_E<8`zS;V- zKTho*>;UQQul^1GT^HCt@I-q?)&4!QDgBndn?3sNKYKCQFU4LGKJ$n@Je$&w9@E$X z^p@iJ(v&`1(tq~1zc>0Vow-KR&vm!GUzT?Eqgnc)leZ9p)-Z*C!zqb=-$XG0 z^!8RfuQs5s>Q~qcz92(a_Q+KH?C*vCTr~UdTiR`JGuNH8v(J|FTiSEcPrBpmHRtmd zI2Jng0J=bXK);YY^rM?jzn?~X-Pe`GbAy{D)Y6D&1GY-EBcy%Bq?bKh?A>DD9DD!p z?{q02wno2sraGUkZv5dx+J8)&K$)No43Zr(*S`FEdL!4C)}WE}vJd%{S6-3VUw>Wp z?Aasv`T0^%P$2vE?L+Qhj~qB~K%eW)xH(=b_jU}TLD&BP*Pc9hz@Z=e0nkpLkWl}> z_5J^i(9Z7$(XG9~I3sY)`OGZ#_L06+Dy4E>UstcP-rU@xJ$&rxvo!n1Ao`P~KLU-8 z{zDgN4-&A6N!kPSYbQ&7sLufi`YtE2uN$S?e&5n>Y4(q#|KP!cc1j)T^QrUXMPFaP z_SoYO8S8G}Z$?AL4`;pE?7J5K8yWqy23>cCT2{=-)+A$X^-I9=e!@J@A&-;Ufc)`H}c(VI&;0x zrrGv()5mjP%jXzS{^|29?bLNXS0bC%p!YXI!;O457rjCEEzMkGf~B3$T}dXBO23tP z+Ci>;5UoM?C@bU@f9G1^X3=ly&ZeFH<@|RnOG--A&)fd)AUgjw?%izq{p(KJ`EP0v z2mU)P!+3t@X14DA=E2RR-|p${GZ9ETX=d+kJRZL$nSa0daI@&oUUxnZg0xd_xu>Vz lzF#z5%kSKX?YLH3ll^(hI(_`L*t#Iva2Ede*Z;>H_ \ No newline at end of file diff --git a/src/Web/Duber.WebSite/wwwroot/images/banner2.svg b/src/Web/Duber.WebSite/wwwroot/images/banner2.svg new file mode 100644 index 0000000..9679c60 --- /dev/null +++ b/src/Web/Duber.WebSite/wwwroot/images/banner2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Web/Duber.WebSite/wwwroot/images/banner3.svg b/src/Web/Duber.WebSite/wwwroot/images/banner3.svg new file mode 100644 index 0000000..9be2c25 --- /dev/null +++ b/src/Web/Duber.WebSite/wwwroot/images/banner3.svg @@ -0,0 +1 @@ +banner3b \ No newline at end of file diff --git a/src/Web/Duber.WebSite/wwwroot/images/banner4.svg b/src/Web/Duber.WebSite/wwwroot/images/banner4.svg new file mode 100644 index 0000000..38b3d7c --- /dev/null +++ b/src/Web/Duber.WebSite/wwwroot/images/banner4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Web/Duber.WebSite/wwwroot/images/car-icon.png b/src/Web/Duber.WebSite/wwwroot/images/car-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..38b007cb1ff0096cc5ab09be7debf8fbcc665fc4 GIT binary patch literal 3893 zcmV-556bX~P)004&%004{+008|`004nN004b?008NW002DY000@xb3BE2000U( zX+uL$P-t&-Z*ypGa3D!TLm+T+Z)Rz1WdHz3$DNjUR8-d%htIutdZEoQ0#b(FyTAa_ zdy`&8VVD_UC<6{NG_fI~0ue<-nj%P0#DLLIBvwSR5EN9f2P6n6F&ITuEN@2Ei>|D^ z_ww@lRz|vC zuzLs)$;-`!o*{AqUjza0dRV*yaMRE;fKCVhpQKsoe1Yhg01=zBIT!&C1$=TK@rP|Ibo3vKKm@PqnO#LJhq6%Ij6Hz*<$V$@wQAMN5qJ)hzm2h zoGcOF60t^#FqJFfH{#e-4l@G)6iI9sa9D{VHW4w29}?su;^hF~NC{tY+*d5%WDCTX za!E_i;d2ub1#}&jF5T4HnnCyEWTkKf0>c0%E1Ah>(_PY1)0w;+02c53Su*0<(nUqK zG_|(0G&D0Z{i;y^b@OjZ+}lNZ8Th$p5Uu}MTtq^NHl*T1?CO*}7&0ztZsv2j*bmJyf3G7=Z`5B*PvzoDiKdLpOAxi2$L0#SX*@cY z_n(^h55xYX#km%V()bZjV~l{*bt*u9?FT3d5g^g~#a;iSZ@&02Abxq_DwB(I|L-^b zXThc7C4-yrInE_0gw7K3GZ**7&k~>k0Z0NWkO#^@9q0fwx1%qj zZ=)yBuQ3=54Wo^*!gyjLF-e%Um=erBOdIALW)L%unZshS@>qSW9o8Sq#0s#5*edK% z>{;v(b^`kbN5rY%%y90wC>#%$kE_5P!JWYk;U;klcqzOl-UjcFXXA75rT9jCH~u<) z0>40zCTJ7v2qAyk54cquI@7b&LHdZ`+zlTss6bJ7%PQ)z$cROu4wBhpu-r)01) zS~6}jY?%U?gEALn#wiFzo#H}aQ8rT=DHkadR18&{>P1bW7E`~Y4p3)hWn`DhhRJ5j z*2tcg9i<^OEt(fCg;q*CP8+7ZTcWhYX$fb^_9d-LhL+6BEtPYWVlfKTBusSTASKKb%HuWJzl+By+?gkLq)?+BTu761 zjmyXF)a;mc^>(B7bo*HQ1NNg1st!zt28YLv>W*y3CdWx9U8f|cqfXDAO`Q48?auQq zHZJR2&bcD49Ip>EY~kKEPV6Wm+eXFV)D)_R=tM0@&p?(!V*Qu1PXHG9o^ zTY0bZ?)4%01p8F`JoeS|<@=<@RE7GY07EYX@lwd>4oW|Yi!o+Su@M`;WuSK z8LKk71XR(_RKHM1xJ5XYX`fk>`6eqY>qNG6HZQwBM=xi4&Sb88?zd}EYguc1@>KIS z<&CX#T35dwS|7K*XM_5Nf(;WJJvJWRMA($P>8E^?{IdL4o5MGE7bq2MEEwP7v8AO@ zqL5!WvekBL-8R%V?zVyL=G&{be=K4bT`e{#t|)$A!YaA?jp;X)-+bB;zhj`(vULAW z%ue3U;av{94wp%n<(7@__S@Z2PA@Mif3+uO&y|X06?J#oSi8M;ejj_^(0<4Lt#wLu#dYrva1Y$6_o(k^&}yhSh&h;f@JVA>W8b%o zZ=0JGnu?n~9O4}sJsfnnx7n(>`H13?(iXTy*fM=I`sj`CT)*pTHEgYKqqP+u1IL8N zo_-(u{qS+0<2@%BCt82d{Gqm;(q7a7b>wu+b|!X?c13m#p7cK1({0<`{-e>4hfb-U zsyQuty7Ua;Ou?B?XLHZaol8GAb3Wnxcu!2v{R_`T4=x`(GvqLI{-*2AOSimk zUAw*F_TX^n@STz9kDQ z$NC=!KfXWC8h`dn#xL(D3Z9UkR7|Q&Hcy#Notk!^zVUSB(}`#4&lYA1f0h2V_PNgU zAAWQEt$#LRcH#y9#i!p(Udq2b^lI6wp1FXzN3T;~FU%Lck$-deE#qz9yYP3D3t8{6 z?<+s(e(3(_^YOu_)K8!O1p}D#{JO;G(*OVf32;bRa{vGf5&!@T5&_cPe*6Fc02y>e zSaefwW^{L9a%BK;VQFr3E^cLXAT%y8E-^XO*0N*(00covL_t(&L+zMbXk1kkhBKK- zCNoKz>&zq*Gv;D$}+pNPL3xlD{Cn?H}?#E=L!o8 zzs<|bTXwtMi#$^peRPwAx#Ifz`X{=(yAKWw44fPu9=;5bzP>)y*4CzKYilEZzyF%Y zi%uGJbc^25@ zD`0ev8GomvqvLyyk*2056$k_r;qn`%58Lhb@3{9dn1PQ$#rOo$AtlPe{4>n_{js){ z9L)49#l^)JT`t!S+SwomPk;wRJzx%?S2{La%=mqnUm|u3VqI&g)n>E($}tpe-qHaW zjGm^AfH{EuV1o^jjoI|q)YQC4n7l+=(%^qdVp&2ZY_x9I^Y9!z4!?jofC$vWy2@5w zUfxJNzdtlI^w;F%WTdXHZd&@Imn|h>@-DmviT9PmQMd}G@pxWgwtbxc4b|1cLAYoP#%@26n+WAm*%0o5&ZFXRpC9Bqw&! z2L}fSS53o&j$v9DA0Jm^V`HkdwN>)Cl^Nj*NK9K@OJo(iAYbYXB)1OO`h(=|m3YIZ zY1x?Atd!(-GK`g(yWu3ry2*tPK?ZWya)d&m70Q9LV%9`VQx+%|vSJZinMj7yAcdW5 zZiy-P&csex)YH>*L*l!=z5Ntnq)e!9;?4gPJ0`d6kexg z00N;DsjRHr&n71EDSyllV!$*c1=!m#3a^4>k$A%<7eFQ;02{X3id>Wm4>R+JF@KX# zGD=~x&3UeU0e?Wev1h?FB*#4ve_i5xC#0;(Os7D5lB2V#sw&LaSRpH|l@4Vv+aO+M zarQGTKeO>!T?(kCYCH=7ldIF&wtT*HC4&HcKq#N2%d|AlHjY`K4qv z05koWl5@!@{YGmGNNSxWI0aS%5Ts004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}00009a7bBm000XT000XT0n*)m z`~Uy|8FWQhbW?9;ba!ELWdLwtX>N2bZe?^JG%heMF*(%MvSa`N2P8>EK~zXf)mCdz zROcDqnM|f>e@G@8B|-3phQ#G6A~#uB_6ED`vat7cVF8h=AYLG%4Yq2WlB69nvDK;3 zV5S+h?WAqgXoC`wi~$687j}UK1c61C<-S0tqw@4!dRl*E+Nm?mk32KaeCO=>_I;lB za=tmNA0%+)$_sNxkG|Zf#ymrJ-d@ZWlwzu&^ucUV z*~z63KKL;c=J|2|K3?n`e|p+HzO>Zge1a~U6N7)KgR9~g=5(2u*JfZoV>jHndog3o z$CM@$6LK|NG8LRjGK`5+m*%qa-eST%9|i^nUNVh6xq5#BXmbH3qM5EO#7?5!JObz8 z3?sDVKS2oy0(wSakfiDyK zzXNB_zUVF~(>UHg-O+9uLCfe8+8!;zG)BT?!0K8;_rz0lIG51zQ7!rvdNf>bMIDLV zw$7lTV-k0-wxC6>#T~l7MI~#ltj2&seT9+u&zLMNEpTV=9$nZ)z13%6#*l@vvPwKS z^BL@4ev2-P13ix(!|r+lbDtai#ys@!lhIi7De7!q=od=xP_6%*k$r}T1%+QsDAbtLXwY4@9}@~KrYLJq z#*-O2u%xNcCQXM^sfSBz#0X_K$P=ND!-GAZhn@r;y2yZyL~R~XT00X(=txOJTTC2m zVmbPyO4t*G7!b>z4wRQatAeAlGHgtz$5?7QOnY=_&eFjtQ(#g>Vul>tNLAw2wVROb z~qawYmBBGDrhqjP6GhD2gGk|Y?VtS_g?ku8?s1pVF~%ULdS zADJ7@Ey4gDICjd^N^P+H`T}~d*2Dh!P4v`WhUs7>?j}odksXTgm8+l*425CiW}Nd~ zkG^<8Gb7JpO`QJi7t`fO;7V0s(31%nu|`HyZv_}k6rh_1=-cR6+zShbWk(cx;<#w% z@Ns$57My?G2es=qpfP+0+R`;>%g%>Mng$b9qls7-5)P4%FM|97pi=4}CPPL_)+9Du zXXII|>3i*;ERMS1I)4%4yNsBUt1wAfkJB8Cq^W4ilhG6r1#>(X)}Rnv3fPM6US5ds zdKuac{xEvKhU1jsZ7OpUCD2B#X(jPnv2nP#H5ltwt-`j@ZP>PLJGO2OgjTJ?k^P6X zj691qcKU4f;>ZLRU9*_7bYbM9GZ@LvU7m?y&+8MHfL_Yl8XSth1iXa+uNClLxe{>^ zF-Vcfz^B(sMtjb@?u$m+*FbHmkB!GQGF`GI0KThNBZ1oH5s#)ymX(xYU&+3FMxG4R z)I>U}YZj-?cFa4cmpL;u24{;I-8utqkn@_*FnmL07OYtVzn8tRflAMbi$@?06q_jE zi*PtH1`XSz(aNWDL6|}L}S&@lbiAj zF?8xza9+QS@w&TscI;*OxUSg!5;TKg^l3!*T zZ5OCqU6NEf$%JT*ipFPwL8zxHlvAKh)X&Xp*HYO92&KM8(Xa|hSVw);%hH=JayV=z z9BZSucbeat)qmf-antf12~{D(khP@I=vB;???S0mzOZrKT6v_ePi2cxl5gP(FIhN= zLpGim6*OSNI1c!9(n*#x;)6?O(`Z(9)5nP-RdJLF^KV242EMV*$LGV4pkRwgAZjIX zIrHfGaFWelBia)1o0#BW28azzHS?fXZpzyGVDY<5;ce8L3*PgEn9tHPK^KQypd{{X)D VoGrM)>9GI+002ovPDHLkV1hVat`z_P literal 0 HcmV?d00001 diff --git a/src/Web/Duber.WebSite/wwwroot/images/car-icon3.png b/src/Web/Duber.WebSite/wwwroot/images/car-icon3.png new file mode 100644 index 0000000000000000000000000000000000000000..a38a73de931672340923ade54d674ffdc9b036e9 GIT binary patch literal 2450 zcmV;D32pX?P)004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}00009a7bBm001r`001r`0jlEN zk^lez8FWQhbW?9;ba!ELWdLwtX>N2bZe?^JG%heMF*(%MvSa`N2=hrqK~!i%?NydnckcbZ|Nj4fAH!5oK?N05 zP(cM1R8T<$72a!jd3pUNBqG-2=xMpDwDi(qiaA9WJP(~Z;~1W>-@?^v>9|?5W{skU z|NKA<{;H%xvADJC#_{f(eOJ5tZa;3j^$4wBeGmWn5DndTo>jO0CO;KX3^z@O6z>#IaXJ zF{t};Ma{}vckVx+j1KTF;n&|iLHF&!jeQ@p3#<3LgywtmcjW(-voVXQT=4&isq_8E zJEyl}MtUbWVY2Z0=9Z+p|E5g#?<~bTWOv(dJthVy4sL8P)^vUY_q1zpNb3SWwF`p8 zYmcmiQ6JJv|MXz7Sidu$yMCgLuOw{R=0kO>qmF!atfU^HiH9NDoroRL`w$+Vj<|i< zNIIO4^9?J$*tfgwP0FE5vH!# z)60!i9AyWc^a>6R9zjd_VIayb)D!2o`+b(znfb}d&NAQQ9Zjx#s;}^N7GC9x&wcAC zseCqLPaT|6+sF7q~ICaEz~oOIj;tALxd2W;YxulawLa+)&6(Xa*;` zmIR>|Tymw%47{V}qyi=&fue#WNWM;tUV!Oe!oggZf|-$Wcw}|K?LZHBJ5R5nWxpFl z#}wnY}b5KbpX`9m~Hr6wotGI+!sMSkM7LB~<# zkzfd7>);Sy2gfiOW+XLZCJ8X5hZ{?NPlCYRdj)1B0NjYm6SVZ9gQ^Li?Ca&00iVMl zpa6!VVwkT?!_1VHezRoJm(%vYAuqlWterAgZpeaFU@o}v&4Z5P#?}+ofkzBEkeaop z*P$}t)v=^Gl4r*AgYXA6U3}~vTt>?uI@qFPZrrFUY|S_W&n3Rt9TkI3Nx5){D1%et zWl}eU30J@;DaKFjP%i$qoo0o~V74X`1{+BLx0b*_B*Vlt2S`1)3_5-=QfdGCGp?it zItEiQ*}?{TAM+|{`9lY1yEJ76=1<=>mM^8@=Lxo*7YCcJ%`NTDXExAN$ z?~*dw$~sim;){D$XfaHd?ZM=A>Ckl#fcx$O7>mxp&_4$pNwrcqkC3%n0V5B8Sot4P zru`e{UN=li!8-5+Y?vGf1k+(^X8G-txhe*vu987^yyQaQ#8UcsuK2l2T#Md z(Kl9#-o8FWL`Gud$dUN6w-;@H?*nI3rqToz6uaO;XzQ6kO-%>tqsPN;cf}j?P=S2; zo)c3Kb73?rESNAeGl!?=q9j`W(7}tT{hBQEoSkt4{-$$?ZV z#i_hpc*U0}vts~HNyeNit%nmiP6mYAg_T2HodG+RJ*L@lAQnrYXX*0%{QzIyVXDj9 z2Fsu?SOPZN0eqeVtSp#wX!%12sll3;7gLhd?Pz^-j2J!whFY3fX={pbXC{(7*vOd6 zN3uH)(JpofbFxLSgEiI)>_}6(3YQ2O((@Hal`7yJeU^xAcx7HA(q*aL-LhGPDO0Se zodVNo{wrg6$!%bg4-czd)iC|E5XRf&m>N=cU;6`-XL@>u#I*gx2@@u0)AEN7?#?qC z1i<`6ctt*Yw;cIrH{e+QW~7}EfgeulIl2zM zNph^uD96&|Qv4}S4(Esxa7fLwBQGewlmz<*j;e0jSbx9)yAK~^k7c|`?o+`NhK9O_7_SDu^Pm`f0+ZAZefoe0m~Mb32z zW=G_LO}>zLN#q{^GM6U95PEtBaC4iBwQK!JEpsu$ZQ(c8;^JuQHT&#{lpoLJ>libJ zF+)RB4-=RkFmzslN%I0=x+EMvK?%3dme^8X63UM8PmIHpBY$o57k{? zSf#E$rdLgEjIz0CsOvOqYcp8%Yb9*l-G4vD!`G0#V1q4l(YD#F-~t~`XwfE4cv%=X zLXpamD8Asv)W|r|7tfO;uY@0c@to~eX|3tPV5n(~RnEI-po5Ig;Jw}gaynDy} zAwbpiXa>V#?AQ+t=tuuE#*G`NON`#7%u<{<(HzT{uf|6onbcG3(ZhaO(9+UbK+N7B zFikq{Rb5@(N%ZSqCbYFDX=v)`Sn25N>(P%?P(cM1R8T<$6;$|{Vc4+$0922&AvVnv Qga7~l07*qoM6N<$f^AEjr2qf` literal 0 HcmV?d00001 diff --git a/src/Web/Duber.WebSite/wwwroot/images/car-icon4.png b/src/Web/Duber.WebSite/wwwroot/images/car-icon4.png new file mode 100644 index 0000000000000000000000000000000000000000..0de4a3c5349963e2819a4448ebdfcad00bf3c884 GIT binary patch literal 3266 zcmV;z3_bISP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGmbN~PnbOGLGA9w%&02y>eSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E-^XO*0N*(01ProL_t(&L+w}xP?TpHMljSxmZdB#EU@&> z(gYC;#YPns6dMRikt!m#2!f)hs7OErRP3OqMi3RSfJl|9ARr*8(Wps`(Mc}la`CzM zUm?k5ax*!T$z*PLW}ca4cmMsL_x+x?{D_TW6r&i$C`K`gQH){~qZq{~{$FtB%o&N= z+FF&SrY6nS)>Z}Ld&3`zLCjyGz|zvt*VmUn{MKI!m6eskt5>hyDJdy=bN>8!T)A@P zhlYlRS3NyFO+!ONd-~Hejy9QEVDXfx-%=3l;mbxaX4|@T`Y9JmP@cmXF)o$6@H%lfsVdcDluj`CVK5O9X4Ba*{t>|!@w!PD3nk&S_#6>szqfo7_E!{WXIj?+6 z`sIRxf;UCSib#ApDsNQb#?2e3tg6JBvwy>Z{dp+cn~hV6YmmBd5fa10kzY`Nwzf9N z%gbY|xHuZ8%|HcD6-kChu(Pv++PHC8%-6zMJ9~6_`(hwE7DF304LwLnSq^`HiD3!9 z4+Yw)j>jUyk(HH&oCCSY&dbJ*-8+%KJsn%q)3ISwGNKb!BR^pcPR7I{KO_{H0rRnb z`gBZnaRb>4R#sLpkspV4yYaZL&P9xgDTG2HR8>_e;bIUUGY0XlZs?DU!qEB+c(OMO zAMM;#)w^bm{96_8h{TyQ^%4^kt1~h(kd(9>TxnU{44WI4UA^$8p+w*yTVb1fsg2AUB z3%VLSghhlQe}67Awx%O0IvRmN!SExicvH4x1N@N^7>EQCvC732o7~;8($f=r_wGen zS{lw$rmMIrxS__yH8oX~sj^|kmWKwLja>KX=p_5x^YTGozykDB&;!fEC>VdVJ5Bso ztF7&i9qw20t5Vd=8vbV`xf7#@N6<)Nsqu7E($GhfA(Aq zE?j~K6!5^ZP;`5Ep~>1BH;oK%&ejGilogRYP69QW8jl#Q!?NF^f8$2I7p0{QUp6%2 z%i3Fbeff$gorQJWNi;$Ej&}NLd|Rv=n{5e(0nEYj$)%v%MX!T!_2$cRLk#v(p6B3$0LVY=j$n0$kPM zqn-j-MJK(VW=Z*_%gEZd58mG1-;?H_Ciy`xuY}aio6+_DCfuVRY^UPviHXMF8R2oO zFt{Qd_X8K9hdM$R_3uuyM;jGxtBDy}j7-pMV2E1+9aQr*af7;2gOL&LPH{yCS*n%9 z_mF@-|M}=;y2rHXXrU~3`OQHG&4b&{&SIwX z$QrhGxM^>TMyi=x=4Pld(8G0s7Ai^fB@Hf0)s(5VDxz3L0VkE&h+|10h}!bWQ>Sp5 zYBDY^9%g1{k4gGiQ2-Bgbrt&b%-%eRSc#qmf#{w+2mLE!ahEyj$y3oz9&fR>#f6E^ zs9CWBbqNW$78Qj$8dR+z%h9N#i$=aS74Q^135md?xq%pR_rw)LbL6OUQKTe?!%9+U zwYEVE74
PlU0T-Vh>m6irBYp9`2T^Z-NiYOt`$CX$(#F0gTq8tt=%ECul9IlkS zy)=Q7lGY-aI+>)TWG+cp5(Ux8wUq1CGWv#sK);_q?uV{Gm!~&6r@GRxa7G&yW(fJ1 zHbgvo_6)tfy|{Mm8Y(L)&_Ku8B+x^Lg-|5?aY#5WEDnMTtphSLG7zU7M|O-D(#DO& zEj}L&bV}7UaZA-z$Q~*<&*k7Wi9Vq$k0Oz18Ifp)`xIqgMY5!u$BB=5#pQB;@Su(~ zVZsD(IGipLK8I*Xlp%_V^=TPqE}$&#QC35XjvhLv&p;OqsBRkHOdv;$jFF}zKqlGe zz<3Ab8XDuQmNu@Dbt-9pxo>%Re|hA z;xEy(Emj_fQzZHbM-GJ~I-mVk_y=s+uae}Y+a^noEuw8Lm87K*HxZLrEY?OIj~79& zorpR_W*2(9)ZNoDnOtZt1WQg1=8{rKRa8V7jrS|$#|k4|RM}YIij^@+jdf7MSHdA> zDI8D`Lmu%g_3<8oF?ym`i3U_Etp{0I`)DUxhjldYn3E7nNuferjWVt*^0i3xaJF;U zvj1R2yGcnN`-SLr#2Dg2qASsas6u42&Itc$h%poGfw^T-v6T=)I%?+VY4L^{O-esm zDU^_pk8`AONO3FwekPj9_WQ&&MCRd!@wOvTKxEpaB$0WD z`R~Gq!F)lA$g`J|b34lCpKa4Oc-ch*sYyo*RXpkvs`5CmB8^f$2l*Tck@$Q%jl*;* zsk&;SL6ei4i(1;9ii=A`_VMxcg{G$F_iPD?opb|6@QCOUi$oI{i?|StiN9`<-vH*z z3dHfmMZ`D?d&3NAnfz@WPR)5W-lIBA{&%U&P+}}G zNmg7u*-lDo-!hi`nfKZ3tC@;QO}R=+9XmOkb_*Sy?`gavA|etAt5+j{b{Y!{3s_oO zek&_0yN9l4ED}caBfgVGek+(CV*-&QGXEK1Ze&ArBhDi-#l;-^YT|k#<6kDT`L?#U zLtb89m^pK%Xz(~WIeku#g*)l9K;no+nBROSi@Xb%8!$Iwvcw!Qb8~H?Ig#nbj0FOS zA;c)+Ds^@BOmlPdA|oTC44RhS^d4go9`O&c$nOPn^N|yhCNhJBLu3T%63vKKL?fa) zk@=kYE_2Oa7EDkhfib7WWcg3A$0$behd@m1-S9%&WRP0m$0DwmGr78#jAVmWJ zH-srjh?Zr$MqA>G)aixp3jm-pp8C>;3;-YjywWjJBie7=xB&zLNk~XYNlD4b$Zp=e zNls2qK|w)DNl8UTMNLh8>((tA8X8(!S~@y9dU|>W28P?WZ!l9ra1 zk&%&=m6el|lb4rQP*6}*R8&$@dj9Cc{um9@RD+2=q5C~*wXlP_)WNd6~Vq#)yY6=E}&CJZq&CM+= zEG#W8t*opd5Qw$4H53Z9v9YnWwY9Uev$wZ*aBy&RbaZlZdj0yfv$L~{iwg_}b9Hrf zb8~ZdclYq{@bvWb^78Wb_V)4d@%8of^Yiof_YVjNc=P5>U|?WSP*8Aia7ai991af+ z4Gjwmd;9h+0)Yq*4}bUWT|`90`}gl7BO{}tqCR~15FH&I6BG0CK_O z$H#yE{Q1k5F9`_=iHV6xNlD4c$tfu*si~=7zkW?iOZ)ciTY7qWMn**3WX{wD=RNAuc)X%qtTU> zl~q+$)z#HCH8r)hwRLrM_4V~Xe*9=?XlQI~Y-(z1ZfROfBx+1>gw+9?&<0I_3Kw}Z*N~;9~O)4@9!TN7#JKJ92y!L9v=Sv`}fGm$e%xd za5&uP=;+wk*!cMP#Kgqp_@Co;IPDtiFnPLZbS3!^uXomt8Zef(9= zzo&%;Jbi=x6PoN1+seVe23|ETm7HSTGTZG_UHuGFZdX~>Xc-t#i5NY&ucfPO-`jdi zU83}i5Z0+tFTNJi*0ysw*uRGlnbA-v?>Pw>Iyu=2m>WFH+WRkp|F_2SePMa|DoeZz zD-zQB^39vCOu)C$M-XICCM{)NQCq{C-$nB~SWf}_o%x!O-+${1<265(TC{k3-M;la zM=hmnva08+^PwqsGRJ{=pDA@={LbX&X4MyvB;ZuzyHMWV9T|M5HLu&H%i0JsZ}(G! zxTPK$03!6q{TB?Tula*?cmwYf)s>u>2z;y2p;fE(tx3l4;3wat#VQ_V70D$EIPdTX za-U@A{d`aQ_#bt>#!Ev0#?q+bY?@a(+~uUBmAOC@;8_Wa5~2JXJKCiu%pI61K?AsY zRXj24k(rX=nN(B1^zk>6Y=O?Et4TB^#52EOR{OIVV5S3mWGULd0L|uCwSGWzlRmfO zqrX`OPuha7_MVW*%WpLINN1&$+VV#O_hv+C$mwnS{3(A~remh2pQ?YOyXTw4ao%G8 zT(LS(mFs7hLq+N8mMkumvChnQnidM8gPtqNK!#D(7wV4~J~abw!_- z&<5H%yJ5LkkBxV>4sRB-Nt1{{!s*V4#*N>uz|2%B1y2p7Iu(mc!vf)autlevq&I>q5 z%kJ)}iV~#v7Cf4H$o4sYd#}r$3Ww`aXh^;U=ks*|qvdwL(DX-)V43+N zK`zk&;(>^%%c`*wZ*LDx3$lCS{P^u>mdQ&b{`Rf7n#IZVJEODjHx7J?{Tot;^8 zCwTrV<=JA<*)-21oizqrZdHlb%F+g(%@)AI5^MfVrQx+%Tg96VUD*n!02Dv|Cc8*9 zhb4x|}s_8*kFE<0`D%AT2@Tkgcnz+D1QH*eJ@NR{3nil!bD?P{bzL z{Fzck+T}&qvi>GZN)<$CCTH}lM5nfdS8aeGi?LjJ)Tm#o)>EA%k8i=pO7M6HJaISN54LutGdu5a1_(_>ceW^D z4^l!C(yD?FzZD!7#_Ihja`5Zg`^{rhP}nD+!X(PjLHo1SsHyzAcl9Gd<7lotP)gcb4<@O&e zqcOkO7JaRx$bGBvJX>>bat)1TQuRBubQ5HC39_9h7?Ar&vLra>Thn&kNxo;!&gC1e zdf7#9bC0BqDvc7VNb8CO$FJ)@HUk9M-t;`{zw4EHzRUK4WdS3*w>=g}85*(!L#$u)`;uEJ?6^2g*U)5k>$VIjXMP_v0d%%_Bc;)VsX6e0et+QKZ~4n{DGxV5_=4Z@D} zA=8slcmD` zz%4?&N$ABPgG>jB^Y!S?IF15k%}1(;9?Uw@h7ukh*Exy|I?tiZ61!vBA>;P~{4LAR zY1k#*r~W_X9-{MEqESLCuFi#cQ-nISj9V@Q4E?lj z2>NED%n2@na48-|-+xC=$1aT_Q16VN{L5E_2c2m36Vg_%w1xtI5@0)-xa;UIc6@r7 z_^D~`a&Xf?bz45`{K5dtjWj|QeJu4kS5~m@!=93qj5h7)7x|oG4wx$0XXRX}BMhvw5J}hJ#fVw=3um!Jn#fT=(^^%HVefp@?SS^>8oQ@uim! z@sRkTXb^t+sZ5eiDo`u$QCDOyF1}N;l?I4?+_-0Q(k|AV6&m2QexA3PGEt8ae;^wo z%xre{1D>BGmB$FqBjE;5MsYHLBfaK_Vz91Tf7lt5DTF(VzZOG&da60Kn5t0Jyd_Vu zF=Bh8Tq2%&n7FC-Ne@csiNwe*f5y1fA4wsxKhJ;u1>t~*6%)HCoa1i#9J@82D^vx_ zrgk2}#aAYE&q+qGsjs|nZQ8|O2-Y1)R0jEy2&QB6n$3-S!Pz`OA=L6AC^33IwMWxH zo9IoN(Rq;Qc(AOUDwHG`h<&7J7HE?%D#-u-FrNXe6uU2jwyo2)a^$!THtjd?W!f8S z>iEw+IPKXD#XP1?_KfyIjNpt5!pLbCYFApY<_m=7?;#4N@WR1+dBnh7ASvDHams(d z2;&;Iu!+Qoe+mvjo(omQ7t4v-(4`YuIxi|+-Q?hQ#1{@1Vy5`~(t0l?*Ad2oyN8LO z*ytn408KMy;MT$Kl604of$NXL3Y$!IWMi4x$j}gH*xv$Ih$|JU43wINQiy+P;B(^^ z>VtYq3TL9M4yRR4OOMMha+CCj>?G-7<@^NlIvec{sC~IaRkz zAmO!F-#)mrBbX_Qls(uhW6B zY9jettZk$wZ#m2+4!oTsvhIJTe@=p*P4#T8p~)A@4IN97yoZCpV4#q!*{1js4kLTu&L3Y7 z_vb+ib6We0c&!K@lbKA?CBf_-(n)4p*cn2REE?WWsDpbjGFVrvLI{Xe9z#Wr&`U?c z5yp2cGCCAn5}@T$uu}o2phs~*OEtJ6p6UV)TEF0 zyJ;@15r*LCXi#605)hV%bLeh|gOvt0%+<_uVljQwh4)7bXgpaifThc~RvzW5RD3<@ zj9?Da(@LZIhv`dh2>Lvh!o^AtW|cd2&()Z}zim(awot1E+fKwJ3&L-ORKTH+8-dRx z0xu$xVbjutXkOAW>xV<@>jO>H6W!g4sP{2!o}%M=AIs7^VJ@Pn1D)bkn#!=N>yqx(b+ug--T`47y9=yV9?T7ymT#D94~Gd}wySUCTw z3$=4>!96mQZcr4d$?C4LPY>@;tEDX_mx-=cObnubRWouJ=lncaMdK7NLO;1|e=M>)h| z%yGNtt7`$HZNKKPW>6Dd?yqxe%}t>sTPOOG;|qv)6RhA-oS{$ax{GAxvNLhFaTqO& z(z!1>Jm}W*dMWK)2&6Xf@@>Q2(cqwrM8DUnn#T6dq$zo(uwRP)`@>f)P5#>x4X~kB zf8^aOvt*keGmX#g0P}AxoQ`jltj~-EdFeE~zha0kTxN_s4uuwYT(ouF7g`;Bufx;>u zkXzxxG93}(qZtzPJ#G6PCFq$)d?|~?q{8Io!@Rb#aR28JQdA zbHBaMBL5>fb*}O`{;(Cgwf{(lLeJ3x>ZfUvxqg)E{~t=05$_lxHH1sW(AtlX6?Ar! zSnU-OlMg$}*ni62y@q4q6@~;`rrNg z(#~5~TIJZO=BxBK#eKXp&bNj)eNTvqg@I=N^Lan#mV+b~y+F)P5dl^r1;&H!A?qL+ z3R(w4D6_WFZh3mO*%QX{fvA`=Tg!BL2;HWK)|GbclA+c$J7_G!4dHNS_N3T9MAO0p zZ~l~tD8+rgpo%Vki28xm>L0U3fR%B*$Q30@@NV+S%5;xVWB=W@Vs^o{igG5r3I2vg zf#B1qrBM}r*ITGavEs2o`Cxg?zSV~k#4KXu_zPlOWsPnn4=$7ij}Mewf4-tXHQJ3< zELtN`>o1CDE{&sQ0am?l_F#GhGG8qu0~9peakWQ7%iTKkDbcUTX3WnbTx1 zUCabvFL=->*f*wo*F^Zp`zWUVN zX{m|7{T9Z3>^lCn?FXte3KWZ^flGv|^Pn%qQSCePJw_^(D2@8}M$m?AYVPq_QGTI@ z*r7js^ne?5s1Ksq+G?v+hFQaGHw=#u$=+`p7!{hirsyF7d7Dk1Ez_1uE%j~)=D;-& za>(-Byp&L*B8>jDKd~X>T|A;)nKeoyM=Fcny)*7V)rf86baj`kmOA>6>d^cHne*^2 zksrO@U*8fXx^8F>*Lb04QIMq<@`CWs98S+~`ZG{kvuGtu-`SNq%+?hy;L#h0DVYk$ z95W7p#;x#pQ`}6kQMJ54x`!~L~CvtwOVu6dlUV{hf=W~$qzak%N z_b1+P|29~`CSR9&?<#$WLlP_9+R{x4_t`?b)F|@Ui#<7^fOT4b8GauP>-bQ*)LJWH z4VP25$v-E!+GfZ{$xzQ-mL15%&^7Xe3dDnccI#7YO!OeZ1bamYCa5$V7(7WXF{v;s z^>67G@i^1CBx#9Kl)E7KT}Zf-iAgJ5^EoFH+q7>MZCYJg2$1BIAPor%IMN^}#pTsJ znGA=IY^*;YcXv^z zKadV*1v|0U9F_a!p)Ou#d~R%9?mLQuxizXW7D; z!9A&#YlIP%(bZBD#J_6Pa=OAgZ-6q?0Y0x{ZYNMff~D|4URVpl)1>rC_M37@P0qXB z70WG+17e~S%vyg<$`0pAInYv|mKB5c#FgdIxAJt>T0qHtS!;yHW2~)JO6aQkTSx)^ zlNaD^(phI4#lAOdCiocfFotc6mep^Q04~d4s;4~%q5^w{4h*;fzaBb8(45+zw$d+o2h$o5?V|Kjz z%33*Ahzs&v4%>0}Zsh3EYRmI|ppctYt`ko+rEonumvFoht1&p?Tfy<}UL`+TuOe-3 zNl+%Q%}Pl*Aekqg!`idHeuF&&({~nXZV~SRpg?Ays}3 z+*&@C+#6#5!(U#W`OY+^PA}u?eNce2HosKs(*y3-U1mo^s6+P5;cSc)A8+?4-bP9R zxJ9RyW#E=86en^V*amr1=H7w+(t1bHbv-9kGdf=J==sU%v6MuKab?7O=!-c4g9??s z$_n^FCj%i+s%>xfXi|u$QQ}Yr`Samu5!RU{_$AKcli~ga{z!`*ZpS19>r@4w$||L2A$ bZ|7;)kd#W{YqJNv|LsmwO;5F485;gSC%~Rn literal 0 HcmV?d00001 diff --git a/src/Web/Duber.WebSite/wwwroot/js/site.js b/src/Web/Duber.WebSite/wwwroot/js/site.js new file mode 100644 index 0000000..30a3a94 --- /dev/null +++ b/src/Web/Duber.WebSite/wwwroot/js/site.js @@ -0,0 +1,167 @@ +// sorry for this ugly JS. +var DuberWebSite = (function () { + + var _directionsDisplay, _directionsService; + var _map, _currentPointIndex = 0; + var _currentPositionMarker; + var _route, _directions = new Array(); + var _places = []; + var _from, _to; + var _simulateTripUrl, _iconUrl; + + var _getDirections = () => { + _route = _route.routes[0]; + var legs = _route.legs; + for (i = 0; i < legs.length; i++) { + var steps = legs[i].steps; + for (j = 0; j < steps.length; j++) { + var nextSegment = steps[j].path; + for (k = 0; k < nextSegment.length; k++) { + _directions.push({ Latitude: nextSegment[k].lat(), Longitude: nextSegment[k].lng(), Description: "" }); + } + } + } + }; + + var _calcRoute = () => { + if (_from === _to) { return; } + var fromPoint = _.find(_places, ['Description', _from]); + var toPoint = _.find(_places, ['Description', _to]); + + var start = new google.maps.LatLng(fromPoint.Latitude, fromPoint.Longitude); + var end = new google.maps.LatLng(toPoint.Latitude, toPoint.Longitude); + + var request = { + origin: start, + destination: end, + travelMode: 'DRIVING' + }; + + _directions = []; + _currentPointIndex = 0; + _directionsService.route(request, + function (response, status) { + if (status === 'OK') { + _directionsDisplay.setDirections(response); + _route = _directionsDisplay.getDirections(); + _getDirections(); + } else { + alert("directions request failed, status=" + status); + } + }); + }; + + var _fromChanged = (event) => { + _from = event.currentTarget.value; + _calcRoute(); + }; + + var _toChanged = (event) => { + _to = event.currentTarget.value; + _calcRoute(); + } + + var _simulate = () => { + + $("#errors").addClass("hidden"); + $("#info").addClass("hidden"); + $('#errors-body').empty(); + $("#simulate").prop("disabled", true); + + $.post(_simulateTripUrl, + { + User: $("#User").val(), + Driver: $("#Driver").val(), + From: $("#From").val(), + To: $("#To").val(), + Directions: _directions, + ConnectionId: sessionStorage.getItem('conectionId') + }, + () => { + console.log("Simulation request sent succesfully."); + }) + .fail((response) => { + console.log("Simulation request returned a bad request"); + console.log(response); + + $("#simulate").prop("disabled", false); + $("#errors").removeClass("hidden"); + if (response.responseJSON) { + response.responseJSON.forEach((name) => { + var li = document.createElement('li'); + li.innerHTML += name; + $('#errors-body').append(li); + }); + } else { + var li = document.createElement('li'); + li.innerHTML += response.statusText; + $('#errors-body').append(li); + } + }); + }; + + var _initMap = () => { + _directionsDisplay = new google.maps.DirectionsRenderer(); + _directionsService = new google.maps.DirectionsService(); + _currentPositionMarker = new google.maps.Marker({}); + + var mapOptions = { + zoom: 7 + } + + _map = new google.maps.Map(document.getElementById('map'), mapOptions); + _directionsDisplay.setMap(_map); + _calcRoute(); + }; + + var updateCurrentPosition = (position) => { + var currentPosition = new google.maps.LatLng(position.latitude, position.longitude); + + _currentPositionMarker.setMap(null); + _currentPositionMarker = new google.maps.Marker({ + position: currentPosition, + map: _map, + title: "Current posistion", + animation: google.maps.Animation.BOUNCE, + //icon: 'http://maps.google.com/mapfiles/ms/icons/green-dot.png' + icon: _iconUrl + }); + }; + + var notifyTripStatus = (message) => { + let className = "alert-info"; + if (message === "Finished") { + className = "alert-success"; + $("#simulate").prop("disabled", false); + } + + $("#info").removeClass("alert-success"); + $("#info").removeClass("alert-info"); + $("#info").addClass(className); + $("#info").removeClass("hidden"); + $("#info").html(`The trip has been ${message}!`); + }; + + var init = (places, simulateTripUrl, iconUrl) => { + $(".trip-from").change(_fromChanged); + $(".trip-to").change(_toChanged); + $("#simulate").click(() => { + _simulate(); + return; + }); + + _from = $(".trip-from").val(); + _to = $(".trip-to").val(); + + _places = places; + _simulateTripUrl = simulateTripUrl; + _iconUrl = iconUrl; + _initMap(); + }; + + return { + init: init, + updateTripPosition: updateCurrentPosition, + notifyTripStatus: notifyTripStatus + }; +})(); \ No newline at end of file diff --git a/src/Web/Duber.WebSite/wwwroot/js/site.min.js b/src/Web/Duber.WebSite/wwwroot/js/site.min.js new file mode 100644 index 0000000..e69de29 From c6315abf11991e7b6567fe2bbf46d79ef9f3c152 Mon Sep 17 00:00:00 2001 From: Houston Haynes Date: Tue, 26 Apr 2022 09:16:41 -0400 Subject: [PATCH 2/7] net6.0 and libary updates --- .../PaymentService/PaymentService.csproj | 2 +- .../PaymentServiceExternal - Web Deploy.pubxml | 2 +- .../Linux/DuberMicroservices/packages.config | 2 +- .../Duber.Invoice.API/Duber.Invoice.API.csproj | 6 +++--- .../Duber.Trip.API/Duber.Trip.API.csproj | 4 ++-- .../Duber.Trip.Notifications.csproj | 2 +- .../Duber.Domain.Driver.UnitTest.csproj | 2 +- .../Duber.Domain.Driver.csproj | 4 ++-- .../Duber.Domain.ACL/Duber.Domain.ACL.csproj | 4 ++-- .../Duber.Domain.SharedKernel.csproj | 2 +- .../Duber.Domain.Invoice.UnitTest.csproj | 2 +- .../Duber.Domain.Invoice.csproj | 2 +- .../Duber.Domain.Trip.UnitTest.csproj | 2 +- .../Duber.Domain.Trip/Duber.Domain.Trip.csproj | 2 +- .../Duber.Domain.User.UnitTest.csproj | 2 +- .../Duber.Domain.User/Duber.Domain.User.csproj | 4 ++-- ...frastructure.Resilience.Abstractions.csproj | 4 ++-- ...Duber.Infrastructure.Resilience.Http.csproj | 2 +- .../Duber.Infrastructure.Resilience.Sql.csproj | 2 +- .../Duber.Infrastructure.WebHost.csproj | 2 +- .../Duber.Infrastructure.csproj | 8 ++++---- ....Infrastructure.EventBus.Idempotency.csproj | 4 ++-- ...ber.Infrastructure.EventBus.RabbitMQ.csproj | 16 ++++++++-------- ...r.Infrastructure.EventBus.ServiceBus.csproj | 18 +++++++++--------- .../Duber.Infrastructure.EventBus.csproj | 2 +- src/Web/Duber.WebSite/Duber.WebSite.csproj | 2 +- 26 files changed, 52 insertions(+), 52 deletions(-) diff --git a/ExternalSystem/PaymentService/PaymentService.csproj b/ExternalSystem/PaymentService/PaymentService.csproj index 094cb5a..0a5881b 100644 --- a/ExternalSystem/PaymentService/PaymentService.csproj +++ b/ExternalSystem/PaymentService/PaymentService.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 ..\..\docker-compose.dcproj diff --git a/ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml b/ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml index 0b0fe35..ddbe333 100644 --- a/ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml +++ b/ExternalSystem/PaymentService/Properties/PublishProfiles/PaymentServiceExternal - Web Deploy.pubxml @@ -14,7 +14,7 @@ by editing this MSBuild file. In order to learn more about this please visit htt https://paymentserviceexternal.azurewebsites.net True False - netcoreapp3.1 + net6.0 8504d9b8-c4e8-4172-8da3-cbd9971e3207 false paymentserviceexternal.scm.azurewebsites.net:443 diff --git a/ServiceFabric/Linux/DuberMicroservices/packages.config b/ServiceFabric/Linux/DuberMicroservices/packages.config index 2b70012..01e1c2b 100644 --- a/ServiceFabric/Linux/DuberMicroservices/packages.config +++ b/ServiceFabric/Linux/DuberMicroservices/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj index 2811158..bc77a6b 100644 --- a/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj +++ b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 ..\..\..\docker-compose.dcproj @@ -21,9 +21,9 @@ - + - + diff --git a/src/Application/Duber.Trip.API/Duber.Trip.API.csproj b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj index 19a38ac..4718897 100644 --- a/src/Application/Duber.Trip.API/Duber.Trip.API.csproj +++ b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 ..\..\..\docker-compose.dcproj @@ -21,7 +21,7 @@ - + diff --git a/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj b/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj index 8869f8b..8480beb 100644 --- a/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj +++ b/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Linux ..\..\.. diff --git a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj index e1d6178..2cd19f0 100644 --- a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj +++ b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj b/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj index 4c339e2..9809ec7 100644 --- a/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj +++ b/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 @@ -21,7 +21,7 @@ - + diff --git a/src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj b/src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj index 86e4070..f64bf9c 100644 --- a/src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj +++ b/src/Domain/Duber.Domain.ACL/Duber.Domain.ACL.csproj @@ -1,11 +1,11 @@  - netcoreapp3.1 + net6.0 - + diff --git a/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj b/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj index 95a2eaa..fa45f3b 100644 --- a/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj +++ b/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj index e1d6178..2cd19f0 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj index 1a7db45..761dfc1 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj index e1d6178..2cd19f0 100644 --- a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj +++ b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj index 8f0dee4..7f4d2c4 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj +++ b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj index e1d6178..2cd19f0 100644 --- a/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj +++ b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj b/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj index 86dc407..6289a80 100644 --- a/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj +++ b/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 @@ -12,7 +12,7 @@ - + diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj index 418b4b6..c3eb489 100644 --- a/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Abstractions/Duber.Infrastructure.Resilience.Abstractions.csproj @@ -1,11 +1,11 @@  - netcoreapp3.1 + net6.0 - + diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj index 9ffb2b0..323cd69 100644 --- a/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj index a7d22ce..a6cb1d4 100644 --- a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj b/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj index 47076f3..c9c0c9e 100644 --- a/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj +++ b/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj b/src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj index bc16783..fb0e9b7 100644 --- a/src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj +++ b/src/Infrastructure/Duber.Infrastructure/Duber.Infrastructure.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 @@ -10,10 +10,10 @@ - + - - + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj index 6942467..7492f36 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1 + net6.0 diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj index 7d3494e..cdfc779 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/Duber.Infrastructure.EventBus.RabbitMQ.csproj @@ -1,17 +1,17 @@  - netcoreapp3.1 + net6.0 - - - - - - - + + + + + + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj index f47b0f8..5007ab8 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/Duber.Infrastructure.EventBus.ServiceBus.csproj @@ -1,18 +1,18 @@  - netcoreapp3.1 + net6.0 - - - - - - - - + + + + + + + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj index 7c4ae4c..fff7812 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Duber.Infrastructure.EventBus.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Web/Duber.WebSite/Duber.WebSite.csproj b/src/Web/Duber.WebSite/Duber.WebSite.csproj index a22caf0..04b8f12 100644 --- a/src/Web/Duber.WebSite/Duber.WebSite.csproj +++ b/src/Web/Duber.WebSite/Duber.WebSite.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 ..\..\..\docker-compose.dcproj From a12aed6d53ac94c9bff3aa920388cdedd32dc714 Mon Sep 17 00:00:00 2001 From: Houston Haynes Date: Tue, 26 Apr 2022 09:41:28 -0400 Subject: [PATCH 3/7] Updated packages and MSBuild version --- .../DuberMicroservices.sfproj | 12 +++++----- .../Duber.Invoice.API.csproj | 18 +++++++-------- .../Duber.Trip.API/Duber.Trip.API.csproj | 16 ++++++------- .../Duber.Trip.Notifications.csproj | 8 +++---- .../Duber.Domain.Driver.UnitTest.csproj | 6 ++--- .../Duber.Domain.Driver.csproj | 9 +++++--- .../Duber.Domain.Invoice.UnitTest.csproj | 6 ++--- .../Duber.Domain.Invoice.csproj | 11 +++++---- .../Duber.Domain.Trip.UnitTest.csproj | 6 ++--- .../Duber.Domain.Trip.csproj | 2 +- .../Duber.Domain.User.UnitTest.csproj | 6 ++--- .../Duber.Domain.User.csproj | 9 +++++--- ...uber.Infrastructure.Resilience.Http.csproj | 2 +- ...Duber.Infrastructure.Resilience.Sql.csproj | 4 ++-- .../Duber.Infrastructure.WebHost.csproj | 2 +- ...Infrastructure.EventBus.Idempotency.csproj | 2 +- src/Web/Duber.WebSite/Duber.WebSite.csproj | 23 +++++++++++-------- 17 files changed, 77 insertions(+), 65 deletions(-) diff --git a/ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj b/ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj index 55ecc9d..d6db967 100644 --- a/ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj +++ b/ServiceFabric/Linux/DuberMicroservices/DuberMicroservices.sfproj @@ -1,11 +1,11 @@  - + d8a868b6-7d03-4368-a52e-64f1e1b357db 2.1 1.5 - 1.6.6 + 1.7.6 v4.6.1 @@ -40,9 +40,9 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Service Fabric Tools\Microsoft.VisualStudio.Azure.Fabric.ApplicationProject.targets - - - - + + + + \ No newline at end of file diff --git a/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj index bc77a6b..1048a05 100644 --- a/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj +++ b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj @@ -20,16 +20,16 @@ - + - - - - - - - - + + + + + + + + diff --git a/src/Application/Duber.Trip.API/Duber.Trip.API.csproj b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj index 4718897..d60f0f0 100644 --- a/src/Application/Duber.Trip.API/Duber.Trip.API.csproj +++ b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj @@ -18,15 +18,15 @@ - - - + + + - - - - - + + + + + diff --git a/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj b/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj index 8480beb..0bdce0c 100644 --- a/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj +++ b/src/Application/Duber.Trip.Notifications/Duber.Trip.Notifications.csproj @@ -7,12 +7,12 @@ - + - + - - + + diff --git a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj index 2cd19f0..27825a0 100644 --- a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj +++ b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj b/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj index 9809ec7..6b8b5d4 100644 --- a/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj +++ b/src/Domain/Driver/Duber.Domain.Driver/Duber.Domain.Driver.csproj @@ -18,9 +18,12 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj index 2cd19f0..27825a0 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj index 761dfc1..8cc55b3 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj @@ -9,10 +9,13 @@ - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj index 2cd19f0..27825a0 100644 --- a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj +++ b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj index 7f4d2c4..20f248d 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj +++ b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj index 2cd19f0..27825a0 100644 --- a/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj +++ b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj b/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj index 6289a80..96e1d44 100644 --- a/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj +++ b/src/Domain/User/Duber.Domain.User/Duber.Domain.User.csproj @@ -9,9 +9,12 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj index 323cd69..36e5c32 100644 --- a/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Http/Duber.Infrastructure.Resilience.Http.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj index a6cb1d4..c7e00dc 100644 --- a/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj +++ b/src/Infrastructure/Duber.Infrastructure.Resilience.Sql/Duber.Infrastructure.Resilience.Sql.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj b/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj index c9c0c9e..3f13e25 100644 --- a/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj +++ b/src/Infrastructure/Duber.Infrastructure.WebHost/Duber.Infrastructure.WebHost.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj index 7492f36..32aa322 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.Idempotency/Duber.Infrastructure.EventBus.Idempotency.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Web/Duber.WebSite/Duber.WebSite.csproj b/src/Web/Duber.WebSite/Duber.WebSite.csproj index 04b8f12..e4e5d0f 100644 --- a/src/Web/Duber.WebSite/Duber.WebSite.csproj +++ b/src/Web/Duber.WebSite/Duber.WebSite.csproj @@ -13,17 +13,20 @@ - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - - + + + + From c60f84ac7c92836e40d57644b7265d8c9bab7603 Mon Sep 17 00:00:00 2001 From: Houston Haynes Date: Tue, 26 Apr 2022 11:37:50 -0400 Subject: [PATCH 4/7] updated refactoring --- main.tf | 0 .../Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj | 1 + .../Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 main.tf diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..e69de29 diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj index 8cc55b3..26b35a0 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj @@ -16,6 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs index 4c6be2c..c977922 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs @@ -181,7 +181,7 @@ private void StartBasicConsume() private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs) { var eventName = eventArgs.RoutingKey; - var message = Encoding.UTF8.GetString(eventArgs.Body); + string message = Encoding.UTF8.GetString(eventArgs.Body.ToArray()); try { From d89b380e687fa74a3b89c67a895db8957a442ba8 Mon Sep 17 00:00:00 2001 From: Houston Haynes Date: Tue, 26 Apr 2022 12:09:36 -0400 Subject: [PATCH 5/7] update packages, dispose refactor --- .../Duber.Domain.Driver.UnitTest.csproj | 4 +- .../Duber.Domain.Invoice.UnitTest.csproj | 4 +- .../Duber.Domain.Invoice.csproj | 2 +- .../Duber.Domain.Trip.UnitTest.csproj | 4 +- .../Duber.Domain.User.UnitTest.csproj | 4 +- .../EventBusRabbitMQ.cs | 123 +++++++++--------- .../Abstractions/IEventBus.cs | 1 + 7 files changed, 72 insertions(+), 70 deletions(-) diff --git a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj index 27825a0..23dcce1 100644 --- a/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj +++ b/src/Domain/Driver/Duber.Domain.Driver.UnitTest/Duber.Domain.Driver.UnitTest.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj index 27825a0..23dcce1 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj index 26b35a0..eede2c6 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj index 27825a0..23dcce1 100644 --- a/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj +++ b/src/Domain/Trip/Duber.Domain.Trip.UnitTest/Duber.Domain.Trip.UnitTest.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj index 27825a0..23dcce1 100644 --- a/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj +++ b/src/Domain/User/Duber.Domain.User.UnitTest/Duber.Domain.User.UnitTest.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs index c977922..5639192 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.RabbitMQ/EventBusRabbitMQ.cs @@ -48,17 +48,16 @@ private void SubsManager_OnEventRemoved(object sender, string eventName) _persistentConnection.TryConnect(); } - using (var channel = _persistentConnection.CreateModel()) - { - channel.QueueUnbind(queue: _queueName, - exchange: BROKER_NAME, - routingKey: eventName); + IModel model = _persistentConnection.CreateModel(); + using IModel channel = model; + channel.QueueUnbind(queue: _queueName, + exchange: BROKER_NAME, + routingKey: eventName); - if (_subsManager.IsEmpty) - { - _queueName = string.Empty; - _consumerChannel.Close(); - } + if (_subsManager.IsEmpty) + { + _queueName = string.Empty; + _consumerChannel.Close(); } } @@ -73,32 +72,30 @@ public void Publish(IntegrationEvent @event) .Or() .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => { - _logger.LogWarning(ex.ToString()); + _logger.LogWarning(message: ex.ToString()); }); - using (var channel = _persistentConnection.CreateModel()) - { - var eventName = @event.GetType().Name; - channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); + using var channel = _persistentConnection.CreateModel(); + var eventName = @event.GetType().Name; + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); - var message = JsonConvert.SerializeObject(@event); - var body = Encoding.UTF8.GetBytes(message); + var message = JsonConvert.SerializeObject(@event); + var body = Encoding.UTF8.GetBytes(message); - // to avoid lossing messages - var properties = channel.CreateBasicProperties(); - properties.DeliveryMode = 2; // persistent - properties.Expiration = "60000"; + // to avoid lossing messages + var properties = channel.CreateBasicProperties(); + properties.DeliveryMode = 2; // persistent + properties.Expiration = "60000"; - policy.Execute(() => - { - channel.BasicPublish( - exchange: BROKER_NAME, - routingKey: eventName, - mandatory: true, - basicProperties: properties, - body: body); - }); - } + policy.Execute(() => + { + channel.BasicPublish( + exchange: BROKER_NAME, + routingKey: eventName, + mandatory: true, + basicProperties: properties, + body: body); + }); } public void SubscribeDynamic(string eventName) @@ -129,12 +126,10 @@ private void DoInternalSubscription(string eventName) _persistentConnection.TryConnect(); } - using (var channel = _persistentConnection.CreateModel()) - { - channel.QueueBind(queue: _queueName, - exchange: BROKER_NAME, - routingKey: eventName); - } + using var channel = _persistentConnection.CreateModel(); + channel.QueueBind(queue: _queueName, + exchange: BROKER_NAME, + routingKey: eventName); } } @@ -185,10 +180,19 @@ private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventA try { + void onRetry(Exception ex, TimeSpan time) + { + if (ex is null) + { + throw new ArgumentNullException(nameof(ex)); + } + + _logger.LogWarning(message: ex.ToString()); + } var policy = Policy.Handle() .Or() .WaitAndRetryAsync(_retryCount, retryAttempt => TimeSpan.FromSeconds(1), - (ex, time) => { _logger.LogWarning(ex.ToString()); }); + onRetry); await policy.ExecuteAsync(async () => await ProcessEvent(eventName, message)); @@ -240,31 +244,28 @@ private async Task ProcessEvent(string eventName, string message) if (_subsManager.HasSubscriptionsForEvent(eventName)) { - using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) + using var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME); + var subscriptions = _subsManager.GetHandlersForEvent(eventName); + foreach (var subscription in subscriptions) { - var subscriptions = _subsManager.GetHandlersForEvent(eventName); - foreach (var subscription in subscriptions) + if (subscription.IsDynamic) + { + if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue; + dynamic eventData = JObject.Parse(message); + + await Task.Yield(); + await handler.Handle(eventData); + } + else { - if (subscription.IsDynamic) - { - var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; - if (handler == null) continue; - dynamic eventData = JObject.Parse(message); - - await Task.Yield(); - await handler.Handle(eventData); - } - else - { - var handler = scope.ResolveOptional(subscription.HandlerType); - if (handler == null) continue; - var eventType = _subsManager.GetEventTypeByName(eventName); - var integrationEvent = JsonConvert.DeserializeObject(message, eventType); - var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); - - await Task.Yield(); - await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); - } + var handler = scope.ResolveOptional(subscription.HandlerType); + if (handler == null) continue; + var eventType = _subsManager.GetEventTypeByName(eventName); + var integrationEvent = JsonConvert.DeserializeObject(message, eventType); + var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); + + await Task.Yield(); + await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); } } } diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs index f51b7e4..a8bb403 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus/Abstractions/IEventBus.cs @@ -18,5 +18,6 @@ void Unsubscribe() where T : IntegrationEvent; void Publish(IntegrationEvent @event); + void Dispose(); } } From f2160cdc75eb8aad311b8da5d9f124dc4d192a3c Mon Sep 17 00:00:00 2001 From: Houston Haynes Date: Wed, 27 Apr 2022 15:03:39 -0400 Subject: [PATCH 6/7] update to APIs, mainly Kledex to OpenCqrs --- .../InvoiceCreatedDomainEventHandler.cs | 5 +-- .../InvoicePaidDomainEventHandler.cs | 5 +-- .../Duber.Invoice.API.csproj | 1 + .../AutofacModules/MediatorModule.cs | 32 +++++++++++-------- src/Application/Duber.Invoice.API/Startup.cs | 4 +-- .../TripCreatedDomainEventHandlerAsync.cs | 2 +- .../TripUpdatedDomainEventHandlerAsync.cs | 2 +- .../Controllers/EventStoreController.cs | 2 +- .../Controllers/TripController.cs | 4 +-- .../Duber.Trip.API/Duber.Trip.API.csproj | 2 ++ .../Extensions/ServiceCollectionExtensions.cs | 22 ++++++------- .../Repository/EventStoreRepository.cs | 6 ++-- .../Repository/IEventStoreRepository.cs | 2 +- .../Repository/IdempotencyStoreProvider.cs | 2 +- .../Duber.Domain.SharedKernel.csproj | 4 +++ .../Duber.Domain.Invoice.UnitTest.csproj | 1 + .../Duber.Domain.Invoice.csproj | 1 + .../Commands/CreateTripCommand.cs | 2 +- .../Handlers/CreateTripCommandHandlerAsync.cs | 2 +- .../Handlers/UpdateTripCommandHandlerAsync.cs | 4 +-- .../Commands/UpdateTripCommand.cs | 2 +- .../Duber.Domain.Trip.csproj | 2 +- .../Events/TripCreatedDomainEvent.cs | 2 +- .../Events/TripUpdatedDomainEvent.cs | 2 +- .../Trip/Duber.Domain.Trip/Model/Trip.cs | 2 +- .../EventBusServiceBus.cs | 9 ++---- 26 files changed, 68 insertions(+), 56 deletions(-) diff --git a/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs index a3f3aa3..9392272 100644 --- a/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs +++ b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoiceCreatedDomainEventHandler.cs @@ -5,10 +5,11 @@ using Duber.Infrastructure.EventBus.Abstractions; using Duber.Invoice.API.Application.IntegrationEvents.Events; using MediatR; +using System.Threading; namespace Duber.Invoice.API.Application.DomainEventHandlers { - public class InvoiceCreatedDomainEventHandler : IAsyncNotificationHandler + public class InvoiceCreatedDomainEventHandler : INotificationHandler { private readonly IEventBus _eventBus; private readonly IMapper _mapper; @@ -19,7 +20,7 @@ public InvoiceCreatedDomainEventHandler(IEventBus eventBus, IMapper mapper) _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } - public async Task Handle(InvoiceCreatedDomainEvent notification) + public async Task Handle(InvoiceCreatedDomainEvent notification, CancellationToken cancellationToken) { // to update the query side (materialized view) var integrationEvent = _mapper.Map(notification); diff --git a/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs index d7f3699..1663f3a 100644 --- a/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs +++ b/src/Application/Duber.Invoice.API/Application/DomainEventHandlers/InvoicePaidDomainEventHandler.cs @@ -5,10 +5,11 @@ using Duber.Infrastructure.EventBus.Abstractions; using Duber.Invoice.API.Application.IntegrationEvents.Events; using MediatR; +using System.Threading; namespace Duber.Invoice.API.Application.DomainEventHandlers { - public class InvoicePaidDomainEventHandler : IAsyncNotificationHandler + public class InvoicePaidDomainEventHandler : INotificationHandler { private readonly IEventBus _eventBus; private readonly IMapper _mapper; @@ -19,7 +20,7 @@ public InvoicePaidDomainEventHandler(IEventBus eventBus, IMapper mapper) _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } - public async Task Handle(InvoicePaidDomainEvent notification) + public async Task Handle(InvoicePaidDomainEvent notification, CancellationToken cancellationToken) { // to update the query side (materialized view) var integrationEvent = _mapper.Map(notification); diff --git a/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj index 1048a05..aa6dc25 100644 --- a/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj +++ b/src/Application/Duber.Invoice.API/Duber.Invoice.API.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs b/src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs index 1a9b247..194a184 100644 --- a/src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs +++ b/src/Application/Duber.Invoice.API/Infrastructure/AutofacModules/MediatorModule.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; -using System.Reflection; -using Autofac; +using Autofac; using Duber.Invoice.API.Application.DomainEventHandlers; using MediatR; +using System.Collections.Generic; +using System.Reflection; namespace Duber.Invoice.API.Infrastructure.AutofacModules { @@ -10,29 +10,33 @@ public class MediatorModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { - builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly) - .AsImplementedInterfaces(); - // Register all the event classes (they implement IAsyncNotificationHandler) in assembly holding the Commands + Autofac.Builder.IRegistrationBuilder registrationBuilder2 = builder.RegisterType().As(); + + // Register all the event classes (they implement INotificationHandler) in assembly holding the Commands builder.RegisterAssemblyTypes(typeof(InvoiceCreatedDomainEventHandler).GetTypeInfo().Assembly) - .AsClosedTypesOf(typeof(IAsyncNotificationHandler<>)); + .AsClosedTypesOf(typeof(INotificationHandler<>)).AsImplementedInterfaces().InstancePerRequest(); - builder.Register(context => + Autofac.Builder.IRegistrationBuilder registrationBuilder1 = builder.Register(context => { - var componentContext = context.Resolve(); - return t => { object o; return componentContext.TryResolve(t, out o) ? o : null; }; + IComponentContext componentContext = context.Resolve(); + return t => + { + ServiceFactory p = t => { return componentContext.TryResolve(t, out object o) ? o : null; }; + return t; + }; }); - builder.Register(context => + Autofac.Builder.IRegistrationBuilder registrationBuilder = builder.Register(context => { - var componentContext = context.Resolve(); - + IComponentContext componentContext = context.Resolve(); return t => { - var resolved = (IEnumerable)componentContext.Resolve(typeof(IEnumerable<>).MakeGenericType(t)); + IEnumerable resolved = (IEnumerable)componentContext.Resolve(typeof(IEnumerable<>).MakeGenericType(t)); return resolved; }; }); } + } } diff --git a/src/Application/Duber.Invoice.API/Startup.cs b/src/Application/Duber.Invoice.API/Startup.cs index af0fd94..725b7e2 100644 --- a/src/Application/Duber.Invoice.API/Startup.cs +++ b/src/Application/Duber.Invoice.API/Startup.cs @@ -36,7 +36,7 @@ public Startup(IConfiguration configuration) public IServiceProvider ConfigureServices(IServiceCollection services) { services - .AddAutoMapper() + .AddAutoMapper(typeof(Startup)) .AddApplicationInsightsTelemetry(Configuration) .AddControllers(options => { @@ -72,7 +72,7 @@ public IServiceProvider ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { diff --git a/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs index 6f5ec9f..5de248c 100644 --- a/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs +++ b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripCreatedDomainEventHandlerAsync.cs @@ -4,7 +4,7 @@ using Duber.Domain.Trip.Events; using Duber.Infrastructure.EventBus.Abstractions; using Duber.Trip.API.Application.IntegrationEvents; -using Kledex.Events; +using OpenCqrs.Events; using Microsoft.Extensions.Logging; namespace Duber.Trip.API.Application.DomainEventHandlers diff --git a/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs index d78792e..f268fad 100644 --- a/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs +++ b/src/Application/Duber.Trip.API/Application/DomainEventHandlers/TripUpdatedDomainEventHandlerAsync.cs @@ -6,7 +6,7 @@ using Duber.Infrastructure.EventBus.Idempotency; using Duber.Trip.API.Application.IntegrationEvents; using Duber.Trip.API.Application.Model; -using Kledex.Events; +using OpenCqrs.Events; using Microsoft.Extensions.Logging; using TripStatus = Duber.Domain.SharedKernel.Model.TripStatus; diff --git a/src/Application/Duber.Trip.API/Controllers/EventStoreController.cs b/src/Application/Duber.Trip.API/Controllers/EventStoreController.cs index 8e1b1d0..3d40822 100644 --- a/src/Application/Duber.Trip.API/Controllers/EventStoreController.cs +++ b/src/Application/Duber.Trip.API/Controllers/EventStoreController.cs @@ -3,7 +3,7 @@ using System.Net; using System.Threading.Tasks; using Duber.Trip.API.Infrastructure.Repository; -using Kledex.Store.Cosmos.Mongo.Documents; +using OpenCqrs.Store.Cosmos.Mongo.Documents; using Microsoft.AspNetCore.Mvc; namespace Duber.Trip.API.Controllers diff --git a/src/Application/Duber.Trip.API/Controllers/TripController.cs b/src/Application/Duber.Trip.API/Controllers/TripController.cs index 9709a97..a1c09db 100644 --- a/src/Application/Duber.Trip.API/Controllers/TripController.cs +++ b/src/Application/Duber.Trip.API/Controllers/TripController.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; using AutoMapper; using Duber.Domain.Trip.Commands; -using Kledex; -using Kledex.Domain; +using OpenCqrs; +using OpenCqrs.Domain; using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.AspNetCore.Mvc; using Action = Duber.Domain.Trip.Commands.Action; diff --git a/src/Application/Duber.Trip.API/Duber.Trip.API.csproj b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj index d60f0f0..ee3c47b 100644 --- a/src/Application/Duber.Trip.API/Duber.Trip.API.csproj +++ b/src/Application/Duber.Trip.API/Duber.Trip.API.csproj @@ -22,6 +22,8 @@ + + diff --git a/src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs b/src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs index d024042..0e0ce59 100644 --- a/src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs +++ b/src/Application/Duber.Trip.API/Extensions/ServiceCollectionExtensions.cs @@ -12,16 +12,16 @@ using Duber.Infrastructure.EventBus.Idempotency; using Duber.Infrastructure.EventBus.RabbitMQ.IoC; using Duber.Infrastructure.EventBus.ServiceBus.IoC; -using Kledex; -using Kledex.Commands; -using Kledex.Configuration; -using Kledex.Domain; -using Kledex.Events; -using Kledex.Extensions; -using Kledex.Queries; -using Kledex.Store.Cosmos.Mongo.Configuration; +using OpenCqrs; +using OpenCqrs.Commands; +using OpenCqrs.Configuration; +using OpenCqrs.Domain; +using OpenCqrs.Events; +using OpenCqrs.Extensions; +using OpenCqrs.Queries; +using OpenCqrs.Store.Cosmos.Mongo.Configuration; using Microsoft.OpenApi.Models; -using Kledex.Store.Cosmos.Mongo.Extensions; +using OpenCqrs.Store.Cosmos.Mongo.Extensions; using MongoDB.Bson.Serialization; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -72,7 +72,7 @@ public static IServiceCollection AddCustomSwagger(this IServiceCollection servic /// /// /// - public static IKledexServiceBuilder AddCustomKledex(this IServiceCollection services, Action setupAction, params Type[] types) + public static IOpenCqrsServiceBuilder AddCustomKledex(this IServiceCollection services, Action setupAction, params Type[] types) { var typeList = types.ToList(); typeList.Add(typeof(IDispatcher)); @@ -85,7 +85,7 @@ public static IKledexServiceBuilder AddCustomKledex(this IServiceCollection serv services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); services.AddCustomAutoMapper(typeList); services.Configure(setupAction); - return new KledexServiceBuilder(services); + return new OpenCqrsServiceBuilder(services); } private static IServiceCollection AddCustomAutoMapper(this IServiceCollection services, List types) diff --git a/src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs b/src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs index 8cd2233..db955df 100644 --- a/src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs +++ b/src/Application/Duber.Trip.API/Infrastructure/Repository/EventStoreRepository.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Kledex.Store.Cosmos.Mongo; -using Kledex.Store.Cosmos.Mongo.Configuration; -using Kledex.Store.Cosmos.Mongo.Documents; +using OpenCqrs.Store.Cosmos.Mongo; +using OpenCqrs.Store.Cosmos.Mongo.Configuration; +using OpenCqrs.Store.Cosmos.Mongo.Documents; using Microsoft.Extensions.Options; using MongoDB.Driver; // ReSharper disable FunctionRecursiveOnAllPaths diff --git a/src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs b/src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs index ae6c1a6..987f175 100644 --- a/src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs +++ b/src/Application/Duber.Trip.API/Infrastructure/Repository/IEventStoreRepository.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Kledex.Store.Cosmos.Mongo.Documents; +using OpenCqrs.Store.Cosmos.Mongo.Documents; namespace Duber.Trip.API.Infrastructure.Repository { diff --git a/src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs b/src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs index 92b6db1..7c8275c 100644 --- a/src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs +++ b/src/Application/Duber.Trip.API/Infrastructure/Repository/IdempotencyStoreProvider.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Duber.Infrastructure.EventBus.Idempotency; -using Kledex.Store.Cosmos.Mongo.Configuration; +using OpenCqrs.Store.Cosmos.Mongo.Configuration; using Microsoft.Extensions.Options; using MongoDB.Driver; diff --git a/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj b/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj index fa45f3b..0c2b5a8 100644 --- a/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj +++ b/src/Domain/Duber.Domain.SharedKernel/Duber.Domain.SharedKernel.csproj @@ -4,6 +4,10 @@ net6.0 + + + + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj index 23dcce1..1da45f7 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice.UnitTest/Duber.Domain.Invoice.UnitTest.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj index eede2c6..9127871 100644 --- a/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj +++ b/src/Domain/Invoice/Duber.Domain.Invoice/Duber.Domain.Invoice.csproj @@ -16,6 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs index 1120adc..50dd919 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/CreateTripCommand.cs @@ -1,6 +1,6 @@ using Duber.Domain.SharedKernel.Model; using Duber.Domain.Trip.Model; -using Kledex.Domain; +using OpenCqrs.Domain; namespace Duber.Domain.Trip.Commands { diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs index ccd333e..ebdffd2 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/CreateTripCommandHandlerAsync.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Kledex.Commands; +using OpenCqrs.Commands; namespace Duber.Domain.Trip.Commands.Handlers { diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs index ec690f2..92378c2 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/Handlers/UpdateTripCommandHandlerAsync.cs @@ -1,8 +1,8 @@ using System; using System.Threading.Tasks; using Duber.Domain.Trip.Exceptions; -using Kledex.Commands; -using Kledex.Domain; +using OpenCqrs.Commands; +using OpenCqrs.Domain; namespace Duber.Domain.Trip.Commands.Handlers { diff --git a/src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs b/src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs index c0d8040..6492705 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs +++ b/src/Domain/Trip/Duber.Domain.Trip/Commands/UpdateTripCommand.cs @@ -1,5 +1,5 @@ using Duber.Domain.Trip.Model; -using Kledex.Domain; +using OpenCqrs.Domain; namespace Duber.Domain.Trip.Commands { diff --git a/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj index 20f248d..9d2c3b9 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj +++ b/src/Domain/Trip/Duber.Domain.Trip/Duber.Domain.Trip.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs b/src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs index 908ddbf..4238ccf 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs +++ b/src/Domain/Trip/Duber.Domain.Trip/Events/TripCreatedDomainEvent.cs @@ -1,6 +1,6 @@ using Duber.Domain.SharedKernel.Model; using Duber.Domain.Trip.Model; -using Kledex.Domain; +using OpenCqrs.Domain; namespace Duber.Domain.Trip.Events { diff --git a/src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs b/src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs index 4eaa11b..e2f4f21 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs +++ b/src/Domain/Trip/Duber.Domain.Trip/Events/TripUpdatedDomainEvent.cs @@ -1,7 +1,7 @@ using System; using Duber.Domain.SharedKernel.Model; using Duber.Domain.Trip.Model; -using Kledex.Domain; +using OpenCqrs.Domain; namespace Duber.Domain.Trip.Events { diff --git a/src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs b/src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs index 69a4750..2f79a93 100644 --- a/src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs +++ b/src/Domain/Trip/Duber.Domain.Trip/Model/Trip.cs @@ -3,7 +3,7 @@ using Duber.Domain.Trip.Events; using Duber.Domain.Trip.Exceptions; using GeoCoordinatePortable; -using Kledex.Domain; +using OpenCqrs.Domain; using Action = Duber.Domain.Trip.Events.Action; // ReSharper disable ConvertToAutoProperty // ReSharper disable UnusedAutoPropertyAccessor.Local diff --git a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs index 5feb638..a5d5ab3 100644 --- a/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs +++ b/src/Infrastructure/EventBus/Duber.Infrastructure.EventBus.ServiceBus/EventBusServiceBus.cs @@ -5,11 +5,11 @@ using Autofac; using Duber.Infrastructure.EventBus.Abstractions; using Duber.Infrastructure.EventBus.Events; -using Microsoft.Azure.ServiceBus; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Polly; +using Microsoft.Azure.ServiceBus; namespace Duber.Infrastructure.EventBus.ServiceBus { @@ -146,9 +146,7 @@ public void Dispose() _subsManager.Clear(); } - private void RegisterSubscriptionClientMessageHandler() - { - _subscriptionClient.RegisterMessageHandler( + private void RegisterSubscriptionClientMessageHandler() => _subscriptionClient.RegisterMessageHandler( async (message, token) => { var eventName = $"{message.Label}{INTEGRATION_EVENT_SUFFIX}"; @@ -164,8 +162,7 @@ private void RegisterSubscriptionClientMessageHandler() // Complete the message so that it is not received again. await _subscriptionClient.CompleteAsync(message.SystemProperties.LockToken); }, - new MessageHandlerOptions(ExceptionReceivedHandler) { MaxConcurrentCalls = 10, AutoComplete = false }); - } + messageHandlerOptions: new MessageHandlerOptions(ExceptionReceivedHandler) { MaxConcurrentCalls = 10, AutoComplete = false }); private static Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs) { From d80a4f2b84644392b187b3aab1f10384a99e8bc3 Mon Sep 17 00:00:00 2001 From: Houston Haynes Date: Fri, 6 May 2022 09:54:56 -0400 Subject: [PATCH 7/7] tweak appsettings, values def in pay controller --- .../PaymentService/Controllers/PaymentController.cs | 9 +++++---- ExternalSystem/PaymentService/appsettings.json | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ExternalSystem/PaymentService/Controllers/PaymentController.cs b/ExternalSystem/PaymentService/Controllers/PaymentController.cs index 4482569..0385f75 100644 --- a/ExternalSystem/PaymentService/Controllers/PaymentController.cs +++ b/ExternalSystem/PaymentService/Controllers/PaymentController.cs @@ -8,8 +8,9 @@ namespace PaymentService.Controllers [Route("api/[controller]")] public class PaymentController : Controller { - private readonly List _paymentStatuses = new List { "Accepted", "Rejected" }; - private readonly List _cardTypes = new List { "Visa", "Master Card", "American Express" }; + public List CardTypes { get; } = new() { "Visa", "Master Card", "American Express" }; + + public List PaymentStatuses { get; } = new() { "Accepted", "Rejected" }; [HttpPost] [Route("performpayment")] @@ -22,8 +23,8 @@ public IEnumerable PerformPayment(int userId, string reference) // the payment system returns the response in a list of string like this: payment status, card type, card number, user and reference return new[] { - _paymentStatuses[new Random().Next(0, 2)], - _cardTypes[new Random().Next(0, 3)], + PaymentStatuses[new Random().Next(0, 2)], + CardTypes[new Random().Next(0, 3)], Guid.NewGuid().ToString(), userId.ToString(), reference diff --git a/ExternalSystem/PaymentService/appsettings.json b/ExternalSystem/PaymentService/appsettings.json index 26bb0ac..5ed162f 100644 --- a/ExternalSystem/PaymentService/appsettings.json +++ b/ExternalSystem/PaymentService/appsettings.json @@ -1,15 +1,15 @@ { "Logging": { - "IncludeScopes": false, - "Debug": { + "Console": { "LogLevel": { "Default": "Warning" } }, - "Console": { + "Debug": { "LogLevel": { "Default": "Warning" } - } + }, + "IncludeScopes": false } }