From 20a83631030df4ad6f661e9e53d903c9ad1c8a9a Mon Sep 17 00:00:00 2001 From: Meysam Hadeli <35596795+meysamhadeli@users.noreply.github.com> Date: Tue, 8 Apr 2025 01:41:18 +0330 Subject: [PATCH] feat: add monolith source --- .../{src => assets}/.gitkeep | 0 1-monolith-architecture-style/booking.rest | 230 +++++++ .../deployments/.gitkeep | 0 .../src/Api/src/Api.csproj | 11 + .../Api/src/Extensions/MediatRExtensions.cs | 23 + .../SharedInfrastructureExtensions.cs | 171 +++++ .../src/Api/src/Program.cs | 24 + .../Api/src/Properties/launchSettings.json | 31 + .../src/Api/src/appsettings.Development.json | 8 + .../src/Api/src/appsettings.docker.json | 2 + .../src/Api/src/appsettings.json | 59 ++ .../src/Api/src/appsettings.test.json | 2 + ...-key-296345BF73910ADD1DAC302B848E47E7.json | 1 + ...-key-73D9025BDA857BF270C99C6594EE4246.json | 1 + .../src/Booking/BookingEventMapper.cs | 26 + .../src/Booking/BookingProjection.cs | 53 ++ .../src/Booking/BookingRoot.cs | 6 + .../Bookings/Dtos/CreateReservation.cs | 4 + .../BookingAlreadyExistException.cs | 10 + .../Exceptions/FlightNotFoundException.cs | 10 + .../Exceptions/InvalidAircraftIdException.cs | 11 + .../InvalidArriveAirportIdException.cs | 11 + .../InvalidDepartureAirportIdException.cs | 11 + .../Exceptions/InvalidFlightDateException.cs | 11 + .../InvalidFlightNumberException.cs | 11 + .../InvalidPassengerNameException.cs | 11 + .../Exceptions/InvalidPriceException.cs | 12 + .../Exceptions/SeatNumberException.cs | 12 + .../Bookings/Features/BookingMappings.cs | 22 + .../Features/CreatingBook/V1/CreateBooking.cs | 136 ++++ .../src/Booking/Bookings/Models/Booking.cs | 49 ++ .../Bookings/Models/BookingReadModel.cs | 12 + .../Bookings/ValueObjects/PassengerInfo.cs | 23 + .../src/Booking/Bookings/ValueObjects/Trip.cs | 69 +++ .../src/Booking/Data/BookingReadDbContext.cs | 18 + .../src/BookingMonolith.csproj | 28 + .../src/BookingMonolithRoot.cs | 6 + .../src/Flight/Aircrafts/Dtos/AircraftDto.cs | 3 + .../AircraftAlreadyExistException.cs | 11 + .../Exceptions/InvalidAircraftIdException.cs | 11 + .../InvalidManufacturingYearException.cs | 10 + .../Exceptions/InvalidModelException.cs | 10 + .../Exceptions/InvalidNameException.cs | 10 + .../Aircrafts/Features/AircraftMappings.cs | 24 + .../CreatingAircraft/V1/CreateAircraft.cs | 103 +++ .../V1/CreateAircraftMongo.cs | 48 ++ .../src/Flight/Aircrafts/Models/Aircraft.cs | 34 + .../Aircrafts/Models/AircraftReadModel.cs | 11 + .../Aircrafts/ValueObjects/AircraftId.cs | 28 + .../ValueObjects/ManufacturingYear.cs | 28 + .../Flight/Aircrafts/ValueObjects/Model.cs | 28 + .../src/Flight/Aircrafts/ValueObjects/Name.cs | 28 + .../src/Flight/Airports/Dtos/AirportDto.cs | 3 + .../AirportAlreadyExistException.cs | 10 + .../Exceptions/InvalidAddressException.cs | 10 + .../Exceptions/InvalidAirportIdException.cs | 11 + .../Exceptions/InvalidCodeException.cs | 10 + .../Exceptions/InvalidNameException.cs | 10 + .../Airports/Features/AirportMappings.cs | 23 + .../CreatingAirport/V1/CreateAirport.cs | 101 +++ .../CreatingAirport/V1/CreateAirportMongo.cs | 48 ++ .../src/Flight/Airports/Models/Airport.cs | 34 + .../Airports/Models/AirportReadModel.cs | 11 + .../Flight/Airports/ValueObjects/Address.cs | 28 + .../Flight/Airports/ValueObjects/AirportId.cs | 28 + .../src/Flight/Airports/ValueObjects/Code.cs | 28 + .../src/Flight/Airports/ValueObjects/Name.cs | 28 + .../Configurations/AircraftConfiguration.cs | 55 ++ .../Configurations/AirportConfiguration.cs | 56 ++ .../Configurations/FlightConfiguration.cs | 112 ++++ .../Data/Configurations/SeatConfiguration.cs | 49 ++ .../Flight/Data/DesignTimeDbContextFactory.cs | 17 + .../src/Flight/Data/EfTxFlightBehavior.cs | 100 +++ .../src/Flight/Data/FlightDbContext.cs | 44 ++ .../src/Flight/Data/FlightReadDbContext.cs | 26 + .../20250407215512_initial.Designer.cs | 584 ++++++++++++++++++ .../Data/Migrations/20250407215512_initial.cs | 182 ++++++ .../FlightDbContextModelSnapshot.cs | 581 +++++++++++++++++ .../RegisterFlightConfigurationAttribute.cs | 4 + .../src/Flight/Data/Seed/FlightDataSeeder.cs | 88 +++ .../src/Flight/Data/Seed/InitialData.cs | 58 ++ .../BookingMonolith/src/Flight/Data/readme.md | 2 + .../src/Flight/FlightEventMapper.cs | 49 ++ .../BookingMonolith/src/Flight/FlightRoot.cs | 6 + .../src/Flight/Flights/Dtos/FlightDto.cs | 4 + .../src/Flight/Flights/Enums/FlightStatus.cs | 10 + .../Exceptions/FlightAlreadyExistException.cs | 10 + .../Flights/Exceptions/FlightExceptions.cs | 14 + .../Exceptions/FlightNotFountException.cs | 10 + .../Exceptions/InvalidArriveDateException.cs | 11 + .../InvalidDepartureDateException.cs | 11 + .../Exceptions/InvalidDurationException.cs | 11 + .../Exceptions/InvalidFlightDateException.cs | 11 + .../Exceptions/InvalidFlightIdException.cs | 11 + .../InvalidFlightNumberException.cs | 11 + .../Exceptions/InvalidPriceException.cs | 11 + .../CreatingFlight/V1/CreateFlight.cs | 123 ++++ .../CreatingFlight/V1/CreateFlightMongo.cs | 49 ++ .../DeletingFlight/V1/DeleteFlight.cs | 109 ++++ .../DeletingFlight/V1/DeleteFlightMongo.cs | 53 ++ .../Flight/Flights/Features/FlightMappings.cs | 47 ++ .../V1/GetAvailableFlights.cs | 84 +++ .../GettingFlightById/V1/GetFlightById.cs | 88 +++ .../UpdatingFlight/V1/UpdateFlight.cs | 121 ++++ .../UpdatingFlight/V1/UpdateFlightMongo.cs | 64 ++ .../src/Flight/Flights/Models/Flight.cs | 101 +++ .../Flight/Flights/Models/FlightReadModel.cs | 18 + .../Flight/Flights/ValueObjects/ArriveDate.cs | 28 + .../Flights/ValueObjects/DepartureDate.cs | 28 + .../Flights/ValueObjects/DurationMinutes.cs | 33 + .../Flight/Flights/ValueObjects/FlightDate.cs | 28 + .../Flight/Flights/ValueObjects/FlightId.cs | 28 + .../Flights/ValueObjects/FlightNumber.cs | 28 + .../src/Flight/Flights/ValueObjects/Price.cs | 28 + .../src/Flight/Seats/Dtos/SeatDto.cs | 3 + .../src/Flight/Seats/Enums/SeatClass.cs | 9 + .../src/Flight/Seats/Enums/SeatType.cs | 9 + .../Seats/Exceptions/AllSeatsFullException.cs | 10 + .../Exceptions/InvalidSeatIdException.cs | 11 + .../Exceptions/InvalidSeatNumberException.cs | 10 + .../Exceptions/SeatAlreadyExistException.cs | 10 + .../SeatNumberIncorrectException.cs | 10 + .../Features/CreatingSeat/V1/CreateSeat.cs | 109 ++++ .../CreatingSeat/V1/CreateSeatMongo.cs | 49 ++ .../V1/GetAvailableSeats.cs | 89 +++ .../Features/ReservingSeat/V1/ReserveSeat.cs | 97 +++ .../ReservingSeat/V1/ReserveSeatMongo.cs | 42 ++ .../src/Flight/Seats/Features/SeatMappings.cs | 34 + .../src/Flight/Seats/Models/Seat.cs | 58 ++ .../src/Flight/Seats/Models/SeatReadModel.cs | 12 + .../src/Flight/Seats/ValueObjects/SeatId.cs | 28 + .../Flight/Seats/ValueObjects/SeatNumber.cs | 28 + .../Identity/Configurations/AuthOptions.cs | 6 + .../src/Identity/Configurations/Config.cs | 70 +++ .../Identity/Configurations/UserValidator.cs | 53 ++ .../Configurations/RoleClaimConfiguration.cs | 22 + .../Data/Configurations/RoleConfiguration.cs | 23 + .../Configurations/UserClaimConfiguration.cs | 22 + .../Data/Configurations/UserConfiguration.cs | 23 + .../Configurations/UserLoginConfiguration.cs | 20 + .../Configurations/UserRoleConfiguration.cs | 19 + .../Configurations/UserTokenConfiguration.cs | 19 + .../Data/DesignTimeDbContextFactory.cs | 16 + .../src/Identity/Data/EfTxIdentityBehavior.cs | 100 +++ .../src/Identity/Data/IdentityContext.cs | 176 ++++++ .../20250407214345_initial.Designer.cs | 381 ++++++++++++ .../Data/Migrations/20250407214345_initial.cs | 263 ++++++++ .../IdentityContextModelSnapshot.cs | 378 ++++++++++++ .../RegisterIdentityConfigurationAttribute.cs | 4 + .../Identity/Data/Seed/IdentityDataSeeder.cs | 99 +++ .../src/Identity/Data/Seed/InitialData.cs | 36 ++ .../src/Identity/Data/readme.md | 2 + .../IdentityServerExtensions.cs | 48 ++ .../Identities/Constants/Constants.cs | 21 + .../RegisterIdentityUserException.cs | 10 + .../Identities/Features/IdentityMappings.cs | 14 + .../RegisteringNewUser/V1/RegisterNewUser.cs | 130 ++++ .../src/Identity/Identities/Models/Role.cs | 9 + .../Identity/Identities/Models/RoleClaim.cs | 9 + .../src/Identity/Identities/Models/User.cs | 12 + .../Identity/Identities/Models/UserClaim.cs | 9 + .../Identity/Identities/Models/UserLogin.cs | 9 + .../Identity/Identities/Models/UserRole.cs | 9 + .../Identity/Identities/Models/UserToken.cs | 9 + .../src/Identity/IdentityEventMapper.cs | 23 + .../src/Identity/IdentityRoot.cs | 6 + .../Configurations/PassengerConfiguration.cs | 60 ++ .../Data/DesignTimeDbContextFactory.cs | 16 + .../Passenger/Data/EfTxPassengerBehavior.cs | 100 +++ .../20250407215445_initial.Designer.cs | 151 +++++ .../Data/Migrations/20250407215445_initial.cs | 48 ++ .../PassengerDbContextModelSnapshot.cs | 148 +++++ .../src/Passenger/Data/PassengerDbContext.cs | 37 ++ .../Passenger/Data/PassengerReadDbContext.cs | 17 + ...RegisterPassengerConfigurationAttribute.cs | 4 + .../src/Passenger/Data/readme.md | 2 + .../Exceptions/InvalidPassengerIdException.cs | 11 + .../V1/PassengerCreatedDomainEvent.cs | 5 + .../RegisteringNewUser/V1/RegisterNewUser.cs | 59 ++ .../src/Passenger/PassengerEventMapper.cs | 32 + .../src/Passenger/PassengerRoot.cs | 6 + .../Passenger/Passengers/Dtos/PassengerDto.cs | 2 + .../Passengers/Enums/PassengerType.cs | 9 + .../Exceptions/InvalidAgeException.cs | 10 + .../Exceptions/InvalidNameException.cs | 10 + .../InvalidPassportNumberException.cs | 10 + .../Exceptions/PassengerAlreadyExist.cs | 10 + .../Exceptions/PassengerNotFoundException.cs | 10 + .../V1/CompleteRegisterPassenger.cs | 116 ++++ .../V1/CompleteRegisterPassengerMongo.cs | 61 ++ .../V1/GetPassengerById.cs | 88 +++ .../Passengers/Features/PassengerMappings.cs | 27 + .../Passenger/Passengers/Models/Passenger.cs | 44 ++ .../Passengers/Models/PassengerReadModel.cs | 11 + .../Passenger/Passengers/ValueObjects/Age.cs | 28 + .../Passenger/Passengers/ValueObjects/Name.cs | 28 + .../Passengers/ValueObjects/PassengerId.cs | 28 + .../Passengers/ValueObjects/PassportNumber.cs | 33 + .../src/BookingMonolith/tests/.gitkeep | 0 .../src/Modules/Booking/tests/.gitkeep | 0 .../Flight/src/Data/FlightDbContext.cs | 3 +- .../src/Modules/Flight/tests/.gitkeep | 0 .../src/Modules/Identity/tests/.gitkeep | 0 .../Passenger/src/UserCreatedHandler.cs | 13 - .../src/Modules/Passenger/tests/.gitkeep | 0 monolith-to-cloud-architecture.sln | 20 + 206 files changed, 9002 insertions(+), 14 deletions(-) rename 1-monolith-architecture-style/{src => assets}/.gitkeep (100%) create mode 100644 1-monolith-architecture-style/booking.rest create mode 100644 1-monolith-architecture-style/deployments/.gitkeep create mode 100644 1-monolith-architecture-style/src/Api/src/Api.csproj create mode 100644 1-monolith-architecture-style/src/Api/src/Extensions/MediatRExtensions.cs create mode 100644 1-monolith-architecture-style/src/Api/src/Extensions/SharedInfrastructureExtensions.cs create mode 100644 1-monolith-architecture-style/src/Api/src/Program.cs create mode 100644 1-monolith-architecture-style/src/Api/src/Properties/launchSettings.json create mode 100644 1-monolith-architecture-style/src/Api/src/appsettings.Development.json create mode 100644 1-monolith-architecture-style/src/Api/src/appsettings.docker.json create mode 100644 1-monolith-architecture-style/src/Api/src/appsettings.json create mode 100644 1-monolith-architecture-style/src/Api/src/appsettings.test.json create mode 100644 1-monolith-architecture-style/src/Api/src/keys/is-signing-key-296345BF73910ADD1DAC302B848E47E7.json create mode 100644 1-monolith-architecture-style/src/Api/src/keys/is-signing-key-73D9025BDA857BF270C99C6594EE4246.json create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingEventMapper.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingProjection.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingRoot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Dtos/CreateReservation.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/BookingAlreadyExistException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/FlightNotFoundException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidAircraftIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidArriveAirportIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidDepartureAirportIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightDateException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightNumberException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPassengerNameException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPriceException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/SeatNumberException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/BookingMappings.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/CreatingBook/V1/CreateBooking.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/Booking.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/BookingReadModel.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/PassengerInfo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/Trip.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Booking/Data/BookingReadDbContext.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolith.csproj create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolithRoot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Dtos/AircraftDto.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidAircraftIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidManufacturingYearException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidModelException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidNameException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/AircraftMappings.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/Aircraft.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/AircraftReadModel.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/AircraftId.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/ManufacturingYear.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Model.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Name.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Dtos/AirportDto.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAddressException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAirportIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidCodeException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidNameException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/AirportMappings.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirport.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/Airport.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/AirportReadModel.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Address.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/AirportId.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Code.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Name.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AircraftConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AirportConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/FlightConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/SeatConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/DesignTimeDbContextFactory.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/EfTxFlightBehavior.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightDbContext.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightReadDbContext.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.Designer.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/RegisterFlightConfigurationAttribute.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/FlightDataSeeder.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/InitialData.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/readme.md create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightEventMapper.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightRoot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Dtos/FlightDto.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Enums/FlightStatus.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightExceptions.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightNotFountException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidArriveDateException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDepartureDateException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDurationException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightDateException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightNumberException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidPriceException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlight.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlightMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlight.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlightMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/FlightMappings.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingAvailableFlights/V1/GetAvailableFlights.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingFlightById/V1/GetFlightById.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlight.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlightMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/Flight.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/FlightReadModel.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/ArriveDate.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DepartureDate.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DurationMinutes.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightDate.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightId.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightNumber.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/Price.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Dtos/SeatDto.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatClass.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatType.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/AllSeatsFullException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatNumberException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeat.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeatMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/GettingAvailableSeats/V1/GetAvailableSeats.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeat.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeatMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/SeatMappings.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/Seat.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/SeatReadModel.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatId.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatNumber.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/AuthOptions.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/Config.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/UserValidator.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleClaimConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserClaimConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserLoginConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserRoleConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserTokenConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/DesignTimeDbContextFactory.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/EfTxIdentityBehavior.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/IdentityContext.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.Designer.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/RegisterIdentityConfigurationAttribute.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/IdentityDataSeeder.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/InitialData.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/readme.md create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Extensions/Infrastructure/IdentityServerExtensions.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Constants/Constants.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Exceptions/RegisterIdentityUserException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/IdentityMappings.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/RegisteringNewUser/V1/RegisterNewUser.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/Role.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/RoleClaim.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/User.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserClaim.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserLogin.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserRole.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserToken.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityEventMapper.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityRoot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Configurations/PassengerConfiguration.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/DesignTimeDbContextFactory.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/EfTxPassengerBehavior.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.Designer.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerDbContext.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerReadDbContext.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/RegisterPassengerConfigurationAttribute.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/readme.md create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Exceptions/InvalidPassengerIdException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/PassengerCreatedDomainEvent.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/RegisterNewUser.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerEventMapper.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerRoot.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Dtos/PassengerDto.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Enums/PassengerType.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidAgeException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidNameException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidPassportNumberException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassenger.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassengerMongo.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/GettingPassengerById/V1/GetPassengerById.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/PassengerMappings.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/Passenger.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/PassengerReadModel.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Age.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Name.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassengerId.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassportNumber.cs create mode 100644 1-monolith-architecture-style/src/BookingMonolith/tests/.gitkeep create mode 100644 2-modular-monolith-architecture-style/src/Modules/Booking/tests/.gitkeep create mode 100644 2-modular-monolith-architecture-style/src/Modules/Flight/tests/.gitkeep create mode 100644 2-modular-monolith-architecture-style/src/Modules/Identity/tests/.gitkeep delete mode 100644 2-modular-monolith-architecture-style/src/Modules/Passenger/src/UserCreatedHandler.cs create mode 100644 2-modular-monolith-architecture-style/src/Modules/Passenger/tests/.gitkeep diff --git a/1-monolith-architecture-style/src/.gitkeep b/1-monolith-architecture-style/assets/.gitkeep similarity index 100% rename from 1-monolith-architecture-style/src/.gitkeep rename to 1-monolith-architecture-style/assets/.gitkeep diff --git a/1-monolith-architecture-style/booking.rest b/1-monolith-architecture-style/booking.rest new file mode 100644 index 0000000..28fb22e --- /dev/null +++ b/1-monolith-architecture-style/booking.rest @@ -0,0 +1,230 @@ + +@booking-monolith-api=https://localhost:5000 + +@contentType = application/json +@flightid = "3c5c0000-97c6-fc34-2eb9-08db322230c9" +@passengerId = "8c9c0000-97c6-fc34-2eb9-66db322230c9" + +################################# Identity API ################################# + +### +# @name Authenticate +POST {{booking-monolith-api}}/connect/token +Content-Type: application/x-www-form-urlencoded + +grant_type=password +&client_id=client +&client_secret=secret +&username=samh +&password=Admin@123456 +&scope=booking-monolith +### + + + +### +# @name Register_New_User +POST {{booking-monolith-api}}/api/v1/identity/register-user +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "firstName": "John", + "lastName": "Do", + "username": "admin", + "passportNumber": "412900000", + "email": "admin@admin.com", + "password": "Admin@12345", + "confirmPassword": "Admin@12345" +} +### + +################################# Flight API ################################# + +### +# @name Create_Seat +Post {{booking-monolith-api}}/api/v1/flight/seat +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "seatNumber": "1255", + "type": 1, + "class": 1, + "flightId": "3c5c0000-97c6-fc34-2eb9-08db322230c9" +} +### + + +### +# @name Reserve_Seat +Post {{booking-monolith-api}}/api/v1/flight/reserve-seat +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "flightId": "3c5c0000-97c6-fc34-2eb9-08db322230c9", + "seatNumber": "1255" +} +### + + +### +# @name Get_Available_Seats +GET {{booking-monolith-api}}/api/v1/flight/get-available-seats/{{flightid}} +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} +### + + +### +# @name Get_Flight_By_Id +GET {{booking-monolith-api}}/api/v1/flight/{{flightid}} +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} +### + + +### +# @name Get_Available_Flights +GET {{booking-monolith-api}}/api/v1/flight/get-available-flights +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} +### + + +### +# @name Create_Flights +POST {{booking-monolith-api}}/api/v1/flight +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "flightNumber": "12BB", + "aircraftId": "3c5c0000-97c6-fc34-fcd3-08db322230c8", + "departureAirportId": "3c5c0000-97c6-fc34-a0cb-08db322230c8", + "departureDate": "2022-03-01T14:55:41.255Z", + "arriveDate": "2022-03-01T14:55:41.255Z", + "arriveAirportId": "3c5c0000-97c6-fc34-fc3c-08db322230c8", + "durationMinutes": 120, + "flightDate": "2022-03-01T14:55:41.255Z", + "status": 1, + "price": 8000 +} +### + + +### +# @name Update_Flights +PUT {{booking-monolith-api}}/api/v1/flight +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "id": 1, + "flightNumber": "BD467", + "aircraftId": "3c5c0000-97c6-fc34-fcd3-08db322230c8", + "departureAirportId": "3c5c0000-97c6-fc34-a0cb-08db322230c8", + "departureDate": "2022-04-23T12:17:45.140Z", + "arriveDate": "2022-04-23T12:17:45.140Z", + "arriveAirportId": "3c5c0000-97c6-fc34-fc3c-08db322230c8", + "durationMinutes": 120, + "flightDate": "2022-04-23T12:17:45.140Z", + "status": 4, + "isDeleted": false, + "price": 99000 +} +### + + +### +# @name Delete_Flights +DELETE {{booking-monolith-api}}/api/v1/flight/{{flightid}} +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} +### + + +### +# @name Create_Airport +POST {{booking-monolith-api}}/api/v1/flight/airport +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "name": "mehrabad", + "address": "tehran", + "code": "12YD" +} +### + + + +### +# @name Create_Aircraft +POST {{booking-monolith-api}}/api/v1/flight/aircraft +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "name": "airbus2", + "model": "322", + "manufacturingYear": 2012 +} +### + + +################################# Passenger API ################################# + + +### +# @name Complete_Registration_Passenger +POST {{booking-monolith-api}}/api/v1/passenger/complete-registration +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "passportNumber": "412900000000", + "passengerType": 1, + "age": 30 +} +### + + +### +# @name Get_Passenger_By_Id +GET {{booking-monolith-api}}/api/v1/passenger/{{passengerId}} +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} +### + + +################################# Booking API ################################# + + +### +# @name Create_Booking +POST {{booking-monolith-api}}/api/v1/booking +accept: application/json +Content-Type: application/json +authorization: bearer {{Authenticate.response.body.access_token}} + +{ + "passengerId": "8c9c0000-97c6-fc34-2eb9-66db322230c9", + "flightId": "3c5c0000-97c6-fc34-2eb9-08db322230c9", + "description": "I want to fly to iran" +} +### diff --git a/1-monolith-architecture-style/deployments/.gitkeep b/1-monolith-architecture-style/deployments/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/1-monolith-architecture-style/src/Api/src/Api.csproj b/1-monolith-architecture-style/src/Api/src/Api.csproj new file mode 100644 index 0000000..a63c543 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/Api.csproj @@ -0,0 +1,11 @@ + + + + Api + + + + + + + diff --git a/1-monolith-architecture-style/src/Api/src/Extensions/MediatRExtensions.cs b/1-monolith-architecture-style/src/Api/src/Extensions/MediatRExtensions.cs new file mode 100644 index 0000000..e67be88 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/Extensions/MediatRExtensions.cs @@ -0,0 +1,23 @@ +using BookingMonolith; +using BuildingBlocks.Caching; +using BuildingBlocks.EFCore; +using BuildingBlocks.Logging; +using BuildingBlocks.Validation; +using MediatR; + +namespace Api.Extensions; + +public static class MediatRExtensions +{ + public static IServiceCollection AddCustomMediatR(this IServiceCollection services) + { + services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies())); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(InvalidateCachingBehavior<,>)); + + return services; + } +} diff --git a/1-monolith-architecture-style/src/Api/src/Extensions/SharedInfrastructureExtensions.cs b/1-monolith-architecture-style/src/Api/src/Extensions/SharedInfrastructureExtensions.cs new file mode 100644 index 0000000..221b356 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/Extensions/SharedInfrastructureExtensions.cs @@ -0,0 +1,171 @@ +using System.Threading.RateLimiting; +using BookingMonolith; +using BookingMonolith.Booking.Data; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Data.Seed; +using BookingMonolith.Identity.Data; +using BookingMonolith.Identity.Data.Seed; +using BookingMonolith.Identity.Extensions.Infrastructure; +using BookingMonolith.Passenger.Data; +using BuildingBlocks.Core; +using BuildingBlocks.EFCore; +using BuildingBlocks.EventStoreDB; +using BuildingBlocks.HealthCheck; +using BuildingBlocks.Jwt; +using BuildingBlocks.Logging; +using BuildingBlocks.Mapster; +using BuildingBlocks.MassTransit; +using BuildingBlocks.Mongo; +using BuildingBlocks.OpenApi; +using BuildingBlocks.OpenTelemetryCollector; +using BuildingBlocks.PersistMessageProcessor; +using BuildingBlocks.ProblemDetails; +using BuildingBlocks.Web; +using Figgle; +using FluentValidation; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc; +using Serilog; + +namespace Api.Extensions; + +public static class SharedInfrastructureExtensions +{ + public static WebApplicationBuilder AddSharedInfrastructure(this WebApplicationBuilder builder) + { + builder.Host.UseDefaultServiceProvider( + (context, options) => + { + // Service provider validation + // ref: https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/ + options.ValidateScopes = context.HostingEnvironment.IsDevelopment() || + context.HostingEnvironment.IsStaging() || + context.HostingEnvironment.IsEnvironment("tests"); + + options.ValidateOnBuild = true; + }); + + var appOptions = builder.Services.GetOptions(nameof(AppOptions)); + Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); + + builder.AddCustomSerilog(builder.Environment); + builder.Services.AddJwt(); + builder.Services.AddScoped(); + builder.Services.AddTransient(); + builder.Services.AddPersistMessageProcessor(); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddControllers(); + builder.Services.AddAspnetOpenApi(); + builder.Services.AddCustomVersioning(); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddScoped(); + builder.Services.AddCustomMediatR(); + + builder.Services.AddCustomMassTransit( + builder.Environment, + TransportType.InMemory, + AppDomain.CurrentDomain.GetAssemblies()); + + + builder.Services.Scan( + scan => scan + .FromAssemblyOf() + .AddClasses(classes => classes.AssignableTo()) + .AsImplementedInterfaces() + .WithScopedLifetime()); + + + builder.AddMinimalEndpoints(assemblies: typeof(BookingMonolithRoot).Assembly); + builder.Services.AddValidatorsFromAssembly(typeof(BookingMonolithRoot).Assembly); + builder.Services.AddCustomMapster(typeof(BookingMonolithRoot).Assembly); + + builder.AddMongoDbContext(); + builder.AddMongoDbContext(); + builder.AddMongoDbContext(); + + builder.AddCustomDbContext(); + builder.Services.AddScoped(); + builder.AddCustomIdentityServer(); + + builder.Services.Configure( + options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); + + builder.AddCustomDbContext(); + builder.Services.AddScoped(); + + builder.AddCustomDbContext(); + + // ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce + builder.Services.AddEventStore(builder.Configuration, typeof(BookingMonolithRoot).Assembly) + .AddEventStoreDBSubscriptionToAll(); + + builder.Services.Configure( + options => options.SuppressModelStateInvalidFilter = true); + + builder.Services.AddRateLimiter( + options => + { + options.GlobalLimiter = PartitionedRateLimiter.Create( + httpContext => + RateLimitPartition.GetFixedWindowLimiter( + partitionKey: httpContext.User.Identity?.Name ?? + httpContext.Request.Headers.Host.ToString(), + factory: partition => new FixedWindowRateLimiterOptions + { + AutoReplenishment = true, + PermitLimit = 10, + QueueLimit = 0, + Window = TimeSpan.FromMinutes(1) + })); + }); + + builder.AddCustomObservability(); + builder.Services.AddCustomHealthCheck(); + + builder.Services.AddEasyCaching( + options => { options.UseInMemory(builder.Configuration, "mem"); }); + + builder.Services.AddProblemDetails(); + + return builder; + } + + + public static WebApplication UserSharedInfrastructure(this WebApplication app) + { + var appOptions = app.Configuration.GetOptions(nameof(AppOptions)); + + app.UseCustomProblemDetails(); + app.UseCustomObservability(); + app.UseCustomHealthCheck(); + + app.UseSerilogRequestLogging( + options => + { + options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest; + }); + + app.UseCorrelationId(); + app.UseRateLimiter(); + app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); + + if (app.Environment.IsDevelopment()) + { + app.UseAspnetOpenApi(); + } + + app.UseForwardedHeaders(); + app.UseMigration(); + app.UseMigration(); + app.UseMigration(); + + app.UseIdentityServer(); + + return app; + } +} diff --git a/1-monolith-architecture-style/src/Api/src/Program.cs b/1-monolith-architecture-style/src/Api/src/Program.cs new file mode 100644 index 0000000..c3b80d5 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/Program.cs @@ -0,0 +1,24 @@ +using Api.Extensions; +using BuildingBlocks.Web; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddSharedInfrastructure(); + +var app = builder.Build(); + +// ref: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-7.0#routing-basics +app.UseAuthentication(); +app.UseAuthorization(); + +app.UserSharedInfrastructure(); +app.MapMinimalEndpoints(); + +app.Run(); + +namespace Api +{ + public partial class Program + { + } +} diff --git a/1-monolith-architecture-style/src/Api/src/Properties/launchSettings.json b/1-monolith-architecture-style/src/Api/src/Properties/launchSettings.json new file mode 100644 index 0000000..c1dd2d1 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:17191", + "sslPort": 44352 + } + }, + "profiles": { + "Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchUrl": "swagger", + "launchBrowser": true, + "applicationUrl": "https://localhost:5000;http://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/1-monolith-architecture-style/src/Api/src/appsettings.Development.json b/1-monolith-architecture-style/src/Api/src/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/1-monolith-architecture-style/src/Api/src/appsettings.docker.json b/1-monolith-architecture-style/src/Api/src/appsettings.docker.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/appsettings.docker.json @@ -0,0 +1,2 @@ +{ +} diff --git a/1-monolith-architecture-style/src/Api/src/appsettings.json b/1-monolith-architecture-style/src/Api/src/appsettings.json new file mode 100644 index 0000000..2b0fced --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/appsettings.json @@ -0,0 +1,59 @@ +{ + "AppOptions": { + "Name": "Booking-Monolith" + }, + "LogOptions": { + "Level": "information", + "LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}", + "File": { + "Enabled": false, + "Path": "logs/logs.txt", + "Interval": "day" + } + }, + "PostgresOptions": { + "ConnectionString": "Server=localhost;Port=5432;Database=booking_monolith;User Id=postgres;Password=postgres;Include Error Detail=true" + }, + "MongoOptions": { + "ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "booking_modular_monolith_read" + }, + "EventStoreOptions": { + "ConnectionString": "esdb://localhost:2113?tls=false" + }, + "PersistMessageOptions": { + "Interval": 30, + "Enabled": true, + "ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" + }, + "Jwt": { + "Authority": "https://localhost:5000", + "Audience": "booking-monolith", + "RequireHttpsMetadata": false + }, + "HealthOptions": { + "Enabled": false + }, + "ObservabilityOptions": { + "InstrumentationName": "booking_monolith_service", + "OTLPOptions": { + "OTLPGrpExporterEndpoint": "http://localhost:4317" + }, + "AspireDashboardOTLPOptions": { + "OTLPGrpExporterEndpoint": "http://localhost:4319" + }, + "ZipkinOptions": { + "HttpExporterEndpoint": "http://localhost:9411/api/v2/spans" + }, + "JaegerOptions": { + "OTLPGrpcExporterEndpoint": "http://localhost:14317", + "HttpExporterEndpoint": "http://localhost:14268/api/traces" + }, + "UsePrometheusExporter": true, + "UseOTLPExporter": true, + "UseAspireOTLPExporter": true, + "UseGrafanaExporter": false, + "ServiceName": "Booking Monolith Service" + }, + "AllowedHosts": "*" +} diff --git a/1-monolith-architecture-style/src/Api/src/appsettings.test.json b/1-monolith-architecture-style/src/Api/src/appsettings.test.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/appsettings.test.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/1-monolith-architecture-style/src/Api/src/keys/is-signing-key-296345BF73910ADD1DAC302B848E47E7.json b/1-monolith-architecture-style/src/Api/src/keys/is-signing-key-296345BF73910ADD1DAC302B848E47E7.json new file mode 100644 index 0000000..cd34668 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/keys/is-signing-key-296345BF73910ADD1DAC302B848E47E7.json @@ -0,0 +1 @@ +{"Version":1,"Id":"296345BF73910ADD1DAC302B848E47E7","Created":"2025-04-06T20:57:25.1670058Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8KNpFTgHKl5Nl-o13RQ8rseNpYdUTB-_FLsIGbeu6jM5x9l_rDfygUsYH6TAnyqXDFeW4U7xf8kJXDDvH1V0jkKcQaHFWYcZnKAirRBnuReu2OcaGWcn0vDccI5liTTRfp_Dknwhf3jgrU-LOBlPDoVGWwTwOQHXa4iHQjfOCG77Ey6CmJZ0w6JMi802tSMbpc2G2376b05GuzpoKwfgG_F8ZJcAtl1cql4KD11CcynPTNqK0fXOcIeGCQKgJDkJp_cHlk-sv4xJTFl5nqRx62v9auoB3AeKxqRqKqserGT-ZFYDeSxOlgmDSreVFezZ4tYd80Iq-ZpQXK5e3uz8gJ1B37_ySzgF_Rkscf67FivIqcpcV-WZzvnKXeQP0Wo7B7Qt8sKwCAW-vh3X5iMdDi-tecOWaRqeNrffbjd4efP1FK5wqmNrZirrcuCZDgPyYUiB1bSUE1HSj5vW4kFy_NF-3k0EVcQf5KqJpJ9QkTQgmTPkFCwwEcyX-P2hA21G64_M_7EjZXnjw29xjnkI7zO3mx3sA5bwPf6sCmxgE6joaI6P7G4u8JB5pbSHynOWWNTWRZm8YHngZCfK1DZa6Y-uAnrNzVQdcbB-uVGwszavP3Ohqtu7NBCaIkIhyT9N783n5-J8KAbSKu2FBF-qwNaUcmSAhbnRLyQwou-F0sxgM9iLwNcCo6jcgqz5g3MBEIcWa5IKdbtYXQQzbj3NlKmYg1l-x0SrxZD9403gNIS-zCEWLPEgpLr-NU06_Y5Vkbvn8jHKAr45nDFvY_osVvCPQ8qFuq_P-z3HMqFrfuPNlLrf_ZoJCpIKYNdXPJwHQXzGpz41QiN2omk52K5N8UCiGnDog66tJZAqGjtWWHpFCF2O4LXA9uEwnlD06N_i1T9umOtLnlGsF5SM0G3YjG_G8APqd6GDnJsM1wBoVXzzldGvS8GEwwxvVRGvLrSljPdR_l-8JifzUzhIOxMdlZUE8D9tQJhqZJovm34kfgx8AA0j4FQxAKPUhz99dapc5rmP1pgkuv7b9ZgtWa9js16GUJUYV3Dd6HC3lWU4zdXnQIsJPBUdgsiKptAXayc1KevAzw7o2PB7_auENui4cj1MuDxbaf9yrq9ZIol_tPw5rdN440vhHPIPzVi-1Wv0YyvyPD0fj2GAU5fL97lVlCprWuM71-rjVtARrn0pbf4ENBYk4ABWWke8ZHppbdFQQbjBv-yKCeiGA7qbRAeb_NMK2wmWUGz9DTVWtb04kmitXvg1-aj6ZwY-uIDSY5uaHPz7b8mxFywlSQ9plQzIkb-EfZk6RWxW1-NqyHMEAryM4QxwdDX9LjSo37CPdHCdOSRsMzH6zGH0_SgeN4W25Lk0RhoBHMMQk_gF5FCXjDteFlAdPFw9K5qmVVohHfEnAMDUZ4xYYgDx0Z254jsbSIFOuqEmUvmb5WvbKFtlcLrpFAuoqcNLfI61JM0LLxZNjpktGOMX5q1E_tYm5Y-bxd6W_2r-UUkCTKWJMp7_wr2CJPyegVshEBuUaO4_GyndCTY9_jh4OEYgyFQliEg66g3CUVy7ZBERrfz1CiHmpTqoKbs3toPsHMrB6yaQe9UtcYzLBRP3OVku3WmE1IylfUsZ64YA2DBU399JjYckXlWU3PxH9GAxcnIW8cU_29XZZ0ueGLFeSRP0mBb8TqVIcqd3xoAUbZGPnleme-D44yco8ZSGFVhQqt5qstzIA2DQALjVf2E5vg-yL8Gj9s5q-xjPjBgZgyW23gbBEO6PynDK3jfnucHXcLsHMpvZwyV1yaU3314mNLUpu241_7ukMuWC5FmxaVWqe4n9h-W479YlK5WmI5TDmWcc12DQI92eIAYCSYA60SOJhnuGsyFHwedfxIjVjouZeAWWHj1QOM3uZ1rwYOB0DZSlqI6C2kecv4D2CFq7gYosBxQX9fSnJ2MyUiGOxbdlRtwGB_dtRtlqWpt-acu-l9i5jNwdM6QnNejoPthvWPCcvfaaqFjWjnZQQKSBBfWV9Coe7mnaJYhyqWFUQ4AKcDuboQ3k3jj2p-5LTnbFlTQ3SzLfsgjLjpD5hqIY1ND5NWOT3D0pQumfh5tJxgSa0g-HhW7XBSGs2783OFdgAMuxCkZUgisdu0MRyQzdqWEY30trlpnpsAGOO3E8MMxQ2COy7Y-WrUykioag1qkJTbT-1FgHgw9Qj4dnQ136_tC3BvVrmO6pId26PzdPDpV_4T5vNLoyuiyyUqnHcKOETFrHpbj2cXmi-sYuqrwfNZKfcPIsAkekmcWlLd9s4glvD3xJ0SB0s6gJURjvupdD96l2kfdHw8qieB0ovlGISrpjnAKGn6F81_32rYULB-NDN8H4aTdiVmhvwf6KkpXHA9Euuyyir4BqHmIknr8WWuugIMxRw3ysCS4OIV5ocsjzIfQ9r_15bNWsoyWPDkVnKcS1ffbURzYPNIqTO6Ik5iC7rk705WAxaojy","DataProtected":true} \ No newline at end of file diff --git a/1-monolith-architecture-style/src/Api/src/keys/is-signing-key-73D9025BDA857BF270C99C6594EE4246.json b/1-monolith-architecture-style/src/Api/src/keys/is-signing-key-73D9025BDA857BF270C99C6594EE4246.json new file mode 100644 index 0000000..5ca82b6 --- /dev/null +++ b/1-monolith-architecture-style/src/Api/src/keys/is-signing-key-73D9025BDA857BF270C99C6594EE4246.json @@ -0,0 +1 @@ +{"Version":1,"Id":"73D9025BDA857BF270C99C6594EE4246","Created":"2024-09-02T18:34:53.8631045Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8MEQ06y_es9CrKY4Ou9Uc7Hf97ujXcMcuawi9V5VrBvxWbAqbdjBlk5zKK_NzrDURTwaFvgOfLUQ__0r-sMvtTXBZLhWhNWJnLn1-KWhtnpvPt5jFTAf2f5mvrjVTTN8E-NTGMGk2yLOYm2TVE3QL7DuLRrazydXLJvIB71O2d_OrAfB7Pq62xrnwLgp4BErb_HphYUExsAEp7jn6uL34QJ5HI6zsb_ct2SHvOl0CzUH2yIjjrY_u-5GAgmqqpUyysGwUh8RpR_CDPCd8ZxgSOiKAlBkp3kQXxf86MF0C-kfBKU-iflrJRSJ1-5R8F5nNPK8FL29I4i7SYCugIkLvqez3wTewPUFv00bTpcfs3V_tmfqBFLyNWm4sppzpU9HGB3elZ0z5bBa_IGbtIrlHC_o4SDSxKKdD4OUZHGLE5X--xWBw5uV9GSgrSH6bcnHZ6rLd3qrt7b82BMr-rVIGyFzGIE6OTLVGkXSL6FdpJ9Lezp06qnZ6DtLFI86lVJGsYIuXR_AIVrz9U8uqCrK6jLwGCk9nR0adPtAUJfgcXGVVTQlvt_YgZv5k5_rYcVl0yLNdgd-BoidXeoqLPJxIJOCohwumVcqTPkf-gB_hjgNk6IEqXZxm0Tzl0d2jpyERDHdHxIvS5o1h4YUfkiMcliRfQMNkLoDxf0H2hFNXRYQKkQ1y6cwS9juq66Uj_-v-vN1etE-hK45ULFCfyppBvb2aTYfTuce5S0ps1t0ZIjvpm7Kvjg3doHEi97N-IqxYaf8r6n8gBcaUwlrfcHaYNPLRyWywX_varEmLm9qGK5KJj2itGNfw0wU0EygIeoV6V_PzqyAkf0JcVTC7lcog37TPdNU2AGzH0S8oXiAQEd49wPs2ZApjiOaiz26efr03I-hD5N91-R2_9ACGjENGPVHMyUtMVV5RPs3-pQMv9f_zweOuLQo7ZfhScqu4HmxGW70amuV4anMxGCQzbi3JWnkTmspptzClJyvE_MJVSTQ6SX04DaS4buSG3wZEc9Qy9SqTj-9CJ7XFGFs8XYmKUj_cIoQF1XuoSnWblsnnEC7EbNRF7y9fG8ZG-Sk3TEEYnalxRrcS-i26wVNdnuUBEmidz_HfsxFxCDKKmx7GHTvHxy72kI84ucMeQeVFjJI3ZDynGn--cL9xBiUbUKM8WDhJ-AgZ76wwh0qAPw5xJ6yHi-15moxySUkvFLjlNkP2Ad5j3_3ab_r6VIunM6zhsq7pSBWIg5povuV5ZwNVZQX0IeLqV9bHug443LaK5a57dTK8wy346AFftV-wc71i4Nt5MIFcOs3lxRPqYij1enbrPYvIV4-N8Sy6aaYj25Qn7VHrGeW72aZPAYY5W-czoPw_Oo6xYGjaPYFFsUSZVg6IQwCzwwAxUoc2gAL33FJQTNvNSnrYBJ5HN-Tqan23Pw_bEus7HHZu2N1daFjqtrl4-oOco46phsppUjH3LGhOPJnFSChr-W8tlk80coJ8IK_AsGludKB09WzId9JBtI5cp3Yu1J7N6nSL7nVTrT6Gw_0hitSoeu5ZLPSS9ooAynAXrvB_s0l0L9aFTRuc5IEhgt4bLzbeqimfQemRlBsNz09JGe04gmOOCmjWD52JHWUiVJQNMavrSGtW9Dy1-Z5h0D_BHzhpTia1S7wx7dSdItJ0-Pm1Au_TNkQGm4ffNFsVDQmNkCYyc8yFnYmZMYYEaPmbw7DvQTs1MHoe7aUzMM0DKcaqboSaxqQK9sxymgElvdoYqOMRWzS7s39UQ1O4TfPngfrWdtN2DogGUtyS-vPfNJpdS6jZvJAj8czgl7PU8buwWyPApE1-FVL32wC6a8dkHvJi4p7fbBjmTfFCnuW8G1KBX3VuToctJvidSjzoSUTX3vgKVni2QW-55Sh7DUYy91FGXGB_ui1yuxEnLmymtbWokcWYkIwcAsl8im70V4oK63ypNSYWea_gaDWMFXT_vANB1iAkr-_zE_ECocOXo93QqSR5UdLmfQFvLiDwjUeovkjFS5C2Z8AjEvHvFkedGWOIK5Bpam-0IEFip3Fvg6RgxwTinFXXa8PiRkcLSlt0J81b85ybrKsDj5WtUA-MFuK2Silyofn9BgD_lh9RU4HPFhVoqey7AuEJjHtGvqz4EnE_05y4A_mKgvJBAs4QiYjCopWtheeOGeeoUa636Ewmu30P66C5mimdAIx36-55xlyJBIM7DFFM6RAGvfAmpyNphjwT0y84B4pOhFEZeOQ2me2sfG-xRJbjjgDhP2SwBBEQ-hCLGeqOD-Xo74FZC4lCTvtn2Sbu1kIw0kz2P_vrq6d6SZwEIrhWYhfRVKTrT8nXj8i48Jdc1d1fyKdRL15USgLhAT-QSNcgVYHRLsVlQx5-b51tGg6Atx6vGCxtXBRSaTwZ3IxbdJs0T62H14K5U81EFu-2Vvf-cMwCm4gCQATxvvAsqToxElou9ZjIVMPt_FQUyAMtJke","DataProtected":true} \ No newline at end of file diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingEventMapper.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingEventMapper.cs new file mode 100644 index 0000000..9743d54 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingEventMapper.cs @@ -0,0 +1,26 @@ +using BookingMonolith.Booking.Bookings.Features.CreatingBook.V1; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; + +namespace BookingMonolith.Booking; + +public sealed class BookingEventMapper : IEventMapper +{ + public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event) + { + return @event switch + { + BookingCreatedDomainEvent e => new BookingCreated(e.Id), + _ => null + }; + } + + public IInternalCommand? MapToInternalCommand(IDomainEvent @event) + { + return @event switch + { + _ => null + }; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingProjection.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingProjection.cs new file mode 100644 index 0000000..7cbff91 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingProjection.cs @@ -0,0 +1,53 @@ +using BookingMonolith.Booking.Bookings.Features.CreatingBook.V1; +using BookingMonolith.Booking.Bookings.Models; +using BookingMonolith.Booking.Data; +using BuildingBlocks.EventStoreDB.Events; +using BuildingBlocks.EventStoreDB.Projections; +using MassTransit; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Booking; + +public class BookingProjection : IProjectionProcessor +{ + private readonly BookingReadDbContext _bookingReadDbContext; + + public BookingProjection(BookingReadDbContext bookingReadDbContext) + { + _bookingReadDbContext = bookingReadDbContext; + } + + public async Task ProcessEventAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) + where T : INotification + { + switch (streamEvent.Data) + { + case BookingCreatedDomainEvent bookingCreatedDomainEvent: + await Apply(bookingCreatedDomainEvent, cancellationToken); + break; + } + } + + private async Task Apply(BookingCreatedDomainEvent @event, CancellationToken cancellationToken = default) + { + var reservation = + await _bookingReadDbContext.Booking.AsQueryable().SingleOrDefaultAsync(x => x.Id == @event.Id && !x.IsDeleted, + cancellationToken); + + if (reservation == null) + { + var bookingReadModel = new BookingReadModel + { + Id = NewId.NextGuid(), + Trip = @event.Trip, + BookId = @event.Id, + PassengerInfo = @event.PassengerInfo, + IsDeleted = @event.IsDeleted + }; + + await _bookingReadDbContext.Booking.InsertOneAsync(bookingReadModel, cancellationToken: cancellationToken); + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingRoot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingRoot.cs new file mode 100644 index 0000000..e5df390 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/BookingRoot.cs @@ -0,0 +1,6 @@ +namespace BookingMonolith.Booking; + +public class BookingRoot +{ + +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Dtos/CreateReservation.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Dtos/CreateReservation.cs new file mode 100644 index 0000000..9d4fe4a --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Dtos/CreateReservation.cs @@ -0,0 +1,4 @@ +namespace BookingMonolith.Booking.Bookings.Dtos; + +public record BookingResponseDto(Guid Id, string Name, string FlightNumber, Guid AircraftId, decimal Price, + DateTime FlightDate, string SeatNumber, Guid DepartureAirportId, Guid ArriveAirportId, string Description); diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/BookingAlreadyExistException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/BookingAlreadyExistException.cs new file mode 100644 index 0000000..a37b831 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/BookingAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class BookingAlreadyExistException : ConflictException +{ + public BookingAlreadyExistException(int? code = default) : base("Booking already exist!", code) + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/FlightNotFoundException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/FlightNotFoundException.cs new file mode 100644 index 0000000..ee67c67 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/FlightNotFoundException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class FlightNotFoundException : NotFoundException +{ + public FlightNotFoundException() : base("Flight doesn't exist!") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidAircraftIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidAircraftIdException.cs new file mode 100644 index 0000000..9e97cbc --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidAircraftIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class InvalidAircraftIdException : BadRequestException +{ + public InvalidAircraftIdException(Guid aircraftId) + : base($"aircraftId: '{aircraftId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidArriveAirportIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidArriveAirportIdException.cs new file mode 100644 index 0000000..de85aa5 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidArriveAirportIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class InvalidArriveAirportIdException : BadRequestException +{ + public InvalidArriveAirportIdException(Guid arriveAirportId) + : base($"arriveAirportId: '{arriveAirportId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidDepartureAirportIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidDepartureAirportIdException.cs new file mode 100644 index 0000000..dbcbb5c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidDepartureAirportIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class InvalidDepartureAirportIdException : BadRequestException +{ + public InvalidDepartureAirportIdException(Guid departureAirportId) + : base($"departureAirportId: '{departureAirportId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightDateException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightDateException.cs new file mode 100644 index 0000000..0a2fbea --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightDateException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class InvalidFlightDateException : BadRequestException +{ + public InvalidFlightDateException(DateTime flightDate) + : base($"Flight Date: '{flightDate}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightNumberException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightNumberException.cs new file mode 100644 index 0000000..c45bbdc --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidFlightNumberException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class InvalidFlightNumberException : BadRequestException +{ + public InvalidFlightNumberException(string flightNumber) + : base($"Flight Number: '{flightNumber}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPassengerNameException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPassengerNameException.cs new file mode 100644 index 0000000..4f3b3d4 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPassengerNameException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class InvalidPassengerNameException : BadRequestException +{ + public InvalidPassengerNameException(string passengerName) + : base($"Passenger Name: '{passengerName}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPriceException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPriceException.cs new file mode 100644 index 0000000..51298d0 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/InvalidPriceException.cs @@ -0,0 +1,12 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class InvalidPriceException : BadRequestException +{ + public InvalidPriceException(decimal price) + : base($"Price: '{price}' must be grater than or equal 0.") + { + } +} + diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/SeatNumberException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/SeatNumberException.cs new file mode 100644 index 0000000..27f6475 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Exceptions/SeatNumberException.cs @@ -0,0 +1,12 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Booking.Bookings.Exceptions; + +public class SeatNumberException : BadRequestException +{ + public SeatNumberException(string seatNumber) + : base($"Seat Number: '{seatNumber}' is invalid.") + { + } +} + diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/BookingMappings.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/BookingMappings.cs new file mode 100644 index 0000000..d962240 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/BookingMappings.cs @@ -0,0 +1,22 @@ +using BookingMonolith.Booking.Bookings.Dtos; +using BookingMonolith.Booking.Bookings.Features.CreatingBook.V1; +using Mapster; + +namespace BookingMonolith.Booking.Bookings.Features; + +public class BookingMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.Default.NameMatchingStrategy(NameMatchingStrategy.Flexible); + + config.NewConfig() + .ConstructUsing(x => new BookingResponseDto(x.Id, x.PassengerInfo.Name, x.Trip.FlightNumber, + x.Trip.AircraftId, x.Trip.Price, x.Trip.FlightDate, x.Trip.SeatNumber, x.Trip.DepartureAirportId, x.Trip.ArriveAirportId, + x.Trip.Description)); + + + config.NewConfig() + .ConstructUsing(x => new CreateBooking(x.PassengerId, x.FlightId, x.Description)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/CreatingBook/V1/CreateBooking.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/CreatingBook/V1/CreateBooking.cs new file mode 100644 index 0000000..b8e4253 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Features/CreatingBook/V1/CreateBooking.cs @@ -0,0 +1,136 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Booking.Bookings.Exceptions; +using BookingMonolith.Booking.Bookings.ValueObjects; +using BookingMonolith.Flight.Flights.Features.GettingFlightById.V1; +using BookingMonolith.Flight.Seats.Features.GettingAvailableSeats.V1; +using BookingMonolith.Flight.Seats.Features.ReservingSeat.V1; +using BookingMonolith.Passenger.Passengers.Features.GettingPassengerById.V1; +using BuildingBlocks.Core; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Core.Model; +using BuildingBlocks.EventStoreDB.Repository; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace BookingMonolith.Booking.Bookings.Features.CreatingBook.V1; + +public record CreateBooking(Guid PassengerId, Guid FlightId, string Description) : ICommand +{ + public Guid Id { get; init; } = NewId.NextGuid(); +} + +public record CreateBookingResult(ulong Id); + +public record BookingCreatedDomainEvent(Guid Id, PassengerInfo PassengerInfo, Trip Trip) : Entity, IDomainEvent; + +public record CreateBookingRequestDto(Guid PassengerId, Guid FlightId, string Description); + +public record CreateBookingResponseDto(ulong Id); + +public class CreateBookingEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/booking", async (CreateBookingRequestDto request, + IMediator mediator, IMapper mapper, + CancellationToken cancellationToken) => + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("CreateBooking") + .WithApiVersionSet(builder.NewApiVersionSet("Booking").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Create Booking") + .WithDescription("Create Booking") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class CreateBookingValidator : AbstractValidator +{ + public CreateBookingValidator() + { + RuleFor(x => x.FlightId).NotNull().WithMessage("FlightId is required!"); + RuleFor(x => x.PassengerId).NotNull().WithMessage("PassengerId is required!"); + } +} + +internal class CreateBookingCommandHandler : ICommandHandler +{ + private readonly IEventStoreDBRepository _eventStoreDbRepository; + private readonly ICurrentUserProvider _currentUserProvider; + private readonly IEventDispatcher _eventDispatcher; + private readonly IMediator _mediator; + + public CreateBookingCommandHandler(IEventStoreDBRepository eventStoreDbRepository, + ICurrentUserProvider currentUserProvider, + IEventDispatcher eventDispatcher, + IMediator mediator) + { + _eventStoreDbRepository = eventStoreDbRepository; + _currentUserProvider = currentUserProvider; + _eventDispatcher = eventDispatcher; + _mediator = mediator; + } + + public async Task Handle(CreateBooking command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + // Directly call the GetFlightById handler instead of gRPC + var flight = await _mediator.Send(new GetFlightById(command.FlightId), cancellationToken); + + if (flight is null) + { + throw new FlightNotFoundException(); + } + + var passenger = await _mediator.Send(new GetPassengerById(command.PassengerId), cancellationToken); + + var emptySeat = (await _mediator.Send(new GetAvailableSeats(command.FlightId), cancellationToken))?.SeatDtos?.FirstOrDefault(); + + var reservation = await _eventStoreDbRepository.Find(command.Id, cancellationToken); + + if (reservation is not null && !reservation.IsDeleted) + { + throw new BookingAlreadyExistException(); + } + + var aggrigate = Models.Booking.Create(command.Id, PassengerInfo.Of(passenger.PassengerDto?.Name), Trip.Of( + flight.FlightDto.FlightNumber, flight.FlightDto.AircraftId, + flight.FlightDto.DepartureAirportId, + flight.FlightDto.ArriveAirportId, flight.FlightDto.FlightDate, + flight.FlightDto.Price, command.Description, + emptySeat?.SeatNumber), + false, _currentUserProvider.GetCurrentUserId()); + + await _eventDispatcher.SendAsync(aggrigate.DomainEvents, cancellationToken: cancellationToken); + + await _mediator.Send(new ReserveSeat(flight.FlightDto.Id, emptySeat?.SeatNumber), cancellationToken); + + var result = await _eventStoreDbRepository.Add( + aggrigate, + cancellationToken); + + return new CreateBookingResult(result); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/Booking.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/Booking.cs new file mode 100644 index 0000000..c1b1729 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/Booking.cs @@ -0,0 +1,49 @@ +using BookingMonolith.Booking.Bookings.Features.CreatingBook.V1; +using BookingMonolith.Booking.Bookings.ValueObjects; +using BuildingBlocks.EventStoreDB.Events; + +namespace BookingMonolith.Booking.Bookings.Models; + +public record Booking : AggregateEventSourcing +{ + public Trip Trip { get; private set; } + public PassengerInfo PassengerInfo { get; private set; } + + public static Booking Create(Guid id, PassengerInfo passengerInfo, Trip trip, bool isDeleted = false, long? userId = null) + { + var booking = new Booking { Id = id, Trip = trip, PassengerInfo = passengerInfo, IsDeleted = isDeleted }; + + var @event = new BookingCreatedDomainEvent(booking.Id, booking.PassengerInfo, booking.Trip) + { + IsDeleted = booking.IsDeleted, + CreatedAt = DateTime.Now, + CreatedBy = userId + }; + + booking.AddDomainEvent(@event); + booking.Apply(@event); + + return booking; + } + + public override void When(object @event) + { + switch (@event) + { + case BookingCreatedDomainEvent bookingCreated: + { + Apply(bookingCreated); + return; + } + } + } + + private void Apply(BookingCreatedDomainEvent @event) + { + Id = @event.Id; + Trip = @event.Trip; + PassengerInfo = @event.PassengerInfo; + IsDeleted = @event.IsDeleted; + Version++; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/BookingReadModel.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/BookingReadModel.cs new file mode 100644 index 0000000..730a6bc --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/Models/BookingReadModel.cs @@ -0,0 +1,12 @@ +using BookingMonolith.Booking.Bookings.ValueObjects; + +namespace BookingMonolith.Booking.Bookings.Models; + +public class BookingReadModel +{ + public required Guid Id { get; init; } + public required Guid BookId { get; init; } + public required Trip Trip { get; init; } + public required PassengerInfo PassengerInfo { get; init; } + public required bool IsDeleted { get; init; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/PassengerInfo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/PassengerInfo.cs new file mode 100644 index 0000000..8a65131 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/PassengerInfo.cs @@ -0,0 +1,23 @@ +using BookingMonolith.Booking.Bookings.Exceptions; + +namespace BookingMonolith.Booking.Bookings.ValueObjects; + +public record PassengerInfo +{ + public string Name { get; } + + private PassengerInfo(string name) + { + Name = name; + } + + public static PassengerInfo Of(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new InvalidPassengerNameException(name); + } + + return new PassengerInfo(name); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/Trip.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/Trip.cs new file mode 100644 index 0000000..da43eaa --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Bookings/ValueObjects/Trip.cs @@ -0,0 +1,69 @@ +using BookingMonolith.Booking.Bookings.Exceptions; + +namespace BookingMonolith.Booking.Bookings.ValueObjects; + +public record Trip +{ + public string FlightNumber { get; } + public Guid AircraftId { get; } + public Guid DepartureAirportId { get; } + public Guid ArriveAirportId { get; } + public DateTime FlightDate { get; } + public decimal Price { get; } + public string Description { get; } + public string SeatNumber { get; } + + private Trip(string flightNumber, Guid aircraftId, Guid departureAirportId, Guid arriveAirportId, + DateTime flightDate, decimal price, string description, string seatNumber) + { + FlightNumber = flightNumber; + AircraftId = aircraftId; + DepartureAirportId = departureAirportId; + ArriveAirportId = arriveAirportId; + FlightDate = flightDate; + Price = price; + Description = description; + SeatNumber = seatNumber; + } + + public static Trip Of(string flightNumber, Guid aircraftId, Guid departureAirportId, Guid arriveAirportId, + DateTime flightDate, decimal price, string description, string seatNumber) + { + if (string.IsNullOrWhiteSpace(flightNumber)) + { + throw new InvalidFlightNumberException(flightNumber); + } + + if (aircraftId == Guid.Empty) + { + throw new InvalidAircraftIdException(aircraftId); + } + + if (departureAirportId == Guid.Empty) + { + throw new InvalidDepartureAirportIdException(departureAirportId); + } + + if (arriveAirportId == Guid.Empty) + { + throw new InvalidArriveAirportIdException(departureAirportId); + } + + if (flightDate == default) + { + throw new InvalidFlightDateException(flightDate); + } + + if (price < 0) + { + throw new InvalidPriceException(price); + } + + if (string.IsNullOrWhiteSpace(seatNumber)) + { + throw new SeatNumberException(seatNumber); + } + + return new Trip(flightNumber, aircraftId, departureAirportId, arriveAirportId, flightDate, price, description, seatNumber); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Data/BookingReadDbContext.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Data/BookingReadDbContext.cs new file mode 100644 index 0000000..8c88c0f --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Booking/Data/BookingReadDbContext.cs @@ -0,0 +1,18 @@ +using BookingMonolith.Booking.Bookings.Models; +using BuildingBlocks.Mongo; +using Humanizer; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace BookingMonolith.Booking.Data; + + +public class BookingReadDbContext : MongoDbContext +{ + public BookingReadDbContext(IOptions options) : base(options) + { + Booking = GetCollection(nameof(Booking).Underscore()); + } + + public IMongoCollection Booking { get; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolith.csproj b/1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolith.csproj new file mode 100644 index 0000000..fb8c398 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolith.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolithRoot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolithRoot.cs new file mode 100644 index 0000000..1ff2461 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/BookingMonolithRoot.cs @@ -0,0 +1,6 @@ +namespace BookingMonolith; + +public class BookingMonolithRoot +{ + +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Dtos/AircraftDto.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Dtos/AircraftDto.cs new file mode 100644 index 0000000..ec95561 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Dtos/AircraftDto.cs @@ -0,0 +1,3 @@ +namespace BookingMonolith.Flight.Aircrafts.Dtos; + +public record AircraftDto(long Id, string Name, string Model, int ManufacturingYear); diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs new file mode 100644 index 0000000..a313d41 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs @@ -0,0 +1,11 @@ +using System.Net; +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Aircrafts.Exceptions; + +public class AircraftAlreadyExistException : AppException +{ + public AircraftAlreadyExistException() : base("Aircraft already exist!", HttpStatusCode.Conflict) + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidAircraftIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidAircraftIdException.cs new file mode 100644 index 0000000..7cd4f1c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidAircraftIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Aircrafts.Exceptions; + +public class InvalidAircraftIdException : BadRequestException +{ + public InvalidAircraftIdException(Guid aircraftId) + : base($"AircraftId: '{aircraftId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidManufacturingYearException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidManufacturingYearException.cs new file mode 100644 index 0000000..d8cd52e --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidManufacturingYearException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Aircrafts.Exceptions; + +public class InvalidManufacturingYearException : BadRequestException +{ + public InvalidManufacturingYearException() : base("ManufacturingYear must be greater than 1900") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidModelException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidModelException.cs new file mode 100644 index 0000000..de04b34 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidModelException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Aircrafts.Exceptions; + +public class InvalidModelException : BadRequestException +{ + public InvalidModelException() : base("Model cannot be empty or whitespace.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidNameException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidNameException.cs new file mode 100644 index 0000000..28f5197 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Exceptions/InvalidNameException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Aircrafts.Exceptions; + +public class InvalidNameException : BadRequestException +{ + public InvalidNameException() : base("Name cannot be empty or whitespace.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/AircraftMappings.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/AircraftMappings.cs new file mode 100644 index 0000000..633c694 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/AircraftMappings.cs @@ -0,0 +1,24 @@ +using BookingMonolith.Flight.Aircrafts.Features.CreatingAircraft.V1; +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using Mapster; +using MassTransit; + +namespace BookingMonolith.Flight.Aircrafts.Features; + +public class AircraftMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.AircraftId, s => AircraftId.Of(s.Id)); + + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.AircraftId, s => AircraftId.Of(s.Id.Value)); + + config.NewConfig() + .ConstructUsing(x => new CreatingAircraft.V1.CreateAircraft(x.Name, x.Model, x.ManufacturingYear)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs new file mode 100644 index 0000000..1ec450f --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs @@ -0,0 +1,103 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Aircrafts.Exceptions; +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using BookingMonolith.Flight.Data; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Flight.Aircrafts.Features.CreatingAircraft.V1; + +public record CreateAircraft(string Name, string Model, int ManufacturingYear) : ICommand, + IInternalCommand +{ + public Guid Id { get; init; } = NewId.NextGuid(); +} + +public record CreateAircraftResult(AircraftId Id); + +public record AircraftCreatedDomainEvent + (Guid Id, string Name, string Model, int ManufacturingYear, bool IsDeleted) : IDomainEvent; + +public record CreateAircraftRequestDto(string Name, string Model, int ManufacturingYear); + +public record CreateAircraftResponseDto(Guid Id); + +public class CreateAircraftEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/aircraft", async (CreateAircraftRequestDto request, + IMediator mediator, IMapper mapper, + CancellationToken cancellationToken) => + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("CreateAircraft") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Create Aircraft") + .WithDescription("Create Aircraft") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class CreateAircraftValidator : AbstractValidator +{ + public CreateAircraftValidator() + { + RuleFor(x => x.Model).NotEmpty().WithMessage("Model is required"); + RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required"); + RuleFor(x => x.ManufacturingYear).NotEmpty().WithMessage("ManufacturingYear is required"); + } +} + +internal class CreateAircraftHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + + public CreateAircraftHandler(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateAircraft request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var aircraft = await _flightDbContext.Aircraft.SingleOrDefaultAsync( + a => a.Model.Value == request.Model, cancellationToken); + + if (aircraft is not null) + { + throw new AircraftAlreadyExistException(); + } + + var aircraftEntity = Aircraft.Create(AircraftId.Of(request.Id), Name.Of(request.Name), Model.Of(request.Model), ManufacturingYear.Of(request.ManufacturingYear)); + + var newAircraft = (await _flightDbContext.Aircraft.AddAsync(aircraftEntity, cancellationToken)).Entity; + + return new CreateAircraftResult(newAircraft.Id); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs new file mode 100644 index 0000000..11e4337 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs @@ -0,0 +1,48 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Aircrafts.Exceptions; +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Data; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Aircrafts.Features.CreatingAircraft.V1; + +public record CreateAircraftMongo(Guid Id, string Name, string Model, int ManufacturingYear, bool IsDeleted = false) : InternalCommand; + +internal class CreateAircraftMongoHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public CreateAircraftMongoHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(CreateAircraftMongo request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var aircraftReadModel = _mapper.Map(request); + + var aircraft = await _flightReadDbContext.Aircraft.AsQueryable() + .FirstOrDefaultAsync(x => x.AircraftId == aircraftReadModel.AircraftId && + !x.IsDeleted, cancellationToken); + + if (aircraft is not null) + { + throw new AircraftAlreadyExistException(); + } + + await _flightReadDbContext.Aircraft.InsertOneAsync(aircraftReadModel, cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/Aircraft.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/Aircraft.cs new file mode 100644 index 0000000..bed0ddb --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/Aircraft.cs @@ -0,0 +1,34 @@ +using BookingMonolith.Flight.Aircrafts.Features.CreatingAircraft.V1; +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using BuildingBlocks.Core.Model; + +namespace BookingMonolith.Flight.Aircrafts.Models; + +public record Aircraft : Aggregate +{ + public Name Name { get; private set; } = default!; + public Model Model { get; private set; } = default!; + public ManufacturingYear ManufacturingYear { get; private set; } = default!; + + public static Aircraft Create(AircraftId id, Name name, Model model, ManufacturingYear manufacturingYear, bool isDeleted = false) + { + var aircraft = new Aircraft + { + Id = id, + Name = name, + Model = model, + ManufacturingYear = manufacturingYear + }; + + var @event = new AircraftCreatedDomainEvent( + aircraft.Id, + aircraft.Name, + aircraft.Model, + aircraft.ManufacturingYear, + isDeleted); + + aircraft.AddDomainEvent(@event); + + return aircraft; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/AircraftReadModel.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/AircraftReadModel.cs new file mode 100644 index 0000000..e4c7f3a --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/Models/AircraftReadModel.cs @@ -0,0 +1,11 @@ +namespace BookingMonolith.Flight.Aircrafts.Models; + +public class AircraftReadModel +{ + public required Guid Id { get; init; } + public required Guid AircraftId { get; init; } + public required string Name { get; init; } + public required string Model { get; init; } + public required int ManufacturingYear { get; init; } + public required bool IsDeleted { get; init; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/AircraftId.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/AircraftId.cs new file mode 100644 index 0000000..4fbe3cb --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/AircraftId.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Aircrafts.Exceptions; + +namespace BookingMonolith.Flight.Aircrafts.ValueObjects; + +public record AircraftId +{ + public Guid Value { get; } + + private AircraftId(Guid value) + { + Value = value; + } + + public static AircraftId Of(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAircraftIdException(value); + } + + return new AircraftId(value); + } + + public static implicit operator Guid(AircraftId aircraftId) + { + return aircraftId.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/ManufacturingYear.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/ManufacturingYear.cs new file mode 100644 index 0000000..63951b9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/ManufacturingYear.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Aircrafts.Exceptions; + +namespace BookingMonolith.Flight.Aircrafts.ValueObjects; + +public record ManufacturingYear +{ + public int Value { get; } + + private ManufacturingYear(int value) + { + Value = value; + } + + public static ManufacturingYear Of(int value) + { + if (value < 1900) + { + throw new InvalidManufacturingYearException(); + } + + return new ManufacturingYear(value); + } + + public static implicit operator int(ManufacturingYear manufacturingYear) + { + return manufacturingYear.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Model.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Model.cs new file mode 100644 index 0000000..f13b6af --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Model.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Aircrafts.Exceptions; + +namespace BookingMonolith.Flight.Aircrafts.ValueObjects; + +public record Model +{ + public string Value { get; } + + private Model(string value) + { + Value = value; + } + + public static Model Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidModelException(); + } + + return new Model(value); + } + + public static implicit operator string(Model model) + { + return model.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Name.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Name.cs new file mode 100644 index 0000000..22136fc --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Aircrafts/ValueObjects/Name.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Aircrafts.Exceptions; + +namespace BookingMonolith.Flight.Aircrafts.ValueObjects; + +public record Name +{ + public string Value { get; } + + private Name(string value) + { + Value = value; + } + + public static Name Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidNameException(); + } + + return new Name(value); + } + + public static implicit operator string(Name name) + { + return name.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Dtos/AirportDto.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Dtos/AirportDto.cs new file mode 100644 index 0000000..db55c61 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Dtos/AirportDto.cs @@ -0,0 +1,3 @@ +namespace BookingMonolith.Flight.Airports.Dtos; + +public record AirportDto(long Id, string Name, string Address, string Code); diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs new file mode 100644 index 0000000..8a9fd76 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Airports.Exceptions; + +public class AirportAlreadyExistException : ConflictException +{ + public AirportAlreadyExistException(int? code = default) : base("Airport already exist!", code) + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAddressException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAddressException.cs new file mode 100644 index 0000000..66785ee --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAddressException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Airports.Exceptions; + +public class InvalidAddressException : BadRequestException +{ + public InvalidAddressException() : base("Address cannot be empty or whitespace.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAirportIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAirportIdException.cs new file mode 100644 index 0000000..8d9004c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidAirportIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Airports.Exceptions; + +public class InvalidAirportIdException : BadRequestException +{ + public InvalidAirportIdException(Guid airportId) + : base($"airportId: '{airportId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidCodeException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidCodeException.cs new file mode 100644 index 0000000..5fb7d87 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidCodeException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Airports.Exceptions; + +public class InvalidCodeException : BadRequestException +{ + public InvalidCodeException() : base("Code cannot be empty or whitespace.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidNameException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidNameException.cs new file mode 100644 index 0000000..922b619 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Exceptions/InvalidNameException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Airports.Exceptions; + +public class InvalidNameException : BadRequestException +{ + public InvalidNameException() : base("Name cannot be empty or whitespace.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/AirportMappings.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/AirportMappings.cs new file mode 100644 index 0000000..ffb4324 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/AirportMappings.cs @@ -0,0 +1,23 @@ +using BookingMonolith.Flight.Airports.Features.CreatingAirport.V1; +using BookingMonolith.Flight.Airports.Models; +using Mapster; +using MassTransit; + +namespace BookingMonolith.Flight.Airports.Features; + +public class AirportMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.AirportId, s => s.Id); + + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.AirportId, s => s.Id.Value); + + config.NewConfig() + .ConstructUsing(x => new CreateAirport(x.Name, x.Address, x.Code)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirport.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirport.cs new file mode 100644 index 0000000..c75ac54 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirport.cs @@ -0,0 +1,101 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Airports.Exceptions; +using BookingMonolith.Flight.Airports.ValueObjects; +using BookingMonolith.Flight.Data; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Flight.Airports.Features.CreatingAirport.V1; + +public record CreateAirport(string Name, string Address, string Code) : ICommand, IInternalCommand +{ + public Guid Id { get; init; } = NewId.NextGuid(); +} + +public record CreateAirportResult(Guid Id); + +public record AirportCreatedDomainEvent + (Guid Id, string Name, string Address, string Code, bool IsDeleted) : IDomainEvent; + +public record CreateAirportRequestDto(string Name, string Address, string Code); + +public record CreateAirportResponseDto(Guid Id); + +public class CreateAirportEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/airport", async (CreateAirportRequestDto request, + IMediator mediator, IMapper mapper, + CancellationToken cancellationToken) => + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("CreateAirport") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Create Airport") + .WithDescription("Create Airport") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class CreateAirportValidator : AbstractValidator +{ + public CreateAirportValidator() + { + RuleFor(x => x.Code).NotEmpty().WithMessage("Code is required"); + RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required"); + RuleFor(x => x.Address).NotEmpty().WithMessage("Address is required"); + } +} + +internal class CreateAirportHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + + public CreateAirportHandler(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateAirport request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var airport = + await _flightDbContext.Airports.SingleOrDefaultAsync(x => x.Code.Value == request.Code, cancellationToken); + + if (airport is not null) + { + throw new AirportAlreadyExistException(); + } + + var airportEntity = Models.Airport.Create(AirportId.Of(request.Id), Name.Of(request.Name), Address.Of(request.Address), Code.Of(request.Code)); + + var newAirport = (await _flightDbContext.Airports.AddAsync(airportEntity, cancellationToken)).Entity; + + return new CreateAirportResult(newAirport.Id); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs new file mode 100644 index 0000000..1a961c8 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs @@ -0,0 +1,48 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Airports.Exceptions; +using BookingMonolith.Flight.Airports.Models; +using BookingMonolith.Flight.Data; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Airports.Features.CreatingAirport.V1; + +public record CreateAirportMongo(Guid Id, string Name, string Address, string Code, bool IsDeleted = false) : InternalCommand; + +internal class CreateAirportMongoHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public CreateAirportMongoHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(CreateAirportMongo request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var airportReadModel = _mapper.Map(request); + + var aircraft = await _flightReadDbContext.Airport.AsQueryable() + .FirstOrDefaultAsync(x => x.AirportId == airportReadModel.AirportId && + !x.IsDeleted, cancellationToken); + + if (aircraft is not null) + { + throw new AirportAlreadyExistException(); + } + + await _flightReadDbContext.Airport.InsertOneAsync(airportReadModel, cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/Airport.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/Airport.cs new file mode 100644 index 0000000..fb25307 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/Airport.cs @@ -0,0 +1,34 @@ +using BookingMonolith.Flight.Airports.Features.CreatingAirport.V1; +using BookingMonolith.Flight.Airports.ValueObjects; +using BuildingBlocks.Core.Model; + +namespace BookingMonolith.Flight.Airports.Models; + +public record Airport : Aggregate +{ + public Name Name { get; private set; } = default!; + public Address Address { get; private set; } = default!; + public Code Code { get; private set; } = default!; + + public static Airport Create(AirportId id, Name name, Address address, Code code, bool isDeleted = false) + { + var airport = new Airport + { + Id = id, + Name = name, + Address = address, + Code = code + }; + + var @event = new AirportCreatedDomainEvent( + airport.Id, + airport.Name, + airport.Address, + airport.Code, + isDeleted); + + airport.AddDomainEvent(@event); + + return airport; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/AirportReadModel.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/AirportReadModel.cs new file mode 100644 index 0000000..a0afa00 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/Models/AirportReadModel.cs @@ -0,0 +1,11 @@ +namespace BookingMonolith.Flight.Airports.Models; + +public class AirportReadModel +{ + public required Guid Id { get; init; } + public required Guid AirportId { get; init; } + public required string Name { get; init; } + public string Address { get; init; } + public required string Code { get; init; } + public required bool IsDeleted { get; init; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Address.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Address.cs new file mode 100644 index 0000000..b4d1dfd --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Address.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Airports.Exceptions; + +namespace BookingMonolith.Flight.Airports.ValueObjects; + +public class Address +{ + public string Value { get; } + + private Address(string value) + { + Value = value; + } + + public static Address Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidAddressException(); + } + + return new Address(value); + } + + public static implicit operator string(Address address) + { + return address.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/AirportId.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/AirportId.cs new file mode 100644 index 0000000..a203d75 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/AirportId.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Airports.Exceptions; + +namespace BookingMonolith.Flight.Airports.ValueObjects; + +public record AirportId +{ + public Guid Value { get; } + + private AirportId(Guid value) + { + Value = value; + } + + public static AirportId Of(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAirportIdException(value); + } + + return new AirportId(value); + } + + public static implicit operator Guid(AirportId airportId) + { + return airportId.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Code.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Code.cs new file mode 100644 index 0000000..d6560d9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Code.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Airports.Exceptions; + +namespace BookingMonolith.Flight.Airports.ValueObjects; + +public record Code +{ + public string Value { get; } + + private Code(string value) + { + Value = value; + } + + public static Code Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidCodeException(); + } + + return new Code(value); + } + + public static implicit operator string(Code code) + { + return code.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Name.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Name.cs new file mode 100644 index 0000000..7e6e1e9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Airports/ValueObjects/Name.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Airports.Exceptions; + +namespace BookingMonolith.Flight.Airports.ValueObjects; + +public record Name +{ + public string Value { get; } + + private Name(string value) + { + Value = value; + } + + public static Name Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidNameException(); + } + + return new Name(value); + } + + public static implicit operator string(Name name) + { + return name.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AircraftConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AircraftConfiguration.cs new file mode 100644 index 0000000..53d6634 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AircraftConfiguration.cs @@ -0,0 +1,55 @@ +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Flight.Data.Configurations; + +[RegisterFlightConfiguration] +public class AircraftConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + builder.ToTable(nameof(Aircraft)); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever() + .HasConversion(aircraftId => aircraftId.Value, dbId => AircraftId.Of(dbId)); + + builder.Property(r => r.Version).IsConcurrencyToken(); + + builder.OwnsOne( + x => x.Name, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Aircraft.Name)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.Model, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Aircraft.Model)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.ManufacturingYear, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Aircraft.ManufacturingYear)) + .HasMaxLength(5) + .IsRequired(); + } + ); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AirportConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AirportConfiguration.cs new file mode 100644 index 0000000..24812b6 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/AirportConfiguration.cs @@ -0,0 +1,56 @@ +using BookingMonolith.Flight.Airports.Models; +using BookingMonolith.Flight.Airports.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Flight.Data.Configurations; + +[RegisterFlightConfiguration] +public class AirportConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + builder.ToTable(nameof(Airport)); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever() + .HasConversion(airportId => airportId.Value, dbId => AirportId.Of(dbId)); + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + + + builder.OwnsOne( + x => x.Name, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Airport.Name)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.Address, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Airport.Address)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.Code, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Airport.Code)) + .HasMaxLength(50) + .IsRequired(); + } + ); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/FlightConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/FlightConfiguration.cs new file mode 100644 index 0000000..b3f3b36 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/FlightConfiguration.cs @@ -0,0 +1,112 @@ +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Airports.Models; +using BookingMonolith.Flight.Flights.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + + +namespace BookingMonolith.Flight.Data.Configurations; + +[RegisterFlightConfiguration] +public class FlightConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + RelationalEntityTypeBuilderExtensions.ToTable((EntityTypeBuilder)builder, nameof(Flight)); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever() + .HasConversion(flight => flight.Value, dbId => FlightId.Of(dbId)); + + builder.Property(r => r.Version).IsConcurrencyToken(); + + + builder.OwnsOne( + x => x.FlightNumber, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Flights.Models.Flight.FlightNumber)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder + .HasOne() + .WithMany() + .HasForeignKey(p => p.AircraftId) + .IsRequired(); + + builder + .HasOne() + .WithMany() + .HasForeignKey(d => d.DepartureAirportId) + .IsRequired(); + + builder + .HasOne() + .WithMany() + .HasForeignKey(d => d.ArriveAirportId) + .IsRequired(); + + + builder.OwnsOne( + x => x.DurationMinutes, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Flights.Models.Flight.DurationMinutes)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder.Property(x => x.Status) + .HasDefaultValue(Flights.Enums.FlightStatus.Unknown) + .HasConversion( + x => x.ToString(), + x => (Flights.Enums.FlightStatus)Enum.Parse(typeof(Flights.Enums.FlightStatus), x)); + + builder.OwnsOne( + x => x.Price, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Flights.Models.Flight.Price)) + .HasMaxLength(10) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.ArriveDate, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Flights.Models.Flight.ArriveDate)) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.DepartureDate, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Flights.Models.Flight.DepartureDate)) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.FlightDate, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Flights.Models.Flight.FlightDate)) + .IsRequired(); + } + ); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/SeatConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/SeatConfiguration.cs new file mode 100644 index 0000000..e76ed31 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Configurations/SeatConfiguration.cs @@ -0,0 +1,49 @@ +using BookingMonolith.Flight.Seats.Models; +using BookingMonolith.Flight.Seats.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Flight.Data.Configurations; + +[RegisterFlightConfiguration] +public class SeatConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Seat)); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever() + .HasConversion(seatId => seatId.Value, dbId => SeatId.Of(dbId)); + + builder.Property(r => r.Version).IsConcurrencyToken(); + + builder.OwnsOne( + x => x.SeatNumber, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Seat.SeatNumber)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder + .HasOne() + .WithMany() + .HasForeignKey(p => p.FlightId); + + builder.Property(x => x.Class) + .HasDefaultValue(Seats.Enums.SeatClass.Unknown) + .HasConversion( + x => x.ToString(), + x => (Seats.Enums.SeatClass)Enum.Parse(typeof(Seats.Enums.SeatClass), x)); + + builder.Property(x => x.Type) + .HasDefaultValue(Seats.Enums.SeatType.Unknown) + .HasConversion( + x => x.ToString(), + x => (Seats.Enums.SeatType)Enum.Parse(typeof(Seats.Enums.SeatType), x)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/DesignTimeDbContextFactory.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..018ec62 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/DesignTimeDbContextFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace BookingMonolith.Flight.Data +{ + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public FlightDbContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + builder.UseNpgsql("Server=localhost;Port=5432;Database=booking_monolith;User Id=postgres;Password=postgres;Include Error Detail=true") + .UseSnakeCaseNamingConvention(); + return new FlightDbContext(builder.Options); + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/EfTxFlightBehavior.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/EfTxFlightBehavior.cs new file mode 100644 index 0000000..44c706b --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/EfTxFlightBehavior.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using System.Transactions; +using BuildingBlocks.Core; +using BuildingBlocks.PersistMessageProcessor; +using BuildingBlocks.Polly; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace BookingMonolith.Flight.Data; + +public class EfTxFlightBehavior : IPipelineBehavior +where TRequest : notnull, IRequest +where TResponse : notnull +{ + private readonly ILogger> _logger; + private readonly FlightDbContext _flightDbContext; + private readonly IPersistMessageDbContext _persistMessageDbContext; + private readonly IEventDispatcher _eventDispatcher; + + public EfTxFlightBehavior( + ILogger> logger, + FlightDbContext flightDbContext, + IPersistMessageDbContext persistMessageDbContext, + IEventDispatcher eventDispatcher + ) + { + _logger = logger; + _flightDbContext = flightDbContext; + _persistMessageDbContext = persistMessageDbContext; + _eventDispatcher = eventDispatcher; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) + { + _logger.LogInformation( + "{Prefix} Handled command {MediatrRequest}", + GetType().Name, + typeof(TRequest).FullName); + + _logger.LogDebug( + "{Prefix} Handled command {MediatrRequest} with content {RequestContent}", + GetType().Name, + typeof(TRequest).FullName, + JsonSerializer.Serialize(request)); + + var response = await next(); + + _logger.LogInformation( + "{Prefix} Executed the {MediatrRequest} request", + GetType().Name, + typeof(TRequest).FullName); + + while (true) + { + var domainEvents = _flightDbContext.GetDomainEvents(); + + if (domainEvents is null || !domainEvents.Any()) + { + return response; + } + + _logger.LogInformation( + "{Prefix} Open the transaction for {MediatrRequest}", + GetType().Name, + typeof(TRequest).FullName); + + using var scope = new TransactionScope( + TransactionScopeOption.Required, + new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }, + TransactionScopeAsyncFlowOption.Enabled); + + await _eventDispatcher.SendAsync( + domainEvents.ToArray(), + typeof(TRequest), + cancellationToken); + + // Save data to database with some retry policy in distributed transaction + await _flightDbContext.RetryOnFailure( + async () => + { + await _flightDbContext.SaveChangesAsync(cancellationToken); + }); + + // Save data to database with some retry policy in distributed transaction + await _persistMessageDbContext.RetryOnFailure( + async () => + { + await _persistMessageDbContext.SaveChangesAsync(cancellationToken); + }); + + scope.Complete(); + + return response; + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightDbContext.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightDbContext.cs new file mode 100644 index 0000000..811848d --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightDbContext.cs @@ -0,0 +1,44 @@ +using System.Reflection; +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Airports.Models; +using BookingMonolith.Flight.Seats.Models; +using BuildingBlocks.EFCore; +using BuildingBlocks.Web; +using Humanizer; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace BookingMonolith.Flight.Data; + +public sealed class FlightDbContext : AppDbContextBase +{ + public FlightDbContext(DbContextOptions options, ICurrentUserProvider? currentUserProvider = null, + ILogger? logger = null) : base( + options, currentUserProvider, logger) + { + } + + public DbSet Flights => Set(); + public DbSet Airports => Set(); + public DbSet Aircraft => Set(); + public DbSet Seats => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + var types = typeof(FlightRoot).Assembly.GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .ToList(); + + foreach (var type in types) + { + dynamic configuration = Activator.CreateInstance(type)!; + builder.ApplyConfiguration(configuration); + } + + builder.HasDefaultSchema(nameof(Flight).Underscore()); + builder.FilterSoftDeletedProperties(); + builder.ToSnakeCaseTables(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightReadDbContext.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightReadDbContext.cs new file mode 100644 index 0000000..2fda15d --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/FlightReadDbContext.cs @@ -0,0 +1,26 @@ +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Airports.Models; +using BookingMonolith.Flight.Flights.Models; +using BookingMonolith.Flight.Seats.Models; +using BuildingBlocks.Mongo; +using Humanizer; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace BookingMonolith.Flight.Data; + +public class FlightReadDbContext : MongoDbContext +{ + public FlightReadDbContext(IOptions options) : base(options) + { + Flight = GetCollection(nameof(Flight).Underscore()); + Aircraft = GetCollection(nameof(Aircraft).Underscore()); + Airport = GetCollection(nameof(Airport).Underscore()); + Seat = GetCollection(nameof(Seat).Underscore()); + } + + public IMongoCollection Flight { get; } + public IMongoCollection Aircraft { get; } + public IMongoCollection Airport { get; } + public IMongoCollection Seat { get; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.Designer.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.Designer.cs new file mode 100644 index 0000000..0a74e59 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.Designer.cs @@ -0,0 +1,584 @@ +// +using System; +using BookingMonolith.Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BookingMonolith.Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20250407215512_initial")] + partial class initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("flight") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookingMonolith.Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_aircraft"); + + b.ToTable("aircraft", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_airport"); + + b.ToTable("airport", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("aircraft_id"); + + b.Property("ArriveAirportId") + .HasColumnType("uuid") + .HasColumnName("arrive_airport_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("DepartureAirportId") + .HasColumnType("uuid") + .HasColumnName("departure_airport_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("status"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_flight"); + + b.HasIndex("AircraftId") + .HasDatabaseName("ix_flight_aircraft_id"); + + b.HasIndex("ArriveAirportId") + .HasDatabaseName("ix_flight_arrive_airport_id"); + + b.HasIndex("DepartureAirportId") + .HasDatabaseName("ix_flight_departure_airport_id"); + + b.ToTable("flight", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Class") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("class"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("flight_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("type"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_seat"); + + b.HasIndex("FlightId") + .HasDatabaseName("ix_seat_flight_id"); + + b.ToTable("seat", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Aircrafts.Models.Aircraft", b => + { + b.OwnsOne("BookingMonolith.Flight.Aircrafts.ValueObjects.ManufacturingYear", "ManufacturingYear", b1 => + { + b1.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(5) + .HasColumnType("integer") + .HasColumnName("manufacturing_year"); + + b1.HasKey("AircraftId") + .HasName("pk_aircraft"); + + b1.ToTable("aircraft", "flight"); + + b1.WithOwner() + .HasForeignKey("AircraftId") + .HasConstraintName("fk_aircraft_aircraft_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Aircrafts.ValueObjects.Model", "Model", b1 => + { + b1.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("model"); + + b1.HasKey("AircraftId") + .HasName("pk_aircraft"); + + b1.ToTable("aircraft", "flight"); + + b1.WithOwner() + .HasForeignKey("AircraftId") + .HasConstraintName("fk_aircraft_aircraft_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Aircrafts.ValueObjects.Name", "Name", b1 => + { + b1.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b1.HasKey("AircraftId") + .HasName("pk_aircraft"); + + b1.ToTable("aircraft", "flight"); + + b1.WithOwner() + .HasForeignKey("AircraftId") + .HasConstraintName("fk_aircraft_aircraft_id"); + }); + + b.Navigation("ManufacturingYear") + .IsRequired(); + + b.Navigation("Model") + .IsRequired(); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Airports.Models.Airport", b => + { + b.OwnsOne("BookingMonolith.Flight.Airports.ValueObjects.Address", "Address", b1 => + { + b1.Property("AirportId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("address"); + + b1.HasKey("AirportId") + .HasName("pk_airport"); + + b1.ToTable("airport", "flight"); + + b1.WithOwner() + .HasForeignKey("AirportId") + .HasConstraintName("fk_airport_airport_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Airports.ValueObjects.Code", "Code", b1 => + { + b1.Property("AirportId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b1.HasKey("AirportId") + .HasName("pk_airport"); + + b1.ToTable("airport", "flight"); + + b1.WithOwner() + .HasForeignKey("AirportId") + .HasConstraintName("fk_airport_airport_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Airports.ValueObjects.Name", "Name", b1 => + { + b1.Property("AirportId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b1.HasKey("AirportId") + .HasName("pk_airport"); + + b1.ToTable("airport", "flight"); + + b1.WithOwner() + .HasForeignKey("AirportId") + .HasConstraintName("fk_airport_airport_id"); + }); + + b.Navigation("Address") + .IsRequired(); + + b.Navigation("Code") + .IsRequired(); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Flights.Models.Flight", b => + { + b.HasOne("BookingMonolith.Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_flight_aircraft_aircraft_id"); + + b.HasOne("BookingMonolith.Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_flight_airport_arrive_airport_id"); + + b.HasOne("BookingMonolith.Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("DepartureAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_flight_airport_departure_airport_id"); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.ArriveDate", "ArriveDate", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasColumnType("timestamp with time zone") + .HasColumnName("arrive_date"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.DepartureDate", "DepartureDate", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasColumnType("timestamp with time zone") + .HasColumnName("departure_date"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.DurationMinutes", "DurationMinutes", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(50) + .HasColumnType("numeric") + .HasColumnName("duration_minutes"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.FlightDate", "FlightDate", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasColumnType("timestamp with time zone") + .HasColumnName("flight_date"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.FlightNumber", "FlightNumber", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("flight_number"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.Price", "Price", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(10) + .HasColumnType("numeric") + .HasColumnName("price"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.Navigation("ArriveDate") + .IsRequired(); + + b.Navigation("DepartureDate") + .IsRequired(); + + b.Navigation("DurationMinutes") + .IsRequired(); + + b.Navigation("FlightDate") + .IsRequired(); + + b.Navigation("FlightNumber") + .IsRequired(); + + b.Navigation("Price") + .IsRequired(); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Seats.Models.Seat", b => + { + b.HasOne("BookingMonolith.Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_seat_flight_flight_id"); + + b.OwnsOne("BookingMonolith.Flight.Seats.ValueObjects.SeatNumber", "SeatNumber", b1 => + { + b1.Property("SeatId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("seat_number"); + + b1.HasKey("SeatId") + .HasName("pk_seat"); + + b1.ToTable("seat", "flight"); + + b1.WithOwner() + .HasForeignKey("SeatId") + .HasConstraintName("fk_seat_seat_id"); + }); + + b.Navigation("SeatNumber") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.cs new file mode 100644 index 0000000..c72b884 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/20250407215512_initial.cs @@ -0,0 +1,182 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BookingMonolith.Flight.Data.Migrations +{ + /// + public partial class initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "flight"); + + migrationBuilder.CreateTable( + name: "aircraft", + schema: "flight", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + model = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + manufacturing_year = table.Column(type: "integer", maxLength: 5, nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "bigint", nullable: true), + last_modified = table.Column(type: "timestamp with time zone", nullable: true), + last_modified_by = table.Column(type: "bigint", nullable: true), + is_deleted = table.Column(type: "boolean", nullable: false), + version = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_aircraft", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "airport", + schema: "flight", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + address = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + code = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "bigint", nullable: true), + last_modified = table.Column(type: "timestamp with time zone", nullable: true), + last_modified_by = table.Column(type: "bigint", nullable: true), + is_deleted = table.Column(type: "boolean", nullable: false), + version = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_airport", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "flight", + schema: "flight", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + flight_number = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + aircraft_id = table.Column(type: "uuid", nullable: false), + departure_airport_id = table.Column(type: "uuid", nullable: false), + arrive_airport_id = table.Column(type: "uuid", nullable: false), + duration_minutes = table.Column(type: "numeric", maxLength: 50, nullable: false), + status = table.Column(type: "text", nullable: false, defaultValue: "Unknown"), + price = table.Column(type: "numeric", maxLength: 10, nullable: false), + arrive_date = table.Column(type: "timestamp with time zone", nullable: false), + departure_date = table.Column(type: "timestamp with time zone", nullable: false), + flight_date = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "bigint", nullable: true), + last_modified = table.Column(type: "timestamp with time zone", nullable: true), + last_modified_by = table.Column(type: "bigint", nullable: true), + is_deleted = table.Column(type: "boolean", nullable: false), + version = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_flight", x => x.id); + table.ForeignKey( + name: "fk_flight_aircraft_aircraft_id", + column: x => x.aircraft_id, + principalSchema: "flight", + principalTable: "aircraft", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_flight_airport_arrive_airport_id", + column: x => x.arrive_airport_id, + principalSchema: "flight", + principalTable: "airport", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_flight_airport_departure_airport_id", + column: x => x.departure_airport_id, + principalSchema: "flight", + principalTable: "airport", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "seat", + schema: "flight", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + seat_number = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + type = table.Column(type: "text", nullable: false, defaultValue: "Unknown"), + @class = table.Column(name: "class", type: "text", nullable: false, defaultValue: "Unknown"), + flight_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "bigint", nullable: true), + last_modified = table.Column(type: "timestamp with time zone", nullable: true), + last_modified_by = table.Column(type: "bigint", nullable: true), + is_deleted = table.Column(type: "boolean", nullable: false), + version = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_seat", x => x.id); + table.ForeignKey( + name: "fk_seat_flight_flight_id", + column: x => x.flight_id, + principalSchema: "flight", + principalTable: "flight", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_flight_aircraft_id", + schema: "flight", + table: "flight", + column: "aircraft_id"); + + migrationBuilder.CreateIndex( + name: "ix_flight_arrive_airport_id", + schema: "flight", + table: "flight", + column: "arrive_airport_id"); + + migrationBuilder.CreateIndex( + name: "ix_flight_departure_airport_id", + schema: "flight", + table: "flight", + column: "departure_airport_id"); + + migrationBuilder.CreateIndex( + name: "ix_seat_flight_id", + schema: "flight", + table: "seat", + column: "flight_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "seat", + schema: "flight"); + + migrationBuilder.DropTable( + name: "flight", + schema: "flight"); + + migrationBuilder.DropTable( + name: "aircraft", + schema: "flight"); + + migrationBuilder.DropTable( + name: "airport", + schema: "flight"); + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs new file mode 100644 index 0000000..b17cc3c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs @@ -0,0 +1,581 @@ +// +using System; +using BookingMonolith.Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BookingMonolith.Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + partial class FlightDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("flight") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookingMonolith.Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_aircraft"); + + b.ToTable("aircraft", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_airport"); + + b.ToTable("airport", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("aircraft_id"); + + b.Property("ArriveAirportId") + .HasColumnType("uuid") + .HasColumnName("arrive_airport_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("DepartureAirportId") + .HasColumnType("uuid") + .HasColumnName("departure_airport_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("status"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_flight"); + + b.HasIndex("AircraftId") + .HasDatabaseName("ix_flight_aircraft_id"); + + b.HasIndex("ArriveAirportId") + .HasDatabaseName("ix_flight_arrive_airport_id"); + + b.HasIndex("DepartureAirportId") + .HasDatabaseName("ix_flight_departure_airport_id"); + + b.ToTable("flight", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Class") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("class"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("flight_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("type"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_seat"); + + b.HasIndex("FlightId") + .HasDatabaseName("ix_seat_flight_id"); + + b.ToTable("seat", "flight"); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Aircrafts.Models.Aircraft", b => + { + b.OwnsOne("BookingMonolith.Flight.Aircrafts.ValueObjects.ManufacturingYear", "ManufacturingYear", b1 => + { + b1.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(5) + .HasColumnType("integer") + .HasColumnName("manufacturing_year"); + + b1.HasKey("AircraftId") + .HasName("pk_aircraft"); + + b1.ToTable("aircraft", "flight"); + + b1.WithOwner() + .HasForeignKey("AircraftId") + .HasConstraintName("fk_aircraft_aircraft_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Aircrafts.ValueObjects.Model", "Model", b1 => + { + b1.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("model"); + + b1.HasKey("AircraftId") + .HasName("pk_aircraft"); + + b1.ToTable("aircraft", "flight"); + + b1.WithOwner() + .HasForeignKey("AircraftId") + .HasConstraintName("fk_aircraft_aircraft_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Aircrafts.ValueObjects.Name", "Name", b1 => + { + b1.Property("AircraftId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b1.HasKey("AircraftId") + .HasName("pk_aircraft"); + + b1.ToTable("aircraft", "flight"); + + b1.WithOwner() + .HasForeignKey("AircraftId") + .HasConstraintName("fk_aircraft_aircraft_id"); + }); + + b.Navigation("ManufacturingYear") + .IsRequired(); + + b.Navigation("Model") + .IsRequired(); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Airports.Models.Airport", b => + { + b.OwnsOne("BookingMonolith.Flight.Airports.ValueObjects.Address", "Address", b1 => + { + b1.Property("AirportId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("address"); + + b1.HasKey("AirportId") + .HasName("pk_airport"); + + b1.ToTable("airport", "flight"); + + b1.WithOwner() + .HasForeignKey("AirportId") + .HasConstraintName("fk_airport_airport_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Airports.ValueObjects.Code", "Code", b1 => + { + b1.Property("AirportId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b1.HasKey("AirportId") + .HasName("pk_airport"); + + b1.ToTable("airport", "flight"); + + b1.WithOwner() + .HasForeignKey("AirportId") + .HasConstraintName("fk_airport_airport_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Airports.ValueObjects.Name", "Name", b1 => + { + b1.Property("AirportId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b1.HasKey("AirportId") + .HasName("pk_airport"); + + b1.ToTable("airport", "flight"); + + b1.WithOwner() + .HasForeignKey("AirportId") + .HasConstraintName("fk_airport_airport_id"); + }); + + b.Navigation("Address") + .IsRequired(); + + b.Navigation("Code") + .IsRequired(); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Flights.Models.Flight", b => + { + b.HasOne("BookingMonolith.Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_flight_aircraft_aircraft_id"); + + b.HasOne("BookingMonolith.Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_flight_airport_arrive_airport_id"); + + b.HasOne("BookingMonolith.Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("DepartureAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_flight_airport_departure_airport_id"); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.ArriveDate", "ArriveDate", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasColumnType("timestamp with time zone") + .HasColumnName("arrive_date"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.DepartureDate", "DepartureDate", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasColumnType("timestamp with time zone") + .HasColumnName("departure_date"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.DurationMinutes", "DurationMinutes", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(50) + .HasColumnType("numeric") + .HasColumnName("duration_minutes"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.FlightDate", "FlightDate", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasColumnType("timestamp with time zone") + .HasColumnName("flight_date"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.FlightNumber", "FlightNumber", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("flight_number"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.OwnsOne("BookingMonolith.Flight.Flights.ValueObjects.Price", "Price", b1 => + { + b1.Property("FlightId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(10) + .HasColumnType("numeric") + .HasColumnName("price"); + + b1.HasKey("FlightId") + .HasName("pk_flight"); + + b1.ToTable("flight", "flight"); + + b1.WithOwner() + .HasForeignKey("FlightId") + .HasConstraintName("fk_flight_flight_id"); + }); + + b.Navigation("ArriveDate") + .IsRequired(); + + b.Navigation("DepartureDate") + .IsRequired(); + + b.Navigation("DurationMinutes") + .IsRequired(); + + b.Navigation("FlightDate") + .IsRequired(); + + b.Navigation("FlightNumber") + .IsRequired(); + + b.Navigation("Price") + .IsRequired(); + }); + + modelBuilder.Entity("BookingMonolith.Flight.Seats.Models.Seat", b => + { + b.HasOne("BookingMonolith.Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_seat_flight_flight_id"); + + b.OwnsOne("BookingMonolith.Flight.Seats.ValueObjects.SeatNumber", "SeatNumber", b1 => + { + b1.Property("SeatId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("seat_number"); + + b1.HasKey("SeatId") + .HasName("pk_seat"); + + b1.ToTable("seat", "flight"); + + b1.WithOwner() + .HasForeignKey("SeatId") + .HasConstraintName("fk_seat_seat_id"); + }); + + b.Navigation("SeatNumber") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/RegisterFlightConfigurationAttribute.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/RegisterFlightConfigurationAttribute.cs new file mode 100644 index 0000000..d0f4259 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/RegisterFlightConfigurationAttribute.cs @@ -0,0 +1,4 @@ +namespace BookingMonolith.Flight.Data; + +[AttributeUsage(AttributeTargets.Class)] +public class RegisterFlightConfigurationAttribute : Attribute { } diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/FlightDataSeeder.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/FlightDataSeeder.cs new file mode 100644 index 0000000..8719bd1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/FlightDataSeeder.cs @@ -0,0 +1,88 @@ +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Airports.Models; +using BookingMonolith.Flight.Flights.Models; +using BookingMonolith.Flight.Seats.Models; +using BuildingBlocks.EFCore; +using MapsterMapper; +using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Data.Seed; + +public class FlightDataSeeder( + FlightDbContext flightDbContext, + FlightReadDbContext flightReadDbContext, + IMapper mapper +) : IDataSeeder +{ + public async Task SeedAllAsync() + { + var pendingMigrations = await flightDbContext.Database.GetPendingMigrationsAsync(); + + if (!pendingMigrations.Any()) + { + await SeedAirportAsync(); + await SeedAircraftAsync(); + await SeedFlightAsync(); + await SeedSeatAsync(); + } + } + + private async Task SeedAirportAsync() + { + if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Airports)) + { + await flightDbContext.Airports.AddRangeAsync(InitialData.Airports); + await flightDbContext.SaveChangesAsync(); + + if (!await MongoQueryable.AnyAsync(flightReadDbContext.Airport.AsQueryable())) + { + await flightReadDbContext.Airport.InsertManyAsync(mapper.Map>(InitialData.Airports)); + } + } + } + + private async Task SeedAircraftAsync() + { + if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Aircraft)) + { + await flightDbContext.Aircraft.AddRangeAsync(InitialData.Aircrafts); + await flightDbContext.SaveChangesAsync(); + + if (!await MongoQueryable.AnyAsync(flightReadDbContext.Aircraft.AsQueryable())) + { + await flightReadDbContext.Aircraft.InsertManyAsync(mapper.Map>(InitialData.Aircrafts)); + } + } + } + + + private async Task SeedSeatAsync() + { + if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Seats)) + { + await flightDbContext.Seats.AddRangeAsync(InitialData.Seats); + await flightDbContext.SaveChangesAsync(); + + if (!await MongoQueryable.AnyAsync(flightReadDbContext.Seat.AsQueryable())) + { + await flightReadDbContext.Seat.InsertManyAsync(mapper.Map>(InitialData.Seats)); + } + } + } + + private async Task SeedFlightAsync() + { + if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Flights)) + { + await flightDbContext.Flights.AddRangeAsync(InitialData.Flights); + await flightDbContext.SaveChangesAsync(); + + if (!await MongoQueryable.AnyAsync(flightReadDbContext.Flight.AsQueryable())) + { + await flightReadDbContext.Flight.InsertManyAsync(mapper.Map>(InitialData.Flights)); + } + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/InitialData.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/InitialData.cs new file mode 100644 index 0000000..9256f81 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/Seed/InitialData.cs @@ -0,0 +1,58 @@ +using BookingMonolith.Flight.Aircrafts.Models; +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using BookingMonolith.Flight.Airports.Models; +using BookingMonolith.Flight.Airports.ValueObjects; +using BookingMonolith.Flight.Flights.ValueObjects; +using BookingMonolith.Flight.Seats.Models; +using BookingMonolith.Flight.Seats.ValueObjects; +using MassTransit; + +namespace BookingMonolith.Flight.Data.Seed; + +using AirportName = Airports.ValueObjects.Name; +using Name = Aircrafts.ValueObjects.Name; + +public static class InitialData +{ + public static List Airports { get; } + public static List Aircrafts { get; } + public static List Seats { get; } + public static List Flights { get; } + + + static InitialData() + { + Airports = new List + { + Airport.Create(AirportId.Of(new Guid("3c5c0000-97c6-fc34-a0cb-08db322230c8")), AirportName.Of("Lisbon International Airport"), Address.Of("LIS"), Code.Of("12988")), + Airport.Create(AirportId.Of(new Guid("3c5c0000-97c6-fc34-fc3c-08db322230c8")), AirportName.Of("Sao Paulo International Airport"), Address.Of("BRZ"), Code.Of("11200")) + }; + + Aircrafts = new List + { + Aircraft.Create(AircraftId.Of(new Guid("3c5c0000-97c6-fc34-fcd3-08db322230c8")), Name.Of("Boeing 737"), Model.Of("B737"), ManufacturingYear.Of(2005)), + Aircraft.Create(AircraftId.Of(new Guid("3c5c0000-97c6-fc34-2e04-08db322230c9")), Name.Of("Airbus 300"), Model.Of("A300"), ManufacturingYear.Of(2000)), + Aircraft.Create(AircraftId.Of(new Guid("3c5c0000-97c6-fc34-2e11-08db322230c9")), Name.Of("Airbus 320"), Model.Of("A320"), ManufacturingYear.Of(2003)) + }; + + + Flights = new List + { + Flight.Flights.Models.Flight.Create(FlightId.Of(new Guid("3c5c0000-97c6-fc34-2eb9-08db322230c9")), FlightNumber.Of("BD467"), AircraftId.Of(Aircrafts.First().Id.Value), AirportId.Of( Airports.First().Id), DepartureDate.Of(new DateTime(2022, 1, 31, 12, 0, 0)), + ArriveDate.Of(new DateTime(2022, 1, 31, 14, 0, 0)), + AirportId.Of(Airports.Last().Id), DurationMinutes.Of(120m), + FlightDate.Of(new DateTime(2022, 1, 31, 13, 0, 0)), Flight.Flights.Enums.FlightStatus.Completed, + Price.Of(8000)) + }; + + Seats = new List + { + Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of( "12A"),Flight.Seats.Enums.SeatType.Window, Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid)Flights.First().Id)), + Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12B"), Flight.Seats.Enums.SeatType.Window, Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid)Flights.First().Id)), + Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12C"), Flight.Seats.Enums.SeatType.Middle, Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)), + Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12D"), Flight.Seats.Enums.SeatType.Middle, Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)), + Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12E"), Flight.Seats.Enums.SeatType.Aisle, Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)), + Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12F"), Flight.Seats.Enums.SeatType.Aisle, Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)) + }; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/readme.md b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/readme.md new file mode 100644 index 0000000..4b712d3 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Data/readme.md @@ -0,0 +1,2 @@ +dotnet ef migrations add initial --context FlightDbContext -o "Flight\Data\Migrations" +dotnet ef database update --context FlightDbContext diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightEventMapper.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightEventMapper.cs new file mode 100644 index 0000000..911683c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightEventMapper.cs @@ -0,0 +1,49 @@ +using BookingMonolith.Flight.Aircrafts.Features.CreatingAircraft.V1; +using BookingMonolith.Flight.Airports.Features.CreatingAirport.V1; +using BookingMonolith.Flight.Flights.Features.CreatingFlight.V1; +using BookingMonolith.Flight.Flights.Features.DeletingFlight.V1; +using BookingMonolith.Flight.Flights.Features.UpdatingFlight.V1; +using BookingMonolith.Flight.Seats.Features.CreatingSeat.V1; +using BookingMonolith.Flight.Seats.Features.ReservingSeat.V1; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; + +namespace BookingMonolith.Flight; + +// ref: https://www.ledjonbehluli.com/posts/domain_to_integration_event/ +public sealed class FlightEventMapper : IEventMapper +{ + public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event) + { + return @event switch + { + FlightCreatedDomainEvent e => new FlightCreated(e.Id), + FlightUpdatedDomainEvent e => new FlightUpdated(e.Id), + FlightDeletedDomainEvent e => new FlightDeleted(e.Id), + AirportCreatedDomainEvent e => new AirportCreated(e.Id), + AircraftCreatedDomainEvent e => new AircraftCreated(e.Id), + SeatCreatedDomainEvent e => new SeatCreated(e.Id), + SeatReservedDomainEvent e => new SeatReserved(e.Id), + _ => null + }; + } + + public IInternalCommand? MapToInternalCommand(IDomainEvent @event) + { + return @event switch + { + FlightCreatedDomainEvent e => new CreateFlightMongo(e.Id, e.FlightNumber, e.AircraftId, e.DepartureDate, e.DepartureAirportId, + e.ArriveDate, e.ArriveAirportId, e.DurationMinutes, e.FlightDate, e.Status, e.Price, e.IsDeleted), + FlightUpdatedDomainEvent e => new UpdateFlightMongo(e.Id, e.FlightNumber, e.AircraftId, e.DepartureDate, e.DepartureAirportId, + e.ArriveDate, e.ArriveAirportId, e.DurationMinutes, e.FlightDate, e.Status, e.Price, e.IsDeleted), + FlightDeletedDomainEvent e => new DeleteFlightMongo(e.Id, e.FlightNumber, e.AircraftId, e.DepartureDate, e.DepartureAirportId, + e.ArriveDate, e.ArriveAirportId, e.DurationMinutes, e.FlightDate, e.Status, e.Price, e.IsDeleted), + AircraftCreatedDomainEvent e => new CreateAircraftMongo(e.Id, e.Name, e.Model, e.ManufacturingYear, e.IsDeleted), + AirportCreatedDomainEvent e => new CreateAirportMongo(e.Id, e.Name, e.Address, e.Code, e.IsDeleted), + SeatCreatedDomainEvent e => new CreateSeatMongo(e.Id, e.SeatNumber, e.Type, e.Class, e.FlightId, e.IsDeleted), + SeatReservedDomainEvent e => new ReserveSeatMongo(e.Id, e.SeatNumber, e.Type, e.Class, e.FlightId, e.IsDeleted), + _ => null + }; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightRoot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightRoot.cs new file mode 100644 index 0000000..634dfb0 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/FlightRoot.cs @@ -0,0 +1,6 @@ +namespace BookingMonolith.Flight; + +public class FlightRoot +{ + +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Dtos/FlightDto.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Dtos/FlightDto.cs new file mode 100644 index 0000000..7538556 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Dtos/FlightDto.cs @@ -0,0 +1,4 @@ +namespace BookingMonolith.Flight.Flights.Dtos; +public record FlightDto(Guid Id, string FlightNumber, Guid AircraftId, Guid DepartureAirportId, + DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, + Enums.FlightStatus Status, decimal Price); diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Enums/FlightStatus.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Enums/FlightStatus.cs new file mode 100644 index 0000000..1a80693 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Enums/FlightStatus.cs @@ -0,0 +1,10 @@ +namespace BookingMonolith.Flight.Flights.Enums; + +public enum FlightStatus +{ + Unknown = 0, + Flying = 1, + Delay = 2, + Canceled = 3, + Completed = 4 +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs new file mode 100644 index 0000000..6f33fc6 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class FlightAlreadyExistException : ConflictException +{ + public FlightAlreadyExistException(int? code = default) : base("Flight already exist!", code) + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightExceptions.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightExceptions.cs new file mode 100644 index 0000000..e7b4802 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightExceptions.cs @@ -0,0 +1,14 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class FlightExceptions : BadRequestException +{ + public FlightExceptions(DateTime departureDate, DateTime arriveDate) : + base($"Departure date: '{departureDate}' must be before arrive date: '{arriveDate}'.") + { } + + public FlightExceptions(DateTime flightDate) : + base($"Flight date: '{flightDate}' must be between departure and arrive dates.") + { } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightNotFountException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightNotFountException.cs new file mode 100644 index 0000000..c936e9c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/FlightNotFountException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class FlightNotFountException : NotFoundException +{ + public FlightNotFountException() : base("Flight not found!") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidArriveDateException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidArriveDateException.cs new file mode 100644 index 0000000..556d883 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidArriveDateException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class InvalidArriveDateException : BadRequestException +{ + public InvalidArriveDateException(DateTime arriveDate) + : base($"Arrive Date: '{arriveDate}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDepartureDateException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDepartureDateException.cs new file mode 100644 index 0000000..ed5d85b --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDepartureDateException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class InvalidDepartureDateException : BadRequestException +{ + public InvalidDepartureDateException(DateTime departureDate) + : base($"Departure Date: '{departureDate}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDurationException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDurationException.cs new file mode 100644 index 0000000..e363b0c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidDurationException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class InvalidDurationException : BadRequestException +{ + public InvalidDurationException() + : base("Duration cannot be negative.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightDateException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightDateException.cs new file mode 100644 index 0000000..7f58b04 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightDateException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class InvalidFlightDateException : BadRequestException +{ + public InvalidFlightDateException(DateTime flightDate) + : base($"Flight Date: '{flightDate}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightIdException.cs new file mode 100644 index 0000000..25467ca --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class InvalidFlightIdException : BadRequestException +{ + public InvalidFlightIdException(Guid flightId) + : base($"flightId: '{flightId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightNumberException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightNumberException.cs new file mode 100644 index 0000000..54b6eaa --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidFlightNumberException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class InvalidFlightNumberException : BadRequestException +{ + public InvalidFlightNumberException(string flightNumber) + : base($"Flight Number: '{flightNumber}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidPriceException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidPriceException.cs new file mode 100644 index 0000000..58f9649 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Exceptions/InvalidPriceException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Flights.Exceptions; + +public class InvalidPriceException : BadRequestException +{ + public InvalidPriceException() + : base($"Price Cannot be negative.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlight.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlight.cs new file mode 100644 index 0000000..825cd34 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlight.cs @@ -0,0 +1,123 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using BookingMonolith.Flight.Airports.ValueObjects; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Exceptions; +using BookingMonolith.Flight.Flights.ValueObjects; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Flight.Flights.Features.CreatingFlight.V1; + +public record CreateFlight(string FlightNumber, Guid AircraftId, Guid DepartureAirportId, + DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId, + decimal DurationMinutes, DateTime FlightDate, Enums.FlightStatus Status, + decimal Price) : ICommand, IInternalCommand +{ + public Guid Id { get; init; } = NewId.NextGuid(); +} + +public record CreateFlightResult(Guid Id); + +public record FlightCreatedDomainEvent(Guid Id, string FlightNumber, Guid AircraftId, DateTime DepartureDate, + Guid DepartureAirportId, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, + DateTime FlightDate, Enums.FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent; + +public record CreateFlightRequestDto(string FlightNumber, Guid AircraftId, Guid DepartureAirportId, + DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId, + decimal DurationMinutes, DateTime FlightDate, Enums.FlightStatus Status, decimal Price); + +public record CreateFlightResponseDto(Guid Id); + +public class CreateFlightEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/flight", async (CreateFlightRequestDto request, + IMediator mediator, IMapper mapper, + CancellationToken cancellationToken) => + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.CreatedAtRoute("GetFlightById", new { id = result.Id }, response); + }) + .RequireAuthorization() + .WithName("CreateFlight") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces(StatusCodes.Status201Created) + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Create Flight") + .WithDescription("Create Flight") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class CreateFlightValidator : AbstractValidator +{ + public CreateFlightValidator() + { + RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0"); + + RuleFor(x => x.Status).Must(p => (p.GetType().IsEnum && + p == Enums.FlightStatus.Flying) || + p == Enums.FlightStatus.Canceled || + p == Enums.FlightStatus.Delay || + p == Enums.FlightStatus.Completed) + .WithMessage("Status must be Flying, Delay, Canceled or Completed"); + + RuleFor(x => x.AircraftId).NotEmpty().WithMessage("AircraftId must be not empty"); + RuleFor(x => x.DepartureAirportId).NotEmpty().WithMessage("DepartureAirportId must be not empty"); + RuleFor(x => x.ArriveAirportId).NotEmpty().WithMessage("ArriveAirportId must be not empty"); + RuleFor(x => x.DurationMinutes).GreaterThan(0).WithMessage("DurationMinutes must be greater than 0"); + RuleFor(x => x.FlightDate).NotEmpty().WithMessage("FlightDate must be not empty"); + } +} + +internal class CreateFlightHandler : ICommandHandler +{ + private readonly FlightDbContext _flightDbContext; + + public CreateFlightHandler(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateFlight request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var flight = await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.Id == request.Id, + cancellationToken); + + if (flight is not null) + { + throw new FlightAlreadyExistException(); + } + + var flightEntity = Models.Flight.Create(FlightId.Of(request.Id), FlightNumber.Of(request.FlightNumber), AircraftId.Of(request.AircraftId), + AirportId.Of(request.DepartureAirportId), DepartureDate.Of(request.DepartureDate), + ArriveDate.Of(request.ArriveDate), AirportId.Of(request.ArriveAirportId), DurationMinutes.Of(request.DurationMinutes), FlightDate.Of(request.FlightDate), request.Status, + Price.Of(request.Price)); + + var newFlight = (await _flightDbContext.Flights.AddAsync(flightEntity, cancellationToken)).Entity; + + return new CreateFlightResult(newFlight.Id); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlightMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlightMongo.cs new file mode 100644 index 0000000..cb1c707 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlightMongo.cs @@ -0,0 +1,49 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Exceptions; +using BookingMonolith.Flight.Flights.Models; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Flights.Features.CreatingFlight.V1; + +public record CreateFlightMongo(Guid Id, string FlightNumber, Guid AircraftId, DateTime DepartureDate, + Guid DepartureAirportId, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, + Enums.FlightStatus Status, decimal Price, bool IsDeleted = false) : InternalCommand; + +internal class CreateFlightMongoHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public CreateFlightMongoHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(CreateFlightMongo request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var flightReadModel = _mapper.Map(request); + + var flight = await _flightReadDbContext.Flight.AsQueryable() + .FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken); + + if (flight is not null) + { + throw new FlightAlreadyExistException(); + } + + await _flightReadDbContext.Flight.InsertOneAsync(flightReadModel, cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlight.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlight.cs new file mode 100644 index 0000000..1c2eee4 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlight.cs @@ -0,0 +1,109 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Exceptions; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Flight.Flights.Features.DeletingFlight.V1; + +public record DeleteFlight(Guid Id) : ICommand, IInternalCommand; + +public record DeleteFlightResult(Guid Id); + +public record FlightDeletedDomainEvent( + Guid Id, + string FlightNumber, + Guid AircraftId, + DateTime DepartureDate, + Guid DepartureAirportId, + DateTime ArriveDate, + Guid ArriveAirportId, + decimal DurationMinutes, + DateTime FlightDate, + Enums.FlightStatus Status, + decimal Price, + bool IsDeleted +) : IDomainEvent; + +public class DeleteFlightEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapDelete( + $"{EndpointConfig.BaseApiPath}/flight/{{id}}", + async (Guid id, IMediator mediator, CancellationToken cancellationToken) => + { + await mediator.Send(new DeleteFlight(id), cancellationToken); + + return Results.NoContent(); + }) + .RequireAuthorization() + .WithName("DeleteFlight") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces(StatusCodes.Status204NoContent) + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Delete Flight") + .WithDescription("Delete Flight") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class DeleteFlightValidator : AbstractValidator +{ + public DeleteFlightValidator() + { + RuleFor(x => x.Id).NotEmpty(); + } +} + +internal class DeleteFlightHandler : ICommandHandler +{ + private readonly FlightDbContext _flightDbContext; + + public DeleteFlightHandler(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task Handle( + DeleteFlight request, + CancellationToken cancellationToken + ) + { + Guard.Against.Null(request, nameof(request)); + + var flight = await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); + + if (flight is null) + { + throw new FlightNotFountException(); + } + + flight.Delete( + flight.Id, + flight.FlightNumber, + flight.AircraftId, + flight.DepartureAirportId, + flight.DepartureDate, + flight.ArriveDate, + flight.ArriveAirportId, + flight.DurationMinutes, + flight.FlightDate, + flight.Status, + flight.Price); + + var deleteFlight = _flightDbContext.Flights.Update(flight).Entity; + + return new DeleteFlightResult(deleteFlight.Id); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlightMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlightMongo.cs new file mode 100644 index 0000000..c90b49b --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlightMongo.cs @@ -0,0 +1,53 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Exceptions; +using BookingMonolith.Flight.Flights.Models; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Flights.Features.DeletingFlight.V1; + +public record DeleteFlightMongo(Guid Id, string FlightNumber, Guid AircraftId, DateTime DepartureDate, + Guid DepartureAirportId, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, + Enums.FlightStatus Status, decimal Price, bool IsDeleted = false) : InternalCommand; + +internal class DeleteFlightMongoCommandHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public DeleteFlightMongoCommandHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(DeleteFlightMongo request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var flightReadModel = _mapper.Map(request); + + var flight = await _flightReadDbContext.Flight.AsQueryable() + .FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken); + + if (flight is null) + { + throw new FlightNotFountException(); + } + + await _flightReadDbContext.Flight.UpdateOneAsync( + x => x.FlightId == flightReadModel.FlightId, + Builders.Update + .Set(x => x.IsDeleted, flightReadModel.IsDeleted), + cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/FlightMappings.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/FlightMappings.cs new file mode 100644 index 0000000..ae2b844 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/FlightMappings.cs @@ -0,0 +1,47 @@ +using BookingMonolith.Flight.Flights.Features.CreatingFlight.V1; +using BookingMonolith.Flight.Flights.Features.DeletingFlight.V1; +using BookingMonolith.Flight.Flights.Features.UpdatingFlight.V1; +using BookingMonolith.Flight.Flights.Models; +using Mapster; +using MassTransit; + +namespace BookingMonolith.Flight.Flights.Features; + +using FlightDto = Dtos.FlightDto; + +public class FlightMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .ConstructUsing(x => new FlightDto(x.Id, x.FlightNumber, x.AircraftId, x.DepartureAirportId, + x.DepartureDate, + x.ArriveDate, x.ArriveAirportId, x.DurationMinutes, x.FlightDate, x.Status, x.Price)); + + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.FlightId, s => s.Id); + + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.FlightId, s => s.Id.Value); + + config.NewConfig() + .Map(d => d.Id, s => s.FlightId); + + config.NewConfig() + .Map(d => d.FlightId, s => s.Id); + + config.NewConfig() + .Map(d => d.FlightId, s => s.Id); + + config.NewConfig() + .ConstructUsing(x => new CreateFlight(x.FlightNumber, x.AircraftId, x.DepartureAirportId, + x.DepartureDate, x.ArriveDate, x.ArriveAirportId, x.DurationMinutes, x.FlightDate, x.Status, x.Price)); + + config.NewConfig() + .ConstructUsing(x => new UpdateFlight(x.Id, x.FlightNumber, x.AircraftId, x.DepartureAirportId, x.DepartureDate, + x.ArriveDate, x.ArriveAirportId, x.DurationMinutes, x.FlightDate, x.Status, x.IsDeleted, x.Price)); + + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingAvailableFlights/V1/GetAvailableFlights.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingAvailableFlights/V1/GetAvailableFlights.cs new file mode 100644 index 0000000..d7f4516 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingAvailableFlights/V1/GetAvailableFlights.cs @@ -0,0 +1,84 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Dtos; +using BookingMonolith.Flight.Flights.Exceptions; +using BuildingBlocks.Caching; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Web; +using Mapster; +using MapsterMapper; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Flights.Features.GettingAvailableFlights.V1; + +public record GetAvailableFlights : IQuery, ICacheRequest +{ + public string CacheKey => "GetAvailableFlights"; + public DateTime? AbsoluteExpirationRelativeToNow => DateTime.Now.AddHours(1); +} + +public record GetAvailableFlightsResult(IEnumerable FlightDtos); + +public record GetAvailableFlightsResponseDto(IEnumerable FlightDtos); + +public class GetAvailableFlightsEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/get-available-flights", + async (IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send(new GetAvailableFlights(), cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("GetAvailableFlights") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Get Available Flights") + .WithDescription("Get Available Flights") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +internal class GetAvailableFlightsHandler : IQueryHandler +{ + private readonly IMapper _mapper; + private readonly FlightReadDbContext _flightReadDbContext; + + public GetAvailableFlightsHandler(IMapper mapper, FlightReadDbContext flightReadDbContext) + { + _mapper = mapper; + _flightReadDbContext = flightReadDbContext; + } + + public async Task Handle(GetAvailableFlights request, + CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var flight = (await _flightReadDbContext.Flight.AsQueryable().ToListAsync(cancellationToken)) + .Where(x => !x.IsDeleted); + + if (!flight.Any()) + { + throw new FlightNotFountException(); + } + + var flightDtos = _mapper.Map>(flight); + + return new GetAvailableFlightsResult(flightDtos); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingFlightById/V1/GetFlightById.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingFlightById/V1/GetFlightById.cs new file mode 100644 index 0000000..45633e8 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/GettingFlightById/V1/GetFlightById.cs @@ -0,0 +1,88 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Dtos; +using BookingMonolith.Flight.Flights.Exceptions; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Flights.Features.GettingFlightById.V1; + +public record GetFlightById(Guid Id) : IQuery; + +public record GetFlightByIdResult(FlightDto FlightDto); + +public record GetFlightByIdResponseDto(FlightDto FlightDto); + +public class GetFlightByIdEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/{{id}}", + async (Guid id, IMediator mediator, IMapper mapper, CancellationToken cancellationToken) => + { + var result = await mediator.Send(new GetFlightById(id), cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("GetFlightById") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Get Flight By Id") + .WithDescription("Get Flight By Id") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class GetFlightByIdValidator : AbstractValidator +{ + public GetFlightByIdValidator() + { + RuleFor(x => x.Id).NotNull().WithMessage("Id is required!"); + } +} + +internal class GetFlightByIdHandler : IQueryHandler +{ + private readonly IMapper _mapper; + private readonly FlightReadDbContext _flightReadDbContext; + + public GetFlightByIdHandler(IMapper mapper, FlightReadDbContext flightReadDbContext) + { + _mapper = mapper; + _flightReadDbContext = flightReadDbContext; + } + + public async Task Handle(GetFlightById request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var flight = await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync( + x => x.FlightId == request.Id && + !x.IsDeleted, cancellationToken); + + if (flight is null) + { + throw new FlightNotFountException(); + } + + var flightDto = _mapper.Map(flight); + + return new GetFlightByIdResult(flightDto); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlight.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlight.cs new file mode 100644 index 0000000..1d78d5c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlight.cs @@ -0,0 +1,121 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using BookingMonolith.Flight.Airports.ValueObjects; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Exceptions; +using BookingMonolith.Flight.Flights.Features.CreatingFlight.V1; +using BookingMonolith.Flight.Flights.ValueObjects; +using BuildingBlocks.Caching; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using MapsterMapper; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Flight.Flights.Features.UpdatingFlight.V1; + +public record UpdateFlight(Guid Id, string FlightNumber, Guid AircraftId, Guid DepartureAirportId, + DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, + Enums.FlightStatus Status, bool IsDeleted, decimal Price) : ICommand, IInternalCommand, + IInvalidateCacheRequest +{ + public string CacheKey => "GetAvailableFlights"; +} + +public record UpdateFlightResult(Guid Id); + +public record FlightUpdatedDomainEvent(Guid Id, string FlightNumber, Guid AircraftId, DateTime DepartureDate, + Guid DepartureAirportId, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, + DateTime FlightDate, Enums.FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent; + +public record UpdateFlightRequestDto(Guid Id, string FlightNumber, Guid AircraftId, Guid DepartureAirportId, + DateTime DepartureDate, DateTime ArriveDate, + Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, Enums.FlightStatus Status, decimal Price, + bool IsDeleted); + +public class UpdateFlightEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPut($"{EndpointConfig.BaseApiPath}/flight", async (UpdateFlightRequestDto request, + IMediator mediator, + IMapper mapper, CancellationToken cancellationToken) => + { + var command = mapper.Map(request); + + await mediator.Send(command, cancellationToken); + + return Results.NoContent(); + }) + .RequireAuthorization() + .WithName("UpdateFlight") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces(StatusCodes.Status204NoContent) + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Update Flight") + .WithDescription("Update Flight") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class UpdateFlightValidator : AbstractValidator +{ + public UpdateFlightValidator() + { + RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0"); + + RuleFor(x => x.Status).Must(p => (p.GetType().IsEnum && + p == Enums.FlightStatus.Flying) || + p == Enums.FlightStatus.Canceled || + p == Enums.FlightStatus.Delay || + p == Enums.FlightStatus.Completed) + .WithMessage("Status must be Flying, Delay, Canceled or Completed"); + + RuleFor(x => x.AircraftId).NotEmpty().WithMessage("AircraftId must be not empty"); + RuleFor(x => x.DepartureAirportId).NotEmpty().WithMessage("DepartureAirportId must be not empty"); + RuleFor(x => x.ArriveAirportId).NotEmpty().WithMessage("ArriveAirportId must be not empty"); + RuleFor(x => x.DurationMinutes).GreaterThan(0).WithMessage("DurationMinutes must be greater than 0"); + RuleFor(x => x.FlightDate).NotEmpty().WithMessage("FlightDate must be not empty"); + } +} + +internal class UpdateFlightHandler : ICommandHandler +{ + private readonly FlightDbContext _flightDbContext; + + public UpdateFlightHandler(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task Handle(UpdateFlight request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var flight = await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.Id == request.Id, + cancellationToken); + + if (flight is null) + { + throw new FlightNotFountException(); + } + + + flight.Update(FlightId.Of(request.Id), FlightNumber.Of(request.FlightNumber), AircraftId.Of(request.AircraftId), AirportId.Of(request.DepartureAirportId), + DepartureDate.Of(request.DepartureDate), + ArriveDate.Of(request.ArriveDate), AirportId.Of(request.ArriveAirportId), DurationMinutes.Of(request.DurationMinutes), FlightDate.Of(request.FlightDate), request.Status, + Price.Of(request.Price), request.IsDeleted); + + var updateFlight = _flightDbContext.Flights.Update(flight).Entity; + + return new UpdateFlightResult(updateFlight.Id); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlightMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlightMongo.cs new file mode 100644 index 0000000..841e20a --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlightMongo.cs @@ -0,0 +1,64 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.Exceptions; +using BookingMonolith.Flight.Flights.Models; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Flights.Features.UpdatingFlight.V1; + +public record UpdateFlightMongo(Guid Id, string FlightNumber, Guid AircraftId, DateTime DepartureDate, + Guid DepartureAirportId, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, + Enums.FlightStatus Status, decimal Price, bool IsDeleted = false) : InternalCommand; + + +internal class UpdateFlightMongoCommandHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public UpdateFlightMongoCommandHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(UpdateFlightMongo request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var flightReadModel = _mapper.Map(request); + + var flight = await _flightReadDbContext.Flight.AsQueryable() + .FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken); + + if (flight is null) + { + throw new FlightNotFountException(); + } + + await _flightReadDbContext.Flight.UpdateOneAsync( + x => x.FlightId == flightReadModel.FlightId, + Builders.Update + .Set(x => x.Price, flightReadModel.Price) + .Set(x => x.ArriveDate, flightReadModel.ArriveDate) + .Set(x => x.AircraftId, flightReadModel.AircraftId) + .Set(x => x.DurationMinutes, flightReadModel.DurationMinutes) + .Set(x => x.DepartureDate, flightReadModel.DepartureDate) + .Set(x => x.FlightDate, flightReadModel.FlightDate) + .Set(x => x.FlightNumber, flightReadModel.FlightNumber) + .Set(x => x.IsDeleted, flightReadModel.IsDeleted) + .Set(x => x.Status, flightReadModel.Status) + .Set(x => x.ArriveAirportId, flightReadModel.ArriveAirportId) + .Set(x => x.DepartureAirportId, flightReadModel.DepartureAirportId), + cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/Flight.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/Flight.cs new file mode 100644 index 0000000..10f3c79 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/Flight.cs @@ -0,0 +1,101 @@ +using BookingMonolith.Flight.Aircrafts.ValueObjects; +using BookingMonolith.Flight.Airports.ValueObjects; +using BookingMonolith.Flight.Flights.Features.CreatingFlight.V1; +using BookingMonolith.Flight.Flights.Features.DeletingFlight.V1; +using BookingMonolith.Flight.Flights.Features.UpdatingFlight.V1; +using BookingMonolith.Flight.Flights.ValueObjects; +using BuildingBlocks.Core.Model; + +namespace BookingMonolith.Flight.Flights.Models; + +public record Flight : Aggregate +{ + public FlightNumber FlightNumber { get; private set; } = default!; + public AircraftId AircraftId { get; private set; } = default!; + public AirportId DepartureAirportId { get; private set; } = default!; + public AirportId ArriveAirportId { get; private set; } = default!; + public DurationMinutes DurationMinutes { get; private set; } = default!; + public Enums.FlightStatus Status { get; private set; } + public Price Price { get; private set; } = default!; + public ArriveDate ArriveDate { get; private set; } = default!; + public DepartureDate DepartureDate { get; private set; } = default!; + public FlightDate FlightDate { get; private set; } = default!; + + public static Flight Create(FlightId id, FlightNumber flightNumber, AircraftId aircraftId, + AirportId departureAirportId, DepartureDate departureDate, ArriveDate arriveDate, + AirportId arriveAirportId, DurationMinutes durationMinutes, FlightDate flightDate, Enums.FlightStatus status, + Price price, bool isDeleted = false) + { + var flight = new Flight + { + Id = id, + FlightNumber = flightNumber, + AircraftId = aircraftId, + DepartureAirportId = departureAirportId, + DepartureDate = departureDate, + ArriveDate = arriveDate, + ArriveAirportId = arriveAirportId, + DurationMinutes = durationMinutes, + FlightDate = flightDate, + Status = status, + Price = price, + IsDeleted = isDeleted, + }; + + var @event = new FlightCreatedDomainEvent(flight.Id, flight.FlightNumber, flight.AircraftId, + flight.DepartureDate, flight.DepartureAirportId, + flight.ArriveDate, flight.ArriveAirportId, flight.DurationMinutes, flight.FlightDate, flight.Status, + flight.Price, flight.IsDeleted); + + flight.AddDomainEvent(@event); + + return flight; + } + + + public void Update(FlightId id, FlightNumber flightNumber, AircraftId aircraftId, + AirportId departureAirportId, DepartureDate departureDate, ArriveDate arriveDate, + AirportId arriveAirportId, DurationMinutes durationMinutes, FlightDate flightDate, Enums.FlightStatus status, + Price price, bool isDeleted = false) + { + this.FlightNumber = flightNumber; + this.AircraftId = aircraftId; + this.DepartureAirportId = departureAirportId; + this.DepartureDate = departureDate; + this.ArriveDate = arriveDate; + this.ArriveAirportId = arriveAirportId; + this.DurationMinutes = durationMinutes; + this.FlightDate = flightDate; + this.Status = status; + this.Price = price; + this.IsDeleted = isDeleted; + + var @event = new FlightUpdatedDomainEvent(id, flightNumber, aircraftId, departureDate, departureAirportId, + arriveDate, arriveAirportId, durationMinutes, flightDate, status, price, isDeleted); + + AddDomainEvent(@event); + } + + public void Delete(FlightId id, FlightNumber flightNumber, AircraftId aircraftId, + AirportId departureAirportId, DepartureDate departureDate, ArriveDate arriveDate, + AirportId arriveAirportId, DurationMinutes durationMinutes, FlightDate flightDate, Enums.FlightStatus status, + Price price, bool isDeleted = true) + { + FlightNumber = flightNumber; + AircraftId = aircraftId; + DepartureAirportId = departureAirportId; + DepartureDate = departureDate; + ArriveDate = arriveDate; + ArriveAirportId = arriveAirportId; + DurationMinutes = durationMinutes; + FlightDate = flightDate; + Status = status; + Price = price; + IsDeleted = isDeleted; + + var @event = new FlightDeletedDomainEvent(id, flightNumber, aircraftId, departureDate, departureAirportId, + arriveDate, arriveAirportId, durationMinutes, flightDate, status, price, isDeleted); + + AddDomainEvent(@event); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/FlightReadModel.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/FlightReadModel.cs new file mode 100644 index 0000000..98a2a4a --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/Models/FlightReadModel.cs @@ -0,0 +1,18 @@ +namespace BookingMonolith.Flight.Flights.Models; + +public class FlightReadModel +{ + public required Guid Id { get; init; } + public required Guid FlightId { get; init; } + public required string FlightNumber { get; init; } + public required Guid AircraftId { get; init; } + public required DateTime DepartureDate { get; init; } + public required Guid DepartureAirportId { get; init; } + public required DateTime ArriveDate { get; init; } + public required Guid ArriveAirportId { get; init; } + public required decimal DurationMinutes { get; init; } + public required DateTime FlightDate { get; init; } + public required Enums.FlightStatus Status { get; init; } + public required decimal Price { get; init; } + public required bool IsDeleted { get; init; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/ArriveDate.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/ArriveDate.cs new file mode 100644 index 0000000..805ff36 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/ArriveDate.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Flights.Exceptions; + +namespace BookingMonolith.Flight.Flights.ValueObjects; + +public record ArriveDate +{ + public DateTime Value { get; } + + private ArriveDate(DateTime value) + { + Value = value; + } + + public static ArriveDate Of(DateTime value) + { + if (value == default) + { + throw new InvalidArriveDateException(value); + } + + return new ArriveDate(value); + } + + public static implicit operator DateTime(ArriveDate arriveDate) + { + return arriveDate.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DepartureDate.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DepartureDate.cs new file mode 100644 index 0000000..891dc96 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DepartureDate.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Flights.Exceptions; + +namespace BookingMonolith.Flight.Flights.ValueObjects; + +public record DepartureDate +{ + public DateTime Value { get; } + + private DepartureDate(DateTime value) + { + Value = value; + } + + public static DepartureDate Of(DateTime value) + { + if (value == default) + { + throw new InvalidDepartureDateException(value); + } + + return new DepartureDate(value); + } + + public static implicit operator DateTime(DepartureDate departureDate) + { + return departureDate.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DurationMinutes.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DurationMinutes.cs new file mode 100644 index 0000000..97a00e8 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/DurationMinutes.cs @@ -0,0 +1,33 @@ +using BookingMonolith.Flight.Flights.Exceptions; + +namespace BookingMonolith.Flight.Flights.ValueObjects; + +public class DurationMinutes +{ + public decimal Value { get; } + + private DurationMinutes(decimal value) + { + Value = value; + } + + public static DurationMinutes Of(decimal value) + { + if (value < 0) + { + throw new InvalidDurationException(); + } + + return new DurationMinutes(value); + } + + public static implicit operator decimal(DurationMinutes duration) + { + return duration.Value; + } + + public static explicit operator DurationMinutes(decimal value) + { + return new DurationMinutes(value); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightDate.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightDate.cs new file mode 100644 index 0000000..35f15c9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightDate.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Flights.Exceptions; + +namespace BookingMonolith.Flight.Flights.ValueObjects; + +public record FlightDate +{ + public DateTime Value { get; } + + private FlightDate(DateTime value) + { + Value = value; + } + + public static FlightDate Of(DateTime value) + { + if (value == default) + { + throw new InvalidFlightDateException(value); + } + + return new FlightDate(value); + } + + public static implicit operator DateTime(FlightDate flightDate) + { + return flightDate.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightId.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightId.cs new file mode 100644 index 0000000..35d74a0 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightId.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Flights.Exceptions; + +namespace BookingMonolith.Flight.Flights.ValueObjects; + +public record FlightId +{ + public Guid Value { get; } + + private FlightId(Guid value) + { + Value = value; + } + + public static FlightId Of(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidFlightIdException(value); + } + + return new FlightId(value); + } + + public static implicit operator Guid(FlightId flightId) + { + return flightId.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightNumber.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightNumber.cs new file mode 100644 index 0000000..25a751d --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/FlightNumber.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Flights.Exceptions; + +namespace BookingMonolith.Flight.Flights.ValueObjects; + +public record FlightNumber +{ + public string Value { get; } + + private FlightNumber(string value) + { + Value = value; + } + + public static FlightNumber Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidFlightNumberException(value); + } + + return new FlightNumber(value); + } + + public static implicit operator string(FlightNumber flightNumber) + { + return flightNumber.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/Price.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/Price.cs new file mode 100644 index 0000000..6229a20 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Flights/ValueObjects/Price.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Flights.Exceptions; + +namespace BookingMonolith.Flight.Flights.ValueObjects; + +public class Price +{ + public decimal Value { get; } + + private Price(decimal value) + { + Value = value; + } + + public static Price Of(decimal value) + { + if (value < 0) + { + throw new InvalidPriceException(); + } + + return new Price(value); + } + + public static implicit operator decimal(Price price) + { + return price.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Dtos/SeatDto.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Dtos/SeatDto.cs new file mode 100644 index 0000000..00f0c1f --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Dtos/SeatDto.cs @@ -0,0 +1,3 @@ +namespace BookingMonolith.Flight.Seats.Dtos; + +public record SeatDto(Guid Id, string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, Guid FlightId); diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatClass.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatClass.cs new file mode 100644 index 0000000..b1498b1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatClass.cs @@ -0,0 +1,9 @@ +namespace BookingMonolith.Flight.Seats.Enums; + +public enum SeatClass +{ + Unknown = 0, + FirstClass, + Business, + Economy +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatType.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatType.cs new file mode 100644 index 0000000..c0adabe --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Enums/SeatType.cs @@ -0,0 +1,9 @@ +namespace BookingMonolith.Flight.Seats.Enums; + +public enum SeatType +{ + Unknown = 0, + Window, + Middle, + Aisle +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/AllSeatsFullException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/AllSeatsFullException.cs new file mode 100644 index 0000000..e93ee46 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/AllSeatsFullException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Seats.Exceptions; + +public class AllSeatsFullException : BadRequestException +{ + public AllSeatsFullException() : base("All seats are full!") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatIdException.cs new file mode 100644 index 0000000..ac70145 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Seats.Exceptions; + +public class InvalidSeatIdException : BadRequestException +{ + public InvalidSeatIdException(Guid seatId) + : base($"seatId: '{seatId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatNumberException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatNumberException.cs new file mode 100644 index 0000000..465f1c4 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/InvalidSeatNumberException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Seats.Exceptions; + +public class InvalidSeatNumberException : BadRequestException +{ + public InvalidSeatNumberException() : base("SeatNumber Cannot be null or negative") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs new file mode 100644 index 0000000..88dee4d --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Seats.Exceptions; + +public class SeatAlreadyExistException : ConflictException +{ + public SeatAlreadyExistException(int? code = default) : base("Seat already exist!", code) + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs new file mode 100644 index 0000000..5cd12e7 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Flight.Seats.Exceptions; + +public class SeatNumberIncorrectException : BadRequestException +{ + public SeatNumberIncorrectException() : base("Seat number is incorrect!") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeat.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeat.cs new file mode 100644 index 0000000..c4a8ac3 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeat.cs @@ -0,0 +1,109 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Flights.ValueObjects; +using BookingMonolith.Flight.Seats.Exceptions; +using BookingMonolith.Flight.Seats.Models; +using BookingMonolith.Flight.Seats.ValueObjects; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Flight.Seats.Features.CreatingSeat.V1; + +public record CreateSeat + (string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, Guid FlightId) : ICommand, + IInternalCommand +{ + public Guid Id { get; init; } = NewId.NextGuid(); +} + +public record CreateSeatResult(Guid Id); + +public record SeatCreatedDomainEvent(Guid Id, string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, + Guid FlightId, bool IsDeleted) : IDomainEvent; + +public record CreateSeatRequestDto(string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, Guid FlightId); + +public record CreateSeatResponseDto(Guid Id); + +public class CreateSeatEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/seat", CreateSeat) + .RequireAuthorization() + .WithName("CreateSeat") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Create Seat") + .WithDescription("Create Seat") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } + + private async Task CreateSeat(CreateSeatRequestDto request, IMediator mediator, IMapper mapper, + CancellationToken cancellationToken) + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + } +} + +public class CreateSeatValidator : AbstractValidator +{ + public CreateSeatValidator() + { + RuleFor(x => x.SeatNumber).NotEmpty().WithMessage("SeatNumber is required"); + RuleFor(x => x.FlightId).NotEmpty().WithMessage("FlightId is required"); + RuleFor(x => x.Class).Must(p => (p.GetType().IsEnum && + p == Enums.SeatClass.FirstClass) || + p == Enums.SeatClass.Business || + p == Enums.SeatClass.Economy) + .WithMessage("Status must be FirstClass, Business or Economy"); + } +} + +internal class CreateSeatCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + + public CreateSeatCommandHandler(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateSeat command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var seat = await _flightDbContext.Seats.SingleOrDefaultAsync(x => x.Id == command.Id, cancellationToken); + + if (seat is not null) + { + throw new SeatAlreadyExistException(); + } + + var seatEntity = Seat.Create(SeatId.Of(command.Id), SeatNumber.Of(command.SeatNumber), command.Type, command.Class, FlightId.Of(command.FlightId)); + + var newSeat = (await _flightDbContext.Seats.AddAsync(seatEntity, cancellationToken)).Entity; + + return new CreateSeatResult(newSeat.Id); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeatMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeatMongo.cs new file mode 100644 index 0000000..7bcbfed --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeatMongo.cs @@ -0,0 +1,49 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Seats.Exceptions; +using BookingMonolith.Flight.Seats.Models; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Seats.Features.CreatingSeat.V1; + +public record CreateSeatMongo(Guid Id, string SeatNumber, Enums.SeatType Type, + Enums.SeatClass Class, Guid FlightId, bool IsDeleted = false) : InternalCommand; + +internal class CreateSeatMongoHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public CreateSeatMongoHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(CreateSeatMongo request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var seatReadModel = _mapper.Map(request); + + var seat = await _flightReadDbContext.Seat.AsQueryable() + .FirstOrDefaultAsync(x => x.SeatId == seatReadModel.SeatId && + !x.IsDeleted, cancellationToken); + + if (seat is not null) + { + throw new SeatAlreadyExistException(); + } + + await _flightReadDbContext.Seat.InsertOneAsync(seatReadModel, cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/GettingAvailableSeats/V1/GetAvailableSeats.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/GettingAvailableSeats/V1/GetAvailableSeats.cs new file mode 100644 index 0000000..413df97 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/GettingAvailableSeats/V1/GetAvailableSeats.cs @@ -0,0 +1,89 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Seats.Dtos; +using BookingMonolith.Flight.Seats.Exceptions; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Flight.Seats.Features.GettingAvailableSeats.V1; + +public record GetAvailableSeats(Guid FlightId) : IQuery; + +public record GetAvailableSeatsResult(IEnumerable SeatDtos); + +public record GetAvailableSeatsResponseDto(IEnumerable SeatDtos); + +public class GetAvailableSeatsEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/get-available-seats/{{id}}", GetAvailableSeats) + .RequireAuthorization() + .WithName("GetAvailableSeats") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Get Available Seats") + .WithDescription("Get Available Seats") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } + + private async Task GetAvailableSeats(Guid id, IMediator mediator, CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetAvailableSeats(id), cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + } +} + +public class GetAvailableSeatsValidator : AbstractValidator +{ + public GetAvailableSeatsValidator() + { + RuleFor(x => x.FlightId).NotNull().WithMessage("FlightId is required!"); + } +} + +internal class GetAvailableSeatsQueryHandler : IRequestHandler +{ + private readonly IMapper _mapper; + private readonly FlightReadDbContext _flightReadDbContext; + + public GetAvailableSeatsQueryHandler(IMapper mapper, FlightReadDbContext flightReadDbContext) + { + _mapper = mapper; + _flightReadDbContext = flightReadDbContext; + } + + + public async Task Handle(GetAvailableSeats query, CancellationToken cancellationToken) + { + Guard.Against.Null(query, nameof(query)); + + var seats = (await _flightReadDbContext.Seat.AsQueryable().ToListAsync(cancellationToken)) + .Where(x => x.FlightId == query.FlightId && !x.IsDeleted); + + if (!seats.Any()) + { + throw new AllSeatsFullException(); + } + + var seatDtos = _mapper.Map>(seats); + + return new GetAvailableSeatsResult(seatDtos); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeat.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeat.cs new file mode 100644 index 0000000..09fd7e1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeat.cs @@ -0,0 +1,97 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Seats.Exceptions; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Flight.Seats.Features.ReservingSeat.V1; + +public record ReserveSeat(Guid FlightId, string SeatNumber) : ICommand, IInternalCommand; + +public record ReserveSeatResult(Guid Id); + +public record SeatReservedDomainEvent(Guid Id, string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, + Guid FlightId, bool IsDeleted) : IDomainEvent; + +public record ReserveSeatRequestDto(Guid FlightId, string SeatNumber); + +public record ReserveSeatResponseDto(Guid Id); + +public class ReserveSeatEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/reserve-seat", ReserveSeat) + .RequireAuthorization() + .WithName("ReserveSeat") + .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Reserve Seat") + .WithDescription("Reserve Seat") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } + + private async Task ReserveSeat(ReserveSeatRequestDto request, IMediator mediator, IMapper mapper, + CancellationToken cancellationToken) + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + } +} + +public class ReserveSeatValidator : AbstractValidator +{ + public ReserveSeatValidator() + { + RuleFor(x => x.FlightId).NotEmpty().WithMessage("FlightId must not be empty"); + RuleFor(x => x.SeatNumber).NotEmpty().WithMessage("SeatNumber must not be empty"); + } +} + +internal class ReserveSeatCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + + public ReserveSeatCommandHandler(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task Handle(ReserveSeat command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var seat = await _flightDbContext.Seats.SingleOrDefaultAsync( + x => x.SeatNumber.Value == command.SeatNumber && + x.FlightId == command.FlightId, cancellationToken); + + if (seat is null) + { + throw new SeatNumberIncorrectException(); + } + + seat.ReserveSeat(); + + var updatedSeat = _flightDbContext.Seats.Update(seat).Entity; + + return new ReserveSeatResult(updatedSeat.Id); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeatMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeatMongo.cs new file mode 100644 index 0000000..09c7cb9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeatMongo.cs @@ -0,0 +1,42 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Flight.Data; +using BookingMonolith.Flight.Seats.Models; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; + +namespace BookingMonolith.Flight.Seats.Features.ReservingSeat.V1; + +public record ReserveSeatMongo(Guid Id, string SeatNumber, Enums.SeatType Type, + Enums.SeatClass Class, Guid FlightId, bool IsDeleted = false) : InternalCommand; + +internal class ReserveSeatMongoHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public ReserveSeatMongoHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(ReserveSeatMongo command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var seatReadModel = _mapper.Map(command); + + await _flightReadDbContext.Seat.UpdateOneAsync( + x => x.SeatId == seatReadModel.SeatId, + Builders.Update + .Set(x => x.IsDeleted, seatReadModel.IsDeleted), + cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/SeatMappings.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/SeatMappings.cs new file mode 100644 index 0000000..4d23097 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Features/SeatMappings.cs @@ -0,0 +1,34 @@ +using BookingMonolith.Flight.Seats.Dtos; +using BookingMonolith.Flight.Seats.Features.CreatingSeat.V1; +using BookingMonolith.Flight.Seats.Features.ReservingSeat.V1; +using BookingMonolith.Flight.Seats.Models; +using Mapster; +using MassTransit; + +namespace BookingMonolith.Flight.Seats.Features; + +public class SeatMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .ConstructUsing(x => new SeatDto(x.Id.Value, x.SeatNumber.Value, x.Type, x.Class, x.FlightId.Value)); + + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.SeatId, s => s.Id); + + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.SeatId, s => s.Id.Value); + + config.NewConfig() + .Map(d => d.SeatId, s => s.Id); + + config.NewConfig() + .ConstructUsing(x => new CreateSeat(x.SeatNumber, x.Type, x.Class, x.FlightId)); + + config.NewConfig() + .ConstructUsing(x => new ReserveSeat(x.FlightId, x.SeatNumber)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/Seat.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/Seat.cs new file mode 100644 index 0000000..485b884 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/Seat.cs @@ -0,0 +1,58 @@ +using BookingMonolith.Flight.Flights.ValueObjects; +using BookingMonolith.Flight.Seats.Features.CreatingSeat.V1; +using BookingMonolith.Flight.Seats.Features.ReservingSeat.V1; +using BookingMonolith.Flight.Seats.ValueObjects; +using BuildingBlocks.Core.Model; + +namespace BookingMonolith.Flight.Seats.Models; + +public record Seat : Aggregate +{ + public SeatNumber SeatNumber { get; private set; } = default!; + public Enums.SeatType Type { get; private set; } + public Enums.SeatClass Class { get; private set; } + public FlightId FlightId { get; private set; } = default!; + + public static Seat Create(SeatId id, SeatNumber seatNumber, Enums.SeatType type, Enums.SeatClass @class, + FlightId flightId, + bool isDeleted = false) + { + var seat = new Seat() + { + Id = id, + Class = @class, + Type = type, + SeatNumber = seatNumber, + FlightId = flightId, + IsDeleted = isDeleted + }; + + var @event = new SeatCreatedDomainEvent( + seat.Id, + seat.SeatNumber, + seat.Type, + seat.Class, + seat.FlightId, + isDeleted); + + seat.AddDomainEvent(@event); + + return seat; + } + + public void ReserveSeat() + { + this.IsDeleted = true; + this.LastModified = DateTime.Now; + + var @event = new SeatReservedDomainEvent( + this.Id, + this.SeatNumber, + this.Type, + this.Class, + this.FlightId, + this.IsDeleted); + + this.AddDomainEvent(@event); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/SeatReadModel.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/SeatReadModel.cs new file mode 100644 index 0000000..6be9235 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/Models/SeatReadModel.cs @@ -0,0 +1,12 @@ +namespace BookingMonolith.Flight.Seats.Models; + +public class SeatReadModel +{ + public required Guid Id { get; init; } + public required Guid SeatId { get; init; } + public required string SeatNumber { get; init; } + public required Enums.SeatType Type { get; init; } + public required Enums.SeatClass Class { get; init; } + public required Guid FlightId { get; init; } + public required bool IsDeleted { get; init; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatId.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatId.cs new file mode 100644 index 0000000..ff0ac33 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatId.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Seats.Exceptions; + +namespace BookingMonolith.Flight.Seats.ValueObjects; + +public record SeatId +{ + public Guid Value { get; } + + private SeatId(Guid value) + { + Value = value; + } + + public static SeatId Of(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidSeatIdException(value); + } + + return new SeatId(value); + } + + public static implicit operator Guid(SeatId seatId) + { + return seatId.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatNumber.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatNumber.cs new file mode 100644 index 0000000..dc551b1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Flight/Seats/ValueObjects/SeatNumber.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Flight.Seats.Exceptions; + +namespace BookingMonolith.Flight.Seats.ValueObjects; + +public record SeatNumber +{ + public string Value { get; } + + private SeatNumber(string value) + { + Value = value; + } + + public static SeatNumber Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidSeatNumberException(); + } + + return new SeatNumber(value); + } + + public static implicit operator string(SeatNumber seatNumber) + { + return seatNumber.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/AuthOptions.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/AuthOptions.cs new file mode 100644 index 0000000..bf201bd --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/AuthOptions.cs @@ -0,0 +1,6 @@ +namespace BookingMonolith.Identity.Configurations; + +public class AuthOptions +{ + public string IssuerUri { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/Config.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/Config.cs new file mode 100644 index 0000000..09fefe6 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/Config.cs @@ -0,0 +1,70 @@ +using BookingMonolith.Identity.Identities.Constants; +using Duende.IdentityServer; +using Duende.IdentityServer.Models; + +namespace BookingMonolith.Identity.Configurations; + +public static class Config +{ + public static IEnumerable IdentityResources => + new List + { + new IdentityResources.OpenId(), + new IdentityResources.Profile(), + new IdentityResources.Email(), + new IdentityResources.Phone(), + new IdentityResources.Address(), + new(Constants.StandardScopes.Roles, new List {"role"}) + }; + + + public static IEnumerable ApiScopes => + new List + { + new(Constants.StandardScopes.FlightApi), + new(Constants.StandardScopes.PassengerApi), + new(Constants.StandardScopes.BookingApi), + new(Constants.StandardScopes.IdentityApi), + new(Constants.StandardScopes.BookingModularMonolith), + new(Constants.StandardScopes.BookingMonolith), + }; + + + public static IList ApiResources => + new List + { + new(Constants.StandardScopes.FlightApi), + new(Constants.StandardScopes.PassengerApi), + new(Constants.StandardScopes.BookingApi), + new(Constants.StandardScopes.IdentityApi), + new(Constants.StandardScopes.BookingModularMonolith), + new(Constants.StandardScopes.BookingMonolith), + }; + + public static IEnumerable Clients => + new List + { + new() + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = + { + new Secret("secret".Sha256()) + }, + AllowedScopes = + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + Constants.StandardScopes.FlightApi, + Constants.StandardScopes.PassengerApi, + Constants.StandardScopes.BookingApi, + Constants.StandardScopes.IdentityApi, + Constants.StandardScopes.BookingModularMonolith, + Constants.StandardScopes.BookingMonolith, + }, + AccessTokenLifetime = 3600, // authorize the client to access protected resources + IdentityTokenLifetime = 3600 // authenticate the user + } + }; +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/UserValidator.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/UserValidator.cs new file mode 100644 index 0000000..4ab41a9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Configurations/UserValidator.cs @@ -0,0 +1,53 @@ +using System.Security.Claims; +using BookingMonolith.Identity.Identities.Models; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Configurations; + +public class UserValidator : IResourceOwnerPasswordValidator +{ + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + + public UserValidator(SignInManager signInManager, + UserManager userManager) + { + _signInManager = signInManager; + _userManager = userManager; + } + + public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + var user = await _userManager.FindByNameAsync(context.UserName); + + var signIn = await _signInManager.PasswordSignInAsync( + user, + context.Password, + isPersistent: true, + lockoutOnFailure: true); + + if (signIn.Succeeded) + { + var userId = user!.Id.ToString(); + + // context set to success + context.Result = new GrantValidationResult( + subject: userId, + authenticationMethod: "custom", + claims: new Claim[] + { + new Claim(ClaimTypes.NameIdentifier, userId), + new Claim(ClaimTypes.Name, user.UserName) + } + ); + + return; + } + + // context set to Failure + context.Result = new GrantValidationResult( + TokenRequestErrors.UnauthorizedClient, "Invalid Credentials"); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleClaimConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleClaimConfiguration.cs new file mode 100644 index 0000000..f662e0a --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleClaimConfiguration.cs @@ -0,0 +1,22 @@ +using BookingMonolith.Identity.Identities.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Identity.Data.Configurations; + +[RegisterIdentityConfiguration] +public class RoleClaimConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(RoleClaim)); + + builder.HasKey(r => r.Id); + + builder.Property(r => r.Id) + .ValueGeneratedOnAdd(); + + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleConfiguration.cs new file mode 100644 index 0000000..1e075b9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/RoleConfiguration.cs @@ -0,0 +1,23 @@ +using BookingMonolith.Identity.Identities.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Identity.Data.Configurations; + +[RegisterIdentityConfiguration] +public class RoleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Role)); + + builder.HasKey(r => r.Id); + + builder.Property(r => r.Id) + .IsRequired() + .ValueGeneratedOnAdd(); + + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserClaimConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserClaimConfiguration.cs new file mode 100644 index 0000000..0288a07 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserClaimConfiguration.cs @@ -0,0 +1,22 @@ +using BookingMonolith.Identity.Identities.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Identity.Data.Configurations; + +[RegisterIdentityConfiguration] +public class UserClaimConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(UserClaim)); + + builder.HasKey(r => r.Id); + + builder.Property(r => r.Id) + .ValueGeneratedOnAdd(); + + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserConfiguration.cs new file mode 100644 index 0000000..1d06eb1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserConfiguration.cs @@ -0,0 +1,23 @@ +using BookingMonolith.Identity.Identities.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Identity.Data.Configurations; + +[RegisterIdentityConfiguration] +public class UserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(User)); + + builder.HasKey(r => r.Id); + + builder.Property(r => r.Id) + .IsRequired() + .ValueGeneratedOnAdd(); // Auto-generate GUID on add + + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserLoginConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserLoginConfiguration.cs new file mode 100644 index 0000000..815e40a --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserLoginConfiguration.cs @@ -0,0 +1,20 @@ +using BookingMonolith.Identity.Identities.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Identity.Data.Configurations; + +[RegisterIdentityConfiguration] +public class UserLoginConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(UserLogin)); + + builder.HasKey(r => new { r.LoginProvider, r.ProviderKey }); + + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + } +} + diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserRoleConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserRoleConfiguration.cs new file mode 100644 index 0000000..8842616 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserRoleConfiguration.cs @@ -0,0 +1,19 @@ +using BookingMonolith.Identity.Identities.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Identity.Data.Configurations; + +[RegisterIdentityConfiguration] +public class UserRoleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(UserRole)); + + builder.HasKey(r => new { r.UserId, r.RoleId }); + + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserTokenConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserTokenConfiguration.cs new file mode 100644 index 0000000..35885a1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Configurations/UserTokenConfiguration.cs @@ -0,0 +1,19 @@ +using BookingMonolith.Identity.Identities.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Identity.Data.Configurations; + +[RegisterIdentityConfiguration] +public class UserTokenConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(UserToken)); + + builder.HasKey(r => new { r.UserId, r.LoginProvider, r.Name }); + + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/DesignTimeDbContextFactory.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..e1b32a6 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/DesignTimeDbContextFactory.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace BookingMonolith.Identity.Data; + +public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory +{ + public IdentityContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + builder.UseNpgsql("Server=localhost;Port=5432;Database=booking_monolith;User Id=postgres;Password=postgres;Include Error Detail=true") + .UseSnakeCaseNamingConvention(); + return new IdentityContext(builder.Options); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/EfTxIdentityBehavior.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/EfTxIdentityBehavior.cs new file mode 100644 index 0000000..9c64951 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/EfTxIdentityBehavior.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using System.Transactions; +using BuildingBlocks.Core; +using BuildingBlocks.PersistMessageProcessor; +using BuildingBlocks.Polly; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace BookingMonolith.Identity.Data; + +public class EfTxIdentityBehavior : IPipelineBehavior +where TRequest : notnull, IRequest +where TResponse : notnull +{ + private readonly ILogger> _logger; + private readonly IdentityContext _identityContext; + private readonly IPersistMessageDbContext _persistMessageDbContext; + private readonly IEventDispatcher _eventDispatcher; + + public EfTxIdentityBehavior( + ILogger> logger, + IdentityContext identityContext, + IPersistMessageDbContext persistMessageDbContext, + IEventDispatcher eventDispatcher + ) + { + _logger = logger; + _identityContext = identityContext; + _persistMessageDbContext = persistMessageDbContext; + _eventDispatcher = eventDispatcher; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) + { + _logger.LogInformation( + "{Prefix} Handled command {MediatrRequest}", + GetType().Name, + typeof(TRequest).FullName); + + _logger.LogDebug( + "{Prefix} Handled command {MediatrRequest} with content {RequestContent}", + GetType().Name, + typeof(TRequest).FullName, + JsonSerializer.Serialize(request)); + + var response = await next(); + + _logger.LogInformation( + "{Prefix} Executed the {MediatrRequest} request", + GetType().Name, + typeof(TRequest).FullName); + + while (true) + { + var domainEvents = _identityContext.GetDomainEvents(); + + if (domainEvents is null || !domainEvents.Any()) + { + return response; + } + + _logger.LogInformation( + "{Prefix} Open the transaction for {MediatrRequest}", + GetType().Name, + typeof(TRequest).FullName); + + using var scope = new TransactionScope( + TransactionScopeOption.Required, + new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }, + TransactionScopeAsyncFlowOption.Enabled); + + await _eventDispatcher.SendAsync( + domainEvents.ToArray(), + typeof(TRequest), + cancellationToken); + + // Save data to database with some retry policy in distributed transaction + await _identityContext.RetryOnFailure( + async () => + { + await _identityContext.SaveChangesAsync(cancellationToken); + }); + + // Save data to database with some retry policy in distributed transaction + await _persistMessageDbContext.RetryOnFailure( + async () => + { + await _persistMessageDbContext.SaveChangesAsync(cancellationToken); + }); + + scope.Complete(); + + return response; + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/IdentityContext.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/IdentityContext.cs new file mode 100644 index 0000000..cdc95b1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/IdentityContext.cs @@ -0,0 +1,176 @@ +using System.Collections.Immutable; +using System.Data; +using System.Reflection; +using BookingMonolith.Identity.Identities.Models; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Core.Model; +using BuildingBlocks.EFCore; +using Humanizer; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; + +namespace BookingMonolith.Identity.Data; + +public sealed class IdentityContext : IdentityDbContext, IDbContext +{ + private readonly ILogger? _logger; + private IDbContextTransaction _currentTransaction; + + public IdentityContext(DbContextOptions options, ILogger? logger = null) : base(options) + { + _logger = logger; + } + + protected override void OnModelCreating(ModelBuilder builder) + { + var types = typeof(IdentityRoot).Assembly.GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .ToList(); + + foreach (var type in types) + { + dynamic configuration = Activator.CreateInstance(type)!; + builder.ApplyConfiguration(configuration); + } + + builder.HasDefaultSchema(nameof(Identity).Underscore()); + base.OnModelCreating(builder); + builder.FilterSoftDeletedProperties(); + builder.ToSnakeCaseTables(); + } + + public IExecutionStrategy CreateExecutionStrategy() => Database.CreateExecutionStrategy(); + + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction != null) + return; + + _currentTransaction = await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); + } + + public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + try + { + await SaveChangesAsync(cancellationToken); + await _currentTransaction?.CommitAsync(cancellationToken)!; + } + catch + { + await RollbackTransactionAsync(cancellationToken); + throw; + } + finally + { + _currentTransaction?.Dispose(); + _currentTransaction = null; + } + } + + public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + try + { + await _currentTransaction?.RollbackAsync(cancellationToken)!; + } + finally + { + _currentTransaction?.Dispose(); + _currentTransaction = null; + } + } + + //ref: https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency#execution-strategies-and-transactions + public Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default) + { + var strategy = CreateExecutionStrategy(); + return strategy.ExecuteAsync(async () => + { + await using var transaction = + await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); + try + { + await SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + } + catch + { + await transaction.RollbackAsync(cancellationToken); + throw; + } + }); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + OnBeforeSaving(); + try + { + return await base.SaveChangesAsync(cancellationToken); + } + //ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#resolving-concurrency-conflicts + catch (DbUpdateConcurrencyException ex) + { + foreach (var entry in ex.Entries) + { + var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken); + + if (databaseValues == null) + { + _logger.LogError("The record no longer exists in the database, The record has been deleted by another user."); + throw; + } + + // Refresh the original values to bypass next concurrency check + entry.OriginalValues.SetValues(databaseValues); + } + + return await base.SaveChangesAsync(cancellationToken); + } + } + + public IReadOnlyList GetDomainEvents() + { + var domainEntities = ChangeTracker + .Entries() + .Where(x => x.Entity.DomainEvents.Any()) + .Select(x => x.Entity) + .ToList(); + + var domainEvents = domainEntities + .SelectMany(x => x.DomainEvents) + .ToImmutableList(); + + domainEntities.ForEach(entity => entity.ClearDomainEvents()); + + return domainEvents.ToImmutableList(); + } + + private void OnBeforeSaving() + { + try + { + foreach (var entry in ChangeTracker.Entries()) + { + switch (entry.State) + { + case EntityState.Modified: + entry.Entity.Version++; + break; + + case EntityState.Deleted: + entry.Entity.Version++; + break; + } + } + } + catch (Exception ex) + { + throw new Exception("try for find IVersion", ex); + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.Designer.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.Designer.cs new file mode 100644 index 0000000..797fa60 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.Designer.cs @@ -0,0 +1,381 @@ +// +using System; +using BookingMonolith.Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BookingMonolith.Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20250407214345_initial")] + partial class initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("identity") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("asp_net_roles", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_role_claims_role_id"); + + b.ToTable("asp_net_role_claims", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PassPortNumber") + .IsRequired() + .HasColumnType("text") + .HasColumnName("pass_port_number"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_users"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("asp_net_users", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_claims_user_id"); + + b.ToTable("asp_net_user_claims", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_asp_net_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_logins_user_id"); + + b.ToTable("asp_net_user_logins", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_asp_net_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_user_roles_role_id"); + + b.ToTable("asp_net_user_roles", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_asp_net_user_tokens"); + + b.ToTable("asp_net_user_tokens", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.RoleClaim", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserClaim", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserLogin", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserRole", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); + + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserToken", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.cs new file mode 100644 index 0000000..d950375 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/20250407214345_initial.cs @@ -0,0 +1,263 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BookingMonolith.Identity.Data.Migrations +{ + /// + public partial class initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "identity"); + + migrationBuilder.CreateTable( + name: "asp_net_roles", + schema: "identity", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + version = table.Column(type: "bigint", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + concurrency_stamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_asp_net_roles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "asp_net_users", + schema: "identity", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + first_name = table.Column(type: "text", nullable: false), + last_name = table.Column(type: "text", nullable: false), + pass_port_number = table.Column(type: "text", nullable: false), + version = table.Column(type: "bigint", nullable: false), + user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + email_confirmed = table.Column(type: "boolean", nullable: false), + password_hash = table.Column(type: "text", nullable: true), + security_stamp = table.Column(type: "text", nullable: true), + concurrency_stamp = table.Column(type: "text", nullable: true), + phone_number = table.Column(type: "text", nullable: true), + phone_number_confirmed = table.Column(type: "boolean", nullable: false), + two_factor_enabled = table.Column(type: "boolean", nullable: false), + lockout_end = table.Column(type: "timestamp with time zone", nullable: true), + lockout_enabled = table.Column(type: "boolean", nullable: false), + access_failed_count = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_asp_net_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "asp_net_role_claims", + schema: "identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + version = table.Column(type: "bigint", nullable: false), + role_id = table.Column(type: "uuid", nullable: false), + claim_type = table.Column(type: "text", nullable: true), + claim_value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_asp_net_role_claims", x => x.id); + table.ForeignKey( + name: "fk_asp_net_role_claims_asp_net_roles_role_id", + column: x => x.role_id, + principalSchema: "identity", + principalTable: "asp_net_roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "asp_net_user_claims", + schema: "identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + version = table.Column(type: "bigint", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + claim_type = table.Column(type: "text", nullable: true), + claim_value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_asp_net_user_claims", x => x.id); + table.ForeignKey( + name: "fk_asp_net_user_claims_asp_net_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "asp_net_users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "asp_net_user_logins", + schema: "identity", + columns: table => new + { + login_provider = table.Column(type: "text", nullable: false), + provider_key = table.Column(type: "text", nullable: false), + version = table.Column(type: "bigint", nullable: false), + provider_display_name = table.Column(type: "text", nullable: true), + user_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_asp_net_user_logins", x => new { x.login_provider, x.provider_key }); + table.ForeignKey( + name: "fk_asp_net_user_logins_asp_net_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "asp_net_users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "asp_net_user_roles", + schema: "identity", + columns: table => new + { + user_id = table.Column(type: "uuid", nullable: false), + role_id = table.Column(type: "uuid", nullable: false), + version = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_asp_net_user_roles", x => new { x.user_id, x.role_id }); + table.ForeignKey( + name: "fk_asp_net_user_roles_asp_net_roles_role_id", + column: x => x.role_id, + principalSchema: "identity", + principalTable: "asp_net_roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_asp_net_user_roles_asp_net_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "asp_net_users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "asp_net_user_tokens", + schema: "identity", + columns: table => new + { + user_id = table.Column(type: "uuid", nullable: false), + login_provider = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: false), + version = table.Column(type: "bigint", nullable: false), + value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_asp_net_user_tokens", x => new { x.user_id, x.login_provider, x.name }); + table.ForeignKey( + name: "fk_asp_net_user_tokens_asp_net_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "asp_net_users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_asp_net_role_claims_role_id", + schema: "identity", + table: "asp_net_role_claims", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + schema: "identity", + table: "asp_net_roles", + column: "normalized_name", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_asp_net_user_claims_user_id", + schema: "identity", + table: "asp_net_user_claims", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_asp_net_user_logins_user_id", + schema: "identity", + table: "asp_net_user_logins", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_asp_net_user_roles_role_id", + schema: "identity", + table: "asp_net_user_roles", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + schema: "identity", + table: "asp_net_users", + column: "normalized_email"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + schema: "identity", + table: "asp_net_users", + column: "normalized_user_name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "asp_net_role_claims", + schema: "identity"); + + migrationBuilder.DropTable( + name: "asp_net_user_claims", + schema: "identity"); + + migrationBuilder.DropTable( + name: "asp_net_user_logins", + schema: "identity"); + + migrationBuilder.DropTable( + name: "asp_net_user_roles", + schema: "identity"); + + migrationBuilder.DropTable( + name: "asp_net_user_tokens", + schema: "identity"); + + migrationBuilder.DropTable( + name: "asp_net_roles", + schema: "identity"); + + migrationBuilder.DropTable( + name: "asp_net_users", + schema: "identity"); + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs new file mode 100644 index 0000000..f80a1af --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs @@ -0,0 +1,378 @@ +// +using System; +using BookingMonolith.Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BookingMonolith.Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + partial class IdentityContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("identity") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("asp_net_roles", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_role_claims_role_id"); + + b.ToTable("asp_net_role_claims", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PassPortNumber") + .IsRequired() + .HasColumnType("text") + .HasColumnName("pass_port_number"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_users"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("asp_net_users", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_asp_net_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_claims_user_id"); + + b.ToTable("asp_net_user_claims", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_asp_net_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_logins_user_id"); + + b.ToTable("asp_net_user_logins", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_asp_net_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_user_roles_role_id"); + + b.ToTable("asp_net_user_roles", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_asp_net_user_tokens"); + + b.ToTable("asp_net_user_tokens", "identity"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.RoleClaim", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserClaim", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserLogin", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserRole", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); + + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); + }); + + modelBuilder.Entity("BookingMonolith.Identity.Identities.Models.UserToken", b => + { + b.HasOne("BookingMonolith.Identity.Identities.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/RegisterIdentityConfigurationAttribute.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/RegisterIdentityConfigurationAttribute.cs new file mode 100644 index 0000000..af4542b --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/RegisterIdentityConfigurationAttribute.cs @@ -0,0 +1,4 @@ +namespace BookingMonolith.Identity.Data; + +[AttributeUsage(AttributeTargets.Class)] +public class RegisterIdentityConfigurationAttribute : Attribute { } diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/IdentityDataSeeder.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/IdentityDataSeeder.cs new file mode 100644 index 0000000..decdb43 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/IdentityDataSeeder.cs @@ -0,0 +1,99 @@ +using BookingMonolith.Identity.Identities.Constants; +using BookingMonolith.Identity.Identities.Models; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Core; +using BuildingBlocks.EFCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Identity.Data.Seed; + +public class IdentityDataSeeder : IDataSeeder +{ + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + private readonly IEventDispatcher _eventDispatcher; + private readonly IdentityContext _identityContext; + + public IdentityDataSeeder( + UserManager userManager, + RoleManager roleManager, + IEventDispatcher eventDispatcher, + IdentityContext identityContext + ) + { + _userManager = userManager; + _roleManager = roleManager; + _eventDispatcher = eventDispatcher; + _identityContext = identityContext; + } + + public async Task SeedAllAsync() + { + var pendingMigrations = await _identityContext.Database.GetPendingMigrationsAsync(); + + if (!pendingMigrations.Any()) + { + await SeedRoles(); + await SeedUsers(); + } + } + + private async Task SeedRoles() + { + if (!await _identityContext.Roles.AnyAsync()) + { + if (await _roleManager.RoleExistsAsync(Constants.Role.Admin) == false) + { + await _roleManager.CreateAsync(new Role { Name = Constants.Role.Admin }); + } + + if (await _roleManager.RoleExistsAsync(Constants.Role.User) == false) + { + await _roleManager.CreateAsync(new Role { Name = Constants.Role.User }); + } + } + } + + private async Task SeedUsers() + { + if (!await _identityContext.Users.AnyAsync()) + { + if (await _userManager.FindByNameAsync("samh") == null) + { + var result = await _userManager.CreateAsync(InitialData.Users.First(), "Admin@123456"); + + if (result.Succeeded) + { + await _userManager.AddToRoleAsync(InitialData.Users.First(), Constants.Role.Admin); + + await _eventDispatcher.SendAsync( + new UserCreated( + InitialData.Users.First().Id, + InitialData.Users.First().FirstName + + " " + + InitialData.Users.First().LastName, + InitialData.Users.First().PassPortNumber)); + } + } + + if (await _userManager.FindByNameAsync("meysamh2") == null) + { + var result = await _userManager.CreateAsync(InitialData.Users.Last(), "User@123456"); + + if (result.Succeeded) + { + await _userManager.AddToRoleAsync(InitialData.Users.Last(), Constants.Role.User); + + await _eventDispatcher.SendAsync( + new UserCreated( + InitialData.Users.Last().Id, + InitialData.Users.Last().FirstName + + " " + + InitialData.Users.Last().LastName, + InitialData.Users.Last().PassPortNumber)); + } + } + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/InitialData.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/InitialData.cs new file mode 100644 index 0000000..db056aa --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/Seed/InitialData.cs @@ -0,0 +1,36 @@ +using BookingMonolith.Identity.Identities.Models; +using MassTransit; + +namespace BookingMonolith.Identity.Data.Seed; + +public static class InitialData +{ + public static List Users { get; } + + static InitialData() + { + Users = new List + { + new User + { + Id = NewId.NextGuid(), + FirstName = "Sam", + LastName = "H", + UserName = "samh", + PassPortNumber = "123456789", + Email = "sam@test.com", + SecurityStamp = Guid.NewGuid().ToString() + }, + new User + { + Id = NewId.NextGuid(), + FirstName = "Sam2", + LastName = "H2", + UserName = "samh2", + PassPortNumber = "987654321", + Email = "sam2@test.com", + SecurityStamp = Guid.NewGuid().ToString() + } + }; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/readme.md b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/readme.md new file mode 100644 index 0000000..ef05f2e --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Data/readme.md @@ -0,0 +1,2 @@ +dotnet ef migrations add initial --context IdentityContext -o "Identity\Data\Migrations" +dotnet ef database update --context IdentityContext diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Extensions/Infrastructure/IdentityServerExtensions.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Extensions/Infrastructure/IdentityServerExtensions.cs new file mode 100644 index 0000000..6c31e85 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Extensions/Infrastructure/IdentityServerExtensions.cs @@ -0,0 +1,48 @@ +using BookingMonolith.Identity.Configurations; +using BookingMonolith.Identity.Data; +using BookingMonolith.Identity.Identities.Models; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace BookingMonolith.Identity.Extensions.Infrastructure; + +public static class IdentityServerExtensions +{ + public static WebApplicationBuilder AddCustomIdentityServer(this WebApplicationBuilder builder) + { + builder.Services.AddValidateOptions(); + var authOptions = builder.Services.GetOptions(nameof(AuthOptions)); + + builder.Services.AddIdentity(config => + { + config.Password.RequiredLength = 6; + config.Password.RequireDigit = false; + config.Password.RequireNonAlphanumeric = false; + config.Password.RequireUppercase = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + var identityServerBuilder = builder.Services.AddIdentityServer(options => + { + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + options.IssuerUri = authOptions.IssuerUri; + }) + .AddInMemoryIdentityResources(Config.IdentityResources) + .AddInMemoryApiResources(Config.ApiResources) + .AddInMemoryApiScopes(Config.ApiScopes) + .AddInMemoryClients(Config.Clients) + .AddAspNetIdentity() + .AddResourceOwnerValidator(); + + //ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html + identityServerBuilder.AddDeveloperSigningCredential(); + + return builder; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Constants/Constants.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Constants/Constants.cs new file mode 100644 index 0000000..ca5f833 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Constants/Constants.cs @@ -0,0 +1,21 @@ +namespace BookingMonolith.Identity.Identities.Constants; + +public static class Constants +{ + public static class Role + { + public const string Admin = "admin"; + public const string User = "user"; + } + + public static class StandardScopes + { + public const string Roles = "roles"; + public const string FlightApi = "flight-api"; + public const string PassengerApi = "passenger-api"; + public const string BookingApi = "booking-api"; + public const string IdentityApi = "identity-api"; + public const string BookingModularMonolith = "booking-modular-monolith"; + public const string BookingMonolith = "booking-monolith"; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Exceptions/RegisterIdentityUserException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Exceptions/RegisterIdentityUserException.cs new file mode 100644 index 0000000..779f61c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Exceptions/RegisterIdentityUserException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Identity.Identities.Exceptions; + +public class RegisterIdentityUserException : AppException +{ + public RegisterIdentityUserException(string error) : base(error) + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/IdentityMappings.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/IdentityMappings.cs new file mode 100644 index 0000000..aac0e04 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/IdentityMappings.cs @@ -0,0 +1,14 @@ +using BookingMonolith.Identity.Identities.Features.RegisteringNewUser.V1; +using Mapster; + +namespace BookingMonolith.Identity.Identities.Features; + +public class IdentityMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .ConstructUsing(x => new RegisterNewUser(x.FirstName, x.LastName, x.Username, x.Email, + x.Password, x.ConfirmPassword, x.PassportNumber)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/RegisteringNewUser/V1/RegisterNewUser.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/RegisteringNewUser/V1/RegisterNewUser.cs new file mode 100644 index 0000000..112ec2c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Features/RegisteringNewUser/V1/RegisterNewUser.cs @@ -0,0 +1,130 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Identity.Identities.Exceptions; +using BookingMonolith.Identity.Identities.Models; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Core; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Routing; + +namespace BookingMonolith.Identity.Identities.Features.RegisteringNewUser.V1; + +public record RegisterNewUser(string FirstName, string LastName, string Username, string Email, + string Password, string ConfirmPassword, string PassportNumber) : ICommand; + +public record RegisterNewUserResult(Guid Id, string FirstName, string LastName, string Username, string PassportNumber); + +public record RegisterNewUserRequestDto(string FirstName, string LastName, string Username, string Email, + string Password, string ConfirmPassword, string PassportNumber); + +public record RegisterNewUserResponseDto(Guid Id, string FirstName, string LastName, string Username, + string PassportNumber); + +public class RegisterNewUserEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/identity/register-user", async ( + RegisterNewUserRequestDto request, IMediator mediator, IMapper mapper, + CancellationToken cancellationToken) => + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("RegisterUser") + .WithApiVersionSet(builder.NewApiVersionSet("Identity").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Register User") + .WithDescription("Register User") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class RegisterNewUserValidator : AbstractValidator +{ + public RegisterNewUserValidator() + { + RuleFor(x => x.Password).NotEmpty().WithMessage("Please enter the password"); + RuleFor(x => x.ConfirmPassword).NotEmpty().WithMessage("Please enter the confirmation password"); + + RuleFor(x => x).Custom((x, context) => + { + if (x.Password != x.ConfirmPassword) + { + context.AddFailure(nameof(x.Password), "Passwords should match"); + } + }); + + RuleFor(x => x.Username).NotEmpty().WithMessage("Please enter the username"); + RuleFor(x => x.FirstName).NotEmpty().WithMessage("Please enter the first name"); + RuleFor(x => x.LastName).NotEmpty().WithMessage("Please enter the last name"); + RuleFor(x => x.Email).NotEmpty().WithMessage("Please enter the last email") + .EmailAddress().WithMessage("A valid email is required"); + } +} + +internal class RegisterNewUserHandler : ICommandHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly UserManager _userManager; + + public RegisterNewUserHandler(UserManager userManager, + IEventDispatcher eventDispatcher) + { + _userManager = userManager; + _eventDispatcher = eventDispatcher; + } + + public async Task Handle(RegisterNewUser request, + CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var applicationUser = new User() + { + FirstName = request.FirstName, + LastName = request.LastName, + UserName = request.Username, + Email = request.Email, + PasswordHash = request.Password, + PassPortNumber = request.PassportNumber + }; + + var identityResult = await _userManager.CreateAsync(applicationUser, request.Password); + var roleResult = await _userManager.AddToRoleAsync(applicationUser, Constants.Constants.Role.User); + + if (identityResult.Succeeded == false) + { + throw new RegisterIdentityUserException(string.Join(',', identityResult.Errors.Select(e => e.Description))); + } + + if (roleResult.Succeeded == false) + { + throw new RegisterIdentityUserException(string.Join(',', roleResult.Errors.Select(e => e.Description))); + } + + await _eventDispatcher.SendAsync(new UserCreated(applicationUser.Id, + applicationUser.FirstName + " " + applicationUser.LastName, + applicationUser.PassPortNumber), cancellationToken: cancellationToken); + + return new RegisterNewUserResult(applicationUser.Id, applicationUser.FirstName, applicationUser.LastName, + applicationUser.UserName, applicationUser.PassPortNumber); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/Role.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/Role.cs new file mode 100644 index 0000000..348ed68 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/Role.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Core.Model; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Identities.Models; + +public class Role : IdentityRole, IVersion +{ + public long Version { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/RoleClaim.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/RoleClaim.cs new file mode 100644 index 0000000..fb2b3e6 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/RoleClaim.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Core.Model; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Identities.Models; + +public class RoleClaim : IdentityRoleClaim, IVersion +{ + public long Version { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/User.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/User.cs new file mode 100644 index 0000000..01607e2 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/User.cs @@ -0,0 +1,12 @@ +using BuildingBlocks.Core.Model; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Identities.Models; + +public class User : IdentityUser, IVersion +{ + public required string FirstName { get; init; } + public required string LastName { get; init; } + public required string PassPortNumber { get; init; } + public long Version { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserClaim.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserClaim.cs new file mode 100644 index 0000000..a6d60dc --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserClaim.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Core.Model; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Identities.Models; + +public class UserClaim : IdentityUserClaim, IVersion +{ + public long Version { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserLogin.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserLogin.cs new file mode 100644 index 0000000..778cff6 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserLogin.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Core.Model; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Identities.Models; + +public class UserLogin : IdentityUserLogin, IVersion +{ + public long Version { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserRole.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserRole.cs new file mode 100644 index 0000000..e1cb34b --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserRole.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Core.Model; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Identities.Models; + +public class UserRole : IdentityUserRole, IVersion +{ + public long Version { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserToken.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserToken.cs new file mode 100644 index 0000000..f40c404 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/Identities/Models/UserToken.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Core.Model; +using Microsoft.AspNetCore.Identity; + +namespace BookingMonolith.Identity.Identities.Models; + +public class UserToken : IdentityUserToken, IVersion +{ + public long Version { get; set; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityEventMapper.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityEventMapper.cs new file mode 100644 index 0000000..824abae --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityEventMapper.cs @@ -0,0 +1,23 @@ +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; + +namespace BookingMonolith.Identity; + +public sealed class IdentityEventMapper : IEventMapper +{ + public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event) + { + return @event switch + { + _ => null + }; + } + + public IInternalCommand? MapToInternalCommand(IDomainEvent @event) + { + return @event switch + { + _ => null + }; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityRoot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityRoot.cs new file mode 100644 index 0000000..510ee6e --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Identity/IdentityRoot.cs @@ -0,0 +1,6 @@ +namespace BookingMonolith.Identity; + +public class IdentityRoot +{ + +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Configurations/PassengerConfiguration.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Configurations/PassengerConfiguration.cs new file mode 100644 index 0000000..d5dc1fc --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Configurations/PassengerConfiguration.cs @@ -0,0 +1,60 @@ +using BookingMonolith.Passenger.Passengers.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BookingMonolith.Passenger.Data.Configurations; + +[RegisterPassengerConfiguration] +public class PassengerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + RelationalEntityTypeBuilderExtensions.ToTable((EntityTypeBuilder)builder, nameof(Passengers.Models.Passenger)); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever() + .HasConversion(passengerId => passengerId.Value, dbId => PassengerId.Of(dbId)); + + builder.Property(r => r.Version).IsConcurrencyToken(); + + builder.OwnsOne( + x => x.Name, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Passengers.Models.Passenger.Name)) + .HasMaxLength(50) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.PassportNumber, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Passengers.Models.Passenger.PassportNumber)) + .HasMaxLength(10) + .IsRequired(); + } + ); + + builder.OwnsOne( + x => x.Age, + a => + { + a.Property(p => p.Value) + .HasColumnName(nameof(Passengers.Models.Passenger.Age)) + .HasMaxLength(3) + .IsRequired(); + } + ); + + builder.Property(x => x.PassengerType) + .IsRequired() + .HasDefaultValue(Passengers.Enums.PassengerType.Unknown) + .HasConversion( + x => x.ToString(), + x => (Passengers.Enums.PassengerType)Enum.Parse(typeof(Passengers.Enums.PassengerType), x)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/DesignTimeDbContextFactory.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..fd31e61 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/DesignTimeDbContextFactory.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace BookingMonolith.Passenger.Data; + +public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory +{ + public PassengerDbContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + builder.UseNpgsql("Server=localhost;Port=5432;Database=booking_monolith;User Id=postgres;Password=postgres;Include Error Detail=true") + .UseSnakeCaseNamingConvention(); + return new PassengerDbContext(builder.Options); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/EfTxPassengerBehavior.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/EfTxPassengerBehavior.cs new file mode 100644 index 0000000..de628d3 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/EfTxPassengerBehavior.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using System.Transactions; +using BuildingBlocks.Core; +using BuildingBlocks.PersistMessageProcessor; +using BuildingBlocks.Polly; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace BookingMonolith.Passenger.Data; + +public class EfTxPassengerBehavior : IPipelineBehavior +where TRequest : notnull, IRequest +where TResponse : notnull +{ + private readonly ILogger> _logger; + private readonly PassengerDbContext _passengerDbContext; + private readonly IPersistMessageDbContext _persistMessageDbContext; + private readonly IEventDispatcher _eventDispatcher; + + public EfTxPassengerBehavior( + ILogger> logger, + PassengerDbContext passengerDbContext, + IPersistMessageDbContext persistMessageDbContext, + IEventDispatcher eventDispatcher + ) + { + _logger = logger; + _passengerDbContext = passengerDbContext; + _persistMessageDbContext = persistMessageDbContext; + _eventDispatcher = eventDispatcher; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) + { + _logger.LogInformation( + "{Prefix} Handled command {MediatrRequest}", + GetType().Name, + typeof(TRequest).FullName); + + _logger.LogDebug( + "{Prefix} Handled command {MediatrRequest} with content {RequestContent}", + GetType().Name, + typeof(TRequest).FullName, + JsonSerializer.Serialize(request)); + + var response = await next(); + + _logger.LogInformation( + "{Prefix} Executed the {MediatrRequest} request", + GetType().Name, + typeof(TRequest).FullName); + + while (true) + { + var domainEvents = _passengerDbContext.GetDomainEvents(); + + if (domainEvents is null || !domainEvents.Any()) + { + return response; + } + + _logger.LogInformation( + "{Prefix} Open the transaction for {MediatrRequest}", + GetType().Name, + typeof(TRequest).FullName); + + using var scope = new TransactionScope( + TransactionScopeOption.Required, + new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }, + TransactionScopeAsyncFlowOption.Enabled); + + await _eventDispatcher.SendAsync( + domainEvents.ToArray(), + typeof(TRequest), + cancellationToken); + + // Save data to database with some retry policy in distributed transaction + await _passengerDbContext.RetryOnFailure( + async () => + { + await _passengerDbContext.SaveChangesAsync(cancellationToken); + }); + + // Save data to database with some retry policy in distributed transaction + await _persistMessageDbContext.RetryOnFailure( + async () => + { + await _persistMessageDbContext.SaveChangesAsync(cancellationToken); + }); + + scope.Complete(); + + return response; + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.Designer.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.Designer.cs new file mode 100644 index 0000000..a0592ac --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.Designer.cs @@ -0,0 +1,151 @@ +// +using System; +using BookingMonolith.Passenger.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BookingMonolith.Passenger.Data.Migrations +{ + [DbContext(typeof(PassengerDbContext))] + [Migration("20250407215445_initial")] + partial class initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("passenger") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookingMonolith.Passenger.Passengers.Models.Passenger", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("PassengerType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("passenger_type"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_passenger"); + + b.ToTable("passenger", "passenger"); + }); + + modelBuilder.Entity("BookingMonolith.Passenger.Passengers.Models.Passenger", b => + { + b.OwnsOne("BookingMonolith.Passenger.Passengers.ValueObjects.Age", "Age", b1 => + { + b1.Property("PassengerId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(3) + .HasColumnType("integer") + .HasColumnName("age"); + + b1.HasKey("PassengerId") + .HasName("pk_passenger"); + + b1.ToTable("passenger", "passenger"); + + b1.WithOwner() + .HasForeignKey("PassengerId") + .HasConstraintName("fk_passenger_passenger_id"); + }); + + b.OwnsOne("BookingMonolith.Passenger.Passengers.ValueObjects.Name", "Name", b1 => + { + b1.Property("PassengerId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b1.HasKey("PassengerId") + .HasName("pk_passenger"); + + b1.ToTable("passenger", "passenger"); + + b1.WithOwner() + .HasForeignKey("PassengerId") + .HasConstraintName("fk_passenger_passenger_id"); + }); + + b.OwnsOne("BookingMonolith.Passenger.Passengers.ValueObjects.PassportNumber", "PassportNumber", b1 => + { + b1.Property("PassengerId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("passport_number"); + + b1.HasKey("PassengerId") + .HasName("pk_passenger"); + + b1.ToTable("passenger", "passenger"); + + b1.WithOwner() + .HasForeignKey("PassengerId") + .HasConstraintName("fk_passenger_passenger_id"); + }); + + b.Navigation("Age"); + + b.Navigation("Name") + .IsRequired(); + + b.Navigation("PassportNumber") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.cs new file mode 100644 index 0000000..69c2ee8 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/20250407215445_initial.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BookingMonolith.Passenger.Data.Migrations +{ + /// + public partial class initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "passenger"); + + migrationBuilder.CreateTable( + name: "passenger", + schema: "passenger", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + passport_number = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + passenger_type = table.Column(type: "text", nullable: false, defaultValue: "Unknown"), + age = table.Column(type: "integer", maxLength: 3, nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "bigint", nullable: true), + last_modified = table.Column(type: "timestamp with time zone", nullable: true), + last_modified_by = table.Column(type: "bigint", nullable: true), + is_deleted = table.Column(type: "boolean", nullable: false), + version = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_passenger", x => x.id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "passenger", + schema: "passenger"); + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs new file mode 100644 index 0000000..7e5a887 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs @@ -0,0 +1,148 @@ +// +using System; +using BookingMonolith.Passenger.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BookingMonolith.Passenger.Data.Migrations +{ + [DbContext(typeof(PassengerDbContext))] + partial class PassengerDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("passenger") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookingMonolith.Passenger.Passengers.Models.Passenger", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasColumnName("created_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint") + .HasColumnName("last_modified_by"); + + b.Property("PassengerType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Unknown") + .HasColumnName("passenger_type"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_passenger"); + + b.ToTable("passenger", "passenger"); + }); + + modelBuilder.Entity("BookingMonolith.Passenger.Passengers.Models.Passenger", b => + { + b.OwnsOne("BookingMonolith.Passenger.Passengers.ValueObjects.Age", "Age", b1 => + { + b1.Property("PassengerId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .HasMaxLength(3) + .HasColumnType("integer") + .HasColumnName("age"); + + b1.HasKey("PassengerId") + .HasName("pk_passenger"); + + b1.ToTable("passenger", "passenger"); + + b1.WithOwner() + .HasForeignKey("PassengerId") + .HasConstraintName("fk_passenger_passenger_id"); + }); + + b.OwnsOne("BookingMonolith.Passenger.Passengers.ValueObjects.Name", "Name", b1 => + { + b1.Property("PassengerId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b1.HasKey("PassengerId") + .HasName("pk_passenger"); + + b1.ToTable("passenger", "passenger"); + + b1.WithOwner() + .HasForeignKey("PassengerId") + .HasConstraintName("fk_passenger_passenger_id"); + }); + + b.OwnsOne("BookingMonolith.Passenger.Passengers.ValueObjects.PassportNumber", "PassportNumber", b1 => + { + b1.Property("PassengerId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("passport_number"); + + b1.HasKey("PassengerId") + .HasName("pk_passenger"); + + b1.ToTable("passenger", "passenger"); + + b1.WithOwner() + .HasForeignKey("PassengerId") + .HasConstraintName("fk_passenger_passenger_id"); + }); + + b.Navigation("Age"); + + b.Navigation("Name") + .IsRequired(); + + b.Navigation("PassportNumber") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerDbContext.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerDbContext.cs new file mode 100644 index 0000000..e9b2f16 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerDbContext.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using BuildingBlocks.EFCore; +using BuildingBlocks.Web; +using Humanizer; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace BookingMonolith.Passenger.Data; + +public sealed class PassengerDbContext : AppDbContextBase +{ + public PassengerDbContext(DbContextOptions options, + ICurrentUserProvider? currentUserProvider = null, ILogger? logger = null) : + base(options, currentUserProvider, logger) + { + } + + public DbSet Passengers => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + var types = typeof(PassengerRoot).Assembly.GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .ToList(); + + foreach (var type in types) + { + dynamic configuration = Activator.CreateInstance(type)!; + builder.ApplyConfiguration(configuration); + } + + builder.HasDefaultSchema(nameof(Passenger).Underscore()); + base.OnModelCreating(builder); + builder.FilterSoftDeletedProperties(); + builder.ToSnakeCaseTables(); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerReadDbContext.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerReadDbContext.cs new file mode 100644 index 0000000..c828e73 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/PassengerReadDbContext.cs @@ -0,0 +1,17 @@ +using BookingMonolith.Passenger.Passengers.Models; +using BuildingBlocks.Mongo; +using Humanizer; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace BookingMonolith.Passenger.Data; + +public class PassengerReadDbContext : MongoDbContext +{ + public PassengerReadDbContext(IOptions options) : base(options) + { + Passenger = GetCollection(nameof(Passenger).Underscore()); + } + + public IMongoCollection Passenger { get; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/RegisterPassengerConfigurationAttribute.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/RegisterPassengerConfigurationAttribute.cs new file mode 100644 index 0000000..571ece9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/RegisterPassengerConfigurationAttribute.cs @@ -0,0 +1,4 @@ +namespace BookingMonolith.Passenger.Data; + +[AttributeUsage(AttributeTargets.Class)] +public class RegisterPassengerConfigurationAttribute : Attribute { } diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/readme.md b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/readme.md new file mode 100644 index 0000000..6f6d486 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Data/readme.md @@ -0,0 +1,2 @@ +dotnet ef migrations add initial --context PassengerDbContext -o "Passenger\Data\Migrations" +dotnet ef database update --context PassengerDbContext diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Exceptions/InvalidPassengerIdException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Exceptions/InvalidPassengerIdException.cs new file mode 100644 index 0000000..3d177b2 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Exceptions/InvalidPassengerIdException.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Passenger.Exceptions; + +public class InvalidPassengerIdException : BadRequestException +{ + public InvalidPassengerIdException(Guid passengerId) + : base($"PassengerId: '{passengerId}' is invalid.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/PassengerCreatedDomainEvent.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/PassengerCreatedDomainEvent.cs new file mode 100644 index 0000000..8775abf --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/PassengerCreatedDomainEvent.cs @@ -0,0 +1,5 @@ +using BuildingBlocks.Core.Event; + +namespace BookingMonolith.Passenger.Identity.Consumers.RegisteringNewUser.V1; + +public record PassengerCreatedDomainEvent(Guid Id, string Name, string PassportNumber, bool IsDeleted = false) : IDomainEvent; diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/RegisterNewUser.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/RegisterNewUser.cs new file mode 100644 index 0000000..4979697 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/RegisterNewUser.cs @@ -0,0 +1,59 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Passenger.Data; +using BookingMonolith.Passenger.Passengers.ValueObjects; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using Humanizer; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace BookingMonolith.Passenger.Identity.Consumers.RegisteringNewUser.V1; + +public class RegisterNewUserHandler : IConsumer +{ + private readonly PassengerDbContext _passengerDbContext; + private readonly IEventDispatcher _eventDispatcher; + private readonly ILogger _logger; + private readonly AppOptions _options; + + public RegisterNewUserHandler(PassengerDbContext passengerDbContext, + IEventDispatcher eventDispatcher, + ILogger logger, + IOptions options) + { + _passengerDbContext = passengerDbContext; + _eventDispatcher = eventDispatcher; + _logger = logger; + _options = options.Value; + } + + public async Task Consume(ConsumeContext context) + { + Guard.Against.Null(context.Message, nameof(UserCreated)); + + _logger.LogInformation($"consumer for {nameof(UserCreated).Underscore()} in {_options.Name}"); + + var passengerExist = + await _passengerDbContext.Passengers.AnyAsync(x => x.PassportNumber.Value == context.Message.PassportNumber); + + if (passengerExist) + { + return; + } + + var passenger = Passengers.Models.Passenger.Create(PassengerId.Of(NewId.NextGuid()), Name.Of(context.Message.Name), + PassportNumber.Of(context.Message.PassportNumber)); + + await _passengerDbContext.AddAsync(passenger); + + await _passengerDbContext.SaveChangesAsync(); + + await _eventDispatcher.SendAsync( + new PassengerCreatedDomainEvent(passenger.Id, passenger.Name, passenger.PassportNumber), + typeof(IInternalCommand)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerEventMapper.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerEventMapper.cs new file mode 100644 index 0000000..4e1ba18 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerEventMapper.cs @@ -0,0 +1,32 @@ +using BookingMonolith.Passenger.Identity.Consumers.RegisteringNewUser.V1; +using BookingMonolith.Passenger.Passengers.Features.CompletingRegisterPassenger.V1; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; + +namespace BookingMonolith.Passenger; + +public sealed class PassengerEventMapper : IEventMapper +{ + public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event) + { + return @event switch + { + PassengerRegistrationCompletedDomainEvent e => new PassengerRegistrationCompleted(e.Id), + PassengerCreatedDomainEvent e => new PassengerCreated(e.Id), + _ => null + }; + } + + public IInternalCommand? MapToInternalCommand(IDomainEvent @event) + { + return @event switch + { + PassengerRegistrationCompletedDomainEvent e => new CompleteRegisterPassengerMongoCommand(e.Id, e.PassportNumber, e.Name, e.PassengerType, + e.Age, e.IsDeleted), + PassengerCreatedDomainEvent e => new CompleteRegisterPassengerMongoCommand(e.Id, e.PassportNumber, e.Name, Passengers.Enums.PassengerType.Unknown, + 0, e.IsDeleted), + _ => null + }; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerRoot.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerRoot.cs new file mode 100644 index 0000000..9cfe57a --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/PassengerRoot.cs @@ -0,0 +1,6 @@ +namespace BookingMonolith.Passenger; + +public class PassengerRoot +{ + +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Dtos/PassengerDto.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Dtos/PassengerDto.cs new file mode 100644 index 0000000..d521741 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Dtos/PassengerDto.cs @@ -0,0 +1,2 @@ +namespace BookingMonolith.Passenger.Passengers.Dtos; +public record PassengerDto(Guid Id, string Name, string PassportNumber, Enums.PassengerType PassengerType, int Age); diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Enums/PassengerType.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Enums/PassengerType.cs new file mode 100644 index 0000000..b13abc3 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Enums/PassengerType.cs @@ -0,0 +1,9 @@ +namespace BookingMonolith.Passenger.Passengers.Enums; + +public enum PassengerType +{ + Unknown = 0, + Male, + Female, + Baby +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidAgeException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidAgeException.cs new file mode 100644 index 0000000..5c35ae1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidAgeException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Passenger.Passengers.Exceptions; + +public class InvalidAgeException : BadRequestException +{ + public InvalidAgeException() : base("Age Cannot be null or negative") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidNameException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidNameException.cs new file mode 100644 index 0000000..6ad6019 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidNameException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Passenger.Passengers.Exceptions; + +public class InvalidNameException : BadRequestException +{ + public InvalidNameException() : base("Name cannot be empty or whitespace.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidPassportNumberException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidPassportNumberException.cs new file mode 100644 index 0000000..99b6ec1 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/InvalidPassportNumberException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Passenger.Passengers.Exceptions; + +public class InvalidPassportNumberException : BadRequestException +{ + public InvalidPassportNumberException() : base("Passport number cannot be empty or whitespace.") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs new file mode 100644 index 0000000..054c92d --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Passenger.Passengers.Exceptions; + +public class PassengerNotExist : BadRequestException +{ + public PassengerNotExist(string code = default) : base("Please register before!") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs new file mode 100644 index 0000000..a02f23c --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace BookingMonolith.Passenger.Passengers.Exceptions; + +public class PassengerNotFoundException : NotFoundException +{ + public PassengerNotFoundException(string code = default) : base("Passenger not found!") + { + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassenger.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassenger.cs new file mode 100644 index 0000000..a590df9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassenger.cs @@ -0,0 +1,116 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Passenger.Data; +using BookingMonolith.Passenger.Passengers.Dtos; +using BookingMonolith.Passenger.Passengers.Exceptions; +using BookingMonolith.Passenger.Passengers.ValueObjects; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace BookingMonolith.Passenger.Passengers.Features.CompletingRegisterPassenger.V1; + +public record CompleteRegisterPassenger + (string PassportNumber, Enums.PassengerType PassengerType, int Age) : ICommand, + IInternalCommand +{ + public Guid Id { get; init; } = NewId.NextGuid(); +} + +public record PassengerRegistrationCompletedDomainEvent(Guid Id, string Name, string PassportNumber, + Enums.PassengerType PassengerType, int Age, bool IsDeleted = false) : IDomainEvent; + +public record CompleteRegisterPassengerResult(PassengerDto PassengerDto); + +public record CompleteRegisterPassengerRequestDto(string PassportNumber, Enums.PassengerType PassengerType, int Age); + +public record CompleteRegisterPassengerResponseDto(PassengerDto PassengerDto); + +public class CompleteRegisterPassengerEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapPost($"{EndpointConfig.BaseApiPath}/passenger/complete-registration", async ( + CompleteRegisterPassengerRequestDto request, IMapper mapper, + IMediator mediator, CancellationToken cancellationToken) => + { + var command = mapper.Map(request); + + var result = await mediator.Send(command, cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("CompleteRegisterPassenger") + .WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Complete Register Passenger") + .WithDescription("Complete Register Passenger") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class CompleteRegisterPassengerValidator : AbstractValidator +{ + public CompleteRegisterPassengerValidator() + { + RuleFor(x => x.PassportNumber).NotNull().WithMessage("The PassportNumber is required!"); + RuleFor(x => x.Age).GreaterThan(0).WithMessage("The Age must be greater than 0!"); + RuleFor(x => x.PassengerType).Must(p => p.GetType().IsEnum && + p == Enums.PassengerType.Baby || + p == Enums.PassengerType.Female || + p == Enums.PassengerType.Male || + p == Enums.PassengerType.Unknown) + .WithMessage("PassengerType must be Male, Female, Baby or Unknown"); + } +} + +internal class CompleteRegisterPassengerCommandHandler : ICommandHandler +{ + private readonly IMapper _mapper; + private readonly PassengerDbContext _passengerDbContext; + + public CompleteRegisterPassengerCommandHandler(IMapper mapper, PassengerDbContext passengerDbContext) + { + _mapper = mapper; + _passengerDbContext = passengerDbContext; + } + + public async Task Handle(CompleteRegisterPassenger request, + CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var passenger = await _passengerDbContext.Passengers.SingleOrDefaultAsync( + x => x.PassportNumber.Value == request.PassportNumber, cancellationToken); + + if (passenger is null) + { + throw new PassengerNotExist(); + } + + passenger.CompleteRegistrationPassenger(passenger.Id, passenger.Name, + passenger.PassportNumber, request.PassengerType, Age.Of(request.Age)); + + var updatePassenger = _passengerDbContext.Passengers.Update(passenger).Entity; + + var passengerDto = _mapper.Map(updatePassenger); + + return new CompleteRegisterPassengerResult(passengerDto); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassengerMongo.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassengerMongo.cs new file mode 100644 index 0000000..d067dcf --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassengerMongo.cs @@ -0,0 +1,61 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Passenger.Data; +using BookingMonolith.Passenger.Passengers.Models; +using BookingMonolith.Passenger.Passengers.ValueObjects; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Core.Event; +using MapsterMapper; +using MediatR; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Passenger.Passengers.Features.CompletingRegisterPassenger.V1; + +public record CompleteRegisterPassengerMongoCommand(Guid Id, string PassportNumber, string Name, + Enums.PassengerType PassengerType, int Age, bool IsDeleted = false) : InternalCommand; + + +internal class CompleteRegisterPassengerMongoHandler : ICommandHandler +{ + private readonly PassengerReadDbContext _passengerReadDbContext; + private readonly IMapper _mapper; + + public CompleteRegisterPassengerMongoHandler( + PassengerReadDbContext passengerReadDbContext, + IMapper mapper) + { + _passengerReadDbContext = passengerReadDbContext; + _mapper = mapper; + } + + public async Task Handle(CompleteRegisterPassengerMongoCommand request, CancellationToken cancellationToken) + { + Guard.Against.Null(request, nameof(request)); + + var passengerReadModel = _mapper.Map(request); + + var passenger = await _passengerReadDbContext.Passenger.AsQueryable() + .FirstOrDefaultAsync(x => x.PassengerId == passengerReadModel.PassengerId && !x.IsDeleted, cancellationToken); + + if (passenger is not null) + { + await _passengerReadDbContext.Passenger.UpdateOneAsync( + x => x.PassengerId == PassengerId.Of(passengerReadModel.PassengerId), + Builders.Update + .Set(x => x.PassengerId, PassengerId.Of(passengerReadModel.PassengerId)) + .Set(x => x.Age, passengerReadModel.Age) + .Set(x => x.Name, passengerReadModel.Name) + .Set(x => x.IsDeleted, passengerReadModel.IsDeleted) + .Set(x => x.PassengerType, passengerReadModel.PassengerType) + .Set(x => x.PassportNumber, passengerReadModel.PassportNumber), + cancellationToken: cancellationToken); + } + else + { + await _passengerReadDbContext.Passenger.InsertOneAsync(passengerReadModel, + cancellationToken: cancellationToken); + } + + return Unit.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/GettingPassengerById/V1/GetPassengerById.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/GettingPassengerById/V1/GetPassengerById.cs new file mode 100644 index 0000000..d427566 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/GettingPassengerById/V1/GetPassengerById.cs @@ -0,0 +1,88 @@ +using Ardalis.GuardClauses; +using BookingMonolith.Passenger.Data; +using BookingMonolith.Passenger.Passengers.Dtos; +using BookingMonolith.Passenger.Passengers.Exceptions; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.Web; +using FluentValidation; +using Mapster; +using MapsterMapper; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace BookingMonolith.Passenger.Passengers.Features.GettingPassengerById.V1; + +public record GetPassengerById(Guid Id) : IQuery; + +public record GetPassengerByIdResult(PassengerDto PassengerDto); + +public record GetPassengerByIdResponseDto(PassengerDto PassengerDto); + +public class GetPassengerByIdEndpoint : IMinimalEndpoint +{ + public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + builder.MapGet($"{EndpointConfig.BaseApiPath}/passenger/{{id}}", + async (Guid id, IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send(new GetPassengerById(id), cancellationToken); + + var response = result.Adapt(); + + return Results.Ok(response); + }) + .RequireAuthorization() + .WithName("GetPassengerById") + .WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build()) + .Produces() + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Get Passenger By Id") + .WithDescription("Get Passenger By Id") + .WithOpenApi() + .HasApiVersion(1.0); + + return builder; + } +} + +public class GetPassengerByIdValidator : AbstractValidator +{ + public GetPassengerByIdValidator() + { + RuleFor(x => x.Id).NotNull().WithMessage("Id is required!"); + } +} + +internal class GetPassengerByIdHandler : IQueryHandler +{ + private readonly IMapper _mapper; + private readonly PassengerReadDbContext _passengerReadDbContext; + + public GetPassengerByIdHandler(IMapper mapper, PassengerReadDbContext passengerReadDbContext) + { + _mapper = mapper; + _passengerReadDbContext = passengerReadDbContext; + } + + public async Task Handle(GetPassengerById query, CancellationToken cancellationToken) + { + Guard.Against.Null(query, nameof(query)); + + var passenger = + await _passengerReadDbContext.Passenger.AsQueryable() + .SingleOrDefaultAsync(x => x.PassengerId == query.Id && x.IsDeleted == false, cancellationToken); + + if (passenger is null) + { + throw new PassengerNotFoundException(); + } + + var passengerDto = _mapper.Map(passenger); + + return new GetPassengerByIdResult(passengerDto); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/PassengerMappings.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/PassengerMappings.cs new file mode 100644 index 0000000..6ef4013 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Features/PassengerMappings.cs @@ -0,0 +1,27 @@ +using BookingMonolith.Passenger.Passengers.Dtos; +using BookingMonolith.Passenger.Passengers.Features.CompletingRegisterPassenger.V1; +using BookingMonolith.Passenger.Passengers.Models; +using BookingMonolith.Passenger.Passengers.ValueObjects; +using Mapster; +using MassTransit; + +namespace BookingMonolith.Passenger.Passengers.Features; + +public class PassengerMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .Map(d => d.Id, s => NewId.NextGuid()) + .Map(d => d.PassengerId, s => PassengerId.Of(s.Id)); + + config.NewConfig() + .ConstructUsing(x => new CompleteRegisterPassenger(x.PassportNumber, x.PassengerType, x.Age)); + + config.NewConfig() + .ConstructUsing(x => new PassengerDto(x.PassengerId, x.Name, x.PassportNumber, x.PassengerType, x.Age)); + + config.NewConfig() + .ConstructUsing(x => new PassengerDto(x.Id.Value, x.Name.Value, x.PassportNumber.Value, x.PassengerType, x.Age.Value)); + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/Passenger.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/Passenger.cs new file mode 100644 index 0000000..79754f9 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/Passenger.cs @@ -0,0 +1,44 @@ +using BookingMonolith.Passenger.Identity.Consumers.RegisteringNewUser.V1; +using BookingMonolith.Passenger.Passengers.Features.CompletingRegisterPassenger.V1; +using BookingMonolith.Passenger.Passengers.ValueObjects; +using BuildingBlocks.Core.Model; + +namespace BookingMonolith.Passenger.Passengers.Models; + +public record Passenger : Aggregate +{ + public PassportNumber PassportNumber { get; private set; } = default!; + public Name Name { get; private set; } = default!; + public Enums.PassengerType PassengerType { get; private set; } + public Age? Age { get; private set; } + + public void CompleteRegistrationPassenger(PassengerId id, Name name, PassportNumber passportNumber, + Enums.PassengerType passengerType, Age age, bool isDeleted = false) + { + this.Id = id; + this.Name = name; + this.PassportNumber = passportNumber; + this.PassengerType = passengerType; + this.Age = age; + this.IsDeleted = isDeleted; + + var @event = new PassengerRegistrationCompletedDomainEvent(this.Id, this.Name, + this.PassportNumber, + this.PassengerType, this.Age, this.IsDeleted); + + this.AddDomainEvent(@event); + } + + + public static Passenger Create(PassengerId id, Name name, PassportNumber passportNumber, bool isDeleted = false) + { + var passenger = new Passenger { Id = id, Name = name, PassportNumber = passportNumber, IsDeleted = isDeleted }; + + var @event = new PassengerCreatedDomainEvent(passenger.Id, passenger.Name, passenger.PassportNumber, + passenger.IsDeleted); + + passenger.AddDomainEvent(@event); + + return passenger; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/PassengerReadModel.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/PassengerReadModel.cs new file mode 100644 index 0000000..76f2e30 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/Models/PassengerReadModel.cs @@ -0,0 +1,11 @@ +namespace BookingMonolith.Passenger.Passengers.Models; +public class PassengerReadModel +{ + public required Guid Id { get; init; } + public required Guid PassengerId { get; init; } + public required string PassportNumber { get; init; } + public required string Name { get; init; } + public required Enums.PassengerType PassengerType { get; init; } + public int Age { get; init; } + public required bool IsDeleted { get; init; } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Age.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Age.cs new file mode 100644 index 0000000..76bca74 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Age.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Passenger.Passengers.Exceptions; + +namespace BookingMonolith.Passenger.Passengers.ValueObjects; + +public record Age +{ + public int Value { get; } + + private Age(int value) + { + Value = value; + } + + public static Age Of(int value) + { + if (value <= 0) + { + throw new InvalidAgeException(); + } + + return new Age(value); + } + + public static implicit operator int(Age age) + { + return age.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Name.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Name.cs new file mode 100644 index 0000000..3b11f8f --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/Name.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Passenger.Passengers.Exceptions; + +namespace BookingMonolith.Passenger.Passengers.ValueObjects; + +public record Name +{ + public string Value { get; } + + private Name(string value) + { + Value = value; + } + + public static Name Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidNameException(); + } + + return new Name(value); + } + + public static implicit operator string(Name name) + { + return name.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassengerId.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassengerId.cs new file mode 100644 index 0000000..7e7a243 --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassengerId.cs @@ -0,0 +1,28 @@ +using BookingMonolith.Passenger.Exceptions; + +namespace BookingMonolith.Passenger.Passengers.ValueObjects; + +public record PassengerId +{ + public Guid Value { get; } + + private PassengerId(Guid value) + { + Value = value; + } + + public static PassengerId Of(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidPassengerIdException(value); + } + + return new PassengerId(value); + } + + public static implicit operator Guid(PassengerId passengerId) + { + return passengerId.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassportNumber.cs b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassportNumber.cs new file mode 100644 index 0000000..ea664fc --- /dev/null +++ b/1-monolith-architecture-style/src/BookingMonolith/src/Passenger/Passengers/ValueObjects/PassportNumber.cs @@ -0,0 +1,33 @@ +using BookingMonolith.Passenger.Passengers.Exceptions; + +namespace BookingMonolith.Passenger.Passengers.ValueObjects; + +public record PassportNumber +{ + public string Value { get; } + + public override string ToString() + { + return Value; + } + + private PassportNumber(string value) + { + Value = value; + } + + public static PassportNumber Of(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidPassportNumberException(); + } + + return new PassportNumber(value); + } + + public static implicit operator string(PassportNumber passportNumber) + { + return passportNumber.Value; + } +} diff --git a/1-monolith-architecture-style/src/BookingMonolith/tests/.gitkeep b/1-monolith-architecture-style/src/BookingMonolith/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/tests/.gitkeep b/2-modular-monolith-architecture-style/src/Modules/Booking/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightDbContext.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightDbContext.cs index 8f9f77c..759240c 100644 --- a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightDbContext.cs +++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightDbContext.cs @@ -1,3 +1,4 @@ +using System.Reflection; using BuildingBlocks.EFCore; using Flight.Aircrafts.Models; using Flight.Airports.Models; @@ -25,7 +26,7 @@ public sealed class FlightDbContext : AppDbContextBase protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); - builder.ApplyConfigurationsFromAssembly(typeof(FlightRoot).Assembly); + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); builder.FilterSoftDeletedProperties(); builder.ToSnakeCaseTables(); } diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/tests/.gitkeep b/2-modular-monolith-architecture-style/src/Modules/Flight/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/2-modular-monolith-architecture-style/src/Modules/Identity/tests/.gitkeep b/2-modular-monolith-architecture-style/src/Modules/Identity/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/2-modular-monolith-architecture-style/src/Modules/Passenger/src/UserCreatedHandler.cs b/2-modular-monolith-architecture-style/src/Modules/Passenger/src/UserCreatedHandler.cs deleted file mode 100644 index 700813c..0000000 --- a/2-modular-monolith-architecture-style/src/Modules/Passenger/src/UserCreatedHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -// using BuildingBlocks.Contracts.EventBus.Messages; -// using MassTransit; -// -// namespace Passenger; -// -// public class UserCreatedHandler : IConsumer -// { -// public Task Consume(ConsumeContext context) -// { -// Console.WriteLine(context.Message.PassportNumber); -// return Task.CompletedTask; -// } -// } diff --git a/2-modular-monolith-architecture-style/src/Modules/Passenger/tests/.gitkeep b/2-modular-monolith-architecture-style/src/Modules/Passenger/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/monolith-to-cloud-architecture.sln b/monolith-to-cloud-architecture.sln index 9020b99..8e921b5 100644 --- a/monolith-to-cloud-architecture.sln +++ b/monolith-to-cloud-architecture.sln @@ -95,6 +95,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "2-modular-monol EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passenger", "2-modular-monolith-architecture-style\src\Modules\Passenger\src\Passenger.csproj", "{1CD81080-9F44-49AA-94F9-EFEBFD8073E4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BookingMonolith", "BookingMonolith", "{DB31E41A-D441-4BB8-B96C-F70395FBFB95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookingMonolith", "1-monolith-architecture-style\src\BookingMonolith\src\BookingMonolith.csproj", "{ECBE72AF-7F47-4086-A3F8-AF011085E253}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{EAAC4A89-D71D-426F-ABB0-127C3E777E54}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "1-monolith-architecture-style\src\Api\src\Api.csproj", "{E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -146,6 +154,10 @@ Global {301AB091-1BBB-4D95-9A54-AA7A8EE928EF} = {3CB44FE8-8DC1-49BD-864A-72FB6A8229C5} {3020E2CD-C6E5-4489-914E-96083705AF0E} = {6C250353-B112-42F5-BBE9-FA2A725870FD} {1CD81080-9F44-49AA-94F9-EFEBFD8073E4} = {254C235E-7E2D-4FEE-9EB4-50E48BDB1295} + {DB31E41A-D441-4BB8-B96C-F70395FBFB95} = {DBAE70CC-011A-4997-9612-58AFAFF73291} + {ECBE72AF-7F47-4086-A3F8-AF011085E253} = {DB31E41A-D441-4BB8-B96C-F70395FBFB95} + {EAAC4A89-D71D-426F-ABB0-127C3E777E54} = {DBAE70CC-011A-4997-9612-58AFAFF73291} + {E7BA185C-B26D-4ACB-A24A-70AB730DD2A0} = {EAAC4A89-D71D-426F-ABB0-127C3E777E54} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6538BDF3-A741-46E9-8988-C859ABB2FBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -232,5 +244,13 @@ Global {1CD81080-9F44-49AA-94F9-EFEBFD8073E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1CD81080-9F44-49AA-94F9-EFEBFD8073E4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1CD81080-9F44-49AA-94F9-EFEBFD8073E4}.Release|Any CPU.Build.0 = Release|Any CPU + {ECBE72AF-7F47-4086-A3F8-AF011085E253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECBE72AF-7F47-4086-A3F8-AF011085E253}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECBE72AF-7F47-4086-A3F8-AF011085E253}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECBE72AF-7F47-4086-A3F8-AF011085E253}.Release|Any CPU.Build.0 = Release|Any CPU + {E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal