Repository: meysamhadeli/booking-microservices-sample Branch: main Commit: d99d1a9a9a9d Files: 555 Total size: 1.5 MB Directory structure: gitextract_4va7s4qm/ ├── .aspire/ │ └── settings.json ├── .config/ │ └── dotnet-tools.json ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── actions/ │ │ ├── build/ │ │ │ └── action.yml │ │ ├── build-test/ │ │ │ └── action.yml │ │ ├── docker-build-publish/ │ │ │ └── action.yml │ │ └── test/ │ │ └── action.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── ci.yml │ └── release-drafter-labeler.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── CONTRIBUTION.md ├── Directory.Build.props ├── LICENSE ├── README.md ├── assets/ │ ├── booking-microservices.drawio │ └── vertical-slice-architecture.excalidraw ├── booking-microservices.sln ├── booking.rest ├── commitlint.config.js ├── deployments/ │ ├── configs/ │ │ ├── dashboards.md │ │ ├── grafana/ │ │ │ ├── dashboards/ │ │ │ │ ├── dotnet-core-endpoint.json │ │ │ │ ├── dotnet-core.json │ │ │ │ ├── node-exporter.json │ │ │ │ ├── postgresql.json │ │ │ │ └── rabbitmq.json │ │ │ └── provisioning/ │ │ │ ├── dashboards/ │ │ │ │ └── dashboard.yml │ │ │ └── datasources/ │ │ │ └── datasource.yml │ │ ├── loki-config.yaml │ │ ├── otel-collector-config.yaml │ │ ├── prometheus.yaml │ │ └── tempo.yaml │ ├── docker-compose/ │ │ ├── docker-compose.infrastructure.yaml │ │ └── docker-compose.yaml │ └── kubernetes/ │ ├── booking-cert-manager.yml │ └── booking-microservices.yml ├── global.json ├── package.json ├── scripts/ │ └── setup_kubectl_gitpod.sh └── src/ ├── ApiGateway/ │ ├── Dockerfile │ └── src/ │ ├── ApiGateway.csproj │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── appsettings.Development.json │ ├── appsettings.docker.json │ └── appsettings.json ├── Aspire/ │ └── src/ │ ├── AppHost/ │ │ ├── AppHost.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ └── ServiceDefaults/ │ ├── Extensions.cs │ └── ServiceDefaults.csproj ├── BuildingBlocks/ │ ├── BuildingBlocks.csproj │ ├── Caching/ │ │ ├── CachingBehavior.cs │ │ ├── ICacheRequest.cs │ │ ├── IInvalidateCacheRequest.cs │ │ └── InvalidateCachingBehavior.cs │ ├── Constants/ │ │ └── IdentityConstant.cs │ ├── Contracts/ │ │ └── EventBus.Messages/ │ │ ├── FlighContracts.cs │ │ ├── IdentityContracts.cs │ │ ├── PassengerContracts.cs │ │ └── ReservationContracts.cs │ ├── Core/ │ │ ├── CQRS/ │ │ │ ├── ICommand.cs │ │ │ ├── ICommandHandler.cs │ │ │ ├── IQuery.cs │ │ │ └── IQueryHandler.cs │ │ ├── CompositeEventMapper.cs │ │ ├── Event/ │ │ │ ├── EventType.cs │ │ │ ├── IDomainEvent.cs │ │ │ ├── IEvent.cs │ │ │ ├── IHaveIntegrationEvent.cs │ │ │ ├── IIntegrationEvent.cs │ │ │ ├── IInternalCommand.cs │ │ │ ├── InternalCommand.cs │ │ │ └── MessageEnvelope.cs │ │ ├── EventDispatcher.cs │ │ ├── IEventDispatcher.cs │ │ ├── IEventMapper.cs │ │ ├── IntegrationEventWrapper.cs │ │ ├── Model/ │ │ │ ├── Aggregate.cs │ │ │ ├── Entity.cs │ │ │ ├── IAggregate.cs │ │ │ ├── IEntity.cs │ │ │ └── IVersion.cs │ │ └── Pagination/ │ │ ├── Extensions.cs │ │ ├── IPageList.cs │ │ ├── IPageQuery.cs │ │ ├── IPageRequest.cs │ │ └── PageList.cs │ ├── EFCore/ │ │ ├── AppDbContextBase.cs │ │ ├── DesignTimeDbContextFactoryBase.cs │ │ ├── EfTxBehavior.cs │ │ ├── Extensions.cs │ │ ├── IDataSeeder.cs │ │ ├── IDbContext.cs │ │ ├── ISeedManager.cs │ │ ├── PostgresOptions.cs │ │ └── SeedManagers.cs │ ├── EventStoreDB/ │ │ ├── BackgroundWorkers/ │ │ │ └── BackgroundWorker.cs │ │ ├── Config.cs │ │ ├── Events/ │ │ │ ├── AggregateEventSourcing.cs │ │ │ ├── AggregateStreamExtensions.cs │ │ │ ├── EventTypeMapper.cs │ │ │ ├── IAggregateEventSourcing.cs │ │ │ ├── IEventHandler.cs │ │ │ ├── IExternalEvent.cs │ │ │ ├── IProjection.cs │ │ │ ├── StreamEvent.cs │ │ │ ├── StreamEventExtensions.cs │ │ │ └── StreamNameMapper.cs │ │ ├── Extensions.cs │ │ ├── Projections/ │ │ │ ├── IProjectionProcessor.cs │ │ │ ├── IProjectionPublisher.cs │ │ │ └── ProjectionPublisher.cs │ │ ├── Repository/ │ │ │ ├── EventStoreDBRepository.cs │ │ │ └── RepositoryExtensions.cs │ │ ├── Serialization/ │ │ │ ├── EventStoreDBSerializer.cs │ │ │ ├── JsonObjectContractProvider.cs │ │ │ ├── NonDefaultConstructorContractResolver.cs │ │ │ └── SerializationExtensions.cs │ │ └── Subscriptions/ │ │ ├── EventStoreDBSubscriptionCheckpointRepository.cs │ │ ├── EventStoreDBSubscriptionToAll.cs │ │ ├── ISubscriptionCheckpointRepository.cs │ │ └── InMemorySubscriptionCheckpointRepository.cs │ ├── Exception/ │ │ ├── AggregateNotFoundException.cs │ │ ├── AppException.cs │ │ ├── BadRequestException.cs │ │ ├── ConflictException.cs │ │ ├── CustomException.cs │ │ ├── DomainException.cs │ │ ├── GrpcExceptionInterceptor.cs │ │ ├── InternalServerException.cs │ │ ├── NotFoundException.cs │ │ ├── ProblemDetailsWithCode.cs │ │ └── ValidationException.cs │ ├── HealthCheck/ │ │ ├── Extensions.cs │ │ └── HealthOptions.cs │ ├── Jwt/ │ │ ├── AuthHeaderHandler.cs │ │ └── JwtExtensions.cs │ ├── Logging/ │ │ └── LoggingBehavior.cs │ ├── Mapster/ │ │ └── Extensions.cs │ ├── MassTransit/ │ │ ├── ConsumeFilter.cs │ │ ├── Extensions.cs │ │ ├── RabbitMqOptions.cs │ │ └── TransportType.cs │ ├── Mongo/ │ │ ├── Extensions.cs │ │ ├── IMongoDbContext.cs │ │ ├── IMongoRepository.cs │ │ ├── IMongoUnitOfWork.cs │ │ ├── IRepository.cs │ │ ├── ITransactionAble.cs │ │ ├── IUnitOfWork.cs │ │ ├── ImmutablePocoConvention.cs │ │ ├── MicroBootstrap.Persistence.Mongo.csproj │ │ ├── MongoDbContext.cs │ │ ├── MongoOptions.cs │ │ ├── MongoRepository.cs │ │ └── MongoUnitOfWork.cs │ ├── OpenApi/ │ │ ├── Extensions.cs │ │ └── SecuritySchemeDocumentTransformer.cs │ ├── OpenTelemetryCollector/ │ │ ├── ActivityExtensions.cs │ │ ├── ActivityInfo.cs │ │ ├── Behaviors/ │ │ │ └── ObservabilityPipelineBehavior.cs │ │ ├── CoreDiagnostics/ │ │ │ ├── Commands/ │ │ │ │ ├── CommandHandlerActivity.cs │ │ │ │ └── CommandHandlerMetrics.cs │ │ │ └── Query/ │ │ │ ├── QueryHandlerActivity.cs │ │ │ └── QueryHandlerMetrics.cs │ │ ├── CreateActivityInfo.cs │ │ ├── DiagnosticsProvider/ │ │ │ ├── CustomeDiagnosticsProvider.cs │ │ │ └── IDiagnosticsProvider.cs │ │ ├── Extensions.cs │ │ ├── ObservabilityConstant.cs │ │ ├── ObservabilityOptions.cs │ │ └── TelemetryTags.cs │ ├── PersistMessageProcessor/ │ │ ├── Extensions.cs │ │ ├── IPersistMessageDbContext.cs │ │ ├── IPersistMessageProcessor.cs │ │ ├── MessageDeliveryType.cs │ │ ├── MessageStatus.cs │ │ ├── PersistMessage.cs │ │ ├── PersistMessageBackgroundService.cs │ │ ├── PersistMessageDbContext.cs │ │ ├── PersistMessageOptions.cs │ │ └── PersistMessageProcessor.cs │ ├── Polly/ │ │ └── Extensions.cs │ ├── ProblemDetails/ │ │ └── Extensions.cs │ ├── TestBase/ │ │ ├── TestBase.cs │ │ └── TestContainers.cs │ ├── Utils/ │ │ ├── NoSynchronizationContextScope.cs │ │ ├── ServiceLocator.cs │ │ └── TypeProvider.cs │ ├── Validation/ │ │ ├── Extensions.cs │ │ ├── ValidationBehavior.cs │ │ ├── ValidationError.cs │ │ └── ValidationResultModel.cs │ └── Web/ │ ├── ApiVersioningExtensions.cs │ ├── AppOptions.cs │ ├── BaseController.cs │ ├── ConfigurationExtensions.cs │ ├── ConfigurationHelper.cs │ ├── CorrelationExtensions.cs │ ├── CurrentUserProvider.cs │ ├── EndpointConfig.cs │ ├── IMinimalEndpoint.cs │ ├── MinimalApiExtensions.cs │ ├── ServiceCollectionExtensions.cs │ ├── ServiceProviderExtensions.cs │ └── SlugifyParameterTransformer.cs └── Services/ ├── Booking/ │ ├── Dockerfile │ ├── src/ │ │ ├── Booking/ │ │ │ ├── AssemblyInfo.cs │ │ │ ├── Booking/ │ │ │ │ ├── Dtos/ │ │ │ │ │ └── CreateReservation.cs │ │ │ │ ├── Exceptions/ │ │ │ │ │ ├── BookingAlreadyExistException.cs │ │ │ │ │ ├── FlightNotFoundException.cs │ │ │ │ │ ├── InvalidAircraftIdException.cs │ │ │ │ │ ├── InvalidArriveAirportIdException.cs │ │ │ │ │ ├── InvalidDepartureAirportIdException.cs │ │ │ │ │ ├── InvalidFlightDateException.cs │ │ │ │ │ ├── InvalidFlightNumberException.cs │ │ │ │ │ ├── InvalidPassengerNameException.cs │ │ │ │ │ ├── InvalidPriceException.cs │ │ │ │ │ └── SeatNumberException.cs │ │ │ │ ├── Features/ │ │ │ │ │ ├── BookingMappings.cs │ │ │ │ │ └── CreatingBook/ │ │ │ │ │ └── V1/ │ │ │ │ │ └── CreateBooking.cs │ │ │ │ ├── Models/ │ │ │ │ │ ├── Booking.cs │ │ │ │ │ └── BookingReadModel.cs │ │ │ │ └── ValueObjects/ │ │ │ │ ├── PassengerInfo.cs │ │ │ │ └── Trip.cs │ │ │ ├── Booking.csproj │ │ │ ├── BookingEventMapper.cs │ │ │ ├── BookingProjection.cs │ │ │ ├── BookingRoot.cs │ │ │ ├── Configuration/ │ │ │ │ └── GrpcOptions.cs │ │ │ ├── Data/ │ │ │ │ └── BookingReadDbContext.cs │ │ │ ├── Extensions/ │ │ │ │ └── Infrastructure/ │ │ │ │ ├── GrpcClientExtensions.cs │ │ │ │ ├── InfrastructureExtensions.cs │ │ │ │ └── MediatRExtensions.cs │ │ │ └── GrpcClient/ │ │ │ └── Protos/ │ │ │ ├── flight.proto │ │ │ └── passenger.proto │ │ └── Booking.Api/ │ │ ├── Booking.Api.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.docker.json │ │ ├── appsettings.json │ │ └── appsettings.test.json │ └── tests/ │ ├── IntegrationTest/ │ │ ├── Booking/ │ │ │ └── Features/ │ │ │ └── CreateBookingTests.cs │ │ ├── BookingIntegrationTestBase.cs │ │ ├── Fakes/ │ │ │ ├── FakeCreateBookingCommand.cs │ │ │ ├── FakeFlightResponse.cs │ │ │ ├── FakeGetAvailableSeatsResponse.cs │ │ │ ├── FakePassengerResponse.cs │ │ │ └── FakeReserveSeatResponse.cs │ │ ├── Integration.Test.csproj │ │ └── xunit.runner.json │ ├── PerformanceTest/ │ │ ├── .openapi-generator/ │ │ │ ├── FILES │ │ │ └── VERSION │ │ ├── .openapi-generator-ignore │ │ ├── README.md │ │ └── script.js │ └── tests.sln ├── Flight/ │ ├── Dockerfile │ ├── src/ │ │ ├── Flight/ │ │ │ ├── Aircrafts/ │ │ │ │ ├── Dtos/ │ │ │ │ │ └── AircraftDto.cs │ │ │ │ ├── Exceptions/ │ │ │ │ │ ├── AircraftAlreadyExistException.cs │ │ │ │ │ ├── InvalidAircraftIdException.cs │ │ │ │ │ ├── InvalidManufacturingYearException.cs │ │ │ │ │ ├── InvalidModelException.cs │ │ │ │ │ └── InvalidNameException.cs │ │ │ │ ├── Features/ │ │ │ │ │ ├── AircraftMappings.cs │ │ │ │ │ └── CreatingAircraft/ │ │ │ │ │ └── V1/ │ │ │ │ │ ├── CreateAircraft.cs │ │ │ │ │ └── CreateAircraftMongo.cs │ │ │ │ ├── Models/ │ │ │ │ │ ├── Aircraft.cs │ │ │ │ │ └── AircraftReadModel.cs │ │ │ │ └── ValueObjects/ │ │ │ │ ├── AircraftId.cs │ │ │ │ ├── ManufacturingYear.cs │ │ │ │ ├── Model.cs │ │ │ │ └── Name.cs │ │ │ ├── Airports/ │ │ │ │ ├── Dtos/ │ │ │ │ │ └── AirportDto.cs │ │ │ │ ├── Exceptions/ │ │ │ │ │ ├── AirportAlreadyExistException.cs │ │ │ │ │ ├── InvalidAddressException.cs │ │ │ │ │ ├── InvalidAirportIdException.cs │ │ │ │ │ ├── InvalidCodeException.cs │ │ │ │ │ └── InvalidNameException.cs │ │ │ │ ├── Features/ │ │ │ │ │ ├── AirportMappings.cs │ │ │ │ │ └── CreatingAirport/ │ │ │ │ │ └── V1/ │ │ │ │ │ ├── CreateAirport.cs │ │ │ │ │ └── CreateAirportMongo.cs │ │ │ │ ├── Models/ │ │ │ │ │ ├── Airport.cs │ │ │ │ │ └── AirportReadModel.cs │ │ │ │ └── ValueObjects/ │ │ │ │ ├── Address.cs │ │ │ │ ├── AirportId.cs │ │ │ │ ├── Code.cs │ │ │ │ └── Name.cs │ │ │ ├── AssemblyInfo.cs │ │ │ ├── Data/ │ │ │ │ ├── Configurations/ │ │ │ │ │ ├── AircraftConfiguration.cs │ │ │ │ │ ├── AirportConfiguration.cs │ │ │ │ │ ├── FlightConfiguration.cs │ │ │ │ │ └── SeatConfiguration.cs │ │ │ │ ├── DesignTimeDbContextFactory.cs │ │ │ │ ├── FlightDbContext.cs │ │ │ │ ├── FlightReadDbContext.cs │ │ │ │ ├── Migrations/ │ │ │ │ │ ├── 20230611230948_initial.Designer.cs │ │ │ │ │ ├── 20230611230948_initial.cs │ │ │ │ │ └── FlightDbContextModelSnapshot.cs │ │ │ │ ├── Seed/ │ │ │ │ │ ├── FlightDataSeeder.cs │ │ │ │ │ └── InitialData.cs │ │ │ │ └── readme.md │ │ │ ├── Extensions/ │ │ │ │ └── Infrastructure/ │ │ │ │ ├── InfrastructureExtensions.cs │ │ │ │ └── MediatRExtensions.cs │ │ │ ├── Flight.csproj │ │ │ ├── FlightEventMapper.cs │ │ │ ├── FlightRoot.cs │ │ │ ├── Flights/ │ │ │ │ ├── Dtos/ │ │ │ │ │ └── FlightDto.cs │ │ │ │ ├── Enums/ │ │ │ │ │ └── FlightStatus.cs │ │ │ │ ├── Exceptions/ │ │ │ │ │ ├── FlightAlreadyExistException.cs │ │ │ │ │ ├── FlightNotFountException.cs │ │ │ │ │ ├── InvalidArriveDateException.cs │ │ │ │ │ ├── InvalidDepartureDateException.cs │ │ │ │ │ ├── InvalidDurationException.cs │ │ │ │ │ ├── InvalidFlightDateException.cs │ │ │ │ │ ├── InvalidFlightIdException.cs │ │ │ │ │ ├── InvalidFlightNumberException.cs │ │ │ │ │ └── InvalidPriceException.cs │ │ │ │ ├── Features/ │ │ │ │ │ ├── CreatingFlight/ │ │ │ │ │ │ └── V1/ │ │ │ │ │ │ ├── CreateFlight.cs │ │ │ │ │ │ └── CreateFlightMongo.cs │ │ │ │ │ ├── DeletingFlight/ │ │ │ │ │ │ └── V1/ │ │ │ │ │ │ ├── DeleteFlight.cs │ │ │ │ │ │ └── DeleteFlightMongo.cs │ │ │ │ │ ├── FlightMappings.cs │ │ │ │ │ ├── GettingAvailableFlights/ │ │ │ │ │ │ └── V1/ │ │ │ │ │ │ └── GetAvailableFlights.cs │ │ │ │ │ ├── GettingFlightById/ │ │ │ │ │ │ └── V1/ │ │ │ │ │ │ └── GetFlightById.cs │ │ │ │ │ └── UpdatingFlight/ │ │ │ │ │ └── V1/ │ │ │ │ │ ├── UpdateFlight.cs │ │ │ │ │ └── UpdateFlightMongo.cs │ │ │ │ ├── Models/ │ │ │ │ │ ├── Flight.cs │ │ │ │ │ └── FlightReadModel.cs │ │ │ │ └── ValueObjects/ │ │ │ │ ├── ArriveDate.cs │ │ │ │ ├── DepartureDate.cs │ │ │ │ ├── DurationMinutes.cs │ │ │ │ ├── FlightDate.cs │ │ │ │ ├── FlightId.cs │ │ │ │ ├── FlightNumber.cs │ │ │ │ └── Price.cs │ │ │ ├── GrpcServer/ │ │ │ │ ├── Protos/ │ │ │ │ │ └── flight.proto │ │ │ │ └── Services/ │ │ │ │ └── FlightGrpcServices.cs │ │ │ └── Seats/ │ │ │ ├── Dtos/ │ │ │ │ └── SeatDto.cs │ │ │ ├── Enums/ │ │ │ │ ├── SeatClass.cs │ │ │ │ └── SeatType.cs │ │ │ ├── Exceptions/ │ │ │ │ ├── AllSeatsFullException.cs │ │ │ │ ├── InvalidSeatIdException.cs │ │ │ │ ├── InvalidSeatNumberException.cs │ │ │ │ ├── SeatAlreadyExistException.cs │ │ │ │ └── SeatNumberIncorrectException.cs │ │ │ ├── Features/ │ │ │ │ ├── CreatingSeat/ │ │ │ │ │ └── V1/ │ │ │ │ │ ├── CreateSeat.cs │ │ │ │ │ └── CreateSeatMongo.cs │ │ │ │ ├── GettingAvailableSeats/ │ │ │ │ │ └── V1/ │ │ │ │ │ └── GetAvailableSeats.cs │ │ │ │ ├── ReservingSeat/ │ │ │ │ │ └── V1/ │ │ │ │ │ ├── ReserveSeat.cs │ │ │ │ │ └── ReserveSeatMongo.cs │ │ │ │ └── SeatMappings.cs │ │ │ ├── Models/ │ │ │ │ ├── Seat.cs │ │ │ │ └── SeatReadModel.cs │ │ │ └── ValueObjects/ │ │ │ ├── SeatId.cs │ │ │ └── SeatNumber.cs │ │ └── Flight.Api/ │ │ ├── Flight.Api.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.docker.json │ │ ├── appsettings.json │ │ └── appsettings.test.json │ └── tests/ │ ├── EndToEndTest/ │ │ ├── EndToEnd.Test.csproj │ │ ├── Fakes/ │ │ │ ├── FakeCreateFlightCommand.cs │ │ │ └── FakeCreateFlightMongoCommand.cs │ │ ├── Flight/ │ │ │ └── Features/ │ │ │ ├── CreateFlightTests.cs │ │ │ └── GetFlightByIdTests.cs │ │ ├── FlightEndToEndTestBase.cs │ │ ├── FlightTestDataSeeder.cs │ │ ├── Routes/ │ │ │ └── ApiRoutes.cs │ │ └── xunit.runner.json │ ├── IntegrationTest/ │ │ ├── Aircraft/ │ │ │ └── Features/ │ │ │ └── CreateAircraftTests.cs │ │ ├── Airport/ │ │ │ └── Features/ │ │ │ └── CreateAirportTests.cs │ │ ├── Fakes/ │ │ │ ├── FakeCreateAircraftCommand.cs │ │ │ ├── FakeCreateAirportCommand.cs │ │ │ ├── FakeCreateFlightCommand.cs │ │ │ ├── FakeCreateFlightMongoCommand.cs │ │ │ ├── FakeCreateSeatCommand.cs │ │ │ ├── FakeCreateSeatMongoCommand.cs │ │ │ └── FakeUpdateFlightCommand.cs │ │ ├── Flight/ │ │ │ └── Features/ │ │ │ ├── CreateFlightTests.cs │ │ │ ├── DeleteFlightTests.cs │ │ │ ├── GetAvailableFlightsTests.cs │ │ │ ├── GetFlightByIdTests.cs │ │ │ └── UpdateFlightTests.cs │ │ ├── FlightIntegrationTestBase.cs │ │ ├── FlightTestDataSeeder.cs │ │ ├── Integration.Test.csproj │ │ ├── Seat/ │ │ │ └── Features/ │ │ │ ├── GetAvailableSeatsTests.cs │ │ │ └── ReserveSeatTests.cs │ │ └── xunit.runner.json │ ├── PerformanceTest/ │ │ ├── .openapi-generator/ │ │ │ ├── FILES │ │ │ └── VERSION │ │ ├── .openapi-generator-ignore │ │ ├── README.md │ │ └── script.js │ ├── UnitTest/ │ │ ├── Aircraft/ │ │ │ └── Features/ │ │ │ └── CreateAircraftTests/ │ │ │ ├── CreateAircraftCommandHandlerTests.cs │ │ │ └── CreateAircraftCommandValidatorTests.cs │ │ ├── Airport/ │ │ │ └── Features/ │ │ │ └── CreateAirportTests/ │ │ │ ├── CreateAirportCommandHandlerTests.cs │ │ │ └── CreateAirportCommandValidatorTests.cs │ │ ├── Common/ │ │ │ ├── DbContextFactory.cs │ │ │ ├── MapperFactory.cs │ │ │ └── UnitTestFixture.cs │ │ ├── Fakes/ │ │ │ ├── FakeCreateAircraftCommand.cs │ │ │ ├── FakeCreateAirportCommand.cs │ │ │ ├── FakeCreateFlightCommand.cs │ │ │ ├── FakeCreateSeatCommand.cs │ │ │ ├── FakeFlightCreate.cs │ │ │ ├── FakeFlightUpdate.cs │ │ │ ├── FakeValidateCreateAircraftCommand.cs │ │ │ ├── FakeValidateCreateAirportCommand.cs │ │ │ ├── FakeValidateCreateFlightCommand.cs │ │ │ └── FakeValidateCreateSeatCommand.cs │ │ ├── Flight/ │ │ │ ├── Features/ │ │ │ │ ├── Domains/ │ │ │ │ │ ├── CreateFlightTests.cs │ │ │ │ │ └── UpdateFlightTests.cs │ │ │ │ └── Handlers/ │ │ │ │ └── CreateFlight/ │ │ │ │ ├── CreateFlightCommandHandlerTests.cs │ │ │ │ └── CreateFlightCommandValidatorTests.cs │ │ │ └── FlightMappingTests.cs │ │ ├── Seat/ │ │ │ ├── Features/ │ │ │ │ ├── CreateSeatCommandHandlerTests.cs │ │ │ │ └── CreateSeatCommandValidatorTests.cs │ │ │ └── SeatMappingTests.cs │ │ ├── Unit.Test.csproj │ │ └── xunit.runner.json │ └── tests.sln ├── Identity/ │ ├── Dockerfile │ ├── src/ │ │ ├── Identity/ │ │ │ ├── AssemblyInfo.cs │ │ │ ├── Configurations/ │ │ │ │ ├── AuthOptions.cs │ │ │ │ ├── Config.cs │ │ │ │ └── UserValidator.cs │ │ │ ├── Data/ │ │ │ │ ├── Configurations/ │ │ │ │ │ ├── RoleClaimConfiguration.cs │ │ │ │ │ ├── RoleConfiguration.cs │ │ │ │ │ ├── UserClaimConfiguration.cs │ │ │ │ │ ├── UserConfiguration.cs │ │ │ │ │ ├── UserLoginConfiguration.cs │ │ │ │ │ ├── UserRoleConfiguration.cs │ │ │ │ │ └── UserTokenConfiguration.cs │ │ │ │ ├── DesignTimeDbContextFactory.cs │ │ │ │ ├── IdentityContext.cs │ │ │ │ ├── Migrations/ │ │ │ │ │ ├── 20230331193410_initial.Designer.cs │ │ │ │ │ ├── 20230331193410_initial.cs │ │ │ │ │ └── IdentityContextModelSnapshot.cs │ │ │ │ ├── Seed/ │ │ │ │ │ ├── IdentityDataSeeder.cs │ │ │ │ │ └── InitialData.cs │ │ │ │ └── readme.md │ │ │ ├── Extensions/ │ │ │ │ └── Infrastructure/ │ │ │ │ ├── IdentityServerExtensions.cs │ │ │ │ ├── InfrastructureExtensions.cs │ │ │ │ └── MediatRExtensions.cs │ │ │ ├── Identity/ │ │ │ │ ├── Constants/ │ │ │ │ │ └── Constants.cs │ │ │ │ ├── Exceptions/ │ │ │ │ │ └── RegisterIdentityUserException.cs │ │ │ │ ├── Features/ │ │ │ │ │ ├── IdentityMappings.cs │ │ │ │ │ └── RegisteringNewUser/ │ │ │ │ │ └── V1/ │ │ │ │ │ └── RegisterNewUser.cs │ │ │ │ └── Models/ │ │ │ │ ├── Role.cs │ │ │ │ ├── RoleClaim.cs │ │ │ │ ├── User.cs │ │ │ │ ├── UserClaim.cs │ │ │ │ ├── UserLogin.cs │ │ │ │ ├── UserRole.cs │ │ │ │ └── UserToken.cs │ │ │ ├── Identity.csproj │ │ │ ├── IdentityEventMapper.cs │ │ │ └── IdentityRoot.cs │ │ └── Identity.Api/ │ │ ├── Identity.Api.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.docker.json │ │ ├── appsettings.json │ │ ├── appsettings.test.json │ │ └── keys/ │ │ ├── is-signing-key-0AC3347A09AA5E44E947F3E30ED54871.json │ │ ├── is-signing-key-A57781A0405849BDE786A79636460E49.json │ │ ├── is-signing-key-B3C31EEE2718D3C5004C6E85AD74F26C.json │ │ └── is-signing-key-E1668D5B7CCDD18C610506FCA7C5D194.json │ └── tests/ │ ├── IntegrationTest/ │ │ ├── Fakes/ │ │ │ └── FakeRegisterNewUserCommand.cs │ │ ├── Identity/ │ │ │ └── Features/ │ │ │ └── RegisterNewUserTests.cs │ │ ├── IdentityIntegrationTestBase.cs │ │ ├── IdentityTestDataSeeder.cs │ │ ├── Integration.Test.csproj │ │ └── xunit.runner.json │ ├── PerformanceTest/ │ │ ├── .openapi-generator/ │ │ │ ├── FILES │ │ │ └── VERSION │ │ ├── .openapi-generator-ignore │ │ ├── README.md │ │ └── script.js │ └── tests.sln └── Passenger/ ├── Dockerfile ├── src/ │ ├── Passenger/ │ │ ├── AssemblyInfo.cs │ │ ├── Data/ │ │ │ ├── Configurations/ │ │ │ │ └── PassengerConfiguration.cs │ │ │ ├── DesignTimeDbContextFactory.cs │ │ │ ├── Migrations/ │ │ │ │ ├── 20230611213031_initial.Designer.cs │ │ │ │ ├── 20230611213031_initial.cs │ │ │ │ └── PassengerDbContextModelSnapshot.cs │ │ │ ├── PassengerDbContext.cs │ │ │ ├── PassengerReadDbContext.cs │ │ │ └── readme.md │ │ ├── Exceptions/ │ │ │ ├── InvalidAgeException.cs │ │ │ ├── InvalidNameException.cs │ │ │ ├── InvalidPassengerIdException.cs │ │ │ ├── InvalidPassportNumberException.cs │ │ │ ├── PassengerAlreadyExist.cs │ │ │ └── PassengerNotFoundException.cs │ │ ├── Extensions/ │ │ │ └── Infrastructure/ │ │ │ ├── InfrastructureExtensions.cs │ │ │ └── MediatRExtensions.cs │ │ ├── GrpcServer/ │ │ │ ├── Protos/ │ │ │ │ └── passenger.proto │ │ │ └── Services/ │ │ │ └── PassengerGrpcServices.cs │ │ ├── Identity/ │ │ │ └── Consumers/ │ │ │ └── RegisteringNewUser/ │ │ │ └── V1/ │ │ │ ├── PassengerCreatedDomainEvent.cs │ │ │ └── RegisterNewUser.cs │ │ ├── Passenger.csproj │ │ ├── PassengerEventMapper.cs │ │ ├── PassengerRoot.cs │ │ └── Passengers/ │ │ ├── Dtos/ │ │ │ └── PassengerDto.cs │ │ ├── Enums/ │ │ │ └── PassengerType.cs │ │ ├── Exceptions/ │ │ │ ├── InvalidAgeException.cs │ │ │ ├── InvalidNameException.cs │ │ │ ├── InvalidPassportNumberException.cs │ │ │ ├── PassengerAlreadyExist.cs │ │ │ └── PassengerNotFoundException.cs │ │ ├── Features/ │ │ │ ├── CompletingRegisterPassenger/ │ │ │ │ └── V1/ │ │ │ │ ├── CompleteRegisterPassenger.cs │ │ │ │ └── CompleteRegisterPassengerMongo.cs │ │ │ ├── GettingPassengerById/ │ │ │ │ └── V1/ │ │ │ │ └── GetPassengerById.cs │ │ │ └── PassengerMappings.cs │ │ ├── Models/ │ │ │ ├── Passenger.cs │ │ │ └── PassengerReadModel.cs │ │ └── ValueObjects/ │ │ ├── Age.cs │ │ ├── Name.cs │ │ ├── PassengerId.cs │ │ └── PassportNumber.cs │ └── Passenger.Api/ │ ├── Passenger.Api.csproj │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── appsettings.Development.json │ ├── appsettings.docker.json │ ├── appsettings.json │ └── appsettings.test.json └── tests/ ├── IntegrationTest/ │ ├── Fakes/ │ │ ├── FakeCompleteRegisterPassengerCommand.cs │ │ └── FakeCompleteRegisterPassengerMongoCommand.cs │ ├── Integration.Test.csproj │ ├── Passenger/ │ │ └── Features/ │ │ ├── CompleteRegisterPassengerTests.cs │ │ └── GetPassengerByIdTests.cs │ ├── PassengerIntegrationTestBase.cs │ └── xunit.runner.json ├── PerformanceTest/ │ ├── .openapi-generator/ │ │ ├── FILES │ │ └── VERSION │ ├── .openapi-generator-ignore │ ├── README.md │ └── script.js └── tests.sln ================================================ FILE CONTENTS ================================================ ================================================ FILE: .aspire/settings.json ================================================ { "appHostPath": "../src/Aspire/src/AppHost/AppHost.csproj" } ================================================ FILE: .config/dotnet-tools.json ================================================ { "version": 1, "isRoot": true, "tools": { "dotnet-outdated-tool": { "version": "4.6.9", "commands": [ "dotnet-outdated" ] }, "dotnet-ef": { "version": "10.0.3", "commands": [ "dotnet-ef" ] }, "aspire.cli": { "version": "13.1.1", "commands": [ "aspire" ] }, "csharpier": { "version": "0.30.6", "commands": [ "dotnet-csharpier" ] } } } ================================================ FILE: .dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin/ **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj/ **/.tye/ **/secrets.dev.yaml **/values.dev.yaml **/*.jwk **/keys LICENSE README.md CHANGELOG.md ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org # https://www.jetbrains.com/help/resharper/Using_EditorConfig.html # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files # When opening a file, EditorConfig plugins look for a file named .editorconfig in the directory of the opened file and in every parent directory. A search for .editorconfig files will stop if the root filepath is reached or an EditorConfig file with `root=true` is found. # Remove the line below if you want to inherit .editorconfig settings from higher directories ################################################################################## ## https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ ## https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ ## Microsoft Rules root = true # All files [*] indent_style = space # Xml files [*.xml] indent_size = 2 # C# files [*.cs] #### Core EditorConfig Options #### # Indentation and spacing indent_size = 4 tab_width = 4 max_line_length = 120 # New line preferences insert_final_newline = false #### .NET Coding Conventions #### [*.{cs,vb}] # Organize usings dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = true file_header_template = unset # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?#enable-on-build # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/language-rules#option-format # this. and Me. preferences dotnet_style_qualification_for_event = false:silent dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_method = false:silent dotnet_style_qualification_for_property = false:silent # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:silent dotnet_style_predefined_type_for_member_access = true:silent # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent # Expression-level preferences dotnet_style_coalesce_expression = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_namespace_match_folder = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_object_initializer = true:suggestion dotnet_style_operator_placement_when_wrapping = beginning_of_line dotnet_style_prefer_auto_properties = true:suggestion dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion dotnet_style_prefer_conditional_expression_over_return = true:suggestion dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion dotnet_style_prefer_simplified_boolean_expressions = true:suggestion dotnet_style_prefer_simplified_interpolation = true:suggestion # Field preferences dotnet_style_readonly_field = true:warning # Parameter preferences dotnet_code_quality_unused_parameters = all:suggestion # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = none #### C# Coding Conventions #### [*.cs] # var preferences csharp_style_var_elsewhere = false:silent csharp_style_var_for_built_in_types = false:silent csharp_style_var_when_type_is_apparent = false:silent # Expression-bodied members csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_indexers = true:silent csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = true:silent # Pattern matching preferences csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_prefer_extended_property_pattern = true:suggestion csharp_style_prefer_not_pattern = true:suggestion csharp_style_prefer_pattern_matching = true:silent csharp_style_prefer_switch_expression = true:suggestion # Null-checking preferences csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences csharp_prefer_static_anonymous_function = true:suggestion csharp_prefer_static_local_function = true:warning csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion csharp_style_prefer_readonly_struct = true:suggestion csharp_style_prefer_readonly_struct_member = true:suggestion # Code-block preferences csharp_prefer_braces = true:silent csharp_prefer_simple_using_statement = true:suggestion csharp_style_namespace_declarations = file_scoped:suggestion csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_primary_constructors = true:suggestion csharp_style_prefer_top_level_statements = true:silent # Expression-level preferences csharp_prefer_simple_default_expression = true:suggestion csharp_style_deconstructed_variable_declaration = true:suggestion csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion csharp_style_prefer_index_operator = true:suggestion csharp_style_prefer_local_over_anonymous_function = true:suggestion csharp_style_prefer_null_check_over_type_check = true:suggestion csharp_style_prefer_range_operator = true:suggestion csharp_style_prefer_tuple_swap = true:suggestion csharp_style_prefer_utf8_string_literals = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_unused_value_assignment_preference = discard_variable:suggestion csharp_style_unused_value_expression_statement_preference = discard_variable:silent # 'using' directive preferences csharp_using_directive_placement = outside_namespace:silent #### C# Formatting Rules #### # New line preferences csharp_new_line_before_catch = true csharp_new_line_before_else = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_case_contents_when_block = true csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_open_square_brackets = false csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # Wrapping preferences csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true #### Naming styles #### [*.{cs,vb}] # Naming rules dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion dotnet_naming_rule.events_should_be_pascalcase.symbols = events dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase # Symbol specifications dotnet_naming_symbols.interfaces.applicable_kinds = interface dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.interfaces.required_modifiers = dotnet_naming_symbols.enums.applicable_kinds = enum dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.enums.required_modifiers = dotnet_naming_symbols.events.applicable_kinds = event dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.events.required_modifiers = dotnet_naming_symbols.methods.applicable_kinds = method dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.methods.required_modifiers = dotnet_naming_symbols.properties.applicable_kinds = property dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.properties.required_modifiers = dotnet_naming_symbols.public_fields.applicable_kinds = field dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal dotnet_naming_symbols.public_fields.required_modifiers = dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_fields.required_modifiers = dotnet_naming_symbols.private_static_fields.applicable_kinds = field dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_static_fields.required_modifiers = static dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.types_and_namespaces.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.non_field_members.required_modifiers = dotnet_naming_symbols.type_parameters.applicable_kinds = namespace dotnet_naming_symbols.type_parameters.applicable_accessibilities = * dotnet_naming_symbols.type_parameters.required_modifiers = dotnet_naming_symbols.private_constant_fields.applicable_kinds = field dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_constant_fields.required_modifiers = const dotnet_naming_symbols.local_variables.applicable_kinds = local dotnet_naming_symbols.local_variables.applicable_accessibilities = local dotnet_naming_symbols.local_variables.required_modifiers = dotnet_naming_symbols.local_constants.applicable_kinds = local dotnet_naming_symbols.local_constants.applicable_accessibilities = local dotnet_naming_symbols.local_constants.required_modifiers = const dotnet_naming_symbols.parameters.applicable_kinds = parameter dotnet_naming_symbols.parameters.applicable_accessibilities = * dotnet_naming_symbols.parameters.required_modifiers = dotnet_naming_symbols.public_constant_fields.applicable_kinds = field dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal dotnet_naming_symbols.public_constant_fields.required_modifiers = const dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static dotnet_naming_symbols.local_functions.applicable_kinds = local_function dotnet_naming_symbols.local_functions.applicable_accessibilities = * dotnet_naming_symbols.local_functions.required_modifiers = # Naming styles dotnet_naming_style.pascalcase.required_prefix = dotnet_naming_style.pascalcase.required_suffix = dotnet_naming_style.pascalcase.word_separator = dotnet_naming_style.pascalcase.capitalization = pascal_case dotnet_naming_style.ipascalcase.required_prefix = I dotnet_naming_style.ipascalcase.required_suffix = dotnet_naming_style.ipascalcase.word_separator = dotnet_naming_style.ipascalcase.capitalization = pascal_case dotnet_naming_style.tpascalcase.required_prefix = T dotnet_naming_style.tpascalcase.required_suffix = dotnet_naming_style.tpascalcase.word_separator = dotnet_naming_style.tpascalcase.capitalization = pascal_case dotnet_naming_style._camelcase.required_prefix = _ dotnet_naming_style._camelcase.required_suffix = dotnet_naming_style._camelcase.word_separator = dotnet_naming_style._camelcase.capitalization = camel_case dotnet_naming_style.camelcase.required_prefix = dotnet_naming_style.camelcase.required_suffix = dotnet_naming_style.camelcase.word_separator = dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_style.s_camelcase.required_prefix = s_ dotnet_naming_style.s_camelcase.required_suffix = dotnet_naming_style.s_camelcase.word_separator = dotnet_naming_style.s_camelcase.capitalization = camel_case ################################################################################## # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ ################################################################################## ## Roslyn Code quality rules dotnet_diagnostic.CA1030.severity = none dotnet_diagnostic.CA1034.severity = none dotnet_diagnostic.CA1062.severity = suggestion dotnet_code_quality.CA1062.exclude_extension_method_this_parameter = true dotnet_code_quality.exclude_extension_method_this_parameter = true dotnet_code_quality.null_check_validation_methods = ThrowIfArgumentIsNull # CA1031: Do not catch general exception types dotnet_diagnostic.CA1031.severity = none # CA1303: Do not pass literals as localized parameters dotnet_diagnostic.CA1303.severity = none # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = error # CA1307: Specify StringComparison for clarity dotnet_diagnostic.CA1307.severity = error # CA1308: Normalize strings to uppercase dotnet_diagnostic.CA1308.severity = none # CA1309: Use ordinal StringComparison dotnet_diagnostic.CA1309.severity = error # CA1724: Type names should not match namespaces dotnet_diagnostic.CA1724.severity = none # CA1819: Properties should not return arrays dotnet_diagnostic.CA1819.severity = none # CA1851: Possible multiple enumerations of IEnumerable collection. Related to GH-issue #2000 dotnet_diagnostic.CA1851.severity = suggestion # CA1859: Use concrete types when possible for improved performance dotnet_diagnostic.CA1859.severity = suggestion # CA1860: Avoid using 'Enumerable.Any()' extension method dotnet_diagnostic.CA1860.severity = warning # CA1861: Avoid constant arrays as arguments dotnet_diagnostic.CA1861.severity = none # CA2007: Do not directly await a Task dotnet_diagnostic.CA2007.severity = none # CA2225: Operator overloads have named alternates dotnet_diagnostic.CA2225.severity = none # CA3075: Insecure DTD Processing dotnet_diagnostic.CA3075.severity = none # CA5369: Use XmlReader for Deserialize dotnet_diagnostic.CA5369.severity = none # CA1305: Specify IFormatProvider dotnet_diagnostic.CA1305.severity = None # CA1063: Implement IDisposable correctly dotnet_diagnostic.CA1063.severity = None # CA2201: Do not raise reserved exception types dotnet_diagnostic.ca2201.severity = Suggestion # CA1848: Use the LoggerMessage delegates dotnet_diagnostic.ca1848.severity = Suggestion # CA1810: Initialize reference type static fields inline dotnet_diagnostic.ca1810.severity = Suggestion # CA1725: Parameter names should match base declaration dotnet_diagnostic.ca1725.severity = Suggestion # CA1515: Consider making public types internal dotnet_diagnostic.CA1515.severity = None # CA2000: Dispose objects before losing scope dotnet_diagnostic.CA2000.severity = Suggestion # CA1707: Identifiers should not contain underscores dotnet_diagnostic.CA1707.severity = None # CA1716: Identifiers should not match keywords dotnet_diagnostic.CA1716.severity = Suggestion # CA1032: Implement standard exception constructors dotnet_diagnostic.CA1032.severity = Suggestion # AV1500: A method should not exceed a predefined number (60-100 lines) of lines dotnet_diagnostic.AV1500.severity = none ################################################################################## # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ ################################################################################## ## Roslyn Code-style rules dotnet_diagnostic.IDE0048.severity = Suggestion dotnet_diagnostic.IDE0028.severity = Suggestion dotnet_diagnostic.IDE0029.severity = Suggestion dotnet_diagnostic.IDE0030 .severity = Suggestion dotnet_diagnostic.IDE0004.severity = error # IDE0005: Remove unnecessary usings/imports dotnet_diagnostic.IDE0005.severity = warning # IDE0051: Remove unused private members (no reads or writes) dotnet_diagnostic.IDE0051.severity = Suggestion # IDE0052: Remove unread private members (writes but no reads) dotnet_diagnostic.IDE0052.severity = warning # Remove unnecessary using directives (IDE0005) dotnet_diagnostic.IDE0005.severity = none # CS1574: XML comment on 'construct' has syntactically incorrect cref attribute 'name' dotnet_diagnostic.CS1574.severity = error # IDE0055: Fix formatting # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/dotnet-formatting-options # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0055 # https://csharpier.com/docs/IntegratingWithLinters#code-analysis-rules dotnet_diagnostic.IDE0055.severity = none ################################################################################## # https://jetbrains.com.xy2401.com/help/resharper/EditorConfig_Index.html # https://jetbrains.com.xy2401.com/help/resharper/Reference__Code_Inspections_CSHARP.html ## Resharper # ReSharper properties resharper_align_linq_query = true resharper_align_multiline_array_and_object_initializer = true resharper_align_multiline_binary_patterns = true resharper_align_multiline_expression = true resharper_align_multiline_extends_list = true resharper_align_multiline_parameter = true resharper_align_multiline_property_pattern = true resharper_align_multiline_switch_expression = true resharper_align_multiple_declaration = true resharper_align_multline_type_parameter_constrains = true resharper_align_multline_type_parameter_list = true resharper_align_tuple_components = true resharper_braces_for_for = required_for_multiline resharper_braces_for_foreach = required_for_multiline resharper_braces_for_ifelse = required_for_multiline resharper_csharp_alignment_tab_fill_style = optimal_fill resharper_csharp_indent_type_constraints = false resharper_csharp_int_align_fix_in_adjacent = false resharper_csharp_outdent_commas = true resharper_csharp_stick_comment = false resharper_csharp_wrap_after_declaration_lpar = true resharper_csharp_wrap_after_invocation_lpar = true resharper_csharp_wrap_arguments_style = chop_if_long resharper_csharp_wrap_before_declaration_rpar = true resharper_csharp_wrap_before_first_type_parameter_constraint = true resharper_csharp_wrap_before_ternary_opsigns = false resharper_csharp_wrap_extends_list_style = chop_if_long resharper_csharp_wrap_multiple_declaration_style = wrap_if_long resharper_csharp_wrap_multiple_type_parameter_constraints_style = chop_always resharper_csharp_wrap_parameters_style = chop_if_long resharper_enforce_line_ending_style = true resharper_indent_anonymous_method_block = true resharper_indent_braces_inside_statement_conditions = false resharper_indent_nested_fixed_stmt = true resharper_indent_nested_foreach_stmt = true resharper_indent_nested_for_stmt = true resharper_indent_nested_lock_stmt = true resharper_indent_nested_usings_stmt = true resharper_indent_nested_while_stmt = true resharper_keep_existing_declaration_block_arrangement = true resharper_keep_existing_declaration_parens_arrangement = false resharper_keep_existing_embedded_arrangement = false resharper_keep_existing_embedded_block_arrangement = true resharper_keep_existing_enum_arrangement = true resharper_keep_existing_invocation_parens_arrangement = false resharper_keep_existing_property_patterns_arrangement = false resharper_keep_existing_switch_expression_arrangement = false resharper_max_array_initializer_elements_on_line = 700 resharper_max_formal_parameters_on_line = 500 resharper_max_invocation_arguments_on_line = 700 resharper_new_line_before_while = true resharper_place_attribute_on_same_line = false resharper_place_linq_into_on_new_line = false resharper_place_simple_case_statement_on_same_line = if_owner_is_single_line resharper_place_simple_property_pattern_on_single_line = false resharper_show_autodetect_configure_formatting_tip = false resharper_space_within_single_line_array_initializer_braces = false resharper_trailing_comma_in_multiline_lists = true resharper_trailing_comma_in_singleline_lists = true resharper_use_heuristics_for_body_style = false resharper_use_indent_from_vs = false resharper_use_roslyn_logic_for_evident_types = true resharper_wrap_array_initializer_style = chop_always resharper_wrap_chained_binary_expressions = chop_if_long resharper_wrap_chained_binary_patterns = chop_if_long resharper_wrap_chained_method_calls = chop_if_long resharper_wrap_for_stmt_header_style = wrap_if_long resharper_wrap_switch_expression = chop_if_long resharper_wrap_verbatim_interpolated_strings = chop_if_long # ReSharper inspection severities resharper_arrange_accessor_owner_body_highlighting = none resharper_arrange_redundant_parentheses_highlighting = hint resharper_arrange_type_member_modifiers_highlighting = hint resharper_arrange_type_modifiers_highlighting = hint resharper_check_namespace_highlighting = none resharper_enforce_if_statement_braces_highlighting = hint resharper_inconsistent_naming_highlighting = suggestion resharper_static_member_in_generic_type_highlighting = none resharper_suggest_var_or_type_built_in_types_highlighting = hint resharper_suggest_var_or_type_elsewhere_highlighting = hint resharper_suggest_var_or_type_simple_types_highlighting = hint resharper_web_config_module_not_resolved_highlighting = warning resharper_web_config_type_not_resolved_highlighting = warning resharper_web_config_wrong_module_highlighting = warning # https://www.jetbrains.com/help/rider/ClassNeverInstantiated.Global.html resharper_class_never_instantiated_global_highlighting = none # Convert lambda expression to method group resharper_convert_closure_to_method_group_highlighting = none # Start each element in a object or collection initializer on a new line resharper_wrap_object_and_collection_initializer_style = chop_always # Force an empty line resharper_blank_lines_after_multiline_statements = 1 # Don't remove existing line breaks resharper_keep_existing_initializer_arrangement = true resharper_keep_existing_arrangement = true # We care about that extra else after an else-if resharper_redundant_if_else_block_highlighting = none # Don't remove explicit default cases in switch statements resharper_redundant_empty_switch_section_highlighting = none resharper_align_multiline_binary_expressions_chain = false # Only use new() when the type is obvious resharper_object_creation_when_type_not_evident = explicitly_typed resharper_object_creation_when_type_evident = target_typed # Indent 4 spaces per necessary indention resharper_continuous_indent_multiplier = 1 # Avoid breaking a generic definition resharper_wrap_before_extends_colon = true resharper_blank_lines_before_multiline_statements = 1 resharper_parentheses_non_obvious_operations = arithmetic, multiplicative, equality, relational, additive resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence ################################################################################## ## https://github.com/DotNetAnalyzers/StyleCopAnalyzers/tree/master/documentation ## https://documentation.help/StyleCop/StyleCop.html ## StyleCop.Analyzers ################################################################################## # Using directive should appear within a namespace declaration dotnet_diagnostic.sa1200.severity = None # Generic type parameter documentation should have text. dotnet_diagnostic.sa1622.severity = None # XML comment analysis is disabled due to project configuration dotnet_diagnostic.sa0001.severity = None # The file header is missing or not located at the top of the file dotnet_diagnostic.sa1633.severity = None # Use string.Empty for empty strings dotnet_diagnostic.sa1122.severity = None # Variable '_' should begin with lower-case letter dotnet_diagnostic.sa1312.severity = None # Parameter '_' should begin with lower-case letter dotnet_diagnostic.sa1313.severity = None # Elements should be documented dotnet_diagnostic.sa1600.severity = None # Prefix local calls with this dotnet_diagnostic.sa1101.severity = None # 'public' members should come before 'private' members dotnet_diagnostic.sa1202.severity = None # Comments should contain text dotnet_diagnostic.sa1120.severity = None # Constant fields should appear before non-constant fields dotnet_diagnostic.sa1203.severity = None # Field '_blah' should not begin with an underscore dotnet_diagnostic.sa1309.severity = None # Use trailing comma in multi-line initializers dotnet_diagnostic.sa1413.severity = None # A method should not follow a class dotnet_diagnostic.sa1201.severity = None # Elements should be separated by blank line dotnet_diagnostic.sa1516.severity = None # The parameter spans multiple lines dotnet_diagnostic.sa1118.severity = None # Static members should appear before non-static members dotnet_diagnostic.sa1204.severity = None # Put constructor initializers on their own line dotnet_diagnostic.sa1128.severity = None # Opening braces should not be preceded by blank line dotnet_diagnostic.sa1509.severity = None # The parameter should begin on the line after the previous parameter dotnet_diagnostic.sa1115.severity = None # File name should match first type name dotnet_diagnostic.sa1649.severity = None # File may only contain a single type dotnet_diagnostic.sa1402.severity = None # Enumeration items should be documented dotnet_diagnostic.sa1602.severity = None # Element should not be on a single line dotnet_diagnostic.sa1502.severity = None # Closing parenthesis should not be preceded by a space dotnet_diagnostic.sa1009.severity = None # Closing parenthesis should be on line of last parameter dotnet_diagnostic.sa1111.severity = None # Braces should not be ommitted dotnet_diagnostic.sa1503.severity = None dotnet_diagnostic.sa1401.severity = None # The parameters to a C# method or indexer call or declaration are not all on the same line or each on a separate line. # dotnet_diagnostic.SA1117.severity = Suggestion # The parameters to a C# method or indexer call or declaration span across multiple lines, but the first parameter does not start on the line after the opening bracket. # dotnet_diagnostic.SA1116.severity = Suggestion # A C# partial element is missing a documentation header. dotnet_diagnostic.sa1601.severity = None # A tag within a C# element’s documentation header is empty. dotnet_diagnostic.sa1614.severity = None # A C# element is missing documentation for its return value. dotnet_diagnostic.sa1615.severity = None # The tag within a C# element’s documentation header is empty. dotnet_diagnostic.sa1616.severity = None # An opening brace within a C# element is not spaced correctly. dotnet_diagnostic.sa1012.severity = Suggestion # A closing brace within a C# element is not spaced correctly. dotnet_diagnostic.sa1013.severity = Suggestion # A call to an instance member of the local class or a base class is not prefixed with 'this.', within a C# code file. dotnet_diagnostic.sa1101.severity = None # The keywords within the declaration of an element do not follow a standard ordering scheme. dotnet_diagnostic.SA1206.severity = None dotnet_diagnostic.SA1106.severity = None # https://csharpier.com/docs/IntegratingWithLinters#stylecopanalyzers # IDE0055: Fix formatting # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/dotnet-formatting-options # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0055 # StyleCopAnalyzers dotnet_diagnostic.SA1000.severity = none dotnet_diagnostic.SA1009.severity = none dotnet_diagnostic.SA1111.severity = none dotnet_diagnostic.SA1118.severity = none dotnet_diagnostic.SA1137.severity = none dotnet_diagnostic.SA1413.severity = none dotnet_diagnostic.SA1500.severity = none dotnet_diagnostic.SA1501.severity = none dotnet_diagnostic.SA1502.severity = none dotnet_diagnostic.SA1504.severity = none dotnet_diagnostic.SA1515.severity = none dotnet_diagnostic.SA1516.severity = none # for csharpier <= 0.21.0 dotnet_diagnostic.SA1127.severity = none dotnet_diagnostic.SA1128.severity = none dotnet_diagnostic.SA1001.severity = none dotnet_diagnostic.SA1002.severity = none dotnet_diagnostic.SA1003.severity = none dotnet_diagnostic.SA1007.severity = none dotnet_diagnostic.SA1008.severity = none dotnet_diagnostic.SA1010.severity = none dotnet_diagnostic.SA1011.severity = none dotnet_diagnostic.SA1012.severity = none dotnet_diagnostic.SA1013.severity = none dotnet_diagnostic.SA1014.severity = none dotnet_diagnostic.SA1015.severity = none dotnet_diagnostic.SA1016.severity = none dotnet_diagnostic.SA1017.severity = none dotnet_diagnostic.SA1018.severity = none dotnet_diagnostic.SA1019.severity = none dotnet_diagnostic.SA1020.severity = none dotnet_diagnostic.SA1021.severity = none dotnet_diagnostic.SA1022.severity = none dotnet_diagnostic.SA1023.severity = none dotnet_diagnostic.SA1024.severity = none dotnet_diagnostic.SA1025.severity = none dotnet_diagnostic.SA1026.severity = none dotnet_diagnostic.SA1027.severity = none dotnet_diagnostic.SA1028.severity = none dotnet_diagnostic.SA1102.severity = none dotnet_diagnostic.SA1103.severity = none dotnet_diagnostic.SA1104.severity = none dotnet_diagnostic.SA1105.severity = none dotnet_diagnostic.SA1107.severity = none dotnet_diagnostic.SA1110.severity = none dotnet_diagnostic.SA1112.severity = none dotnet_diagnostic.SA1113.severity = none dotnet_diagnostic.SA1114.severity = none dotnet_diagnostic.SA1115.severity = none dotnet_diagnostic.SA1116.severity = none dotnet_diagnostic.SA1117.severity = none dotnet_diagnostic.SA1127.severity = none dotnet_diagnostic.SA1128.severity = none dotnet_diagnostic.SA1136.severity = none dotnet_diagnostic.SA1505.severity = none dotnet_diagnostic.SA1506.severity = none dotnet_diagnostic.SA1507.severity = none dotnet_diagnostic.SA1508.severity = none dotnet_diagnostic.SA1509.severity = none dotnet_diagnostic.SA1510.severity = none dotnet_diagnostic.SA1511.severity = none dotnet_diagnostic.SA1517.severity = none dotnet_diagnostic.SA1518.severity = none ################################################################################## ## https://github.com/meziantou/Meziantou.Analyzer/tree/main/docs ## Meziantou.Analyzer # MA0048: File name must match type name dotnet_diagnostic.ma0048.severity = Suggestion # MA0051: Method is too long dotnet_diagnostic.ma0051.severity = Suggestion # https://www.meziantou.net/string-comparisons-are-harder-than-it-seems.htm # MA0006 - Use String.Equals instead of equality operator dotnet_diagnostic.ma0006.severity = Suggestion # MA0002 - IEqualityComparer or IComparer is missing dotnet_diagnostic.ma0002.severity = Suggestion # MA0001 - StringComparison is missing dotnet_diagnostic.ma0001.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#13-pass-cancellation-token # MA0040: Specify a cancellation token dotnet_diagnostic.ma0032.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#13-pass-cancellation-token # MA0040: Flow the cancellation token when available dotnet_diagnostic.ma0040.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#14-using-cancellation-token-with-iasyncenumerable # MA0079: Use a cancellation token using .WithCancellation() dotnet_diagnostic.ma0079.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#14-using-cancellation-token-with-iasyncenumerable # MA0080: Use a cancellation token using .WithCancellation() dotnet_diagnostic.ma0080.severity = Suggestion # Use Task.ConfigureAwait(false) as the current SynchronizationContext is not needed dotnet_diagnostic.MA0004.severity = none # Add regex evaluation timeout dotnet_diagnostic.MA0009.severity = none # Use an overload of 'ToString' that has a 'System.IFormatProvider' parameter. Already caught by CA1305. dotnet_diagnostic.MA0011.severity = none # Use an overload of 'System.ArgumentException' with the parameter name. Just a suggestion since we have a bunch of justified exceptions. dotnet_diagnostic.MA0015.severity = suggestion # Use an explicit StringComparer to compute hash codes dotnet_diagnostic.MA0021.severity = none # Declare types in namespaces. Already caught by CA1050 dotnet_diagnostic.MA0047.severity = none # Use an overload of 'GetHashCode' that has a StringComparison parameter dotnet_diagnostic.MA0074.severity = none # MA0049: Type name should not match containing namespace dotnet_diagnostic.MA0049.severity = none ################################################################################## ## https://github.com/JosefPihrt/Roslynator/blob/main/docs/Configuration.md ## https://josefpihrt.github.io/docs/ ## Roslynator ################################################################################## # RCS1036 - Remove redundant empty line. dotnet_diagnostic.rcs1036.severity = None # RCS1037 - Remove trailing white-space. dotnet_diagnostic.rcs1037.severity = None # RCS1194: Implement exception constructors dotnet_diagnostic.rcs1194.severity = None # https://cezarypiatek.github.io/post/async-analyzers-p1/#1-redundant-asyncawait # RCS1174: Remove redundant async/await. dotnet_diagnostic.rcs1174.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p2/#10-returning-null-from-a-task-returning-method # RCS1210: Return Task.FromResult instead of returning null. dotnet_diagnostic.rcs1210.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p2/#9-missing-configureawaitbool # RCS1090: Call 'ConfigureAwait(false)'. dotnet_diagnostic.rcs1090.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#11-asynchronous-method-names-should-end-with-async #RCS1046: Asynchronous method name should end with 'Async'. dotnet_diagnostic.rcs1046.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#12-non-asynchronous-method-names-shouldnt-end-with-async # RCS1047: Non-asynchronous method name should not end with 'Async'. dotnet_diagnostic.rcs1047.severity = error # https://github.com/JosefPihrt/Roslynator/blob/master/docs/analyzers/RCS1174.md # RCS1174: Remove redundant async/await dotnet_diagnostic.rcs1174.severity = Suggestion # Combine 'Enumerable.Where' method chain. It doesn't make it more readable in all cases. dotnet_diagnostic.RCS1112.severity = suggestion # Inline local variable. dotnet_diagnostic.RCS1124.severity = suggestion # Add exception to documentation comment. Nice suggestion, but we don't want to document exceptions for internal code. dotnet_diagnostic.RCS1140.severity = suggestion # Missing documentation dotnet_diagnostic.RCS1141.severity = suggestion dotnet_diagnostic.RCS1142.severity = suggestion # Use conditional access. Suggestion because it doesn't always improve readability dotnet_diagnostic.RCS1146.severity = suggestion # Enum should declare explicit values. Disabled because we're not storing them. dotnet_diagnostic.RCS1161.severity = none # Static member in generic type should use a type parameter. Disabled because it's not always applicable. dotnet_diagnostic.RCS1158.severity = none # Add region name to #endregion. dotnet_diagnostic.RCS1189.severity = none # Convert comment to documentation comment. Disabled because it also complains about SMELL/REFACTOR comments dotnet_diagnostic.RCS1181.severity = none # Use Regex instance instead of static method. Disabled because it's not always worth it. dotnet_diagnostic.RCS1186.severity = none # Use bit shift operator. dotnet_diagnostic.RCS1237.severity = none # RCS1228: Unused element in documentation comment. (Equivalent to SA1614) dotnet_diagnostic.RCS1228.severity = suggestion # RCS1047: Non-asynchronous method name should not end with 'Async' #dotnet_diagnostic.RCS1047.severity = suggestion ################################################################################## ## https://github.com/semihokur/asyncfixer ## AsyncFixer01 ################################################################################## # https://cezarypiatek.github.io/post/async-analyzers-p1/#1-redundant-asyncawait # AsyncFixer01: Unnecessary async/await usage dotnet_diagnostic.asyncfixer01.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p1/#2-calling-synchronous-method-inside-the-async-method # AsyncFixer02: Long-running or blocking operations inside an async method dotnet_diagnostic.asyncfixer02.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p1/#3-async-void-method # AsyncFixer03: Fire & forget async void methods dotnet_diagnostic.asyncfixer03.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p1/#6-not-awaited-task-inside-the-using-block # AsyncFixer04: Fire & forget async call inside a using block dotnet_diagnostic.asyncfixer04.severity = error ################################################################################## ## https://github.com/microsoft/vs-threading ## Microsoft.VisualStudio.Threading.Analyzers ################################################################################## # https://cezarypiatek.github.io/post/async-analyzers-p1/#2-calling-synchronous-method-inside-the-async-method # VSTHRD103: Call async methods when in an async method dotnet_diagnostic.vsthrd103.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p1/#3-async-void-method # VSTHRD100: Avoid async void methods dotnet_diagnostic.vsthrd100.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p1/#4-unsupported-async-delegates # VSTHRD101: Avoid unsupported async delegates dotnet_diagnostic.vsthrd101.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p1/#5-not-awaited-task-within-using-expression # VSTHRD107: Await Task within using expression dotnet_diagnostic.vsthrd107.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p1/#7-unobserved-result-of-asynchronous-method # VSTHRD110: Observe result of async calls dotnet_diagnostic.vsthrd110.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#8-synchronous-waits # VSTHRD002: Avoid problematic synchronous waits dotnet_diagnostic.vsthrd002.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#9-missing-configureawaitbool # VSTHRD111: Use ConfigureAwait(bool) dotnet_diagnostic.vsthrd111.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#10-returning-null-from-a-task-returning-method # VSTHRD114: Avoid returning a null Task dotnet_diagnostic.vsthrd114.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p2/#11-asynchronous-method-names-should-end-with-async # VSTHRD200: Use "Async" suffix for async methods dotnet_diagnostic.vsthrd200.severity = Suggestion # https://cezarypiatek.github.io/post/async-analyzers-p2/#12-non-asynchronous-method-names-shouldnt-end-with-async # VSTHRD200: Use "Async" suffix for async methods dotnet_diagnostic.vsthrd200.severity = Suggestion # VSTHRD003 Avoid awaiting foreign Tasks dotnet_diagnostic.VSTHRD003.severity = Suggestion ################################################################################## ## https://github.com/hvanbakel/Asyncify-CSharp ## Asyncify ################################################################################## # https://cezarypiatek.github.io/post/async-analyzers-p2/#8-synchronous-waits # AsyncifyInvocation: Use Task Async dotnet_diagnostic.asyncifyinvocation.severity = error # https://cezarypiatek.github.io/post/async-analyzers-p2/#8-synchronous-waits # AsyncifyVariable: Use Task Async dotnet_diagnostic.asyncifyvariable.severity = error ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto *.sh text eol=lf ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/actions/build/action.yml ================================================ # https://docs.github.com/en/actions/creating-actions/creating-a-composite-action # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions # https://doug.sh/posts/using-composite-actions-with-github-actions/ # https://wallis.dev/blog/composite-github-actions name: "Build" description: "Build service" # Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables.(so they are just string) # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs inputs: project-path: description: Project path required: true service-name: description: Service name required: true # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions runs: using: "composite" steps: # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows # https://devblogs.microsoft.com/dotnet/dotnet-loves-github-actions/ # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net#caching-dependencies - name: Cache NuGet Packages uses: actions/cache@v4 if: success() with: path: ~/.nuget/packages key: ${{ runner.os }}-dotnet-nuget - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '10.x.x' # https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools - name: Restore .NET Tools shell: bash run: dotnet tool restore # Note: `Ubuntu` file and folder names are case sensitive, be aware about naming them in solution references. because `Windows` file and folder names as case-insensitive. # prevent windows case-insensitive for our project with: git config core.ignorecase false; - https://stackoverflow.com/a/27139487/581476 - name: Restore NuGet packages shell: bash if: success() # restore root solution run: dotnet restore # npm install, runs `prepare` script automatically in the initialize step - name: Install NPM Dependencies shell: bash if: success() run: npm install - name: Format Service shell: bash if: ${{ success()}} run: | npm run ci-format - name: Build Service shell: bash if: ${{ success()}} working-directory: ${{ inputs.project-path }} run: | dotnet build -c Release --no-restore ================================================ FILE: .github/actions/build-test/action.yml ================================================ # https://docs.github.com/en/actions/creating-actions/creating-a-composite-action # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions # https://doug.sh/posts/using-composite-actions-with-github-actions/ # https://wallis.dev/blog/composite-github-actions name: "Build-Test" description: "Build and test service" # Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables.(so they are just string) # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs inputs: project-path: description: Project path required: true tests-path: description: Test path required: false default: '' reports-path: description: Test report path required: true reports-output-path: description: Test report output path required: true service-name: description: Service name required: true # https://stackoverflow.com/questions/70098241/using-secrets-in-composite-actions-github token: description: A Github PAT required: true # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions runs: using: "composite" steps: - name: Call Composite Action build uses: ./.github/actions/build if: success() id: build-step with: project-path: ${{ inputs.project-path }} service-name: ${{ inputs.service-name }} - name: Call Composite Action test uses: ./.github/actions/test if: ${{ success() && inputs.tests-path != ''}} id: test-step with: tests-path: ${{ inputs.tests-path }} # wildcard search for files with the ".cobertura.xml" extension in all subdirectories of the current directory # https://www.jamescroft.co.uk/combining-multiple-code-coverage-results-in-azure-devops/ # https://stackoverflow.com/questions/53255065/dotnet-unit-test-with-coverlet-how-to-get-coverage-for-entire-solution-and-not reports-path: ${{ github.workspace }}/**/*.cobertura.xml reports-output-path: ${{ github.workspace }}/output/test-results service-name: ${{ inputs.service-name }} token: ${{ inputs.token }} no-restore: true ================================================ FILE: .github/actions/docker-build-publish/action.yml ================================================ name: "Publish and Build Docker" description: "Publish and Build Docker " # Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables.(so they are just string) # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs inputs: tag-name: description: "Tag Name" required: true image-name: description: "Image Name" required: true registry-username: description: "Registry username" required: true registry-password: description: "Registry password" required: true dockerfile-path: description: "Dockerfile path" required: true # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions runs: using: "composite" steps: ##ref: https://docs.docker.com/language/golang/configure-ci-cd/ ##ref: https://event-driven.io/en/how_to_buid_and_push_docker_image_with_github_actions - name: Login to DockerHub uses: docker/login-action@v2 if: ${{ github.ref == 'refs/heads/main' && success() }} with: username: ${{ inputs.registry-username }} password: ${{ inputs.registry-password }} - name: Docker Tag Info shell: bash run: echo "Docker tag version is:" ${{ inputs.tag-name }} - name: Build Docker Image if: ${{ github.ref == 'refs/heads/main' && success() }} shell: bash run: | docker build -t ${{ inputs.registry-username }}/${{ inputs.image-name }}:${{ inputs.tag-name }} -f "${{ github.workspace }}/${{ inputs.dockerfile-path }}" . - name: Publish Docker Image if: ${{ github.ref == 'refs/heads/main' && success() }} shell: bash run: | docker push ${{ inputs.registry-username }}/${{ inputs.image-name }}:${{ inputs.tag-name }} ================================================ FILE: .github/actions/test/action.yml ================================================ # https://docs.github.com/en/actions/creating-actions/creating-a-composite-action # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions # https://doug.sh/posts/using-composite-actions-with-github-actions/ # https://wallis.dev/blog/composite-github-actions name: "Test" description: "Test service" # Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables.(so they are just string) # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs inputs: tests-path: description: Test path required: true reports-path: description: Test report path required: true reports-output-path: description: Test report output path required: true service-name: description: Service name required: true # https://stackoverflow.com/questions/70098241/using-secrets-in-composite-actions-github token: description: A Github PAT required: true no-restore: description: No restore nuget packages, but building tests because they don't build in the build composition action default: 'true' # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions runs: using: "composite" steps: # see here https://samlearnsazure.blog/2021/01/05/code-coverage-in-github-with-net-core/ # https://www.jamescroft.co.uk/combining-multiple-code-coverage-results-in-azure-devops/ # https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test#filter-option-details # https://josef.codes/dotnet-core-filter-out-specific-test-projects-when-running-dotnet-test/ # https://learn.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=xunit # https://stackoverflow.com/questions/53255065/dotnet-unit-test-with-coverlet-how-to-get-coverage-for-entire-solution-and-not # https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md # https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md#filters - name: Tests shell: bash id: tests-step working-directory: ${{ inputs.tests-path }} # https://stackoverflow.com/questions/3779701/msbuild-error-msb1008-only-one-project-can-be-specified # https://octopus.com/blog/githubactions-running-unit-tests # we should not do 'no-build' here, because our tests not build in build phase (build composite action) and should build here run: | for file in $(find . -name "*.csproj" -type f); do echo "Testing $file" if [ ${{ inputs.no-restore }} == 'true' ]; then echo "run tests in no-restore mode" dotnet test "$file" -c Release --no-restore --logger "trx;LogFileName=test-results.trx" || true else echo "run tests in restore nuget mode" dotnet test "$file" -c Release --logger "trx;LogFileName=test-results.trx" || true fi done # GitHub Api call permissions problem here # https://github.com/dorny/test-reporter/issues/168 # https://octopus.com/blog/githubactions-running-unit-tests # https://github.com/dorny/test-reporter/issues/67 # https://github.com/phoenix-actions/test-reporting/pull/21 - name: Test Results uses: phoenix-actions/test-reporting@v10 id: test-report if: always() with: name: ${{ inputs.service-name }} Test Reports reporter: dotnet-trx token: ${{ inputs.token }} # only-summary: 'true' output-to: "step-summary" path: "**/test-results.trx" # Set action as failed if test report contains any failed test fail-on-error: true ## https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories ## https://github.com/dorny/test-reporter/blob/0d9714ddc7ff86918ec725a527a3a069419d301a/src/utils/github-utils.ts#L44 ## artifact name to download trx test result if it is in seperated workflow with github rest call, if it is not in another workflow skip this # artifact: "' ================================================ FILE: .github/release-drafter.yml ================================================ # https://johanneskonings.dev/github/2021/02/28/github_automatic_releases_and-changelog/ # https://tiagomichaelsousa.dev/articles/stop-writing-your-changelogs-manually # https://github.com/release-drafter/release-drafter/issues/551 # https://github.com/release-drafter/release-drafter/pull/1013 # https://github.com/release-drafter/release-drafter/issues/139 # https://github.com/atk4/data/blob/develop/.github/release-drafter.yml # This release drafter follows the conventions from https://keepachangelog.com, https://common-changelog.org/ # https://www.conventionalcommits.org name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' template: | ## What Changed 👀 $CHANGES **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION categories: - title: 🚀 Features labels: - feature - title: 🐛 Bug Fixes labels: - fix - bug - title: 🧪 Test labels: - test - title: 👷 CI labels: - ci - title: ♻️ Refactor labels: - changed - enhancement - refactor - title: ⛔️ Deprecated labels: - deprecated - title: 🔐 Security labels: - security - title: 📄 Documentation labels: - docs - documentation - title: 🧩 Dependency Updates labels: - deps - dependencies - title: 🧰 Maintenance label: 'chore' - title: 📝 Other changes ## putting no labels pr to `Other Changes` category with no label - https://github.com/release-drafter/release-drafter/issues/139#issuecomment-480473934 # https://www.trywilco.com/post/wilco-ci-cd-github-heroku # https://github.com/release-drafter/release-drafter#autolabeler # https://github.com/fuxingloh/multi-labeler # Using regex for defining rules - https://regexr.com/ - https://regex101.com/ autolabeler: - label: 'chore' branch: - '/(chore)\/.*/' - label: 'security' branch: - '/(security)\/.*/' - label: 'refactor' branch: - '/(refactor)\/.*/' - label: 'docs' branch: - '/(docs)\/.*/' - label: 'ci' branch: - '/(ci)\/.*/' - label: 'test' branch: - '/(test)\/.*/' - label: 'bug' branch: - '/(fix)\/.*/' - label: 'feature' branch: - '/(feat)\/.*/' - label: 'minor' branch: - '/(feat)\/.*/' - label: 'patch' branch: - '/(fix)\/.*/' body: - '/JIRA-[0-9]{1,4}/' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. version-resolver: major: labels: - major minor: labels: - minor patch: labels: - patch default: patch exclude-labels: - skip-changelog ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ "main"] paths-ignore: - "README.md" pull_request: branches: [ "main"] paths-ignore: - "README.md" concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.sha }} cancel-in-progress: true jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build and Test Flight Microservice uses: ./.github/actions/build-test if: success() id: build-test-flight-step with: project-path: 'src/Services/Flight/src/Flight.Api' tests-path: 'src/Services/Flight/tests/' # wildcard search for files with the ".cobertura.xml" extension in all subdirectories of the current directory # https://www.jamescroft.co.uk/combining-multiple-code-coverage-results-in-azure-devops/ # https://stackoverflow.com/questions/53255065/dotnet-unit-test-with-coverlet-how-to-get-coverage-for-entire-solution-and-not reports-path: ${{ github.workspace }}/**/*.cobertura.xml reports-output-path: ${{ github.workspace }}/output/test-results service-name: 'Flight' token: ${{ secrets.GITHUB_TOKEN }} - name: Build and Test Identity Microservice uses: ./.github/actions/build-test if: success() id: build-test-identity-step with: project-path: 'src/Services/Identity/src/Identity.Api' tests-path: 'src/Services/Identity/tests/' # wildcard search for files with the ".cobertura.xml" extension in all subdirectories of the current directory # https://www.jamescroft.co.uk/combining-multiple-code-coverage-results-in-azure-devops/ # https://stackoverflow.com/questions/53255065/dotnet-unit-test-with-coverlet-how-to-get-coverage-for-entire-solution-and-not reports-path: ${{ github.workspace }}/**/*.cobertura.xml reports-output-path: ${{ github.workspace }}/output/test-results service-name: 'Identity' token: ${{ secrets.GITHUB_TOKEN }} - name: Build and Test Passenger Microservice uses: ./.github/actions/build-test if: success() id: build-test-passenger-step with: project-path: 'src/Services/Passenger/src/Passenger.Api' tests-path: 'src/Services/Passenger/tests/' # wildcard search for files with the ".cobertura.xml" extension in all subdirectories of the current directory # https://www.jamescroft.co.uk/combining-multiple-code-coverage-results-in-azure-devops/ # https://stackoverflow.com/questions/53255065/dotnet-unit-test-with-coverlet-how-to-get-coverage-for-entire-solution-and-not reports-path: ${{ github.workspace }}/**/*.cobertura.xml reports-output-path: ${{ github.workspace }}/output/test-results service-name: 'Passenger' token: ${{ secrets.GITHUB_TOKEN }} - name: Build and Test Booking Microservice uses: ./.github/actions/build-test if: success() id: build-test-booking-step with: project-path: 'src/Services/Booking/src/Booking.Api' tests-path: 'src/Services/Booking/tests/' # wildcard search for files with the ".cobertura.xml" extension in all subdirectories of the current directory # https://www.jamescroft.co.uk/combining-multiple-code-coverage-results-in-azure-devops/ # https://stackoverflow.com/questions/53255065/dotnet-unit-test-with-coverlet-how-to-get-coverage-for-entire-solution-and-not reports-path: ${{ github.workspace }}/**/*.cobertura.xml reports-output-path: ${{ github.workspace }}/output/test-results service-name: 'Booking' token: ${{ secrets.GITHUB_TOKEN }} - name: Update Release Drafter if: ${{ github.ref == 'refs/heads/main' && success() }} id: last_release uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Release Version Info run: echo "Release version is:" ${{ steps.last_release.outputs.tag_name }} - name: Build and Publish Identity Microservice to Docker if: ${{ github.ref == 'refs/heads/main' && success() }} uses: ./.github/actions/docker-build-publish with: tag-name: ${{ steps.last_release.outputs.tag_name }} registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_PASSWORD }} dockerfile-path: 'src/Services/Identity/Dockerfile' image-name: 'booking-microservices-identity' - name: Build and Publish Flight Microservice to Docker if: ${{ github.ref == 'refs/heads/main' && success() }} uses: ./.github/actions/docker-build-publish with: tag-name: ${{ steps.last_release.outputs.tag_name }} registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_PASSWORD }} dockerfile-path: 'src/Services/Flight/Dockerfile' image-name: 'booking-microservices-flight' - name: Build and Publish Passenger Microservice to Docker if: ${{ github.ref == 'refs/heads/main' && success() }} uses: ./.github/actions/docker-build-publish with: tag-name: ${{ steps.last_release.outputs.tag_name }} registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_PASSWORD }} dockerfile-path: 'src/Services/Passenger/Dockerfile' image-name: 'booking-microservices-passenger' - name: Build and Publish Booking Microservice to Docker if: ${{ github.ref == 'refs/heads/main' && success() }} uses: ./.github/actions/docker-build-publish with: tag-name: ${{ steps.last_release.outputs.tag_name }} registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_PASSWORD }} dockerfile-path: 'src/Services/Booking/Dockerfile' image-name: 'booking-microservices-booking' ================================================ FILE: .github/workflows/release-drafter-labeler.yml ================================================ name: Release Drafter Auto Labeler on: pull_request: types: - opened - synchronize - reopened - labeled - unlabeled jobs: auto-labeler: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v5 with: config-name: release-drafter.yml disable-releaser: true # only run auto-labeler for PRs env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # SonarLint plugin .idea/sonarlint/ # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser # JetBrains Rider .idea/ *.sln.iml # Tye .tye/ *.jwk # Monitoring **/grafana-data # EventStore **/eventstore ================================================ FILE: .husky/commit-msg ================================================ npx --no -- commitlint --edit ${1} ================================================ FILE: .husky/pre-commit ================================================ npm run format npm run ci-format ================================================ FILE: CONTRIBUTION.md ================================================ ## Contribution This is great that you'd like to contribute to this project. All change requests should go through the steps described below. ## Pull Requests **Please, make sure you open an issue before starting with a Pull Request, unless it's a typo or a really obvious error.** Pull requests are the best way to propose changes. ## Conventional commits Our repository follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification. Releasing to GitHub and NuGet is done with the support of [semantic-release](https://semantic-release.gitbook.io/semantic-release/). Pull requests should have a title that follows the specification, otherwise, merging is blocked. If you are not familiar with the specification simply ask maintainers to modify. You can also use this cheatsheet if you want: - `fix: ` prefix in the title indicates that PR is a bug fix and PATCH release must be triggered. - `feat: ` prefix in the title indicates that PR is a feature and MINOR release must be triggered. - `docs: ` prefix in the title indicates that PR is only related to the documentation and there is no need to trigger release. - `chore: ` prefix in the title indicates that PR is only related to cleanup in the project and there is no need to trigger release. - `test: ` prefix in the title indicates that PR is only related to tests and there is no need to trigger release. - `refactor: ` prefix in the title indicates that PR is only related to refactoring and there is no need to trigger release. ## Resources - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) - [GitHub Help](https://help.github.com) ================================================ FILE: Directory.Build.props ================================================ net10.0 enable enable $(MSBuildThisFileDirectory) runtime; build; native; contentfiles; analyzers; buildtransitive runtime; build; native; contentfiles; analyzers; buildtransitive runtime; build; native; contentfiles; analyzers; buildtransitive runtime; build; native; contentfiles; analyzers; buildtransitive runtime; build; native; contentfiles; analyzers; buildtransitive runtime; build; native; contentfiles; analyzers; buildtransitive runtime; build; native; contentfiles; analyzers; buildtransitive runtime; build; native; contentfiles; analyzers; buildtransitive true true false latest-Recommended Recommended ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Meysam Hadeli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
booking-microservices
ci-status build-status
> 🚀 **A practical microservices with the latest technologies and architectures like Vertical Slice Architecture, Event Sourcing, CQRS, DDD, gRpc, MongoDB, RabbitMq, Masstransit, and Aspire in .Net 10.** ## You can find other version of this project here: - [Booking with Modular Monolith Architecture](https://github.com/meysamhadeli/booking-modular-monolith) - [Booking with Monolith Architecture](https://github.com/meysamhadeli/booking-monolith)
Open in GitHub Codespaces
# Table of Contents - [The Goals of This Project](#the-goals-of-this-project) - [Technologies - Libraries](#technologies---libraries) - [Key Features](#key-features) - [When to Use](#when-to-use) - [Challenges](#challenges) - [The Domain and Bounded Context - Service Boundary](#the-domain-and-bounded-context---service-boundary) - [Structure of Project](#structure-of-project) - [Development Setup](#development-setup) - [Dotnet Tools Packages](#dotnet-tools-packages) - [Husky](#husky) - [Upgrade Nuget Packages](#upgrade-nuget-packages) - [How to Run](#how-to-run) - [Config Certificate](#config-certificate) - [Aspire](#aspire) - [Docker Compose](#docker-compose) - [Kubernetes](#kubernetes) - [Build](#build) - [Run](#run) - [Test](#test) - [Documentation Apis](#documentation-apis) - [Support](#support) - [Contribution](#contribution) ## The Goals of This Project - :sparkle: Using `Vertical Slice Architecture` for `architecture` level. - :sparkle: Using `Domain Driven Design (DDD)` to implement all `business logic`. - :sparkle: Using `Rabbitmq` on top of `Masstransit` for `Event Driven Architecture`. - :sparkle: Using `gRPC` for `internal communication`. - :sparkle: Using `CQRS` implementation with `MediatR` library. - :sparkle: Using `Postgres` for `write side` database. - :sparkle: Using `MongoDB` for `read side` database. - :sparkle: Using `Event Store` for `write side` of Booking Microservice/Module to store all `historical change` of aggregate. - :sparkle: Using `Inbox Pattern` for ensuring message idempotency for receiver and `Exactly once Delivery`. - :sparkle: Using `Outbox Pattern` for ensuring no message is lost and there is at `At Least One Delivery`. - :sparkle: Using `Unit Testing` for testing small units and mocking our dependencies with `Nsubstitute`. - :sparkle: Using `End-To-End Testing` and `Integration Testing` for testing `features` with all dependencies using `testcontainers`. - :sparkle: Using `Fluent Validation` and a `Validation Pipeline Behaviour` on top of `MediatR`. - :sparkle: Using `Minimal API` for all endpoints. - :sparkle: Using `AspNetCore OpenApi` for `generating` built-in support `OpenAPI documentation` in ASP.NET Core. - :sparkle: Using `Health Check` for `reporting` the `health` of app infrastructure components. - :sparkle: Using `Docker-Compose` and `Kubernetes` for our deployment mechanism. - :sparkle: Using `Kibana` on top of `Serilog` for `logging`. - :sparkle: Using `OpenTelemetry` for distributed tracing on top of `Jaeger`. - :sparkle: Using `OpenTelemetry` for monitoring on top of `Prometheus` and `Grafana`. - :sparkle: Using `IdentityServer` for authentication and authorization base on `OpenID-Connect` and `OAuth2`. - :sparkle: Using `Yarp` as a microservices `gateway`. - :sparkle: Using `Kubernetes` to achieve efficient `scaling` and ensure `high availability` for each of our microservices. - :sparkle: Using `Nginx Ingress Controller` for `load balancing` between our microservices top of `Kubernetes`. - :sparkle: Using `cert-manager` to Configure `TLS` in `kubernetes cluster`. - :sparkle: Using `Aspire` for `service discovery`, `observability`, and `local orchestration` of microservices. ## Technologies - Libraries - ✔️ **[`.NET 10`](https://github.com/dotnet/aspnetcore)** - .NET Framework and .NET Core, including ASP.NET and ASP.NET Core. - ✔️ **[`MVC Versioning API`](https://github.com/microsoft/aspnet-api-versioning)** - Set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core. - ✔️ **[`EF Core`](https://github.com/dotnet/efcore)** - Modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations. - ✔️ **[`AspNetCore OpenApi`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi)** - Provides built-in support for OpenAPI document generation in ASP.NET Core. - ✔️ **[`Masstransit`](https://github.com/MassTransit/MassTransit)** - Distributed Application Framework for .NET. - ✔️ **[`MediatR`](https://github.com/jbogard/MediatR)** - Simple, unambitious mediator implementation in .NET. - ✔️ **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules. - ✔️ **[`Scalar`](https://github.com/scalar/scalar/tree/main/packages/scalar.aspnetcore)** - Scalar provides an easy way to render beautiful API references based on OpenAPI/Swagger documents. - ✔️ **[`Swagger UI`](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)** - Swagger tools for documenting API's built on ASP.NET Core. - ✔️ **[`Serilog`](https://github.com/serilog/serilog)** - Simple .NET logging with fully-structured events - ✔️ **[`Polly`](https://github.com/App-vNext/Polly)** - Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. - ✔️ **[`Scrutor`](https://github.com/khellang/Scrutor)** - Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection - ✔️ **[`Opentelemetry-dotnet`](https://github.com/open-telemetry/opentelemetry-dotnet)** - The OpenTelemetry .NET Client - ✔️ **[`DuendeSoftware IdentityServer`](https://github.com/DuendeSoftware/IdentityServer)** - The most flexible and standards-compliant OpenID Connect and OAuth 2.x framework for ASP.NET Core. - ✔️ **[`EasyCaching`](https://github.com/dotnetcore/EasyCaching)** - Open source caching library that contains basic usages and some advanced usages of caching which can help us to handle caching more easier. - ✔️ **[`Mapster`](https://github.com/MapsterMapper/Mapster)** - Convention-based object-object mapper in .NET. - ✔️ **[`Hellang.Middleware.ProblemDetails`](https://github.com/khellang/Middleware/tree/master/src/ProblemDetails)** - A middleware for handling exception in .Net Core. - ✔️ **[`NewId`](https://github.com/phatboyg/NewId)** - NewId can be used as an embedded unique ID generator that produces 128 bit (16 bytes) sequential IDs. - ✔️ **[`Yarp`](https://github.com/microsoft/reverse-proxy)** - Reverse proxy toolkit for building fast proxy servers in .NET. - ✔️ **[`Tye`](https://github.com/dotnet/tye)** - Developer tool that makes developing, testing, and deploying microservices and distributed applications easier. - ✔️ **[`gRPC-dotnet`](https://github.com/grpc/grpc-dotnet)** - gRPC functionality for .NET. - ✔️ **[`EventStore`](https://github.com/EventStore/EventStore)** - The open-source, functional database with Complex Event Processing. - ✔️ **[`MongoDB.Driver`](https://github.com/mongodb/mongo-csharp-driver)** - .NET Driver for MongoDB. - ✔️ **[`xUnit.net`](https://github.com/xunit/xunit)** - A free, open source, community-focused unit testing tool for the .NET Framework. - ✔️ **[`Respawn`](https://github.com/jbogard/Respawn)** - Respawn is a small utility to help in resetting test databases to a clean state. - ✔️ **[`Testcontainers`](https://github.com/testcontainers/testcontainers-dotnet)** - Testcontainers for .NET is a library to support tests with throwaway instances of Docker containers. - ✔️ **[`K6`](https://github.com/grafana/k6)** - Modern load testing for developers and testers in the DevOps era. - ✔️ **[`Aspire`](https://github.com/dotnet/aspire)** - .NET stack for building and orchestrating observable, distributed cloud-native applications. ## Key Features 1. **Independent Services**: Each service is a separate project with its own database and deployment pipeline, enabling independent development and deployment. 2. **Decentralized Communication**: Services communicate via APIs (REST, gRPC) or message brokers (RabbitMQ, Kafka), ensuring loose coupling and resilience. 3. **Scalability**: Services can be scaled independently based on demand, allowing efficient resource utilization. 4. **Fault Tolerance**: Failures are isolated, preventing cascading failures and ensuring high availability. 5. **Technology Agnostic**: Services can use different technologies, frameworks, or databases, providing flexibility. ## When to Use 1. **Large and Complex Projects**: Ideal for applications with complex business logic that can be broken into smaller, manageable services. 2. **High Scalability Needs**: Suitable for applications requiring independent scaling of components. 3. **Fault Tolerance and High Availability**: Perfect for systems where failure isolation and uptime are critical. 4. **Distributed Teams**: Enables teams to work independently on different services. 5. **Frequent Updates**: Supports continuous deployment and A/B testing for individual services. 6. **Technology Diversity**: Allows the use of different technologies for different services. ## Challenges - Increased complexity in management, DevOps overhead, data consistency, latency, and higher costs. ## The Domain And Bounded Context - Service Boundary - `Identity Service`: The Identity Service is a bounded context for the authentication and authorization of users using [Identity Server](https://github.com/DuendeSoftware/IdentityServer). This service is responsible for creating new users and their corresponding roles and permissions using [.Net Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity) and Jwt authentication and authorization. - `Flight Service`: The Flight Service is a bounded context `CRUD` service to handle flight related operations. - `Passenger Service`: The Passenger Service is a bounded context for managing passenger information, tracking activities and subscribing to get notification for out of stock products. - `Booking Service`: The Booking Service is a bounded context for managing all operation related to booking ticket. ![](./assets/booking-microservices.png) ## Structure of Project In this project, I used [vertical slice architecture](https://jimmybogard.com/vertical-slice-architecture/) at the architectural level and [feature folder structure](http://www.kamilgrzybek.com/design/feature-folders/) to structure my files. I treat each request as a distinct use case or slice, encapsulating and grouping all concerns from front-end to back. When adding or changing a feature in an application in n-tire architecture, we are typically touching many "layers" in an application. We are changing the user interface, adding fields to models, modifying validation, and so on. Instead of coupling across a layer, we couple vertically along a slice. We `minimize coupling` `between slices`, and `maximize coupling` `in a slice`. With this approach, each of our vertical slices can decide for itself how to best fulfill the request. New features only add code, we're not changing shared code and worrying about side effects.
Instead of grouping related action methods in one controller, as found in traditional ASP.net controllers, I used the [REPR pattern](https://deviq.com/design-patterns/repr-design-pattern). Each action gets its own small endpoint, consisting of a route, the action, and an `IMediator` instance (see [MediatR](https://github.com/jbogard/MediatR)). The request is passed to the `IMediator` instance, routed through a [`Mediatr pipeline`](https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/) where custom [middleware](https://github.com/jbogard/MediatR/wiki/Behaviors) can log, validate and intercept requests. The request is then handled by a request specific `IRequestHandler` which performs business logic before returning the result. The use of the [mediator pattern](https://dotnetcoretutorials.com/2019/04/30/the-mediator-pattern-in-net-core-part-1-whats-a-mediator/) in my controllers creates clean and [thin controllers](https://codeopinion.com/thin-controllers-cqrs-mediatr/). By separating action logic into individual handlers we support the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle) and [Don't Repeat Yourself principles](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), this is because traditional controllers tend to become bloated with large action methods and several injected `Services` only being used by a few methods. I used CQRS to decompose my features into small parts that makes our application: - Maximize performance, scalability and simplicity. - Easy to maintain and add features to. Changes only affect one command or query, avoiding breaking changes or creating side effects. - It gives us better separation of concerns and cross-cutting concern (with help of mediatr behavior pipelines), instead of bloated service classes doing many things. Using the CQRS pattern, we cut each business functionality into vertical slices, for each of these slices we group classes (see [technical folders structure](http://www.kamilgrzybek.com/design/feature-folders)) specific to that feature together (command, handlers, infrastructure, repository, controllers, etc). In our CQRS pattern each command/query handler is a separate slice. This is where you can reduce coupling between layers. Each handler can be a separated code unit, even copy/pasted. Thanks to that, we can tune down the specific method to not follow general conventions (e.g. use custom SQL query or even different storage). In a traditional layered architecture, when we change the core generic mechanism in one layer, it can impact all methods. ## Development Setup ### Dotnet Tools Packages For installing our requirement packages with .NET cli tools, we need to install `dotnet tool manifest`. ```bash dotnet new tool-manifest ``` And after that we can restore our dotnet tools packages with .NET cli tools from `.config` folder and `dotnet-tools.json` file. ``` dotnet tool restore ``` ### Husky Here we use `husky` to handel some pre commit rules and we used `conventional commits` rules and `formatting` as pre commit rules, here in [package.json](.././package.json). of course, we can add more rules for pre commit in future. (find more about husky in the [documentation](https://typicode.github.io/husky/get-started.html)) We need to install `husky` package for `manage` `pre commits hooks` and also I add two packages `@commitlint/cli` and `@commitlint/config-conventional` for handling conventional commits rules in [package.json](.././package.json). Run the command bellow in the root of project to install all npm dependencies related to husky: ```bash npm install ``` > Note: In the root of project we have `.husky` folder and it has `commit-msg` file for handling conventional commits rules with provide user friendly message and `pre-commit` file that we can run our `scripts` as a `pre-commit` hooks. that here we call `format` script from [package.json](./package.json) for formatting purpose. ### Upgrade Nuget Packages For upgrading our nuget packages to last version, we use the great package [dotnet-outdated](https://github.com/dotnet-outdated/dotnet-outdated). Run the command below in the root of project to upgrade all of packages to last version: ```bash dotnet outdated -u ``` ## How to Run > ### Config Certificate Run the following commands to [Config SSL](https://docs.microsoft.com/en-us/aspnet/core/security/docker-compose-https?view=aspnetcore-6.0) in your system: #### Windows using Linux containers ```bash dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p password dotnet dev-certs https --trust ``` > Note: for running this command in `powershell` use `$env:USERPROFILE` instead of `%USERPROFILE%`* #### macOS or Linux ```bash dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p $CREDENTIAL_PLACEHOLDER$ dotnet dev-certs https --trust ``` ### Aspire To run the application using the `Aspire App Host`, execute the following command from the solution root: ```bash aspire run ``` > Note:The `Aspire dashboard` will be available at `http://localhost:18888` > ### Docker Compose To run this app in `Docker`, use the [docker-compose.yaml](./deployments/docker-compose/docker-compose.yaml) and execute the below command at the `root` of the application: ```bash docker-compose -f ./deployments/docker-compose/docker-compose.yaml up -d ``` > ### Kubernetes To `configure TLS` in the `Kubernetes cluster`, we need to install `cert-manager` based on the [docs](https://cert-manager.io/docs/installation) and run the following commands to apply TLS in our application. Here, we use [Let's Encrypt](https://letsencrypt.org/) to encrypt our certificate. ```bash kubectl apply -f ./deployments/kubernetes/booking-cert-manager.yml ``` To apply all necessary `deployments`, `pods`, `services`, `ingress`, and `config maps`, please run the following command: ```bash kubectl apply -f ./deployments/kubernetes/booking-microservices.yml ``` > ### Build To `build` all microservices, run this command in the `root` of the project: ```bash dotnet build ``` > ### Run To `run` each microservice, run this command in the root of the `Api` folder of each microservice where the `csproj` file is located: ```bash dotnet run ``` > ### Test To `test` all microservices, run this command in the `root` of the project: ```bash dotnet test ``` > ### Documentation Apis Each microservice provides `API documentation` and navigate to `/swagger` for `Swagger OpenAPI` or `/scalar/v1` for `Scalar OpenAPI` to visit list of endpoints. As part of API testing, I created the [booking.rest](./booking.rest) file which can be run with the [REST Client](https://github.com/Huachao/vscode-restclient) `VSCode plugin`. # Support If you like my work, feel free to: - ⭐ this repository. And we will be happy together :) Thanks a bunch for supporting me! ## Contribution Thanks to all [contributors](https://github.com/meysamhadeli/booking-microservices/graphs/contributors), you're awesome and this wouldn't be possible without you! The goal is to build a categorized, community-driven collection of very well-known resources. Please follow this [contribution guideline](./CONTRIBUTION.md) to submit a pull request or create the issue. ## Project References & Credits - [https://github.com/jbogard/ContosoUniversityDotNetCore-Pages](https://github.com/jbogard/ContosoUniversityDotNetCore-Pages) - [https://github.com/kgrzybek/modular-monolith-with-ddd](https://github.com/kgrzybek/modular-monolith-with-ddd) - [https://github.com/oskardudycz/EventSourcing.NetCore](https://github.com/oskardudycz/EventSourcing.NetCore) - [https://github.com/thangchung/clean-architecture-dotnet](https://github.com/thangchung/clean-architecture-dotnet) - [https://github.com/pdevito3/MessageBusTestingInMemHarness](https://github.com/pdevito3/MessageBusTestingInMemHarness) ## License This project is made available under the MIT license. See [LICENSE](https://github.com/meysamhadeli/booking-microservices/blob/main/LICENSE) for details. ================================================ FILE: assets/booking-microservices.drawio ================================================ ================================================ FILE: assets/vertical-slice-architecture.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "type": "rectangle", "version": 242, "versionNonce": 1509780320, "isDeleted": false, "id": "80OGzNPG6Gk8NAvbV3XaF", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 648, "y": 187, "strokeColor": "#000000", "backgroundColor": "#a8bffe", "width": 538, "height": 62, "seed": 246982778, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "46GLDhDwmnc8RGy3v8OK8" } ], "updated": 1679316672934, "link": null, "locked": false }, { "type": "text", "version": 137, "versionNonce": 703919968, "isDeleted": false, "id": "46GLDhDwmnc8RGy3v8OK8", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 897.848014831543, "y": 201.2, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 38.30397033691406, "height": 33.6, "seed": 2080176422, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1679315949309, "link": null, "locked": false, "fontSize": 28, "fontFamily": 1, "text": "Api", "textAlign": "center", "verticalAlign": "middle", "containerId": "80OGzNPG6Gk8NAvbV3XaF", "originalText": "Api" }, { "type": "rectangle", "version": 358, "versionNonce": 356515488, "isDeleted": false, "id": "nZuYK7wbLObwRvpRRLHay", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 648, "y": 263, "strokeColor": "#000000", "backgroundColor": "#fea8d5", "width": 538, "height": 62, "seed": 287502970, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "OALII-DXtatRPgn_EkHfp" } ], "updated": 1679316735759, "link": null, "locked": false }, { "type": "text", "version": 246, "versionNonce": 1126108000, "isDeleted": false, "id": "OALII-DXtatRPgn_EkHfp", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 845.628044128418, "y": 277.2, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 142.74391174316406, "height": 33.6, "seed": 1016531494, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1679315949309, "link": null, "locked": false, "fontSize": 28, "fontFamily": 1, "text": "Application", "textAlign": "center", "verticalAlign": "middle", "containerId": "nZuYK7wbLObwRvpRRLHay", "originalText": "Application" }, { "type": "rectangle", "version": 282, "versionNonce": 787808928, "isDeleted": false, "id": "za_4vz64MSfPF5TWmD7wj", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 650, "y": 338, "strokeColor": "#000000", "backgroundColor": "#f30358", "width": 538, "height": 62, "seed": 676018342, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "6CqYCSOKHqhqJ8nf4b-Sv" } ], "updated": 1679316783390, "link": null, "locked": false }, { "type": "text", "version": 189, "versionNonce": 1441177440, "isDeleted": false, "id": "6CqYCSOKHqhqJ8nf4b-Sv", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 816.618049621582, "y": 352.2, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 204.76390075683594, "height": 33.6, "seed": 1067355322, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1679315949309, "link": null, "locked": false, "fontSize": 28, "fontFamily": 1, "text": "Infrastructure", "textAlign": "center", "verticalAlign": "middle", "containerId": "za_4vz64MSfPF5TWmD7wj", "originalText": "Infrastructure" }, { "type": "rectangle", "version": 326, "versionNonce": 1669046112, "isDeleted": false, "id": "t2sZwLLvmq3y2ndIbEomB", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 70, "angle": 0, "x": 648, "y": 413, "strokeColor": "#000000", "backgroundColor": "#9d9ca2", "width": 538, "height": 62, "seed": 1173221990, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "b3wdaWjaVmgHpzMD26uKD" } ], "updated": 1679316844215, "link": null, "locked": false }, { "type": "text", "version": 224, "versionNonce": 1385935712, "isDeleted": false, "id": "b3wdaWjaVmgHpzMD26uKD", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 70, "angle": 0, "x": 886.5500106811523, "y": 427.2, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 60.89997863769531, "height": 33.6, "seed": 1307397882, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1679315949310, "link": null, "locked": false, "fontSize": 28, "fontFamily": 1, "text": "Core", "textAlign": "center", "verticalAlign": "middle", "containerId": "t2sZwLLvmq3y2ndIbEomB", "originalText": "Core" }, { "type": "rectangle", "version": 202, "versionNonce": 1461187232, "isDeleted": false, "id": "FQZImjU2-VUOATU9Yeyly", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 678, "y": 154, "strokeColor": "#000000", "backgroundColor": "#fefda8", "width": 48, "height": 361, "seed": 1254939642, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [], "updated": 1679316609154, "link": null, "locked": false }, { "type": "rectangle", "version": 249, "versionNonce": 1540775776, "isDeleted": false, "id": "_Vw9EnXAyzxRDEzXCTfeL", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 742, "y": 153.5, "strokeColor": "#000000", "backgroundColor": "#fefda8", "width": 48, "height": 361, "seed": 523058342, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [], "updated": 1679316594766, "link": null, "locked": false }, { "type": "text", "version": 249, "versionNonce": 871687840, "isDeleted": false, "id": "hyJiOwPt7LFndn5R0xgfL", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 4.707547804955119, "x": 637.1248451774691, "y": 317.9455509364301, "strokeColor": "#000000", "backgroundColor": "#f9e79f", "width": 130.73194885253906, "height": 33.6, "seed": 678740006, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1679315961675, "link": null, "locked": false, "fontSize": 28, "fontFamily": 1, "text": "Feature 1", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Feature 1" }, { "type": "text", "version": 182, "versionNonce": 1494113120, "isDeleted": false, "id": "7KOHd5JA_wVMmwXPVT1N3", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 80, "angle": 4.7123889803846915, "x": 695.6880416870117, "y": 313.20000000000005, "strokeColor": "#000000", "backgroundColor": "#f9e79f", "width": 143.07994079589844, "height": 33.6, "seed": 1387191482, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1679315949310, "link": null, "locked": false, "fontSize": 28, "fontFamily": 1, "text": "Feature 2", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Feature 2" }, { "type": "text", "version": 163, "versionNonce": 1243581088, "isDeleted": false, "id": "SuFNrbzZGowiIybusnadN", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 70, "angle": 0, "x": 748, "y": 96, "strokeColor": "#000000", "backgroundColor": "#f9e79f", "width": 360.47186279296875, "height": 33.6, "seed": 2006173690, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1679315949310, "link": null, "locked": false, "fontSize": 28, "fontFamily": 1, "text": "Vertical Slice Architecture", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Vertical Slice Architecture" } ], "appState": { "gridSize": null, "viewBackgroundColor": "#ffffff" }, "files": {} } ================================================ FILE: booking-microservices.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CD4A4407-C3B0-422D-BB8C-2A810CED9938}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiGateway", "ApiGateway", "{CDFA86FA-BBBA-4A5B-A833-3BE219E373E5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{B19FD14B-4DFE-26B6-646B-3D5D94CC4D36}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildingBlocks", "BuildingBlocks", "{C734CEF7-A2AC-3076-84D8-694B7490AA9D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Flight", "Flight", "{A3579DE0-F7C5-67E8-3CF8-3AC89B64E059}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Booking", "Booking", "{C6034A5C-F49A-5FA4-86A6-65B2CB19613F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Passenger", "Passenger", "{9D4F3958-FE6E-C048-E6F9-6F53D8AF03CA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{B465D535-05D9-3A0A-08BF-35A1C18CEC46}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A2834164-BF04-BF13-ADC5-A97145852861}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1B4FBE3A-43F5-1B1E-2877-3036AC5431EF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DDEDC5E0-5D13-A45C-2393-A774DD4A1A07}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{51D8F471-B8EB-AD1C-0E89-AA84C5D0C759}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{773BFBD8-04CD-79F8-8301-C81308C3ED45}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4B043475-1AFA-C467-FE09-A46D09CD6936}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CED3889-AECF-A6CD-55DC-F680D3C18861}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{54BCCDE8-25E6-6FCB-4A9E-D5D2AF76D352}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Booking.Api", "src\Services\Booking\src\Booking.Api\Booking.Api.csproj", "{D3BF565A-C413-4185-9528-BE1B4F46993C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Booking", "src\Services\Booking\src\Booking\Booking.csproj", "{3EA375C7-2900-4927-B1E5-C9D31E67F4A8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flight", "src\Services\Flight\src\Flight\Flight.csproj", "{BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flight.Api", "src\Services\Flight\src\Flight.Api\Flight.Api.csproj", "{836D1466-3C20-4D74-B54A-FA09C0EE0FA2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "src\Services\Identity\src\Identity\Identity.csproj", "{BCDEAB10-6373-46E7-B408-846A3B0B508B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity.Api", "src\Services\Identity\src\Identity.Api\Identity.Api.csproj", "{B0EC74C5-9B2D-492C-ABAE-3E868397B122}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passenger", "src\Services\Passenger\src\Passenger\Passenger.csproj", "{9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passenger.Api", "src\Services\Passenger\src\Passenger.Api\Passenger.Api.csproj", "{101FFD12-17A4-4615-9438-F347BBF4CC85}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildingBlocks", "src\BuildingBlocks\BuildingBlocks.csproj", "{AEDB3219-5E1D-4716-8DE2-F5F9391913A2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{427BE8BE-DA7B-FC74-412B-547671E05463}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiGateway", "src\ApiGateway\src\ApiGateway.csproj", "{C015BF35-6977-407B-8948-636A9C81C5BE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "src\Services\Booking\tests\IntegrationTest\Integration.Test.csproj", "{19A89F36-FD3A-448D-90D1-04A1B67BB255}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndToEnd.Test", "src\Services\Flight\tests\EndToEndTest\EndToEnd.Test.csproj", "{B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "src\Services\Flight\tests\IntegrationTest\Integration.Test.csproj", "{BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unit.Test", "src\Services\Flight\tests\UnitTest\Unit.Test.csproj", "{B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "src\Services\Identity\tests\IntegrationTest\Integration.Test.csproj", "{0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "src\Services\Passenger\tests\IntegrationTest\Integration.Test.csproj", "{A85AE27D-81ED-485A-BA4B-161B25BEB8A5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C4287034-6833-4505-A6EB-704A86392ECB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "src\Aspire\src\AppHost\AppHost.csproj", "{490BCB11-314C-473C-9B85-A32164783507}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "src\Aspire\src\ServiceDefaults\ServiceDefaults.csproj", "{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {D3BF565A-C413-4185-9528-BE1B4F46993C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Debug|x64.ActiveCfg = Debug|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Debug|x64.Build.0 = Debug|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Debug|x86.ActiveCfg = Debug|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Debug|x86.Build.0 = Debug|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Release|Any CPU.Build.0 = Release|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Release|x64.ActiveCfg = Release|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Release|x64.Build.0 = Release|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Release|x86.ActiveCfg = Release|Any CPU {D3BF565A-C413-4185-9528-BE1B4F46993C}.Release|x86.Build.0 = Release|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Debug|Any CPU.Build.0 = Debug|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Debug|x64.ActiveCfg = Debug|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Debug|x64.Build.0 = Debug|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Debug|x86.ActiveCfg = Debug|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Debug|x86.Build.0 = Debug|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Release|Any CPU.ActiveCfg = Release|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Release|Any CPU.Build.0 = Release|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Release|x64.ActiveCfg = Release|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Release|x64.Build.0 = Release|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Release|x86.ActiveCfg = Release|Any CPU {3EA375C7-2900-4927-B1E5-C9D31E67F4A8}.Release|x86.Build.0 = Release|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Debug|x64.ActiveCfg = Debug|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Debug|x64.Build.0 = Debug|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Debug|x86.ActiveCfg = Debug|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Debug|x86.Build.0 = Debug|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Release|Any CPU.Build.0 = Release|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Release|x64.ActiveCfg = Release|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Release|x64.Build.0 = Release|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Release|x86.ActiveCfg = Release|Any CPU {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279}.Release|x86.Build.0 = Release|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Debug|x64.ActiveCfg = Debug|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Debug|x64.Build.0 = Debug|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Debug|x86.ActiveCfg = Debug|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Debug|x86.Build.0 = Debug|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Release|Any CPU.ActiveCfg = Release|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Release|Any CPU.Build.0 = Release|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Release|x64.ActiveCfg = Release|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Release|x64.Build.0 = Release|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Release|x86.ActiveCfg = Release|Any CPU {836D1466-3C20-4D74-B54A-FA09C0EE0FA2}.Release|x86.Build.0 = Release|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Debug|x64.ActiveCfg = Debug|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Debug|x64.Build.0 = Debug|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Debug|x86.ActiveCfg = Debug|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Debug|x86.Build.0 = Debug|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Release|Any CPU.Build.0 = Release|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Release|x64.ActiveCfg = Release|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Release|x64.Build.0 = Release|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Release|x86.ActiveCfg = Release|Any CPU {BCDEAB10-6373-46E7-B408-846A3B0B508B}.Release|x86.Build.0 = Release|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Debug|x64.ActiveCfg = Debug|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Debug|x64.Build.0 = Debug|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Debug|x86.ActiveCfg = Debug|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Debug|x86.Build.0 = Debug|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Release|Any CPU.Build.0 = Release|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Release|x64.ActiveCfg = Release|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Release|x64.Build.0 = Release|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Release|x86.ActiveCfg = Release|Any CPU {B0EC74C5-9B2D-492C-ABAE-3E868397B122}.Release|x86.Build.0 = Release|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Debug|x64.ActiveCfg = Debug|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Debug|x64.Build.0 = Debug|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Debug|x86.ActiveCfg = Debug|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Debug|x86.Build.0 = Debug|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Release|Any CPU.Build.0 = Release|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Release|x64.ActiveCfg = Release|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Release|x64.Build.0 = Release|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Release|x86.ActiveCfg = Release|Any CPU {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538}.Release|x86.Build.0 = Release|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Debug|Any CPU.Build.0 = Debug|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Debug|x64.ActiveCfg = Debug|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Debug|x64.Build.0 = Debug|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Debug|x86.ActiveCfg = Debug|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Debug|x86.Build.0 = Debug|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Release|Any CPU.ActiveCfg = Release|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Release|Any CPU.Build.0 = Release|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Release|x64.ActiveCfg = Release|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Release|x64.Build.0 = Release|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Release|x86.ActiveCfg = Release|Any CPU {101FFD12-17A4-4615-9438-F347BBF4CC85}.Release|x86.Build.0 = Release|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Debug|x64.ActiveCfg = Debug|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Debug|x64.Build.0 = Debug|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Debug|x86.ActiveCfg = Debug|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Debug|x86.Build.0 = Debug|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Release|Any CPU.Build.0 = Release|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Release|x64.ActiveCfg = Release|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Release|x64.Build.0 = Release|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Release|x86.ActiveCfg = Release|Any CPU {AEDB3219-5E1D-4716-8DE2-F5F9391913A2}.Release|x86.Build.0 = Release|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Debug|x64.ActiveCfg = Debug|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Debug|x64.Build.0 = Debug|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Debug|x86.ActiveCfg = Debug|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Debug|x86.Build.0 = Debug|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Release|Any CPU.Build.0 = Release|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Release|x64.ActiveCfg = Release|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Release|x64.Build.0 = Release|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Release|x86.ActiveCfg = Release|Any CPU {C015BF35-6977-407B-8948-636A9C81C5BE}.Release|x86.Build.0 = Release|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Debug|Any CPU.Build.0 = Debug|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Debug|x64.ActiveCfg = Debug|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Debug|x64.Build.0 = Debug|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Debug|x86.ActiveCfg = Debug|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Debug|x86.Build.0 = Debug|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Release|Any CPU.ActiveCfg = Release|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Release|Any CPU.Build.0 = Release|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Release|x64.ActiveCfg = Release|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Release|x64.Build.0 = Release|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Release|x86.ActiveCfg = Release|Any CPU {19A89F36-FD3A-448D-90D1-04A1B67BB255}.Release|x86.Build.0 = Release|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Debug|x64.ActiveCfg = Debug|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Debug|x64.Build.0 = Debug|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Debug|x86.ActiveCfg = Debug|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Debug|x86.Build.0 = Debug|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Release|Any CPU.Build.0 = Release|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Release|x64.ActiveCfg = Release|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Release|x64.Build.0 = Release|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Release|x86.ActiveCfg = Release|Any CPU {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4}.Release|x86.Build.0 = Release|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Debug|x64.ActiveCfg = Debug|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Debug|x64.Build.0 = Debug|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Debug|x86.ActiveCfg = Debug|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Debug|x86.Build.0 = Debug|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Release|Any CPU.Build.0 = Release|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Release|x64.ActiveCfg = Release|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Release|x64.Build.0 = Release|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Release|x86.ActiveCfg = Release|Any CPU {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B}.Release|x86.Build.0 = Release|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Debug|x64.ActiveCfg = Debug|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Debug|x64.Build.0 = Debug|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Debug|x86.ActiveCfg = Debug|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Debug|x86.Build.0 = Debug|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Release|Any CPU.Build.0 = Release|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Release|x64.ActiveCfg = Release|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Release|x64.Build.0 = Release|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Release|x86.ActiveCfg = Release|Any CPU {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB}.Release|x86.Build.0 = Release|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Debug|Any CPU.Build.0 = Debug|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Debug|x64.ActiveCfg = Debug|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Debug|x64.Build.0 = Debug|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Debug|x86.ActiveCfg = Debug|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Debug|x86.Build.0 = Debug|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Release|Any CPU.ActiveCfg = Release|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Release|Any CPU.Build.0 = Release|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Release|x64.ActiveCfg = Release|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Release|x64.Build.0 = Release|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Release|x86.ActiveCfg = Release|Any CPU {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01}.Release|x86.Build.0 = Release|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Debug|x64.ActiveCfg = Debug|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Debug|x64.Build.0 = Debug|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Debug|x86.ActiveCfg = Debug|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Debug|x86.Build.0 = Debug|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|Any CPU.Build.0 = Release|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|x64.ActiveCfg = Release|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|x64.Build.0 = Release|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|x86.ActiveCfg = Release|Any CPU {A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|x86.Build.0 = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Debug|Any CPU.Build.0 = Debug|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Debug|x64.ActiveCfg = Debug|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Debug|x64.Build.0 = Debug|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Debug|x86.ActiveCfg = Debug|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Debug|x86.Build.0 = Debug|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|Any CPU.ActiveCfg = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|Any CPU.Build.0 = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|x64.ActiveCfg = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|x64.Build.0 = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|x86.ActiveCfg = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|x86.Build.0 = Release|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x64.ActiveCfg = Debug|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x64.Build.0 = Debug|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x86.ActiveCfg = Debug|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x86.Build.0 = Debug|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|Any CPU.Build.0 = Release|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x64.ActiveCfg = Release|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x64.Build.0 = Release|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x86.ActiveCfg = Release|Any CPU {5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {CDFA86FA-BBBA-4A5B-A833-3BE219E373E5} = {CD4A4407-C3B0-422D-BB8C-2A810CED9938} {B19FD14B-4DFE-26B6-646B-3D5D94CC4D36} = {CD4A4407-C3B0-422D-BB8C-2A810CED9938} {C734CEF7-A2AC-3076-84D8-694B7490AA9D} = {CD4A4407-C3B0-422D-BB8C-2A810CED9938} {A3579DE0-F7C5-67E8-3CF8-3AC89B64E059} = {B19FD14B-4DFE-26B6-646B-3D5D94CC4D36} {C6034A5C-F49A-5FA4-86A6-65B2CB19613F} = {B19FD14B-4DFE-26B6-646B-3D5D94CC4D36} {9D4F3958-FE6E-C048-E6F9-6F53D8AF03CA} = {B19FD14B-4DFE-26B6-646B-3D5D94CC4D36} {B465D535-05D9-3A0A-08BF-35A1C18CEC46} = {B19FD14B-4DFE-26B6-646B-3D5D94CC4D36} {A2834164-BF04-BF13-ADC5-A97145852861} = {C6034A5C-F49A-5FA4-86A6-65B2CB19613F} {1B4FBE3A-43F5-1B1E-2877-3036AC5431EF} = {C6034A5C-F49A-5FA4-86A6-65B2CB19613F} {DDEDC5E0-5D13-A45C-2393-A774DD4A1A07} = {A3579DE0-F7C5-67E8-3CF8-3AC89B64E059} {51D8F471-B8EB-AD1C-0E89-AA84C5D0C759} = {A3579DE0-F7C5-67E8-3CF8-3AC89B64E059} {773BFBD8-04CD-79F8-8301-C81308C3ED45} = {B465D535-05D9-3A0A-08BF-35A1C18CEC46} {4B043475-1AFA-C467-FE09-A46D09CD6936} = {B465D535-05D9-3A0A-08BF-35A1C18CEC46} {5CED3889-AECF-A6CD-55DC-F680D3C18861} = {9D4F3958-FE6E-C048-E6F9-6F53D8AF03CA} {54BCCDE8-25E6-6FCB-4A9E-D5D2AF76D352} = {9D4F3958-FE6E-C048-E6F9-6F53D8AF03CA} {D3BF565A-C413-4185-9528-BE1B4F46993C} = {A2834164-BF04-BF13-ADC5-A97145852861} {3EA375C7-2900-4927-B1E5-C9D31E67F4A8} = {A2834164-BF04-BF13-ADC5-A97145852861} {BCC8A8A6-C2ED-42D2-86BB-A05C790D7279} = {DDEDC5E0-5D13-A45C-2393-A774DD4A1A07} {836D1466-3C20-4D74-B54A-FA09C0EE0FA2} = {DDEDC5E0-5D13-A45C-2393-A774DD4A1A07} {BCDEAB10-6373-46E7-B408-846A3B0B508B} = {773BFBD8-04CD-79F8-8301-C81308C3ED45} {B0EC74C5-9B2D-492C-ABAE-3E868397B122} = {773BFBD8-04CD-79F8-8301-C81308C3ED45} {9B4BDD42-56F3-4DB9-B3E5-74ABB7C19538} = {5CED3889-AECF-A6CD-55DC-F680D3C18861} {101FFD12-17A4-4615-9438-F347BBF4CC85} = {5CED3889-AECF-A6CD-55DC-F680D3C18861} {AEDB3219-5E1D-4716-8DE2-F5F9391913A2} = {C734CEF7-A2AC-3076-84D8-694B7490AA9D} {427BE8BE-DA7B-FC74-412B-547671E05463} = {CDFA86FA-BBBA-4A5B-A833-3BE219E373E5} {C015BF35-6977-407B-8948-636A9C81C5BE} = {427BE8BE-DA7B-FC74-412B-547671E05463} {19A89F36-FD3A-448D-90D1-04A1B67BB255} = {1B4FBE3A-43F5-1B1E-2877-3036AC5431EF} {B27759CD-5A7D-43A4-A55C-FE1154DC4CC4} = {51D8F471-B8EB-AD1C-0E89-AA84C5D0C759} {BD23EEF8-9196-4E0F-BF33-E14E99D34C1B} = {51D8F471-B8EB-AD1C-0E89-AA84C5D0C759} {B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB} = {51D8F471-B8EB-AD1C-0E89-AA84C5D0C759} {0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01} = {4B043475-1AFA-C467-FE09-A46D09CD6936} {A85AE27D-81ED-485A-BA4B-161B25BEB8A5} = {54BCCDE8-25E6-6FCB-4A9E-D5D2AF76D352} {D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA} = {CD4A4407-C3B0-422D-BB8C-2A810CED9938} {C4287034-6833-4505-A6EB-704A86392ECB} = {D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA} {490BCB11-314C-473C-9B85-A32164783507} = {C4287034-6833-4505-A6EB-704A86392ECB} {5B7BF918-E47F-4932-B5C5-E8C2C35890E4} = {C4287034-6833-4505-A6EB-704A86392ECB} EndGlobalSection EndGlobal ================================================ FILE: booking.rest ================================================ ## uncommnet this line for use kubernetes ingress controller instead of Yarp //@api-gateway=https://booking-microservices.com @api-gateway=https://localhost:5000 @identity-api=http://localhost:6005 @flight-api=http://localhost:5004 @passenger-api=http://localhost:6012 @booking-api=http://localhost:6010 @contentType = application/json @flightid = "3c5c0000-97c6-fc34-2eb9-08db322230c9" @passengerId = "8c9c0000-97c6-fc34-2eb9-66db322230c9" ################################# Identity API ################################# ### # @name ApiRoot_Identity GET {{identity-api}} ### ### # @name Authenticate POST {{api-gateway}}/identity/connect/token Content-Type: application/x-www-form-urlencoded grant_type=password &client_id=client &client_secret=secret &username=samh &password=Admin@123456 &scope=flight-api role ### change scope base on microservices scope (eg. passenger-api, ...) ### ### # @name Register_New_User POST {{api-gateway}}/identity/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": "41290000", "email": "admin@admin.com", "password": "Admin@12345", "confirmPassword": "Admin@12345" } ### ################################# Flight API ################################# ### # @name ApiRoot_Flight GET {{flight-api}} ### ### # @name Create_Seat Post {{api-gateway}}/flight/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 {{api-gateway}}/flight/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 {{api-gateway}}/flight/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 {{api-gateway}}/flight/api/v1/flight/{{flightid}} accept: application/json Content-Type: application/json authorization: bearer {{Authenticate.response.body.access_token}} ### ### # @name Get_Available_Flights GET {{api-gateway}}/flight/api/v1/flight/get-available-flights accept: application/json Content-Type: application/json authorization: bearer {{Authenticate.response.body.access_token}} ### ### # @name Create_Flights POST {{api-gateway}}/flight/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 {{api-gateway}}/flight/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 {{api-gateway}}/flight/api/v1/flight/{{flightid}} accept: application/json Content-Type: application/json authorization: bearer {{Authenticate.response.body.access_token}} ### ### # @name Create_Airport POST {{api-gateway}}/flight/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 {{api-gateway}}/flight/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 ApiRoot_Passenger GET {{passenger-api}} ### ### # @name Complete_Registration_Passenger POST {{api-gateway}}/passenger/api/v1/passenger/complete-registration accept: application/json Content-Type: application/json authorization: bearer {{Authenticate.response.body.access_token}} { "passportNumber": "41290000", "passengerType": 1, "age": 30 } ### ### # @name Get_Passenger_By_Id GET {{api-gateway}}/passenger/api/v1/passenger/{{passengerId}} accept: application/json Content-Type: application/json authorization: bearer {{Authenticate.response.body.access_token}} ### ################################# Booking API ################################# ### # @name ApiRoot_Booking GET {{booking-api}} ### ### # @name Create_Booking POST {{api-gateway}}/booking/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" } ### ================================================ FILE: commitlint.config.js ================================================ module.exports = { extends: ["@commitlint/config-conventional"], } ================================================ FILE: deployments/configs/dashboards.md ================================================ # Dashboards - [Introducing ASP.NET Core metrics and Grafana dashboards in .NET 8](https://devblogs.microsoft.com/dotnet/introducing-aspnetcore-metrics-and-grafana-dashboards-in-dotnet-8/) - [ASP.NET Core](https://grafana.com/grafana/dashboards/19924-asp-net-core/) - [ASP.NET Core Endpoint](https://grafana.com/grafana/dashboards/19925-asp-net-core-endpoint/) - [Node Exporter Quickstart and Dashboard](https://grafana.com/grafana/dashboards/13978-node-exporter-quickstart-and-dashboard/) - [PostgreSQL Exporter Quickstart and Dashboard](https://grafana.com/grafana/dashboards/14114-postgres-overview/) - [RabbitMQ-Overview](https://grafana.com/grafana/dashboards/10991-rabbitmq-overview/) ================================================ FILE: deployments/configs/grafana/dashboards/dotnet-core-endpoint.json ================================================ { "__inputs": [ { "name": "DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY", "label": "Managed_Prometheus_jamesnk-telemetry", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": {}, "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "9.4.8" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "stat", "name": "Stat", "version": "" }, { "type": "panel", "id": "table", "name": "Table", "version": "" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": null, "links": [ { "asDropdown": false, "icon": "dashboard", "includeVars": false, "keepTime": true, "tags": [], "targetBlank": false, "title": " ASP.NET Core", "tooltip": "", "type": "link", "url": "/d/KdDACDp4z/asp-net-core-metrics" } ], "liveNow": false, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "fieldConfig": { "defaults": { "color": { "fixedColor": "dark-green", "mode": "continuous-GrYlRd", "seriesBy": "max" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "axisSoftMin": 0, "barAlignment": 0, "drawStyle": "line", "fillOpacity": 50, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "smooth", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [ { "__systemRef": "hideSeriesFrom", "matcher": { "id": "byNames", "options": { "mode": "exclude", "names": [ "p50" ], "prefix": "All except:", "readOnly": true } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": false, "tooltip": false, "viz": false } } ] } ] }, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 }, "id": 40, "options": { "legend": { "calcs": [ "lastNotNull", "min", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.50, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m])) by (le))", "legendFormat": "p50", "range": true, "refId": "p50" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.75, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m])) by (le))", "hide": false, "legendFormat": "p75", "range": true, "refId": "p75" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.90, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m])) by (le))", "hide": false, "legendFormat": "p90", "range": true, "refId": "p90" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.95, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m])) by (le))", "hide": false, "legendFormat": "p95", "range": true, "refId": "p95" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.98, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m])) by (le))", "hide": false, "legendFormat": "p98", "range": true, "refId": "p98" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.99, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m])) by (le))", "hide": false, "legendFormat": "p99", "range": true, "refId": "p99" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.999, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m])) by (le))", "hide": false, "legendFormat": "p99.9", "range": true, "refId": "p99.9" } ], "title": "Requests Duration - $method $route", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic", "seriesBy": "max" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 50, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "smooth", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byName", "options": "All" }, "properties": [ { "id": "color", "value": { "fixedColor": "dark-orange", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "4XX" }, "properties": [ { "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "5XX" }, "properties": [ { "id": "color", "value": { "fixedColor": "dark-red", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 0 }, "id": 46, "options": { "legend": { "calcs": [ "lastNotNull", "min", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\", status_code=~\"4..|5..\"}[5m]) or vector(0)) / sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m]))", "legendFormat": "All", "range": true, "refId": "All" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\", status_code=~\"4..\"}[5m]) or vector(0)) / sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m]))", "hide": false, "legendFormat": "4XX", "range": true, "refId": "4XX" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\", status_code=~\"5..\"}[5m]) or vector(0)) / sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[5m]))", "hide": false, "legendFormat": "5XX", "range": true, "refId": "5XX" } ], "title": "Errors Rate - $method $route", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "inspect": false }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [ { "matcher": { "id": "byName", "options": "Requests" }, "properties": [ { "id": "custom.width", "value": 300 }, { "id": "custom.cellOptions", "value": { "mode": "gradient", "type": "gauge" } }, { "id": "color", "value": { "mode": "continuous-YlRd" } } ] }, { "matcher": { "id": "byName", "options": "Route" }, "properties": [ { "id": "links", "value": [ { "title": "", "url": "/d/NagEsjE4z/asp-net-core-endpoint-details?var-route=${__data.fields.Route}&var-method=${__data.fields.Method}&${__url_time_range}" } ] } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 9 }, "hideTimeOverride": false, "id": 44, "options": { "footer": { "countRows": false, "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true, "sortBy": [ { "desc": true, "displayName": "Value" } ] }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "exemplar": false, "expr": "sum by (exception_name) (\r\n max_over_time(http_server_request_duration_s_count{route=\"$route\", method=\"$method\", exception_name!=\"\"}[$__rate_interval])\r\n)", "format": "table", "instant": true, "interval": "", "legendFormat": "{{route}}", "range": false, "refId": "A" } ], "title": "Unhandled Exceptions", "transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "method": false }, "indexByName": { "Time": 0, "Value": 2, "exception_name": 1 }, "renameByName": { "Value": "Requests", "exception_name": "Exception", "method": "Method", "route": "Route" } } } ], "type": "table" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "fieldConfig": { "defaults": { "color": { "fixedColor": "blue", "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 12, "x": 12, "y": 9 }, "id": 42, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "max" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum by (status_code) (\r\n max_over_time(http_server_request_duration_s_count{route=\"$route\", method=\"$method\"}[$__rate_interval])\r\n )", "legendFormat": "Status {{status_code}}", "range": true, "refId": "A" } ], "title": "Requests HTTP Status Code", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "fixedColor": "green", "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 13 }, "id": 48, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "max" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum by (scheme) (\r\n max_over_time(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[$__rate_interval])\r\n )", "legendFormat": "{{scheme}}", "range": true, "refId": "A" } ], "title": "Requests Secured", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "fixedColor": "purple", "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 13 }, "id": 50, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "max" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum by (protocol) (\r\n max_over_time(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route=\"$route\", method=\"$method\"}[$__rate_interval])\r\n )", "legendFormat": "{{protocol}}", "range": true, "refId": "A" } ], "title": "Requests HTTP Protocol", "type": "stat" } ], "refresh": "", "revision": 1, "schemaVersion": 38, "style": "dark", "tags": [ "dotnet", "prometheus", "aspnetcore" ], "templating": { "list": [ { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "definition": "label_values(http_server_current_requests, job)", "hide": 0, "includeAll": false, "label": "Job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(http_server_current_requests, job)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "type": "query" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "definition": "label_values(http_server_current_requests{job=~\"$job\"}, instance)", "hide": 0, "includeAll": false, "label": "Instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(http_server_current_requests{job=~\"$job\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "type": "query" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "definition": "label_values(http_server_request_duration_s_count, route)", "description": "Route", "hide": 0, "includeAll": false, "label": "Route", "multi": false, "name": "route", "options": [], "query": { "query": "label_values(http_server_request_duration_s_count, route)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "type": "query" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "definition": "label_values(http_server_request_duration_s_count{route=~\"$route\"}, method)", "hide": 0, "includeAll": false, "label": "Method", "multi": false, "name": "method", "options": [], "query": { "query": "label_values(http_server_request_duration_s_count{route=~\"$route\"}, method)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "type": "query" } ] }, "time": { "from": "now-30m", "to": "now" }, "timepicker": {}, "timezone": "", "title": "ASP.NET Core Endpoint", "uid": "NagEsjE4z", "version": 10, "weekStart": "" } ================================================ FILE: deployments/configs/grafana/dashboards/dotnet-core.json ================================================ { "__inputs": [ { "name": "DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY", "label": "Managed_Prometheus_jamesnk-telemetry", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": {}, "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "9.4.8" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "stat", "name": "Stat", "version": "" }, { "type": "panel", "id": "table", "name": "Table", "version": "" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "description": "ASP.NET Core metrics from OpenTelemetry NuGet", "editable": true, "fiscalYearStartMonth": 0, "gnetId": 17706, "graphTooltip": 0, "id": null, "links": [], "liveNow": false, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "fieldConfig": { "defaults": { "color": { "fixedColor": "dark-green", "mode": "continuous-GrYlRd", "seriesBy": "max" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "axisSoftMin": 0, "barAlignment": 0, "drawStyle": "line", "fillOpacity": 50, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "smooth", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [ { "__systemRef": "hideSeriesFrom", "matcher": { "id": "byNames", "options": { "mode": "exclude", "names": [ "p50" ], "prefix": "All except:", "readOnly": true } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": false, "tooltip": false, "viz": false } } ] } ] }, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 }, "id": 40, "options": { "legend": { "calcs": [ "lastNotNull", "min", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.50, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", "legendFormat": "p50", "range": true, "refId": "p50" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.75, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", "hide": false, "legendFormat": "p75", "range": true, "refId": "p75" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.90, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", "hide": false, "legendFormat": "p90", "range": true, "refId": "p90" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.95, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", "hide": false, "legendFormat": "p95", "range": true, "refId": "p95" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.98, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", "hide": false, "legendFormat": "p98", "range": true, "refId": "p98" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.99, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", "hide": false, "legendFormat": "p99", "range": true, "refId": "p99" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "histogram_quantile(0.999, sum(rate(http_server_request_duration_s_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", "hide": false, "legendFormat": "p99.9", "range": true, "refId": "p99.9" } ], "title": "Requests Duration", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic", "seriesBy": "max" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 50, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "smooth", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byName", "options": "All" }, "properties": [ { "id": "color", "value": { "fixedColor": "dark-orange", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "4XX" }, "properties": [ { "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "5XX" }, "properties": [ { "id": "color", "value": { "fixedColor": "dark-red", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 0 }, "id": 47, "options": { "legend": { "calcs": [ "lastNotNull", "min", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", status_code=~\"4..|5..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval]))", "legendFormat": "All", "range": true, "refId": "All" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", status_code=~\"4..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval]))", "hide": false, "legendFormat": "4XX", "range": true, "refId": "4XX" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", status_code=~\"5..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval]))", "hide": false, "legendFormat": "5XX", "range": true, "refId": "5XX" } ], "title": "Errors Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 0, "y": 9 }, "id": 49, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": false }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(kestrel_current_connections{job=\"$job\", instance=\"$instance\"})", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Current Connections", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 6, "y": 9 }, "id": 55, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": false }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum(http_server_current_requests{job=\"$job\", instance=\"$instance\"})", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Current Requests", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "fieldConfig": { "defaults": { "color": { "fixedColor": "blue", "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 9 }, "id": 58, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "value" }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "exemplar": false, "expr": "sum(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\"})", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Total Requests", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "fieldConfig": { "defaults": { "color": { "fixedColor": "dark-red", "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 9 }, "id": 59, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "value" }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "exemplar": false, "expr": "sum(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", exception_name!=\"\"})", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Total Unhandled Exceptions", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "fixedColor": "green", "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 13 }, "id": 60, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "max" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum by (scheme) (\r\n max_over_time(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval])\r\n )", "legendFormat": "{{scheme}}", "range": true, "refId": "A" } ], "title": "Requests Secured", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "fixedColor": "purple", "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 13 }, "id": 42, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "max" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "expr": "sum by (protocol) (\r\n max_over_time(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval])\r\n )", "legendFormat": "{{protocol}}", "range": true, "refId": "A" } ], "title": "Requests HTTP Protocol", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "inspect": false }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [ { "matcher": { "id": "byName", "options": "Requests" }, "properties": [ { "id": "custom.width", "value": 300 }, { "id": "custom.cellOptions", "value": { "mode": "gradient", "type": "gauge" } }, { "id": "color", "value": { "mode": "continuous-BlPu" } } ] }, { "matcher": { "id": "byName", "options": "Endpoint" }, "properties": [ { "id": "links", "value": [ { "targetBlank": false, "title": "Test", "url": "/d/NagEsjE4z/asp-net-core-endpoint-details?var-route=${__data.fields.route}&var-method=${__data.fields.method}&${__url_time_range}" } ] } ] }, { "matcher": { "id": "byName", "options": "route" }, "properties": [ { "id": "custom.hidden", "value": true } ] }, { "matcher": { "id": "byName", "options": "method" }, "properties": [ { "id": "custom.hidden", "value": true } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 17 }, "hideTimeOverride": false, "id": 51, "options": { "footer": { "countRows": false, "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true, "sortBy": [ { "desc": true, "displayName": "Value" } ] }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "exemplar": false, "expr": " topk(10,\r\n sum by (route, method, method_route) (\r\n label_join(max_over_time(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route!=\"\"}[$__rate_interval]), \"method_route\", \" \", \"method\", \"route\")\r\n ))", "format": "table", "instant": true, "interval": "", "legendFormat": "{{route}}", "range": false, "refId": "A" } ], "title": "Top 10 Requested Endpoints", "transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "method": false, "route": false }, "indexByName": { "Time": 0, "Value": 4, "method": 2, "method_route": 3, "route": 1 }, "renameByName": { "Value": "Requests", "method": "", "method_route": "Endpoint", "route": "" } } } ], "type": "table" }, { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "inspect": false }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [ { "matcher": { "id": "byName", "options": "Requests" }, "properties": [ { "id": "custom.width", "value": 300 }, { "id": "custom.cellOptions", "value": { "mode": "gradient", "type": "gauge" } }, { "id": "color", "value": { "mode": "continuous-YlRd" } } ] }, { "matcher": { "id": "byName", "options": "Endpoint" }, "properties": [ { "id": "links", "value": [ { "title": "", "url": "/d/NagEsjE4z/asp-net-core-endpoint-details?var-route=${__data.fields.route}&var-method=${__data.fields.method}&${__url_time_range}" } ] } ] }, { "matcher": { "id": "byName", "options": "route" }, "properties": [ { "id": "custom.hidden", "value": true } ] }, { "matcher": { "id": "byName", "options": "method" }, "properties": [ { "id": "custom.hidden", "value": true } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 17 }, "hideTimeOverride": false, "id": 54, "options": { "footer": { "countRows": false, "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true, "sortBy": [ { "desc": true, "displayName": "Value" } ] }, "pluginVersion": "9.4.8", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "editorMode": "code", "exemplar": false, "expr": " topk(10,\r\n sum by (route, method, method_route) (\r\n label_join(max_over_time(http_server_request_duration_s_count{job=\"$job\", instance=\"$instance\", route!=\"\", exception_name!=\"\"}[$__rate_interval]), \"method_route\", \" \", \"method\", \"route\")\r\n ))", "format": "table", "instant": true, "interval": "", "legendFormat": "{{route}}", "range": false, "refId": "A" } ], "title": "Top 10 Unhandled Exception Endpoints", "transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "method": false }, "indexByName": { "Time": 0, "Value": 4, "method": 2, "method_route": 3, "route": 1 }, "renameByName": { "Value": "Requests", "method": "", "method_route": "Endpoint", "route": "" } } } ], "type": "table" } ], "refresh": "", "revision": 1, "schemaVersion": 38, "style": "dark", "tags": [ "dotnet", "prometheus", "aspnetcore" ], "templating": { "list": [ { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "definition": "label_values(http_server_current_requests, job)", "hide": 0, "includeAll": false, "label": "Job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(http_server_current_requests, job)", "refId": "StandardVariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "type": "query" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_MANAGED_PROMETHEUS_JAMESNK-TELEMETRY}" }, "definition": "label_values(http_server_current_requests{job=~\"$job\"}, instance)", "hide": 0, "includeAll": false, "label": "Instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(http_server_current_requests{job=~\"$job\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "type": "query" } ] }, "time": { "from": "now-30m", "to": "now" }, "timepicker": {}, "timezone": "", "title": "ASP.NET Core", "uid": "KdDACDp4z", "version": 5, "weekStart": "" } ================================================ FILE: deployments/configs/grafana/dashboards/node-exporter.json ================================================ { "__inputs": [], "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "7.4.3" }, { "type": "panel", "id": "graph", "name": "Graph", "version": "" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" } ], "annotations": { "list": [] }, "editable": false, "gnetId": 13978, "graphTooltip": 0, "hideControls": false, "id": null, "links": [], "refresh": "", "rows": [ { "collapse": false, "collapsed": false, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 1, "fillGradient": 0, "gridPos": {}, "id": 2, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "sideWidth": null, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "(\n (1 - rate(node_cpu_seconds_total{job=\"node\", mode=\"idle\", instance=\"$instance\"}[$__interval]))\n/ ignoring(cpu) group_left\n count without (cpu)( node_cpu_seconds_total{job=\"node\", mode=\"idle\", instance=\"$instance\"})\n)\n", "format": "time_series", "interval": "1m", "intervalFactor": 5, "legendFormat": "{{cpu}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "CPU Usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "percentunit", "label": null, "logBase": 1, "max": 1, "min": 0, "show": true }, { "format": "percentunit", "label": null, "logBase": 1, "max": 1, "min": 0, "show": true } ] }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 0, "fillGradient": 0, "gridPos": {}, "id": 3, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "sideWidth": null, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_load1{job=\"node\", instance=\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "1m load average", "refId": "A" }, { "expr": "node_load5{job=\"node\", instance=\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "5m load average", "refId": "B" }, { "expr": "node_load15{job=\"node\", instance=\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "15m load average", "refId": "C" }, { "expr": "count(node_cpu_seconds_total{job=\"node\", instance=\"$instance\", mode=\"idle\"})", "format": "time_series", "intervalFactor": 2, "legendFormat": "logical cores", "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Load Average", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6", "type": "row" }, { "collapse": false, "collapsed": false, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 1, "fillGradient": 0, "gridPos": {}, "id": 4, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "sideWidth": null, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [], "spaceLength": 10, "span": 9, "stack": true, "steppedLine": false, "targets": [ { "expr": "(\n node_memory_MemTotal_bytes{job=\"node\", instance=\"$instance\"}\n-\n node_memory_MemFree_bytes{job=\"node\", instance=\"$instance\"}\n-\n node_memory_Buffers_bytes{job=\"node\", instance=\"$instance\"}\n-\n node_memory_Cached_bytes{job=\"node\", instance=\"$instance\"}\n)\n", "format": "time_series", "intervalFactor": 2, "legendFormat": "memory used", "refId": "A" }, { "expr": "node_memory_Buffers_bytes{job=\"node\", instance=\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "memory buffers", "refId": "B" }, { "expr": "node_memory_Cached_bytes{job=\"node\", instance=\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "memory cached", "refId": "C" }, { "expr": "node_memory_MemFree_bytes{job=\"node\", instance=\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "memory free", "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Memory Usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true } ] }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "datasource": "$datasource", "format": "percent", "gauge": { "maxValue": 100, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": {}, "id": 5, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "span": 3, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "100 -\n(\n avg(node_memory_MemAvailable_bytes{job=\"node\", instance=\"$instance\"})\n/\n avg(node_memory_MemTotal_bytes{job=\"node\", instance=\"$instance\"})\n* 100\n)\n", "format": "time_series", "intervalFactor": 2, "legendFormat": "", "refId": "A" } ], "thresholds": "80, 90", "title": "Memory Usage", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6", "type": "row" }, { "collapse": false, "collapsed": false, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 0, "fillGradient": 0, "gridPos": {}, "id": 6, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "sideWidth": null, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [ { "alias": "/ read| written/", "yaxis": 1 }, { "alias": "/ io time/", "yaxis": 2 } ], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_disk_read_bytes_total{job=\"node\", instance=\"$instance\", device!=\"\"}[$__interval])", "format": "time_series", "interval": "1m", "intervalFactor": 2, "legendFormat": "{{device}} read", "refId": "A" }, { "expr": "rate(node_disk_written_bytes_total{job=\"node\", instance=\"$instance\", device!=\"\"}[$__interval])", "format": "time_series", "interval": "1m", "intervalFactor": 2, "legendFormat": "{{device}} written", "refId": "B" }, { "expr": "rate(node_disk_io_time_seconds_total{job=\"node\", instance=\"$instance\", device!=\"\"}[$__interval])", "format": "time_series", "interval": "1m", "intervalFactor": 2, "legendFormat": "{{device}} io time", "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Disk I/O", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "s", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ] }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 1, "fillGradient": 0, "gridPos": {}, "id": 7, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "sideWidth": null, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [ { "alias": "used", "color": "#E0B400" }, { "alias": "available", "color": "#73BF69" } ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum(\n max by (device) (\n node_filesystem_size_bytes{job=\"node\", instance=\"$instance\", fstype!=\"\"}\n -\n node_filesystem_avail_bytes{job=\"node\", instance=\"$instance\", fstype!=\"\"}\n )\n)\n", "format": "time_series", "intervalFactor": 2, "legendFormat": "used", "refId": "A" }, { "expr": "sum(\n max by (device) (\n node_filesystem_avail_bytes{job=\"node\", instance=\"$instance\", fstype!=\"\"}\n )\n)\n", "format": "time_series", "intervalFactor": 2, "legendFormat": "available", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Disk Space Usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6", "type": "row" }, { "collapse": false, "collapsed": false, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 0, "fillGradient": 0, "gridPos": {}, "id": 8, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "sideWidth": null, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_network_receive_bytes_total{job=\"node\", instance=\"$instance\", device!=\"lo\"}[$__interval])", "format": "time_series", "interval": "1m", "intervalFactor": 2, "legendFormat": "{{device}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Network Received", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true } ] }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 0, "fillGradient": 0, "gridPos": {}, "id": 9, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "sideWidth": null, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_network_transmit_bytes_total{job=\"node\", instance=\"$instance\", device!=\"lo\"}[$__interval])", "format": "time_series", "interval": "1m", "intervalFactor": 2, "legendFormat": "{{device}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Network Transmitted", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6", "type": "row" } ], "schemaVersion": 14, "style": "dark", "tags": [], "templating": { "list": [ { "current": { "text": "Prometheus", "value": "Prometheus" }, "hide": 0, "label": null, "name": "datasource", "options": [], "query": "prometheus", "refresh": 1, "regex": "", "type": "datasource" }, { "allValue": null, "current": {}, "datasource": "$datasource", "hide": 0, "includeAll": false, "label": null, "multi": false, "name": "instance", "options": [], "query": "label_values(node_exporter_build_info{job=\"node\"}, instance)", "refresh": 2, "regex": "", "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "browser", "title": "Node Exporter Quickstart and Dashboard", "version": 0, "description": "A quickstart to setup Prometheus Node Exporter with preconfigured dashboards, alerting rules, and recording rules." } ================================================ FILE: deployments/configs/grafana/dashboards/postgresql.json ================================================ { "__inputs": [], "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "7.2.0" }, { "type": "panel", "id": "graph", "name": "Graph", "version": "" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "A quickstart to setup the Prometheus PostgreSQL Exporter with preconfigured dashboards, alerting rules, and recording rules.", "editable": true, "gnetId": 14114, "graphTooltip": 0, "id": 1, "iteration": 1603191461722, "links": [], "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 20, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 1, "isNew": true, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.2.1", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "alias": "fetched", "dsType": "prometheus", "expr": "sum(irate(pg_stat_database_tup_fetched{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "fetched", "measurement": "postgresql", "policy": "default", "refId": "A", "resultFormat": "time_series", "select": [ [ { "params": [ "tup_fetched" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [ "10s" ], "type": "non_negative_derivative" } ] ], "step": 120, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "fetched", "dsType": "prometheus", "expr": "sum(irate(pg_stat_database_tup_returned{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "returned", "measurement": "postgresql", "policy": "default", "refId": "B", "resultFormat": "time_series", "select": [ [ { "params": [ "tup_fetched" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [ "10s" ], "type": "non_negative_derivative" } ] ], "step": 120, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "fetched", "dsType": "prometheus", "expr": "sum(irate(pg_stat_database_tup_inserted{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "inserted", "measurement": "postgresql", "policy": "default", "refId": "C", "resultFormat": "time_series", "select": [ [ { "params": [ "tup_fetched" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [ "10s" ], "type": "non_negative_derivative" } ] ], "step": 120, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "fetched", "dsType": "prometheus", "expr": "sum(irate(pg_stat_database_tup_updated{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "updated", "measurement": "postgresql", "policy": "default", "refId": "D", "resultFormat": "time_series", "select": [ [ { "params": [ "tup_fetched" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [ "10s" ], "type": "non_negative_derivative" } ] ], "step": 120, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "fetched", "dsType": "prometheus", "expr": "sum(irate(pg_stat_database_tup_deleted{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "deleted", "measurement": "postgresql", "policy": "default", "refId": "E", "resultFormat": "time_series", "select": [ [ { "params": [ "tup_fetched" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [ "10s" ], "type": "non_negative_derivative" } ] ], "step": 120, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Rows", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "$datasource", "decimals": 0, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 4, "x": 20, "y": 0 }, "height": "55px", "id": 11, "interval": null, "isNew": true, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": true, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "dsType": "prometheus", "expr": "sum(irate(pg_stat_database_xact_commit{datname=~\"$db\",instance=~\"$instance\"}[5m])) + sum(irate(pg_stat_database_xact_rollback{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "measurement": "postgresql", "policy": "default", "refId": "A", "resultFormat": "time_series", "select": [ [ { "params": [ "xact_commit" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [ "10s" ], "type": "non_negative_derivative" } ] ], "step": 1800, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] } ], "thresholds": "", "title": "QPS", "transparent": true, "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "avg" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "decimals": 1, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 7 }, "hiddenSeries": false, "id": 2, "isNew": true, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.2.1", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "alias": "Buffers Allocated", "dsType": "prometheus", "expr": "irate(pg_stat_bgwriter_buffers_alloc{instance=~'$instance'}[5m])", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "buffers_alloc", "measurement": "postgresql", "policy": "default", "refId": "A", "resultFormat": "time_series", "select": [ [ { "params": [ "buffers_alloc" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [], "type": "difference" } ] ], "step": 240, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "Buffers Allocated", "dsType": "prometheus", "expr": "irate(pg_stat_bgwriter_buffers_backend_fsync{instance=~'$instance'}[5m])", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "buffers_backend_fsync", "measurement": "postgresql", "policy": "default", "refId": "B", "resultFormat": "time_series", "select": [ [ { "params": [ "buffers_alloc" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [], "type": "difference" } ] ], "step": 240, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "Buffers Allocated", "dsType": "prometheus", "expr": "irate(pg_stat_bgwriter_buffers_backend{instance=~'$instance'}[5m])", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "buffers_backend", "measurement": "postgresql", "policy": "default", "refId": "C", "resultFormat": "time_series", "select": [ [ { "params": [ "buffers_alloc" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [], "type": "difference" } ] ], "step": 240, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "Buffers Allocated", "dsType": "prometheus", "expr": "irate(pg_stat_bgwriter_buffers_clean{instance=~'$instance'}[5m])", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "buffers_clean", "measurement": "postgresql", "policy": "default", "refId": "D", "resultFormat": "time_series", "select": [ [ { "params": [ "buffers_alloc" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [], "type": "difference" } ] ], "step": 240, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "Buffers Allocated", "dsType": "prometheus", "expr": "irate(pg_stat_bgwriter_buffers_checkpoint{instance=~'$instance'}[5m])", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "buffers_checkpoint", "measurement": "postgresql", "policy": "default", "refId": "E", "resultFormat": "time_series", "select": [ [ { "params": [ "buffers_alloc" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [], "type": "difference" } ] ], "step": 240, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Buffers", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 7 }, "hiddenSeries": false, "id": 3, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.2.1", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "alias": "conflicts", "dsType": "prometheus", "expr": "sum(rate(pg_stat_database_deadlocks{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "deadlocks", "measurement": "postgresql", "policy": "default", "refId": "A", "resultFormat": "time_series", "select": [ [ { "params": [ "conflicts" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [], "type": "difference" } ] ], "step": 240, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] }, { "alias": "deadlocks", "dsType": "prometheus", "expr": "sum(rate(pg_stat_database_conflicts{datname=~\"$db\",instance=~\"$instance\"}[5m]))", "format": "time_series", "groupBy": [ { "params": [ "$interval" ], "type": "time" }, { "params": [ "null" ], "type": "fill" } ], "intervalFactor": 2, "legendFormat": "conflicts", "measurement": "postgresql", "policy": "default", "refId": "B", "resultFormat": "time_series", "select": [ [ { "params": [ "deadlocks" ], "type": "field" }, { "params": [], "type": "mean" }, { "params": [], "type": "difference" } ] ], "step": 240, "tags": [ { "key": "instance", "operator": "=~", "value": "/^$instance$/" } ] } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Conflicts/Deadlocks", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 12, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": true, "pluginVersion": "7.2.1", "pointradius": 1, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(pg_stat_database_blks_hit{datname=~\"$db\",instance=~\"$instance\"}) / (sum(pg_stat_database_blks_hit{datname=~\"$db\",instance=~\"$instance\"}) + sum(pg_stat_database_blks_read{datname=~\"$db\",instance=~\"$instance\"}))", "format": "time_series", "intervalFactor": 2, "legendFormat": "cache hit rate", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Cache hit ratio", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "percentunit", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 13, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.2.1", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_database_numbackends{datname=~\"$db\",instance=~\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{__name__}}", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Number of active connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "refresh": false, "schemaVersion": 26, "style": "dark", "tags": [ "postgres" ], "templating": { "list": [ { "allValue": ".*", "current": { "selected": false, "text": "All", "value": "$__all" }, "datasource": "$datasource", "definition": "", "hide": 0, "includeAll": true, "label": null, "multi": false, "name": "instance", "options": [], "query": "label_values(up{job=~\"postgres.*\"},instance)", "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": ".*", "current": { "selected": false, "text": "All", "value": "$__all" }, "datasource": "$datasource", "definition": "label_values(pg_stat_database_tup_fetched{instance=~\"$instance\",datname!~\"template.*|postgres\"},datname)", "hide": 0, "includeAll": true, "label": "db", "multi": false, "name": "db", "options": [], "query": "label_values(pg_stat_database_tup_fetched{instance=~\"$instance\",datname!~\"template.*|postgres\"},datname)", "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": false, "text": "Postgres Overview", "value": "Postgres Overview" }, "hide": 0, "includeAll": false, "label": "datasource", "multi": false, "name": "datasource", "options": [], "query": "prometheus", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "allValue": null, "current": { "selected": true, "text": "postgres", "value": "postgres" }, "datasource": "$datasource", "definition": "label_values(pg_up, job)", "hide": 0, "includeAll": false, "label": "job", "multi": false, "name": "job", "options": [ { "selected": true, "text": "postgres", "value": "postgres" } ], "query": "label_values(pg_up, job)", "refresh": 0, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "browser", "title": "PostgreSQL Exporter Quickstart and Dashboard", "uid": "wGgaPlciz", "version": 5 } ================================================ FILE: deployments/configs/grafana/dashboards/rabbitmq.json ================================================ { "__inputs": [ { "name": "DS_PROMETHEUS", "label": "prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": [], "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "8.3.4" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "stat", "name": "Stat", "version": "" }, { "type": "panel", "id": "table", "name": "Table", "version": "" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "description": "A new RabbitMQ Management Overview", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, "id": null, "iteration": 1659711638455, "links": [ { "icon": "doc", "tags": [], "targetBlank": true, "title": "Monitoring with Prometheus & Grafana", "tooltip": "", "type": "link", "url": "https://www.rabbitmq.com/prometheus.html" } ], "liveNow": false, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#37872D", "value": null }, { "color": "#1F60C4", "value": 10000 }, { "color": "#C4162A", "value": 100000 } ] }, "unit": "short" }, "overrides": [] }, "id": 64, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_queue_messages_ready * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Ready messages", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#C4162A", "value": null }, { "color": "#1F60C4", "value": -1 }, { "color": "#37872D", "value": 50 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 6, "y": 0 }, "id": 62, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_received_total[60s]) * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Incoming messages / s", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#C4162A", "value": null }, { "color": "#1F60C4", "value": 0 }, { "color": "#37872D", "value": 10 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 12, "y": 0 }, "id": 66, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_global_publishers * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Publishers", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#C4162A", "value": null }, { "color": "#1F60C4", "value": 0 }, { "color": "#37872D", "value": 10 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 16, "y": 0 }, "id": 37, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_connections * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Connections", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#C4162A", "value": null }, { "color": "#1F60C4", "value": 0 }, { "color": "#37872D", "value": 10 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 20, "y": 0 }, "id": 40, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_queues * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Queues", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#37872D", "value": null }, { "color": "#1F60C4", "value": 100 }, { "color": "#C4162A", "value": 500 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 0, "y": 3 }, "id": 65, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_queue_messages_unacked * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "hide": false, "instant": false, "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Unacknowledged messages", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#C4162A", "value": null }, { "color": "#1F60C4", "value": -1 }, { "color": "#37872D", "value": 50 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 6, "y": 3 }, "id": 63, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_redelivered_total[60s]) * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) +\nsum(rate(rabbitmq_global_messages_delivered_consume_auto_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) +\nsum(rate(rabbitmq_global_messages_delivered_consume_manual_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) +\nsum(rate(rabbitmq_global_messages_delivered_get_auto_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) +\nsum(rate(rabbitmq_global_messages_delivered_get_manual_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "hide": false, "instant": false, "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Outgoing messages / s", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#C4162A", "value": null }, { "color": "#1F60C4", "value": 0 }, { "color": "#37872D", "value": 10 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 12, "y": 3 }, "id": 41, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_consumers * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Consumers", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#C4162A", "value": null }, { "color": "#1F60C4", "value": 0 }, { "color": "#37872D", "value": 10 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 16, "y": 3 }, "id": 38, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_channels * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Channels", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#1F60C4", "value": null }, { "color": "#37872D", "value": 3 }, { "color": "#C4162A", "value": 8 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 20, "y": 3 }, "id": 67, "links": [], "maxDataPoints": 100, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_build_info * on(instance, job) group_left(rabbitmq_cluster) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Nodes", "type": "stat" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 }, "id": 4, "panels": [], "title": "NODES", "type": "row" }, { "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "auto", "displayMode": "auto" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [ { "matcher": { "id": "byName", "options": "erlang_version" }, "properties": [ { "id": "displayName", "value": "Erlang/OTP" }, { "id": "unit", "value": "none" }, { "id": "custom.align" }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)" } ] } } ] }, { "matcher": { "id": "byName", "options": "rabbitmq_version" }, "properties": [ { "id": "displayName", "value": "RabbitMQ" }, { "id": "unit", "value": "none" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [ { "color": "rgba(245, 54, 54, 0.9)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)" } ] } } ] }, { "matcher": { "id": "byName", "options": "instance" }, "properties": [ { "id": "displayName", "value": "Host" }, { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" } ] }, { "matcher": { "id": "byName", "options": "rabbitmq_node" }, "properties": [ { "id": "displayName", "value": "Node name" }, { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [ { "color": "rgba(245, 54, 54, 0.9)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)" } ] } } ] }, { "matcher": { "id": "byName", "options": "Time" }, "properties": [ { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" } ] }, { "matcher": { "id": "byName", "options": "Value" }, "properties": [ { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" } ] }, { "matcher": { "id": "byName", "options": "job" }, "properties": [ { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" } ] }, { "matcher": { "id": "byName", "options": "rabbitmq_cluster" }, "properties": [ { "id": "displayName", "value": "Cluster" }, { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" } ] }, { "matcher": { "id": "byName", "options": "prometheus_client_version" }, "properties": [ { "id": "displayName", "value": "prometheus.erl" }, { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" } ] }, { "matcher": { "id": "byName", "options": "prometheus_plugin_version" }, "properties": [ { "id": "displayName", "value": "rabbitmq_prometheus" }, { "id": "unit", "value": "short" }, { "id": "decimals", "value": 2 }, { "id": "custom.align" } ] } ] }, "gridPos": { "h": 4, "w": 24, "x": 0, "y": 7 }, "id": 69, "links": [], "options": { "footer": { "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true }, "pluginVersion": "8.3.4", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": false, "expr": "rabbitmq_build_info * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}", "format": "table", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "transformations": [ { "id": "merge", "options": { "reducers": [] } } ], "type": "table" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "If the value is zero or less, the memory alarm will be triggered and all publishing connections across all cluster nodes will be blocked.\n\nThis value can temporarily go negative because the memory alarm is triggered with a slight delay.\n\nThe kernel's view of the amount of memory used by the node can differ from what the node itself can observe. This means that this value can be negative for a sustained period of time.\n\nBy default nodes use resident set size (RSS) to compute how much memory they use. This strategy can be changed (see the guides below).\n\n* [Alarms](https://www.rabbitmq.com/alarms.html)\n* [Memory Alarms](https://www.rabbitmq.com/memory.html)\n* [Reasoning About Memory Use](https://www.rabbitmq.com/memory-use.html)\n* [Blocked Connection Notifications](https://www.rabbitmq.com/connection-blocked.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "line+area" } }, "decimals": 1, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "orange", "value": 0 }, { "color": "transparent", "value": 536870912 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 11 }, "id": 7, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "(rabbitmq_resident_memory_limit_bytes * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) -\n(rabbitmq_process_resident_memory_bytes * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Memory available before publishers blocked", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "This metric is reported for the partition where the RabbitMQ data directory is stored.\n\nIf the value is zero or less, the disk alarm will be triggered and all publishing connections across all cluster nodes will be blocked.\n\nThis value can temporarily go negative because the free disk space alarm is triggered with a slight delay.\n\n* [Alarms](https://www.rabbitmq.com/alarms.html)\n* [Disk Space Alarms](https://www.rabbitmq.com/disk-alarms.html)\n* [Disk Space](https://www.rabbitmq.com/production-checklist.html#resource-limits-disk-space)\n* [Persistence Configuration](https://www.rabbitmq.com/persistence-conf.html)\n* [Blocked Connection Notifications](https://www.rabbitmq.com/connection-blocked.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "line+area" } }, "decimals": 1, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "orange", "value": 1073741824 }, { "color": "transparent", "value": 5368709120 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 8, "w": 8, "x": 12, "y": 11 }, "id": 8, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "rabbitmq_disk_space_available_bytes * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Disk space available before publishers blocked", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "When this value reaches zero, new connections will not be accepted and disk write operations may fail.\n\nClient libraries, peer nodes and CLI tools will not be able to connect when the node runs out of available file descriptors.\n\n* [Open File Handles Limit](https://www.rabbitmq.com/production-checklist.html#resource-limits-file-handle-limit)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "orange", "value": 500 }, { "color": "transparent", "value": 1000 } ] }, "unit": "none" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 11 }, "id": 2, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "(rabbitmq_process_max_fds * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) -\n(rabbitmq_process_open_fds * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "File descriptors available", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "When this value reaches zero, new connections will not be accepted.\n\nClient libraries, peer nodes and CLI tools will not be able to connect when the node runs out of available file descriptors.\n\n* [Networking and RabbitMQ](https://www.rabbitmq.com/networking.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "orange", "value": 500 }, { "color": "transparent", "value": 1000 } ] }, "unit": "none" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 15 }, "id": 5, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "(rabbitmq_process_max_tcp_sockets * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) -\n(rabbitmq_process_open_tcp_sockets * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "TCP sockets available", "type": "timeseries" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 19 }, "id": 27, "panels": [], "title": "QUEUED MESSAGES", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "Total number of ready messages ready to be delivered to consumers.\n\nAim to keep this value as low as possible. RabbitMQ behaves best when messages are flowing through it. It's OK for publishers to occasionally outpace consumers, but the expectation is that consumers will eventually process all ready messages.\n\nIf this metric keeps increasing, your system will eventually run out of memory and/or disk space. Consider using TTL or Queue Length Limit to prevent unbounded message growth.\n\n* [Queues](https://www.rabbitmq.com/queues.html)\n* [Consumers](https://www.rabbitmq.com/consumers.html)\n* [Queue Length Limit](https://www.rabbitmq.com/maxlength.html)\n* [Time-To-Live and Expiration](https://www.rabbitmq.com/ttl.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 20 }, "id": 9, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_queue_messages_ready * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages ready to be delivered to consumers", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The total number of messages that are either in-flight to consumers, currently being processed by consumers or simply waiting for the consumer acknowledgements to be processed by the queue. Until the queue processes the message acknowledgement, the message will remain unacknowledged.\n\n* [Queues](https://www.rabbitmq.com/queues.html)\n* [Confirms and Acknowledgements](https://www.rabbitmq.com/confirms.html)\n* [Consumer Prefetch](https://www.rabbitmq.com/consumer-prefetch.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 20 }, "id": 19, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rabbitmq_queue_messages_unacked * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages pending consumer acknowledgement", "type": "timeseries" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 25 }, "id": 11, "panels": [], "title": "INCOMING MESSAGES", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The incoming message rate before any routing rules are applied.\n\nIf this value is lower than the number of messages published to queues, it may indicate that some messages are delivered to more than one queue.\n\nIf this value is higher than the number of messages published to queues, messages cannot be routed and will either be dropped or returned to publishers.\n\n* [Publishers](https://www.rabbitmq.com/publishers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 26 }, "id": 13, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_received_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages published / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages confirmed by the broker to publishers. Publishers must opt-in to receive message confirmations.\n\nIf this metric is consistently at zero it may suggest that publisher confirms are not used by clients. The safety of published messages is likely to be at risk.\n\n* [Publisher Confirms](https://www.rabbitmq.com/confirms.html#publisher-confirms)\n* [Publisher Confirms and Data Safety](https://www.rabbitmq.com/publishers.html#data-safety)\n* [When Will Published Messages Be Confirmed by the Broker?](https://www.rabbitmq.com/confirms.html#when-publishes-are-confirmed)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 26 }, "id": 18, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_confirmed_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages confirmed to publishers / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages received from publishers and successfully routed to the master queue replicas.\n\n* [Queues](https://www.rabbitmq.com/queues.html)\n* [Publishers](https://www.rabbitmq.com/publishers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 31 }, "id": 61, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_routed_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages routed to queues / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages received from publishers that have publisher confirms enabled and the broker has not confirmed yet.\n\n* [Publishers](https://www.rabbitmq.com/publishers.html)\n* [Confirms and Acknowledgements](https://www.rabbitmq.com/confirms.html)\n* [When Will Published Messages Be Confirmed by the Broker?](https://www.rabbitmq.com/confirms.html#when-publishes-are-confirmed)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": true, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 31 }, "id": 12, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_received_confirm_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"} - \nrate(rabbitmq_global_messages_confirmed_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}\n) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages unconfirmed to publishers / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages that cannot be routed and are dropped. \n\nAny value above zero means message loss and likely suggests a routing problem on the publisher end.\n\n* [Unroutable Message Handling](https://www.rabbitmq.com/publishers.html#unroutable)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 0 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/rabbit/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C4162A", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 36 }, "id": 34, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_unroutable_dropped_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Unroutable messages dropped / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages that cannot be routed and are returned back to publishers.\n\nSustained values above zero may indicate a routing problem on the publisher end.\n\n* [Unroutable Message Handling](https://www.rabbitmq.com/publishers.html#unroutable)\n* [When Will Published Messages Be Confirmed by the Broker?](https://www.rabbitmq.com/confirms.html#when-publishes-are-confirmed)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 0 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/rabbit/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C4162A", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 36 }, "id": 16, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_unroutable_returned_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Unroutable messages returned to publishers / s", "type": "timeseries" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 41 }, "id": 29, "panels": [], "title": "OUTGOING MESSAGES", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages delivered to consumers. It includes messages that have been redelivered.\n\nThis metric does not include messages that have been fetched by consumers using `basic.get` (consumed by polling).\n\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 42 }, "id": 14, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(\n (rate(rabbitmq_global_messages_delivered_consume_auto_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) +\n (rate(rabbitmq_global_messages_delivered_consume_manual_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"})\n) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages delivered / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages that have been redelivered to consumers. It includes messages that have been requeued automatically and redelivered due to channel exceptions or connection closures.\n\nHaving some redeliveries is expected, but if this metric is consistently non-zero, it is worth investigating why.\n\n* [Negative Acknowledgement and Requeuing of Deliveries](https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue)\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 20 }, { "color": "red", "value": 100 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 42 }, "id": 15, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_redelivered_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages redelivered / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of message deliveries to consumers that use manual acknowledgement mode.\n\nWhen this mode is used, RabbitMQ waits for consumers to acknowledge messages before more messages can be delivered.\n\nThis is the safest way of consuming messages.\n\n* [Consumer Acknowledgements](https://www.rabbitmq.com/confirms.html)\n* [Consumer Prefetch](https://www.rabbitmq.com/consumer-prefetch.html)\n* [Consumer Acknowledgement Modes, Prefetch and Throughput](https://www.rabbitmq.com/confirms.html#channel-qos-prefetch-throughput)\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 47 }, "id": 20, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_delivered_consume_manual_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages delivered with manual ack / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of message deliveries to consumers that use automatic acknowledgement mode.\n\nWhen this mode is used, RabbitMQ does not wait for consumers to acknowledge message deliveries.\n\nThis mode is fire-and-forget and does not offer any delivery safety guarantees. It tends to provide higher throughput and it may lead to consumer overload and higher consumer memory usage.\n\n* [Consumer Acknowledgement Modes, Prefetch and Throughput](https://www.rabbitmq.com/confirms.html#channel-qos-prefetch-throughput)\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 47 }, "id": 21, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_delivered_consume_auto_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages delivered auto ack / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of message acknowledgements coming from consumers that use manual acknowledgement mode.\n\n* [Consumer Acknowledgements](https://www.rabbitmq.com/confirms.html)\n* [Consumer Prefetch](https://www.rabbitmq.com/consumer-prefetch.html)\n* [Consumer Acknowledgement Modes, Prefetch and Throughput](https://www.rabbitmq.com/confirms.html#channel-qos-prefetch-throughput)\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 52 }, "id": 22, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_acknowledged_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Messages acknowledged / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages delivered to polling consumers that use automatic acknowledgement mode.\n\nThe use of polling consumers is highly inefficient and therefore strongly discouraged.\n\n* [Fetching individual messages](https://www.rabbitmq.com/consumers.html#fetching)\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 0 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/rabbit/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C4162A", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 52 }, "id": 24, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_delivered_get_auto_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Polling operations with auto ack / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of polling consumer operations that yield no result.\n\nAny value above zero means that RabbitMQ resources are wasted by polling consumers.\n\nCompare this metric to the other polling consumer metrics to see the inefficiency rate.\n\nThe use of polling consumers is highly inefficient and therefore strongly discouraged.\n\n* [Fetching individual messages](https://www.rabbitmq.com/consumers.html#fetching)\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 0 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/rabbit/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C4162A", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 57 }, "id": 25, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_get_empty_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Polling operations that yield no result / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of messages delivered to polling consumers that use manual acknowledgement mode.\n\nThe use of polling consumers is highly inefficient and therefore strongly discouraged.\n\n* [Fetching individual messages](https://www.rabbitmq.com/consumers.html#fetching)\n* [Consumers](https://www.rabbitmq.com/consumers.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 0 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/rabbit/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C4162A", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 12, "y": 57 }, "id": 23, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_global_messages_delivered_get_manual_ack_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Polling operations with manual ack / s", "type": "timeseries" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 62 }, "id": 53, "panels": [], "title": "QUEUES", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "Total number of queue masters per node. \n\nThis metric makes it easy to see sub-optimal queue distribution in a cluster.\n\n* [Queue Masters, Data Locality](https://www.rabbitmq.com/ha.html#master-migration-data-locality)\n* [Queues](https://www.rabbitmq.com/queues.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 63 }, "id": 57, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "rabbitmq_queues * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Total queues", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of queue declarations performed by clients.\n\nLow sustained values above zero are to be expected. High rates may be indicative of queue churn or high rates of connection recovery. Confirm connection recovery rates by using the _Connections opened_ metric.\n\n* [Queues](https://www.rabbitmq.com/queues.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 2 }, { "color": "red", "value": 10 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 4, "x": 12, "y": 63 }, "id": 58, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_queues_declared_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Queues declared / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of new queues created (as opposed to redeclarations).\n\nLow sustained values above zero are to be expected. High rates may be indicative of queue churn or high rates of connection recovery. Confirm connection recovery rates by using the _Connections opened_ metric.\n\n* [Queues](https://www.rabbitmq.com/queues.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 2 }, { "color": "red", "value": 10 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 4, "x": 16, "y": 63 }, "id": 60, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_queues_created_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Queues created / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of queues deleted.\n\nLow sustained values above zero are to be expected. High rates may be indicative of queue churn or high rates of connection recovery. Confirm connection recovery rates by using the _Connections opened_ metric.\n\n* [Queues](https://www.rabbitmq.com/queues.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 2 }, { "color": "red", "value": 10 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 4, "x": 20, "y": 63 }, "id": 59, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_queues_deleted_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Queues deleted / s", "type": "timeseries" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 68 }, "id": 51, "panels": [], "title": "CHANNELS", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "Total number of channels on all currently opened connections.\n\nIf this metric grows monotonically it is highly likely a channel leak in one of the applications. Confirm channel leaks by using the _Channels opened_ and _Channels closed_ metrics.\n\n* [Channel Leak](https://www.rabbitmq.com/channels.html#channel-leaks)\n* [Channels](https://www.rabbitmq.com/channels.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 69 }, "id": 54, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "rabbitmq_channels * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Total channels", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of new channels opened by applications across all connections. Channels are expected to be long-lived.\n\nLow sustained values above zero are to be expected. High rates may be indicative of channel churn or mass connection recovery. Confirm connection recovery rates by using the _Connections opened_ metric.\n\n* [High Channel Churn](https://www.rabbitmq.com/channels.html#high-channel-churn)\n* [Channels](https://www.rabbitmq.com/channels.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 2 }, { "color": "red", "value": 10 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 6, "x": 12, "y": 69 }, "id": 55, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_channels_opened_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Channels opened / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of channels closed by applications across all connections. Channels are expected to be long-lived.\n\nLow sustained values above zero are to be expected. High rates may be indicative of channel churn or mass connection recovery. Confirm connection recovery rates by using the _Connections opened_ metric.\n\n* [High Channel Churn](https://www.rabbitmq.com/channels.html#high-channel-churn)\n* [Channels](https://www.rabbitmq.com/channels.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 2 }, { "color": "red", "value": 10 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 6, "x": 18, "y": 69 }, "id": 56, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_channels_closed_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Channels closed / s", "type": "timeseries" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 74 }, "id": 46, "panels": [], "title": "CONNECTIONS", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "Total number of client connections.\n\nIf this metric grows monotonically it is highly likely a connection leak in one of the applications. Confirm connection leaks by using the _Connections opened_ and _Connections closed_ metrics.\n\n* [Connection Leak](https://www.rabbitmq.com/connections.html#monitoring)\n* [Connections](https://www.rabbitmq.com/connections.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 12, "x": 0, "y": 75 }, "id": 47, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "rabbitmq_connections * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Total connections", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of new connections opened by clients. Connections are expected to be long-lived.\n\nLow sustained values above zero are to be expected. High rates may be indicative of connection churn or mass connection recovery.\n\n* [Connection Leak](https://www.rabbitmq.com/connections.html#monitoring)\n* [Connections](https://www.rabbitmq.com/connections.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 2 }, { "color": "red", "value": 10 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 6, "x": 12, "y": 75 }, "id": 48, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_connections_opened_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Connections opened / s", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The rate of connections closed. Connections are expected to be long-lived.\n\nLow sustained values above zero are to be expected. High rates may be indicative of connection churn or mass connection recovery.\n\n* [Connections](https://www.rabbitmq.com/connections.html)", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "line+area" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "orange", "value": 2 }, { "color": "red", "value": 10 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?0(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#56A64B", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?1(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2CC0C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?2(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#3274D9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?3(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#A352CC", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?4(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF780A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?5(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#96D98D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?6(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFEE52", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?7(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#8AB8FF", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?8(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CA95E5", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^rabbit@[a-zA-Z\\.\\-]*?9(\\b|\\.)/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFB357", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 5, "w": 6, "x": 18, "y": 75 }, "id": 49, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "hidden", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.4", "targets": [ { "expr": "sum(rate(rabbitmq_connections_closed_total[60s]) * on(instance, job) group_left(rabbitmq_cluster, rabbitmq_node) rabbitmq_identity_info{rabbitmq_cluster=\"$rabbitmq_cluster\", namespace=\"$namespace\"}) by(rabbitmq_node)", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "{{rabbitmq_node}}", "refId": "A" } ], "title": "Connections closed / s", "type": "timeseries" } ], "refresh": "15s", "schemaVersion": 34, "style": "dark", "tags": [ "rabbitmq-prometheus" ], "templating": { "list": [ { "current": { "selected": false, "text": "default", "value": "default" }, "datasource": "PBFA97CFB590B2093", "hide": 2, "includeAll": false, "label": "datasource", "multi": false, "name": "DS_PROMETHEUS", "options": [], "query": "prometheus", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "definition": "label_values(rabbitmq_identity_info, namespace)", "hide": 0, "includeAll": false, "label": "Namespace", "multi": false, "name": "namespace", "options": [], "query": { "query": "label_values(rabbitmq_identity_info, namespace)", "refId": "Prometheus-namespace-Variable-Query" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "definition": "label_values(rabbitmq_identity_info{namespace=\"$namespace\"}, rabbitmq_cluster)", "hide": 0, "includeAll": false, "label": "RabbitMQ Cluster", "multi": false, "name": "rabbitmq_cluster", "options": [], "query": { "query": "label_values(rabbitmq_identity_info{namespace=\"$namespace\"}, rabbitmq_cluster)", "refId": "Prometheus-rabbitmq_cluster-Variable-Query" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-15m", "to": "now" }, "timepicker": { "refresh_intervals": [ "15s", "30s", "1m", "5m", "10m" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "", "title": "RabbitMQ-Overview", "uid": "Kn5xm-gZk", "version": 20220805, "weekStart": "", "gnetId": 10991 } ================================================ FILE: deployments/configs/grafana/provisioning/dashboards/dashboard.yml ================================================ # https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards apiVersion: 1 providers: - name: "default" orgId: 1 folder: "" type: file disableDeletion: false editable: true allowUiUpdates: true updateIntervalSeconds: 5 # how often Grafana will scan for changed dashboards options: path: /var/lib/grafana/dashboards # path to dashboards on disk ================================================ FILE: deployments/configs/grafana/provisioning/datasources/datasource.yml ================================================ # https://grafana.com/docs/grafana/latest/administration/provisioning/ # https://github.com/grafana/tempo/blob/main/example/docker-compose/shared/grafana-datasources.yaml # https://github.com/grafana/intro-to-mltp/blob/main/grafana/provisioning/datasources/datasources.yaml apiVersion: 1 datasources: # https://github.com/grafana/tempo/blob/main/example/docker-compose/shared/grafana-datasources.yaml - name: Prometheus type: prometheus typeName: Prometheus uid: prometheus-uid access: proxy orgId: 1 url: http://prometheus:9090 basicAuth: false isDefault: true readOnly: false user: '' database: '' version: 1 editable: false jsonData: httpMethod: GET - name: Jaeger type: jaeger access: proxy url: http://jaeger-all-in-one:16686 editable: false uid: jaeger-uid - name: Zipkin type: zipkin access: proxy url: http://zipkin-all-in-one:9411 editable: false uid: zipkin-uid # https://github.com/grafana/tempo/blob/main/example/docker-compose/shared/grafana-datasources.yaml - name: Tempo type: tempo access: proxy orgId: 1 url: http://tempo:3200 basicAuth: false isDefault: false version: 1 editable: false apiVersion: 1 uid: tempo-uid jsonData: httpMethod: GET serviceMap: datasourceUid: prometheus-uid streamingEnabled: search: true #https://github.com/grafana/intro-to-mltp/blob/main/grafana/provisioning/datasources/datasources.yaml - name: Loki type: loki access: proxy uid: loki-uid url: http://loki:3100 user: '' database: '' readOnly: false jsonData: derivedFields: - datasourceUid: tempo-uid matcherRegex: "^.*?traceI[d|D]=(\\w+).*$" name: traceId url: '$${__value.raw}' - name: Kibana type: elasticsearch url: http://elasticsearch:9200 access: proxy isDefault: false uid: kibana-uid jsonData: esVersion: 7 timeField: "@timestamp" maxConcurrentShardRequests: 256 interval: Daily logMessageField: "message" # Optional: Field for log messages logLevelField: "level" # Optional: Field for log levels editable: true ================================================ FILE: deployments/configs/loki-config.yaml ================================================ # https://grafana.com/docs/loki/latest/configure/examples/configuration-examples/ # https://github.com/grafana/loki/issues/2018#issuecomment-970789233 # https://grafana.com/docs/opentelemetry/collector/send-logs-to-loki/ # https://github.com/grafana/loki/blob/main/examples/getting-started/loki-config.yaml # https://github.com/grafana/loki/blob/main/cmd/loki/loki-local-config.yaml # https://grafana.com/docs/loki/latest/configure/examples/configuration-examples/#1-local-configuration-exampleyaml --- # https://grafana.com/docs/loki/latest/configure/examples/configuration-examples/#1-local-configuration-exampleyaml auth_enabled: false # This is a complete configuration to deploy Loki backed by the filesystem. # The index will be shipped to the storage via tsdb-shipper. server: http_listen_port: 3100 common: ring: instance_addr: 127.0.0.1 kvstore: store: inmemory replication_factor: 1 path_prefix: /tmp/loki schema_config: configs: - from: 2020-05-15 store: tsdb object_store: filesystem schema: v13 index: prefix: index_ period: 24h storage_config: filesystem: directory: /tmp/loki/chunks # https://grafana.com/docs/loki/latest/send-data/otel/ # https://grafana.com/docs/loki/latest/send-data/otel/#changing-the-default-mapping-of-otlp-to-loki-format limits_config: # this attribute should be `true` when we use `otlphttp/loki`, but if we want to use `loki component` from `opentelemetry-collector-contrib` it should be false. allow_structured_metadata: true ================================================ FILE: deployments/configs/otel-collector-config.yaml ================================================ # ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/examples/demo/otel-collector-config.yaml # https://opentelemetry.io/docs/collector/configuration/ # https://opentelemetry.io/docs/collector/architecture/ # https://betterstack.com/community/guides/observability/opentelemetry-collector/ # https://signoz.io/blog/opentelemetry-collector-complete-guide/ # This configuration sets up an OpenTelemetry Collector that receives trace data via the OTLP protocol over HTTP on port 4318, applies batch processing, and then exports the processed traces # to exporter components like `Jaeger` endpoint located at `jaeger-all-in-one:4317`. It also includes a health_check extension for monitoring the collector's status. # Receivers in the OpenTelemetry Collector are components that collect telemetry data (traces, metrics, and logs) from various sources, such as instrumented applications or agents. # They act as entry points, converting incoming data into OpenTelemetry's internal format for processing and export. # https://betterstack.com/community/guides/observability/opentelemetry-collector/#exploring-the-opentelemetry-collector-components # https://opentelemetry.io/docs/collector/architecture/#receivers # https://github.com/open-telemetry/opentelemetry-collector/blob/main/receiver/README.md # https://opentelemetry.io/docs/collector/configuration/#receivers receivers: # supported receivers # https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver # instead of specifying details explicitly we can just use `otlp` and it uses both grpc and http otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 # prometheus: # config: # scrape_configs: # - job_name: 'node-exporter' # scrape_interval: 10s # static_configs: # - targets: [ 'node-exporter:9100' ] # Processors in the OpenTelemetry Collector modify and enhance telemetry data by filtering, transforming, enriching, or batching it to prepare it for export. # https://github.com/open-telemetry/opentelemetry-collector/tree/main/processor processors: batch: # Batches logs for better performance # - Exporters in the OpenTelemetry Collector send processed telemetry data to backend systems like observability platforms, databases, or cloud services for storage, visualization, and analysis. # - The `key` follows the `type/name` format, where `type` specifies the exporter `type` (e.g., otlp, kafka, prometheus), and `name` (optional) can be appended to provide a unique name for multiple instance of the same type # https://betterstack.com/community/guides/observability/opentelemetry-collector/#exploring-the-opentelemetry-collector-components # https://opentelemetry.io/docs/collector/architecture/#exporters # https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter # https://github.com/open-telemetry/opentelemetry-collector/tree/main/exporter # https://opentelemetry.io/docs/collector/configuration/#exporters exporters: # valid values: [prometheusremotewrite zipkin otlphttp file kafka prometheus debug nop otlp opencensus] # Prometheus exporter metrics prometheus: endpoint: "0.0.0.0:8889" prometheusremotewrite: endpoint: "http://prometheus:9090/api/v1/write" # https://grafana.com/docs/loki/latest/send-data/otel/ # https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/lokiexporter/README.md otlphttp/loki: endpoint: "http://loki:3100/otlp" tls: insecure: true # # we can also use `loki component` from `opentelemetry-collector-contrib` if we don't want to use builtin `otlphttp` exporter type and `http://loki:3100/otlp` loki endpoint # loki: # endpoint: "http://loki:3100/loki/api/v1/push" # tls: # insecure: true debug: # https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/elasticsearchexporter # using `elasticsearch` from `opentelemetry-collector-contrib` components because it doesn't exist in `opentelemetry-collector` elasticsearch: endpoint: "http://elasticsearch:9200" zipkin: endpoint: "http://zipkin-all-in-one:9411/api/v2/spans" format: proto # export collected telemetry traces to jaeger OTLP grpc port, we can send data to other available endpoints and ports on jaeger as well otlp/jaeger: endpoint: "http://jaeger-all-in-one:4317" tls: insecure: true otlp/tempo: endpoint: "http://tempo:4317" tls: insecure: true # seq-otlp: # endpoint: "http://seq:5341/ingest/otlp" # https://opentelemetry.io/docs/collector/configuration/#extensions # https://github.com/open-telemetry/opentelemetry-collector/blob/main/extension/README.md extensions: pprof: endpoint: 0.0.0.0:1888 zpages: endpoint: 0.0.0.0:55679 health_check: endpoint: 0.0.0.0:13133 # - The service section is used to configure what components are enabled in the Collector based on the configuration found in the receivers, processors, exporters, and extensions sections. # - If a component is configured, but not defined within the service section, then it’s not enabled. # https://betterstack.com/community/guides/observability/opentelemetry-collector/#exploring-the-opentelemetry-collector-components # https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/README.md # https://opentelemetry.io/docs/collector/architecture/ # https://opentelemetry.io/docs/collector/configuration/#service service: # The `service.extensions` subsection determines which of the configured extensions will be enabled extensions: [pprof, zpages, health_check] # The `service.pipeline` Each pipeline starts with one or more receivers collecting data, which is then processed sequentially by processors (applying transformations, filtering, or sampling). # The processed data is finally sent to all configured exporters, ensuring each receives a copy. Components must be pre-configured in their respective sections before being used in a pipeline. # pipeline activate predefined components, defined components are disabled by default pipelines: traces: receivers: [otlp] processors: [batch] # https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection exporters: [debug, zipkin, otlp/jaeger, otlp/tempo] metrics: receivers: [otlp] processors: [batch] exporters: [debug, prometheusremotewrite, prometheus] logs: receivers: [otlp] processors: [batch] exporters: [otlphttp/loki, elasticsearch] ================================================ FILE: deployments/configs/prometheus.yaml ================================================ # ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/examples/demo/prometheus.yaml # https://prometheus.io/docs/introduction/first_steps/ # https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/ global: scrape_interval: 5s scrape_configs: # when we use otel-collector we should comment other jobs in prometheus config, and we read configs from `otel-collector-config` - job_name: "otel-collector" scrape_interval: 10s static_configs: # otel-collector Prometheus exporter metrics - targets: [ 'otel-collector:8889' ] - targets: [ 'otel-collector:8888' ] - job_name: "prometheus" static_configs: - targets: ["prometheus:9090"] # # https://prometheus.io/docs/guides/node-exporter/ # # https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/ # - job_name: "node-exporter" # static_configs: # - targets: [ 'node-exporter:9100' ] # # if we don't use otel collector we should uncomment this # # scrap application metrics # # http://localhost:4000/metrics by AddPrometheusExporter() # - job_name: vertical-slice-template-api # scrape_interval: 10s # metrics_path: /metrics # static_configs: # - targets: ['host.docker.internal:4000'] # # # if we don't use otel collector we should uncomment this # # scrap application health metrics # # http://localhost:4000/health/metrics by AddPrometheusExporter() # - job_name: vertical-slice-template-api-healthchecks # scrape_interval: 10s # metrics_path: /health/metrics # static_configs: # - targets: ['host.docker.internal:4000'] ## https://github.com/grafana/tempo/blob/main/example/docker-compose/shared/prometheus.yaml # - job_name: 'tempo' # static_configs: # - targets: [ 'tempo:3200' ] ================================================ FILE: deployments/configs/tempo.yaml ================================================ # https://grafana.com/docs/tempo/latest/configuration/ # https://github.com/grafana/tempo/blob/main/example/docker-compose/local/tempo.yaml # https://github.com/grafana/tempo/blob/main/example/docker-compose/shared/tempo.yaml stream_over_http_enabled: true server: http_listen_port: 3200 log_level: info distributor: receivers: otlp: protocols: grpc: endpoint: "tempo:4317" ingester: max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally compactor: compaction: block_retention: 1h # overall Tempo trace retention. set for demo purposes metrics_generator: registry: external_labels: source: tempo cluster: docker-compose storage: path: /var/tempo/generator/wal remote_write: - url: http://prometheus:9090/api/v1/write send_exemplars: true traces_storage: path: /var/tempo/generator/traces storage: trace: backend: local # backend configuration to use wal: path: /var/tempo/wal # where to store the wal locally local: path: /var/tempo/blocks overrides: defaults: metrics_generator: processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator generate_native_histograms: both ================================================ FILE: deployments/docker-compose/docker-compose.infrastructure.yaml ================================================ # ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/examples/demo/docker-compose.yaml # ref: https://github.com/joaofbantunes/DotNetMicroservicesObservabilitySample/blob/main/docker-compose.yml # ref: https://github.com/oskardudycz/EventSourcing.NetCore/blob/main/docker-compose.yml # https://github.com/grafana/intro-to-mltp # https://stackoverflow.com/questions/65272764/ports-are-not-available-listen-tcp-0-0-0-0-50070-bind-an-attempt-was-made-to name: booking-microservices-infrastructure services: ####################################################### # rabbitmq ####################################################### rabbitmq: image: rabbitmq:management container_name: rabbitmq restart: unless-stopped ports: - "5672:5672" - "15672:15672" # volumes: # - rabbitmq:/var/lib/rabbitmq networks: - infrastructure ####################################################### # postgres ####################################################### postgres: image: postgres:latest container_name: postgres restart: unless-stopped ports: - '5432:5432' environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres command: - "postgres" - "-c" - "wal_level=logical" - "-c" - "max_prepared_transactions=10" volumes: - postgres-data:/var/lib/postgresql/data networks: - infrastructure ####################################################### # EventStoreDB ####################################################### eventstore: container_name: eventstore image: eventstore/eventstore:latest restart: unless-stopped environment: - EVENTSTORE_CLUSTER_SIZE=1 - EVENTSTORE_RUN_PROJECTIONS=All - EVENTSTORE_START_STANDARD_PROJECTIONS=True - EVENTSTORE_HTTP_PORT=2113 - EVENTSTORE_INSECURE=True - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=True ports: - "2113:2113" networks: - infrastructure # ####################################################### # # Mongo # ####################################################### mongo: image: mongo:latest container_name: mongo restart: unless-stopped # environment: # - MONGO_INITDB_ROOT_USERNAME=root # - MONGO_INITDB_ROOT_PASSWORD=secret ports: - 27017:27017 networks: - infrastructure # ####################################################### # # Redis # ####################################################### redis: image: redis container_name: redis restart: unless-stopped ports: - 6379:6379 networks: - infrastructure # ####################################################### # # jaeger # # https://www.jaegertracing.io/docs/1.64/deployment/ # # https://www.jaegertracing.io/docs/1.6/deployment/ # ####################################################### jaeger-all-in-one: image: jaegertracing/all-in-one:latest container_name: jaeger-all-in-one restart: unless-stopped ports: - "6831:6831/udp" # UDP port for Jaeger agent - "16686:16686" # endpoints and Jaeger UI - "14268:14268" # HTTP port for accept trace spans directly from clients - "14317:4317" # OTLP gRPC receiver for jaeger - "14318:4318" # OTLP http receiver for jaeger # - "9411" # Accepts Zipkin spans - /api/v2/spans networks: - infrastructure # ####################################################### # # zipkin # # https://zipkin.io/pages/quickstart # ####################################################### zipkin-all-in-one: image: openzipkin/zipkin:latest container_name: zipkin-all-in-one restart: unless-stopped ports: - "9411:9411" networks: - infrastructure # ####################################################### # # otel-collector # # https://opentelemetry.io/docs/collector/installation/ # # https://github.com/open-telemetry/opentelemetry-collector # # https://github.com/open-telemetry/opentelemetry-collector-contrib # # we can use none contrib docker `otel/opentelemetry-collector` version from `https://github.com/open-telemetry/opentelemetry-collector` repository but, # # if we need more components like `elasticsearch` we should use `otel/opentelemetry-collector-contrib` image of `https://github.com/open-telemetry/opentelemetry-collector-contrib` repository. # ####################################################### otel-collector: image: otel/opentelemetry-collector-contrib:latest container_name: otel-collector restart: unless-stopped command: ["--config=/etc/otelcol-contrib/config.yaml"] volumes: - ./../configs/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml ports: - "11888:1888" # pprof extension - "8888:8888" # Prometheus metrics exposed by the Collector - "8889:8889" # Prometheus exporter metrics - "13133:13133" # health_check extension - "4317:4317" # OTLP gRPC receiver - "4318:4318" # OTLP http receiver - "55679:55679" # zpages extension networks: - infrastructure # ####################################################### # # prometheus # # https://prometheus.io/docs/introduction/first_steps/ # # https://prometheus.io/docs/prometheus/3.1/installation/ # # https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/ # ####################################################### prometheus: image: prom/prometheus:latest restart: unless-stopped ports: - "9090:9090" volumes: - ./../configs/prometheus.yaml:/etc/prometheus/prometheus.yml # to passe one flag, such as "--log.level=debug" or "--web.enable-remote-write-receiver", we need to override the whole command, as we can't just pass one extra argument command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--web.console.libraries=/usr/share/prometheus/console_libraries" - "--web.console.templates=/usr/share/prometheus/consoles" # need this for the OpenTelemetry collector to be able to put metrics into Prometheus - "--web.enable-remote-write-receiver" # - "--log.level=debug" networks: - infrastructure # ####################################################### # # node-exporter # # https://prometheus.io/docs/guides/node-exporter/ # # https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/ # ####################################################### node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: unless-stopped volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - '--path.procfs=/host/proc' - '--path.rootfs=/rootfs' - '--path.sysfs=/host/sys' ports: - "9101:9100" networks: - infrastructure # ####################################################### # # grafana # # https://grafana.com/docs/grafana/latest/administration/provisioning/ # # https://grafana.com/docs/grafana/latest/setup-grafana/installation/docker/ # # https://grafana.com/docs/grafana/latest/setup-grafana/configure-docker/ # # https://github.com/grafana/intro-to-mltp/blob/main/grafana/provisioning/datasources/datasources.yaml # ####################################################### grafana: image: grafana/grafana:latest container_name: grafana restart: unless-stopped environment: - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor # - GF_AUTH_ANONYMOUS_ENABLED=true # - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin # - GF_AUTH_DISABLE_LOGIN_FORM=true depends_on: - prometheus ports: - "3000:3000" volumes: - ./../configs/grafana/provisioning:/etc/grafana/provisioning - ./../configs/grafana/dashboards:/var/lib/grafana/dashboards ## https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/ # - ./../configs/grafana/grafana.ini:/etc/grafana/grafana.ini networks: - infrastructure # ####################################################### # # tempo # # https://github.com/grafana/tempo/blob/main/example/docker-compose/otel-collector/docker-compose.yaml # # https://github.com/grafana/tempo/blob/main/example/docker-compose/shared # # https://github.com/grafana/tempo/blob/main/example/docker-compose/local # # https://github.com/grafana/tempo/tree/main/example/docker-compose # ####################################################### tempo: image: grafana/tempo:latest container_name: tempo restart: unless-stopped command: [ "-config.file=/etc/tempo.yaml" ] volumes: - ./../configs/tempo.yaml:/etc/tempo.yaml ports: - "3200" # tempo - "24317:4317" # otlp grpc - "24318:4318" # otlp http networks: - infrastructure # ####################################################### # # loki # # https://grafana.com/docs/opentelemetry/collector/send-logs-to-loki/ # # https://github.com/grafana/loki/blob/main/production/docker-compose.yaml # # https://github.com/grafana/loki/blob/main/examples/getting-started/docker-compose.yaml # ####################################################### loki: image: grafana/loki:latest hostname: loki container_name: loki ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml volumes: - ./../configs/loki-config.yaml:/etc/loki/local-config.yaml networks: - infrastructure # ####################################################### # # elasticsearch # # https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docker.html#docker-compose-file # ####################################################### elasticsearch: container_name: elasticsearch restart: unless-stopped image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0 environment: - discovery.type=single-node - cluster.name=docker-cluster - node.name=docker-node - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - xpack.security.enabled=false - xpack.security.http.ssl.enabled=false - xpack.security.transport.ssl.enabled=false - network.host=0.0.0.0 - http.port=9200 - transport.host=localhost - bootstrap.memory_lock=true - cluster.routing.allocation.disk.threshold_enabled=false ulimits: memlock: soft: -1 hard: -1 volumes: - elastic-data:/usr/share/elasticsearch/data ports: - ${ELASTIC_HOST_PORT:-9200}:${ELASTIC_PORT:-9200} - 9300:9300 networks: - infrastructure # ####################################################### # # kibana # # https://www.elastic.co/guide/en/kibana/current/docker.html # ####################################################### kibana: image: docker.elastic.co/kibana/kibana:8.17.0 container_name: kibana restart: unless-stopped environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: - ${KIBANA_HOST_PORT:-5601}:${KIBANA_PORT:-5601} depends_on: - elasticsearch networks: - infrastructure # ####################################################### # # cadvisor # ####################################################### cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: cadvisor restart: unless-stopped ports: - "8080:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro devices: - /dev/kmsg networks: - infrastructure networks: infrastructure: name: infrastructure driver: bridge volumes: elastic-data: postgres-data: ================================================ FILE: deployments/docker-compose/docker-compose.yaml ================================================ name: booking-microservices services: ####################################################### # rabbitmq ####################################################### rabbitmq: image: rabbitmq:management container_name: rabbitmq restart: unless-stopped ports: - "5672:5672" - "15672:15672" # volumes: # - rabbitmq:/var/lib/rabbitmq networks: - booking ####################################################### # postgres ####################################################### postgres: image: postgres:latest container_name: postgres restart: unless-stopped ports: - '5432:5432' environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres command: - "postgres" - "-c" - "wal_level=logical" - "-c" - "max_prepared_transactions=10" volumes: - postgres-data:/var/lib/postgresql/data networks: - booking ####################################################### # EventStoreDB ####################################################### eventstore: container_name: eventstore image: eventstore/eventstore:latest restart: unless-stopped environment: - EVENTSTORE_CLUSTER_SIZE=1 - EVENTSTORE_RUN_PROJECTIONS=All - EVENTSTORE_START_STANDARD_PROJECTIONS=True - EVENTSTORE_HTTP_PORT=2113 - EVENTSTORE_INSECURE=True - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=True ports: - "2113:2113" networks: - booking ####################################################### # Mongo ####################################################### mongo: image: mongo:latest container_name: mongo restart: unless-stopped # environment: # - MONGO_INITDB_ROOT_USERNAME=root # - MONGO_INITDB_ROOT_PASSWORD=secret ports: - 27017:27017 networks: - booking ####################################################### # Redis ####################################################### redis: image: redis container_name: redis restart: unless-stopped ports: - 6379:6379 networks: - booking ####################################################### # jaeger # https://www.jaegertracing.io/docs/1.64/deployment/ # https://www.jaegertracing.io/docs/1.6/deployment/ ####################################################### jaeger-all-in-one: image: jaegertracing/all-in-one:latest container_name: jaeger-all-in-one restart: unless-stopped ports: - "6831:6831/udp" # UDP port for Jaeger agent - "16686:16686" # endpoints and Jaeger UI - "14268:14268" # HTTP port for accept trace spans directly from clients - "14317:4317" # OTLP gRPC receiver for jaeger - "14318:4318" # OTLP http receiver for jaeger # - "9411" # Accepts Zipkin spans - /api/v2/spans networks: - booking ####################################################### # zipkin # https://zipkin.io/pages/quickstart ####################################################### zipkin-all-in-one: image: openzipkin/zipkin:latest container_name: zipkin-all-in-one restart: unless-stopped ports: - "9411:9411" networks: - booking ####################################################### # otel-collector # https://opentelemetry.io/docs/collector/installation/ # https://github.com/open-telemetry/opentelemetry-collector # https://github.com/open-telemetry/opentelemetry-collector-contrib # we can use none contrib docker `otel/opentelemetry-collector` version from `https://github.com/open-telemetry/opentelemetry-collector` repository but, # if we need more components like `elasticsearch` we should use `otel/opentelemetry-collector-contrib` image of `https://github.com/open-telemetry/opentelemetry-collector-contrib` repository. ####################################################### otel-collector: image: otel/opentelemetry-collector-contrib:latest container_name: otel-collector restart: unless-stopped command: ["--config=/etc/otelcol-contrib/config.yaml"] volumes: - ./../configs/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml ports: - "11888:1888" # pprof extension - "8888:8888" # Prometheus metrics exposed by the Collector - "8889:8889" # Prometheus exporter metrics - "13133:13133" # health_check extension - "4317:4317" # OTLP gRPC receiver - "4318:4318" # OTLP http receiver - "55679:55679" # zpages extension networks: - booking ####################################################### # prometheus # https://prometheus.io/docs/introduction/first_steps/ # https://prometheus.io/docs/prometheus/3.1/installation/ # https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/ ####################################################### prometheus: image: prom/prometheus:latest restart: unless-stopped ports: - "9090:9090" volumes: - ./../configs/prometheus.yaml:/etc/prometheus/prometheus.yml # to passe one flag, such as "--log.level=debug" or "--web.enable-remote-write-receiver", we need to override the whole command, as we can't just pass one extra argument command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--web.console.libraries=/usr/share/prometheus/console_libraries" - "--web.console.templates=/usr/share/prometheus/consoles" # need this for the OpenTelemetry collector to be able to put metrics into Prometheus - "--web.enable-remote-write-receiver" # - "--log.level=debug" networks: - booking ####################################################### # node-exporter # https://prometheus.io/docs/guides/node-exporter/ # https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/ ####################################################### node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: unless-stopped volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - '--path.procfs=/host/proc' - '--path.rootfs=/rootfs' - '--path.sysfs=/host/sys' ports: - "9101:9100" networks: - booking ####################################################### # grafana # https://grafana.com/docs/grafana/latest/administration/provisioning/ # https://grafana.com/docs/grafana/latest/setup-grafana/installation/docker/ # https://grafana.com/docs/grafana/latest/setup-grafana/configure-docker/ # https://github.com/grafana/intro-to-mltp/blob/main/grafana/provisioning/datasources/datasources.yaml ####################################################### grafana: image: grafana/grafana:latest container_name: grafana restart: unless-stopped environment: - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor # - GF_AUTH_ANONYMOUS_ENABLED=true # - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin # - GF_AUTH_DISABLE_LOGIN_FORM=true depends_on: - prometheus ports: - "3000:3000" volumes: - ./../configs/grafana/provisioning:/etc/grafana/provisioning - ./../configs/grafana/dashboards:/var/lib/grafana/dashboards ## https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/ # - ./../configs/grafana/grafana.ini:/etc/grafana/grafana.ini networks: - booking ####################################################### # tempo # https://github.com/grafana/tempo/blob/main/example/docker-compose/otel-collector/docker-compose.yaml # https://github.com/grafana/tempo/blob/main/example/docker-compose/shared # https://github.com/grafana/tempo/blob/main/example/docker-compose/local # https://github.com/grafana/tempo/tree/main/example/docker-compose ####################################################### tempo: image: grafana/tempo:latest container_name: tempo restart: unless-stopped command: [ "-config.file=/etc/tempo.yaml" ] volumes: - ./../configs/tempo.yaml:/etc/tempo.yaml ports: - "3200" # tempo - "24317:4317" # otlp grpc - "24318:4318" # otlp http networks: - booking ####################################################### # loki # https://grafana.com/docs/opentelemetry/collector/send-logs-to-loki/ # https://github.com/grafana/loki/blob/main/production/docker-compose.yaml # https://github.com/grafana/loki/blob/main/examples/getting-started/docker-compose.yaml ####################################################### loki: image: grafana/loki:latest hostname: loki container_name: loki ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml volumes: - ./../configs/loki-config.yaml:/etc/loki/local-config.yaml networks: - booking ####################################################### # elasticsearch # https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docker.html#docker-compose-file ####################################################### elasticsearch: container_name: elasticsearch restart: unless-stopped image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0 environment: - discovery.type=single-node - cluster.name=docker-cluster - node.name=docker-node - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - xpack.security.enabled=false - xpack.security.http.ssl.enabled=false - xpack.security.transport.ssl.enabled=false - network.host=0.0.0.0 - http.port=9200 - transport.host=localhost - bootstrap.memory_lock=true - cluster.routing.allocation.disk.threshold_enabled=false ulimits: memlock: soft: -1 hard: -1 volumes: - elastic-data:/usr/share/elasticsearch/data ports: - ${ELASTIC_HOST_PORT:-9200}:${ELASTIC_PORT:-9200} - 9300:9300 networks: - booking ####################################################### # kibana # https://www.elastic.co/guide/en/kibana/current/docker.html ####################################################### kibana: image: docker.elastic.co/kibana/kibana:8.17.0 container_name: kibana restart: unless-stopped environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: - ${KIBANA_HOST_PORT:-5601}:${KIBANA_PORT:-5601} depends_on: - elasticsearch networks: - booking ####################################################### # cadvisor ####################################################### cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: cadvisor restart: unless-stopped ports: - "8080:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro devices: - /dev/kmsg networks: - booking ###################################################### # Api-Gateway ###################################################### api-gateway: image: api-gateway build: args: Version: "1" context: ../../ dockerfile: src/ApiGateway/Dockerfile container_name: api-gateway ports: - "5001:80" - "5000:443" volumes: - ~/.aspnet/https:/https:ro environment: - ASPNETCORE_ENVIRONMENT=docker - ASPNETCORE_URLS=https://+;http://+ - ASPNETCORE_HTTPS_PORT=5000 - ASPNETCORE_HTTP_PORT=5001 - ASPNETCORE_Kestrel__Certificates__Default__Password=password - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx networks: - booking ####################################################### # Flight ####################################################### flight: image: flight build: args: Version: "1" context: ../../ dockerfile: src/Services/Flight/Dockerfile container_name: flight ports: - 5004:80 - 5003:443 volumes: - ~/.aspnet/https:/https:ro environment: - ASPNETCORE_ENVIRONMENT=docker - ASPNETCORE_URLS=https://+;http://+ - ASPNETCORE_HTTPS_PORT=5003 - ASPNETCORE_HTTP_PORT=5004 - ASPNETCORE_Kestrel__Certificates__Default__Password=password - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx networks: - booking ####################################################### # Identity ####################################################### identity: image: identity build: args: Version: "1" context: ../../ dockerfile: src/Services/Identity/Dockerfile container_name: identity ports: - 6005:80 - 5005:443 volumes: - ~/.aspnet/https:/https:ro environment: - ASPNETCORE_ENVIRONMENT=docker - ASPNETCORE_URLS=https://+;http://+ - ASPNETCORE_HTTPS_PORT=5005 - ASPNETCORE_HTTP_PORT=6005 - ASPNETCORE_Kestrel__Certificates__Default__Password=password - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx networks: - booking ####################################################### # Passenger ####################################################### passenger: image: passenger build: args: Version: "1" context: ../../ dockerfile: src/Services/Passenger/Dockerfile container_name: passenger ports: - 6012:80 - 5012:443 volumes: - ~/.aspnet/https:/https:ro environment: - ASPNETCORE_ENVIRONMENT=docker - ASPNETCORE_URLS=https://+;http://+ - ASPNETCORE_HTTPS_PORT=5012 - ASPNETCORE_HTTP_PORT=6012 - ASPNETCORE_Kestrel__Certificates__Default__Password=password - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx networks: - booking ####################################################### # Booking ####################################################### booking: image: booking build: args: Version: "1" context: ../../ dockerfile: src/Services/Booking/Dockerfile container_name: booking ports: - 6010:80 - 5010:443 volumes: - ~/.aspnet/https:/https:ro environment: - ASPNETCORE_ENVIRONMENT=docker - ASPNETCORE_URLS=https://+;http://+ - ASPNETCORE_HTTPS_PORT=5010 - ASPNETCORE_HTTP_PORT=6010 - ASPNETCORE_Kestrel__Certificates__Default__Password=password - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx networks: - booking networks: booking: name: booking driver: bridge volumes: elastic-data: postgres-data: ================================================ FILE: deployments/kubernetes/booking-cert-manager.yml ================================================ apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-staging spec: acme: # Staging API server: https://acme-staging-v02.api.letsencrypt.org/directory # server: https://acme-v02.api.letsencrypt.org/directory email: test@email.com privateKeySecretRef: name: letsencrypt-staging solvers: - http01: ingress: class: nginx ================================================ FILE: deployments/kubernetes/booking-microservices.yml ================================================ ## ref: https://kompose.io ####################################################### # Network ####################################################### apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: creationTimestamp: null name: booking spec: ingress: - from: - podSelector: matchLabels: io.kompose.network/booking: "true" podSelector: matchLabels: io.kompose.network/booking: "true" --- ####################################################### # ElasticSearch ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: elasticsearch spec: replicas: 1 selector: matchLabels: app: elasticsearch template: metadata: labels: app: elasticsearch spec: containers: - name: elasticsearch image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0 env: - name: discovery.type value: "single-node" - name: cluster.name value: "docker-cluster" - name: node.name value: "docker-node" - name: ES_JAVA_OPTS value: "-Xms512m -Xmx512m" - name: xpack.security.enabled value: "false" - name: xpack.security.http.ssl.enabled value: "false" - name: xpack.security.transport.ssl.enabled value: "false" - name: network.host value: "0.0.0.0" - name: http.port value: "9200" - name: transport.host value: "localhost" - name: bootstrap.memory_lock value: "true" - name: cluster.routing.allocation.disk.threshold_enabled value: "false" ports: - containerPort: 9200 - containerPort: 9300 volumeMounts: - mountPath: /usr/share/elasticsearch/data name: elastic-data volumes: - name: elastic-data persistentVolumeClaim: claimName: elasticsearch-pvc --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: elasticsearch-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: v1 kind: Service metadata: name: elasticsearch spec: selector: app: elasticsearch ports: - port: 9200 targetPort: 9200 - port: 9300 targetPort: 9300 type: ClusterIP --- apiVersion: v1 kind: PersistentVolume metadata: name: elasticsearch-pv labels: type: local spec: storageClassName: manual capacity: storage: 10Gi accessModes: - ReadWriteOnce hostPath: path: "/mnt/data" --- ####################################################### # Kibana ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: kibana spec: replicas: 1 selector: matchLabels: app: kibana template: metadata: labels: app: kibana spec: containers: - name: kibana image: docker.elastic.co/kibana/kibana:8.17.0 env: - name: ELASTICSEARCH_HOSTS value: "http://elasticsearch:9200" ports: - containerPort: 5601 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: kibana spec: selector: app: kibana ports: - port: 5601 targetPort: 5601 type: ClusterIP --- ####################################################### # Tempo ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: tempo spec: replicas: 1 selector: matchLabels: app: tempo template: metadata: labels: app: tempo spec: containers: - name: tempo image: grafana/tempo:latest args: - "-config.file=/etc/tempo.yaml" ports: - containerPort: 3200 - containerPort: 4317 - containerPort: 4318 volumeMounts: - mountPath: /etc/tempo.yaml name: tempo-config subPath: tempo.yaml volumes: - name: tempo-config configMap: name: tempo-config --- apiVersion: v1 kind: ConfigMap metadata: name: tempo-config data: tempo.yaml: | # Your Tempo configuration here --- apiVersion: v1 kind: Service metadata: name: tempo spec: selector: app: tempo ports: - port: 3200 targetPort: 3200 - port: 4317 targetPort: 4317 - port: 4318 targetPort: 4318 type: ClusterIP --- ####################################################### # Looki ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: loki spec: replicas: 1 selector: matchLabels: app: loki template: metadata: labels: app: loki spec: containers: - name: loki image: grafana/loki:latest args: - "-config.file=/etc/loki/local-config.yaml" ports: - containerPort: 3100 volumeMounts: - mountPath: /etc/loki/local-config.yaml name: loki-config subPath: local-config.yaml volumes: - name: loki-config configMap: name: loki-config --- apiVersion: v1 kind: ConfigMap metadata: name: loki-config data: local-config.yaml: | # Your Loki configuration here --- apiVersion: v1 kind: Service metadata: name: loki spec: selector: app: loki ports: - port: 3100 targetPort: 3100 type: ClusterIP --- ####################################################### # Event Store ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: eventstore spec: replicas: 1 selector: matchLabels: app: eventstore template: metadata: labels: app: eventstore spec: containers: - name: eventstore image: eventstore/eventstore:latest env: - name: EVENTSTORE_CLUSTER_SIZE value: "1" - name: EVENTSTORE_RUN_PROJECTIONS value: "All" - name: EVENTSTORE_START_STANDARD_PROJECTIONS value: "True" - name: EVENTSTORE_HTTP_PORT value: "2113" - name: EVENTSTORE_INSECURE value: "True" - name: EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP value: "True" ports: - containerPort: 2113 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: eventstore spec: selector: app: eventstore ports: - port: 2113 targetPort: 2113 type: ClusterIP --- ####################################################### # Jaeger ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: jaeger spec: replicas: 1 selector: matchLabels: app: jaeger template: metadata: labels: app: jaeger spec: containers: - name: jaeger image: jaegertracing/all-in-one:latest ports: - containerPort: 6831 protocol: UDP - containerPort: 16686 - containerPort: 14268 - containerPort: 4317 - containerPort: 4318 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: jaeger spec: selector: app: jaeger ports: - port: 6831 targetPort: 6831 protocol: UDP - port: 16686 targetPort: 16686 - port: 14268 targetPort: 14268 - port: 4317 targetPort: 4317 - port: 4318 targetPort: 4318 type: ClusterIP --- ####################################################### # Zipkin ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: zipkin spec: replicas: 1 selector: matchLabels: app: zipkin template: metadata: labels: app: zipkin spec: containers: - name: zipkin image: openzipkin/zipkin:latest ports: - containerPort: 9411 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: zipkin spec: selector: app: zipkin ports: - port: 9411 targetPort: 9411 type: ClusterIP --- ####################################################### # OpenTelemetry Collector ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: otel-collector spec: replicas: 1 selector: matchLabels: app: otel-collector template: metadata: labels: app: otel-collector spec: containers: - name: otel-collector image: otel/opentelemetry-collector-contrib:latest args: ["--config=/etc/otelcol-contrib/config.yaml"] ports: - containerPort: 11888 - containerPort: 8888 - containerPort: 8889 - containerPort: 13133 - containerPort: 4317 - containerPort: 4318 - containerPort: 55679 volumeMounts: - mountPath: /etc/otelcol-contrib/config.yaml name: otel-config subPath: config.yaml volumes: - name: otel-config configMap: name: otel-collector-config --- apiVersion: v1 kind: ConfigMap metadata: name: otel-collector-config data: config.yaml: | # Your OpenTelemetry Collector configuration here --- apiVersion: v1 kind: Service metadata: name: otel-collector spec: selector: app: otel-collector ports: - port: 11888 targetPort: 11888 - port: 8888 targetPort: 8888 - port: 8889 targetPort: 8889 - port: 13133 targetPort: 13133 - port: 4317 targetPort: 4317 - port: 4318 targetPort: 4318 - port: 55679 targetPort: 55679 type: ClusterIP --- ####################################################### # Prometheus ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: prometheus spec: replicas: 1 selector: matchLabels: app: prometheus template: metadata: labels: app: prometheus spec: containers: - name: prometheus image: prom/prometheus:latest args: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--web.console.libraries=/usr/share/prometheus/console_libraries" - "--web.console.templates=/usr/share/prometheus/consoles" - "--web.enable-remote-write-receiver" ports: - containerPort: 9090 volumeMounts: - mountPath: /etc/prometheus/prometheus.yml name: prometheus-config subPath: prometheus.yml volumes: - name: prometheus-config configMap: name: prometheus-config --- apiVersion: v1 kind: ConfigMap metadata: name: prometheus-config data: prometheus.yml: | # Your Prometheus configuration here --- apiVersion: v1 kind: Service metadata: name: prometheus spec: selector: app: prometheus ports: - port: 9090 targetPort: 9090 type: ClusterIP --- ####################################################### # Grafana ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: grafana spec: replicas: 1 selector: matchLabels: app: grafana template: metadata: labels: app: grafana spec: containers: - name: grafana image: grafana/grafana:latest env: - name: GF_INSTALL_PLUGINS value: "grafana-clock-panel,grafana-simple-json-datasource" - name: GF_SECURITY_ADMIN_USER value: "admin" - name: GF_SECURITY_ADMIN_PASSWORD value: "admin" - name: GF_FEATURE_TOGGLES_ENABLE value: "traceqlEditor" ports: - containerPort: 3000 volumeMounts: - mountPath: /etc/grafana/provisioning name: grafana-provisioning - mountPath: /var/lib/grafana/dashboards name: grafana-dashboards volumes: - name: grafana-provisioning configMap: name: grafana-provisioning - name: grafana-dashboards configMap: name: grafana-dashboards --- apiVersion: v1 kind: ConfigMap metadata: name: grafana-provisioning data: # Your Grafana provisioning configuration here --- apiVersion: v1 kind: ConfigMap metadata: name: grafana-dashboards data: # Your Grafana dashboards configuration here --- apiVersion: v1 kind: Service metadata: name: grafana spec: selector: app: grafana ports: - port: 3000 targetPort: 3000 type: ClusterIP --- ####################################################### # Node Exporter ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: node-exporter spec: replicas: 1 selector: matchLabels: app: node-exporter template: metadata: labels: app: node-exporter spec: containers: - name: node-exporter image: prom/node-exporter:latest args: - "--path.procfs=/host/proc" - "--path.rootfs=/rootfs" - "--path.sysfs=/host/sys" ports: - containerPort: 9100 volumeMounts: - mountPath: /host/proc name: proc readOnly: true - mountPath: /host/sys name: sys readOnly: true - mountPath: /rootfs name: rootfs readOnly: true volumes: - name: proc hostPath: path: /proc - name: sys hostPath: path: /sys - name: rootfs hostPath: path: / --- apiVersion: v1 kind: Service metadata: name: node-exporter spec: selector: app: node-exporter ports: - port: 9100 targetPort: 9100 type: ClusterIP --- ####################################################### # Cadvisor ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: cadvisor spec: replicas: 1 selector: matchLabels: app: cadvisor template: metadata: labels: app: cadvisor spec: containers: - name: cadvisor image: gcr.io/cadvisor/cadvisor:latest ports: - containerPort: 8080 volumeMounts: - mountPath: /rootfs name: rootfs readOnly: true - mountPath: /var/run name: var-run readOnly: true - mountPath: /sys name: sys readOnly: true - mountPath: /var/lib/docker name: var-lib-docker readOnly: true - mountPath: /dev/disk name: dev-disk readOnly: true volumes: - name: rootfs hostPath: path: / - name: var-run hostPath: path: /var/run - name: sys hostPath: path: /sys - name: var-lib-docker hostPath: path: /var/lib/docker - name: dev-disk hostPath: path: /dev/disk --- apiVersion: v1 kind: Service metadata: name: cadvisor spec: selector: app: cadvisor ports: - port: 8080 targetPort: 8080 type: ClusterIP --- ####################################################### # Mongo ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: mongo spec: replicas: 1 selector: matchLabels: app: mongo template: metadata: labels: app: mongo spec: containers: - name: mongo image: mongo:latest ports: - containerPort: 27017 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: mongo spec: selector: app: mongo ports: - port: 27017 targetPort: 27017 type: ClusterIP --- ####################################################### # Postgres ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: postgres spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:latest env: - name: POSTGRES_USER value: postgres - name: POSTGRES_PASSWORD value: postgres ports: - containerPort: 5432 volumeMounts: - mountPath: /var/lib/postgresql/data name: postgres-data volumes: - name: postgres-data persistentVolumeClaim: claimName: postgres-pvc --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: v1 kind: Service metadata: name: postgres spec: selector: app: postgres ports: - port: 5432 targetPort: 5432 type: ClusterIP --- ####################################################### # Rabbitmq ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: rabbitmq spec: replicas: 1 selector: matchLabels: app: rabbitmq template: metadata: labels: app: rabbitmq spec: containers: - name: rabbitmq image: rabbitmq:management ports: - containerPort: 5672 - containerPort: 15672 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: rabbitmq spec: selector: app: rabbitmq ports: - port: 5672 targetPort: 5672 - port: 15672 targetPort: 15672 type: ClusterIP --- ####################################################### # Redis ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: redis spec: replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis ports: - containerPort: 6379 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: redis spec: selector: app: redis ports: - port: 6379 targetPort: 6379 type: ClusterIP --- ####################################################### # ConfigMap AppSettings ####################################################### apiVersion: v1 kind: ConfigMap metadata: name: appsettings data: appsettings.json: |- { .Files.Get "settings/appsettings.docker.json" } #ref: https://www.mrjamiebowman.com/software-development/dotnet/kubernetes-configmaps-with-net-core/ --- ####################################################### # Flight ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: flight-deployment labels: app: flight spec: replicas: 1 selector: matchLabels: app: flight template: metadata: labels: app: flight spec: containers: - image: meysamh66/booking-microservices-flight:v1.6.7 name: flight ports: - containerPort: 80 env: - name: ASPNETCORE_ENVIRONMENT value: docker - name: ASPNETCORE_URLS value: http://+ volumeMounts: - name: appsettings-volume mountPath: /app/Settings volumes: - name: appsettings-volume configMap: name: appsettings --- apiVersion: v1 kind: Service metadata: name: flight spec: selector: app: flight ports: - name: http protocol: TCP port: 80 targetPort: 80 type: ClusterIP --- ####################################################### # Identity ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: identity-deployment labels: app: identity spec: replicas: 1 selector: matchLabels: app: identity template: metadata: labels: app: identity spec: containers: - image: meysamh66/booking-microservices-identity:v1.6.7 name: identity ports: - containerPort: 80 env: - name: ASPNETCORE_ENVIRONMENT value: docker - name: ASPNETCORE_URLS value: http://+ volumeMounts: - name: appsettings-volume mountPath: /app/Settings volumes: - name: appsettings-volume configMap: name: appsettings --- apiVersion: v1 kind: Service metadata: name: identity spec: selector: app: identity ports: - name: http protocol: TCP port: 80 targetPort: 80 type: ClusterIP --- ####################################################### # Booking ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: booking-deployment labels: app: booking spec: replicas: 1 selector: matchLabels: app: booking template: metadata: labels: app: booking spec: containers: - image: meysamh66/booking-microservices-booking:v1.6.7 name: booking ports: - containerPort: 80 env: - name: ASPNETCORE_ENVIRONMENT value: docker - name: ASPNETCORE_URLS value: http://+ volumeMounts: - name: appsettings-volume mountPath: /app/Settings volumes: - name: appsettings-volume configMap: name: appsettings --- apiVersion: v1 kind: Service metadata: name: booking spec: selector: app: booking ports: - name: http protocol: TCP port: 80 targetPort: 80 type: ClusterIP --- ####################################################### # Passenger ####################################################### apiVersion: apps/v1 kind: Deployment metadata: name: passenger-deployment labels: app: passenger spec: replicas: 1 selector: matchLabels: app: passenger template: metadata: labels: app: passenger spec: containers: - image: meysamh66/booking-microservices-passenger:v1.6.7 name: passenger ports: - containerPort: 80 env: - name: ASPNETCORE_ENVIRONMENT value: docker - name: ASPNETCORE_URLS value: http://+ volumeMounts: - name: appsettings-volume mountPath: /app/Settings volumes: - name: appsettings-volume configMap: name: appsettings --- ####################################################### # Ingress Controller ####################################################### apiVersion: v1 kind: Service metadata: name: passenger spec: selector: app: passenger ports: - name: http protocol: TCP port: 80 targetPort: 80 type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: booking-microservies annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1 cert-manager.io/cluster-issuer: "letsencrypt-staging" nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" nginx.ingress.kubernetes.io/proxy-buffers: "4 256k" nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "256k" nginx.ingress.kubernetes.io/client-header-buffer-size: "64k" nginx.ingress.kubernetes.io/http2-max-field-size: "16k" nginx.ingress.kubernetes.io/http2-max-header-size: "128k" nginx.ingress.kubernetes.io/large-client-header-buffers: "8 64k" spec: ingressClassName: nginx tls: - hosts: - booking-microservices.com secretName: letsencrypt-staging rules: - host: booking-microservices.com http: paths: - path: /identity pathType: Prefix backend: service: name: flight port: number: 80 - path: /identity/(.*) pathType: Prefix backend: service: name: identity port: number: 80 - path: /flight pathType: Prefix backend: service: name: flight port: number: 80 - path: /flight/(.*) pathType: Prefix backend: service: name: flight port: number: 80 - path: /passenger pathType: Prefix backend: service: name: passenger port: number: 80 - path: /passenger/(.*) pathType: Prefix backend: service: name: passenger port: number: 80 - path: /booking pathType: Prefix backend: service: name: booking port: number: 80 - path: /booking/(.*) pathType: Prefix backend: service: name: booking port: number: 80 ================================================ FILE: global.json ================================================ { "sdk": { "version": "10.0.103", "rollForward": "latestFeature" } } ================================================ FILE: package.json ================================================ { "name": "booking-microservices", "version": "1.0.0", "description": "booking-microservices", "author": "Meysam Hadeli", "license": "MIT", "main": "index.js", "scripts": { "prepare": "husky && dotnet tool restore", "format": "dotnet tool run dotnet-csharpier booking-microservices.sln", "ci-format": "dotnet tool run dotnet-csharpier booking-microservices.sln --check", "upgrade-packages": "dotnet outdated --upgrade" }, "devDependencies": { "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", "husky": "^9.1.6" } } ================================================ FILE: scripts/setup_kubectl_gitpod.sh ================================================ #!/bin/bash # Download kubectl binary curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -L -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" # Make the kubectl binary executable chmod +x kubectl # Move the binary in to PATH sudo mv kubectl /usr/local/bin/ ================================================ FILE: src/ApiGateway/Dockerfile ================================================ # ---------- Build Stage ---------- FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy solution-level files COPY .editorconfig . COPY global.json . COPY Directory.Build.props . # Copy project files first (better Docker caching) COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/ COPY src/ApiGateway/src/ApiGateway.csproj src/ApiGateway/src/ COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/ # Restore dependencies RUN dotnet restore src/ApiGateway/src/ApiGateway.csproj # Copy the rest of the source code COPY src ./src # Publish (build included) RUN dotnet publish src/ApiGateway/src/ApiGateway.csproj \ -c Release \ -o /app/publish \ --no-restore # ---------- Runtime Stage ---------- FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . ENV ASPNETCORE_URLS=http://+:80 ENV ASPNETCORE_ENVIRONMENT=docker EXPOSE 80 ENTRYPOINT ["dotnet", "ApiGateway.dll"] ================================================ FILE: src/ApiGateway/src/ApiGateway.csproj ================================================ ================================================ FILE: src/ApiGateway/src/Program.cs ================================================ using BuildingBlocks.Web; using Figgle; using Figgle.Fonts; var builder = WebApplication.CreateBuilder(args); var env = builder.Environment; var appOptions = builder.Services.GetOptions("AppOptions"); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); builder.Services.AddControllers(); builder.Services.AddHttpContextAccessor(); builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("Yarp")); var app = builder.Build(); app.UseCorrelationId(); app.UseRouting(); app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapReverseProxy(); }); app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); app.Run(); ================================================ FILE: src/ApiGateway/src/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "ApiGateway": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:5000;http://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/ApiGateway/src/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } ================================================ FILE: src/ApiGateway/src/appsettings.docker.json ================================================ { "LogOptions": { "Level": "Information", "LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}", "ElasticUri": "elasticsearch:9200" }, "Yarp": { "clusters": { "flight": { "destinations": { "destination1": { "address": "http://flight:80" } } }, "identity": { "destinations": { "destination1": { "address": "http://identity:80" } } }, "passenger": { "destinations": { "destination1": { "address": "http://passenger:80" } } }, "booking": { "destinations": { "destination1": { "address": "http://booking:80" } } } } } } ================================================ FILE: src/ApiGateway/src/appsettings.json ================================================ { "AppOptions": { "Name": "ApiGateway" }, "LogOptions": { "Level": "Information", "LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}", "ElasticUri": "http://localhost:9200" }, "HealthOptions": { "Enabled": false }, "Yarp": { "routes": { "identity": { "clusterId": "identity", "match": { "path": "identity/{**catch-all}" }, "Transforms": [ { "PathRemovePrefix": "identity" } ] }, "flight": { "clusterId": "flight", "match": { "path": "flight/{**catch-all}" }, "Transforms": [ { "PathRemovePrefix": "flight" } ] }, "passenger": { "clusterId": "passenger", "match": { "path": "passenger/{**catch-all}" }, "Transforms": [ { "PathRemovePrefix": "passenger" } ] }, "booking": { "clusterId": "booking", "match": { "path": "booking/{**catch-all}" }, "Transforms": [ { "PathRemovePrefix": "booking" } ] } }, "clusters": { "flight": { "destinations": { "destination1": { "address": "http://localhost:5004" } } }, "identity": { "destinations": { "destination1": { "address": "http://localhost:6005" } } }, "passenger": { "destinations": { "destination1": { "address": "http://localhost:6012" } } }, "booking": { "destinations": { "destination1": { "address": "http://localhost:6010" } } } } }, "AllowedHosts": "*" } ================================================ FILE: src/Aspire/src/AppHost/AppHost.csproj ================================================ Exe net10.0 enable enable true bde28db3-85ba-4201-b889-0f3faba24169 ================================================ FILE: src/Aspire/src/AppHost/Program.cs ================================================ using System.Net.Sockets; var builder = DistributedApplication.CreateBuilder(args); builder.AddDockerComposeEnvironment("docker-compose"); // 1. Database Services var pgUsername = builder.AddParameter("pg-username", "postgres", secret: true); var pgPassword = builder.AddParameter("pg-password", "postgres", secret: true); var postgres = builder.AddPostgres("postgres", pgUsername, pgPassword) .WithImage("postgres:latest") .WithEndpoint( "tcp", e => { e.Port = 5432; e.TargetPort = 5432; e.IsProxied = true; e.IsExternal = false; }) .WithArgs( "-c", "wal_level=logical", "-c", "max_prepared_transactions=10"); if (builder.ExecutionContext.IsPublishMode) { postgres.WithDataVolume("postgres-data") .WithLifetime(ContainerLifetime.Persistent); } var flightDb = postgres.AddDatabase("flight"); var passengerDb = postgres.AddDatabase("passenger"); var identityDb = postgres.AddDatabase("identity"); var persistMessageDb = postgres.AddDatabase("persist-message"); var mongoUsername = builder.AddParameter("mongo-username", "root", secret: true); var mongoPassword = builder.AddParameter("mongo-password", "secret", secret: true); var mongo = builder.AddMongoDB("mongo", userName: mongoUsername, password: mongoPassword) .WithImage("mongo") .WithImageTag("latest") .WithEndpoint( "tcp", e => { e.Port = 27017; e.TargetPort = 27017; e.IsProxied = true; e.IsExternal = false; }); if (builder.ExecutionContext.IsPublishMode) { mongo.WithDataVolume("mongo-data") .WithLifetime(ContainerLifetime.Persistent); } var redis = builder.AddRedis("redis") .WithImage("redis:latest") .WithEndpoint( "tcp", e => { e.Port = 6379; e.TargetPort = 6379; e.IsProxied = true; e.IsExternal = false; }); if (builder.ExecutionContext.IsPublishMode) { redis.WithDataVolume("redis-data") .WithLifetime(ContainerLifetime.Persistent); } var eventstore = builder.AddEventStore("eventstore") .WithImage("eventstore/eventstore") .WithEnvironment("EVENTSTORE_CLUSTER_SIZE", "1") .WithEnvironment("EVENTSTORE_RUN_PROJECTIONS", "All") .WithEnvironment("EVENTSTORE_START_STANDARD_PROJECTIONS", "True") .WithEnvironment("EVENTSTORE_INSECURE", "True") .WithEnvironment("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "True") .WithEndpoint( "http", e => { e.TargetPort = 2113; e.Port = 2113; e.IsProxied = true; e.IsExternal = true; }) .WithEndpoint( port: 1113, targetPort: 1113, name: "tcp", isProxied: true, isExternal: false); if (builder.ExecutionContext.IsPublishMode) { eventstore.WithDataVolume("eventstore-data") .WithLifetime(ContainerLifetime.Persistent); } // 2. Messaging Services var rabbitmqUsername = builder.AddParameter("rabbitmq-username", "guest", secret: true); var rabbitmqPassword = builder.AddParameter("rabbitmq-password", "guest", secret: true); var rabbitmq = builder.AddRabbitMQ("rabbitmq", rabbitmqUsername, rabbitmqPassword) .WithManagementPlugin() .WithEndpoint( "tcp", e => { e.TargetPort = 5672; e.Port = 5672; e.IsProxied = true; e.IsExternal = false; }) .WithEndpoint( "management", e => { e.TargetPort = 15672; e.Port = 15672; e.IsProxied = true; e.IsExternal = true; }); if (builder.ExecutionContext.IsPublishMode) { rabbitmq.WithLifetime(ContainerLifetime.Persistent); } // // 3. Observability Services var jaeger = builder.AddContainer("jaeger-all-in-one", "jaegertracing/all-in-one") .WithEndpoint( port: 6831, targetPort: 6831, name: "agent", protocol: ProtocolType.Udp, isProxied: true, isExternal: false) .WithEndpoint(port: 16686, targetPort: 16686, name: "http", isProxied: true, isExternal: true) .WithEndpoint(port: 14268, targetPort: 14268, name: "collector", isProxied: true, isExternal: false) .WithEndpoint(port: 14317, targetPort: 4317, name: "otlp-grpc", isProxied: true, isExternal: false) .WithEndpoint(port: 14318, targetPort: 4318, name: "otlp-http", isProxied: true, isExternal: false); if (builder.ExecutionContext.IsPublishMode) { jaeger.WithLifetime(ContainerLifetime.Persistent); } var zipkin = builder.AddContainer("zipkin-all-in-one", "openzipkin/zipkin") .WithEndpoint(port: 9411, targetPort: 9411, name: "http", isProxied: true, isExternal: true); if (builder.ExecutionContext.IsPublishMode) { zipkin.WithLifetime(ContainerLifetime.Persistent); } var otelCollector = builder.AddContainer("otel-collector", "otel/opentelemetry-collector-contrib") .WithBindMount( "../../../../deployments/configs/otel-collector-config.yaml", "/etc/otelcol-contrib/config.yaml", isReadOnly: true) .WithArgs("--config=/etc/otelcol-contrib/config.yaml") .WithEndpoint(port: 11888, targetPort: 1888, name: "otel-pprof", isProxied: true, isExternal: true) .WithEndpoint(port: 8888, targetPort: 8888, name: "otel-metrics", isProxied: true, isExternal: true) .WithEndpoint(port: 8889, targetPort: 8889, name: "otel-exporter-metrics", isProxied: true, isExternal: true) .WithEndpoint(port: 13133, targetPort: 13133, name: "otel-health", isProxied: true, isExternal: true) .WithEndpoint(port: 4317, targetPort: 4317, name: "otel-grpc", isProxied: true, isExternal: true) .WithEndpoint(port: 4318, targetPort: 4318, name: "otel-http", isProxied: true, isExternal: true) .WithEndpoint(port: 55679, targetPort: 55679, name: "otel-zpages", isProxied: true, isExternal: true); if (builder.ExecutionContext.IsPublishMode) { otelCollector.WithLifetime(ContainerLifetime.Persistent); } var prometheus = builder.AddContainer("prometheus", "prom/prometheus") .WithBindMount("../../../../deployments/configs/prometheus.yaml", "/etc/prometheus/prometheus.yml") .WithArgs( "--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.path=/prometheus", "--web.console.libraries=/usr/share/prometheus/console_libraries", "--web.console.templates=/usr/share/prometheus/consoles", "--web.enable-remote-write-receiver") .WithEndpoint(port: 9090, targetPort: 9090, name: "http", isProxied: true, isExternal: true); if (builder.ExecutionContext.IsPublishMode) { prometheus.WithLifetime(ContainerLifetime.Persistent); } var grafana = builder.AddContainer("grafana", "grafana/grafana") .WithEnvironment("GF_INSTALL_PLUGINS", "grafana-clock-panel,grafana-simple-json-datasource") .WithEnvironment("GF_SECURITY_ADMIN_USER", "admin") .WithEnvironment("GF_SECURITY_ADMIN_PASSWORD", "admin") .WithEnvironment("GF_FEATURE_TOGGLES_ENABLE", "traceqlEditor") .WithBindMount("../../../../deployments/configs/grafana/provisioning", "/etc/grafana/provisioning") .WithBindMount("../../../../deployments/configs/grafana/dashboards", "/var/lib/grafana/dashboards") .WithEndpoint(port: 3000, targetPort: 3000, name: "http", isProxied: true, isExternal: true); if (builder.ExecutionContext.IsPublishMode) { grafana.WithLifetime(ContainerLifetime.Persistent); } var nodeExporter = builder.AddContainer("node-exporter", "prom/node-exporter") .WithBindMount("/proc", "/host/proc", isReadOnly: true) .WithBindMount("/sys", "/host/sys", isReadOnly: true) .WithBindMount("/", "/rootfs", isReadOnly: true) .WithArgs( "--path.procfs=/host/proc", "--path.rootfs=/rootfs", "--path.sysfs=/host/sys") .WithEndpoint(port: 9101, targetPort: 9100, name: "http", isProxied: true, isExternal: true); if (builder.ExecutionContext.IsPublishMode) { nodeExporter.WithLifetime(ContainerLifetime.Persistent); } var tempo = builder.AddContainer("tempo", "grafana/tempo") .WithBindMount("../../../../deployments/configs/tempo.yaml", "/etc/tempo.yaml", isReadOnly: true) .WithArgs("--config.file=/etc/tempo.yaml") .WithEndpoint(port: 3200, targetPort: 3200, name: "http", isProxied: true, isExternal: false) .WithEndpoint(port: 9095, targetPort: 9095, name: "grpc", isProxied: true, isExternal: false) .WithEndpoint(port: 4317, targetPort: 4317, name: "otlp-grpc", isProxied: true, isExternal: false) .WithEndpoint(port: 4318, targetPort: 4318, name: "otlp-http", isProxied: true, isExternal: false); if (builder.ExecutionContext.IsPublishMode) { tempo.WithLifetime(ContainerLifetime.Persistent); } var loki = builder.AddContainer("loki", "grafana/loki") .WithBindMount("../../../../deployments/configs/loki-config.yaml", "/etc/loki/local-config.yaml", isReadOnly: true) .WithArgs("-config.file=/etc/loki/local-config.yaml") .WithEndpoint(port: 3100, targetPort: 3100, name: "http", isProxied: true, isExternal: false) .WithEndpoint(port: 9096, targetPort: 9096, name: "grpc", isProxied: true, isExternal: false); if (builder.ExecutionContext.IsPublishMode) { loki.WithLifetime(ContainerLifetime.Persistent); } var elasticsearch = builder.AddElasticsearch("elasticsearch") .WithImage("docker.elastic.co/elasticsearch/elasticsearch:8.17.0") .WithEnvironment("discovery.type", "single-node") .WithEnvironment("cluster.name", "docker-cluster") .WithEnvironment("node.name", "docker-node") .WithEnvironment("ES_JAVA_OPTS", "-Xms512m -Xmx512m") .WithEnvironment("xpack.security.enabled", "false") .WithEnvironment("xpack.security.http.ssl.enabled", "false") .WithEnvironment("xpack.security.transport.ssl.enabled", "false") .WithEnvironment("network.host", "0.0.0.0") .WithEnvironment("http.port", "9200") .WithEnvironment("transport.host", "localhost") .WithEnvironment("bootstrap.memory_lock", "true") .WithEnvironment("cluster.routing.allocation.disk.threshold_enabled", "false") .WithEndpoint( "http", e => { e.TargetPort = 9200; e.Port = 9200; e.IsProxied = true; e.IsExternal = false; }) .WithEndpoint( "internal", e => { e.TargetPort = 9300; e.Port = 9300; e.IsProxied = true; e.IsExternal = false; }) .WithDataVolume("elastic-data"); if (builder.ExecutionContext.IsPublishMode) { elasticsearch.WithLifetime(ContainerLifetime.Persistent); } var kibana = builder.AddContainer("kibana", "docker.elastic.co/kibana/kibana:8.17.0") .WithEnvironment("ELASTICSEARCH_HOSTS", "http://elasticsearch:9200") .WithEndpoint(port: 5601, targetPort: 5601, name: "http", isProxied: true, isExternal: true) .WithReference(elasticsearch) .WaitFor(elasticsearch); if (builder.ExecutionContext.IsPublishMode) { kibana.WithLifetime(ContainerLifetime.Persistent); } // 5. Application Services var identity = builder.AddProject("identity-service") .WithReference(persistMessageDb) .WaitFor(persistMessageDb) .WithReference(identityDb) .WaitFor(identityDb) .WithReference(mongo) .WaitFor(mongo) .WithReference(rabbitmq) .WaitFor(rabbitmq) .WithHttpEndpoint(port: 6005, name: "identity-http") .WithHttpsEndpoint(port: 5005, name: "identity-https"); var passenger = builder.AddProject("passenger-service") .WithReference(persistMessageDb) .WaitFor(persistMessageDb) .WithReference(passengerDb) .WaitFor(passengerDb) .WithReference(mongo) .WaitFor(mongo) .WithReference(rabbitmq) .WaitFor(rabbitmq) .WithHttpEndpoint(port: 6012, name: "passenger-http") .WithHttpsEndpoint(port: 5012, name: "passenger-https"); var flight = builder.AddProject("flight-service") .WithReference(persistMessageDb) .WaitFor(persistMessageDb) .WithReference(flightDb) .WaitFor(flightDb) .WithReference(mongo) .WaitFor(mongo) .WithReference(rabbitmq) .WaitFor(rabbitmq) .WithHttpEndpoint(port: 5004, name: "flight-http") .WithHttpsEndpoint(port: 5003, name: "flight-https"); var booking = builder.AddProject("booking-service") .WithReference(persistMessageDb) .WaitFor(persistMessageDb) .WithReference(eventstore) .WaitFor(eventstore) .WithReference(mongo) .WaitFor(mongo) .WithReference(rabbitmq) .WaitFor(rabbitmq) .WithHttpEndpoint(port: 6010, name: "booking-http") .WithHttpsEndpoint(port: 5010, name: "booking-https"); var gateway = builder.AddProject("api-gateway") .WithReference(flight) .WaitFor(flight) .WithReference(passenger) .WaitFor(passenger) .WithReference(identity) .WaitFor(identity) .WithReference(booking) .WaitFor(booking) .WithHttpEndpoint(port: 5001, name: "gateway-http") .WithHttpsEndpoint(port: 5000, name: "gateway-https"); builder.Build().Run(); ================================================ FILE: src/Aspire/src/AppHost/Properties/launchSettings.json ================================================ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "AppHost": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:18888", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://otel-collector:4317", "ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://otel-collector:4318" } } } } ================================================ FILE: src/Aspire/src/AppHost/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } ================================================ FILE: src/Aspire/src/AppHost/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: src/Aspire/src/ServiceDefaults/Extensions.cs ================================================ using BuildingBlocks.HealthCheck; using BuildingBlocks.OpenTelemetryCollector; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ServiceDefaults; public static class Extensions { public static IHostApplicationBuilder AddServiceDefaults(this WebApplicationBuilder builder) { builder.Services.AddCustomHealthCheck(); builder.AddCustomObservability(); builder.Services.AddServiceDiscovery(); builder.Services.ConfigureHttpClientDefaults(http => { http.AddStandardResilienceHandler(options => { var timeSpan = TimeSpan.FromMinutes(1); options.CircuitBreaker.SamplingDuration = timeSpan * 2; options.TotalRequestTimeout.Timeout = timeSpan * 3; options.Retry.MaxRetryAttempts = 3; }); // Turn on service discovery by default http.AddServiceDiscovery(); }); return builder; } public static WebApplication UseServiceDefaults(this WebApplication app) { app.UseCustomHealthCheck(); app.UseCustomObservability(); return app; } } ================================================ FILE: src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj ================================================  net10.0 enable enable ================================================ FILE: src/BuildingBlocks/BuildingBlocks.csproj ================================================ all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/BuildingBlocks/Caching/CachingBehavior.cs ================================================ using EasyCaching.Core; using MediatR; using Microsoft.Extensions.Logging; namespace BuildingBlocks.Caching; public class CachingBehavior : IPipelineBehavior where TRequest : notnull, IRequest where TResponse : notnull { private readonly IEasyCachingProvider _cachingProvider; private readonly ILogger> _logger; private readonly int defaultCacheExpirationInHours = 1; public CachingBehavior(IEasyCachingProviderFactory cachingFactory, ILogger> logger) { _logger = logger; _cachingProvider = cachingFactory.GetCachingProvider("mem"); } public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { if (request is not ICacheRequest cacheRequest) // No cache request found, so just continue through the pipeline return await next(); var cacheKey = cacheRequest.CacheKey; var cachedResponse = await _cachingProvider.GetAsync(cacheKey); if (cachedResponse.Value != null) { _logger.LogDebug("Response retrieved {TRequest} from cache. CacheKey: {CacheKey}", typeof(TRequest).FullName, cacheKey); return cachedResponse.Value; } var response = await next(); var expirationTime = cacheRequest.AbsoluteExpirationRelativeToNow ?? DateTime.Now.AddHours(defaultCacheExpirationInHours); await _cachingProvider.SetAsync(cacheKey, response, expirationTime.TimeOfDay); _logger.LogDebug("Caching response for {TRequest} with cache key: {CacheKey}", typeof(TRequest).FullName, cacheKey); return response; } } ================================================ FILE: src/BuildingBlocks/Caching/ICacheRequest.cs ================================================ namespace BuildingBlocks.Caching; public interface ICacheRequest { string CacheKey { get; } DateTime? AbsoluteExpirationRelativeToNow { get; } } ================================================ FILE: src/BuildingBlocks/Caching/IInvalidateCacheRequest.cs ================================================ namespace BuildingBlocks.Caching { public interface IInvalidateCacheRequest { string CacheKey { get; } } } ================================================ FILE: src/BuildingBlocks/Caching/InvalidateCachingBehavior.cs ================================================ using EasyCaching.Core; using MediatR; using Microsoft.Extensions.Logging; namespace BuildingBlocks.Caching { public class InvalidateCachingBehavior : IPipelineBehavior where TRequest : notnull, IRequest where TResponse : notnull { private readonly ILogger> _logger; private readonly IEasyCachingProvider _cachingProvider; public InvalidateCachingBehavior(IEasyCachingProviderFactory cachingFactory, ILogger> logger) { _logger = logger; _cachingProvider = cachingFactory.GetCachingProvider("mem"); } public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { if (request is not IInvalidateCacheRequest invalidateCacheRequest) { // No cache request found, so just continue through the pipeline return await next(); } var cacheKey = invalidateCacheRequest.CacheKey; var response = await next(); await _cachingProvider.RemoveAsync(cacheKey); _logger.LogDebug("Cache data with cache key: {CacheKey} removed.", cacheKey); return response; } } } ================================================ FILE: src/BuildingBlocks/Constants/IdentityConstant.cs ================================================ namespace BuildingBlocks.Constants; public static class IdentityConstant { public static class Role { public const string Admin = "admin"; public const string User = "user"; } } ================================================ FILE: src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Contracts.EventBus.Messages; public record FlightCreated(Guid Id) : IIntegrationEvent; public record FlightUpdated(Guid Id) : IIntegrationEvent; public record FlightDeleted(Guid Id) : IIntegrationEvent; public record AircraftCreated(Guid Id) : IIntegrationEvent; public record AirportCreated(Guid Id) : IIntegrationEvent; public record SeatCreated(Guid Id) : IIntegrationEvent; public record SeatReserved(Guid Id) : IIntegrationEvent; ================================================ FILE: src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Contracts.EventBus.Messages; public record UserCreated(Guid Id, string Name, string PassportNumber) : IIntegrationEvent; ================================================ FILE: src/BuildingBlocks/Contracts/EventBus.Messages/PassengerContracts.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Contracts.EventBus.Messages; public record PassengerRegistrationCompleted(Guid Id) : IIntegrationEvent; public record PassengerCreated(Guid Id) : IIntegrationEvent; ================================================ FILE: src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Contracts.EventBus.Messages; public record BookingCreated(Guid Id) : IIntegrationEvent; ================================================ FILE: src/BuildingBlocks/Core/CQRS/ICommand.cs ================================================ using MediatR; namespace BuildingBlocks.Core.CQRS; public interface ICommand : ICommand { } public interface ICommand : IRequest where T : notnull { } ================================================ FILE: src/BuildingBlocks/Core/CQRS/ICommandHandler.cs ================================================ using MediatR; namespace BuildingBlocks.Core.CQRS; public interface ICommandHandler : ICommandHandler where TCommand : ICommand { } public interface ICommandHandler : IRequestHandler where TCommand : ICommand where TResponse : notnull { } ================================================ FILE: src/BuildingBlocks/Core/CQRS/IQuery.cs ================================================ using MediatR; namespace BuildingBlocks.Core.CQRS; public interface IQuery : IRequest where T : notnull { } ================================================ FILE: src/BuildingBlocks/Core/CQRS/IQueryHandler.cs ================================================ using MediatR; namespace BuildingBlocks.Core.CQRS; public interface IQueryHandler : IRequestHandler where TQuery : IQuery where TResponse : notnull { } ================================================ FILE: src/BuildingBlocks/Core/CompositeEventMapper.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Core; public class CompositeEventMapper : IEventMapper { private readonly IEnumerable _mappers; public CompositeEventMapper(IEnumerable mappers) { _mappers = mappers; } public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event) { foreach (var mapper in _mappers) { var integrationEvent = mapper.MapToIntegrationEvent(@event); if (integrationEvent is not null) return integrationEvent; } return null; } public IInternalCommand? MapToInternalCommand(IDomainEvent @event) { foreach (var mapper in _mappers) { var internalCommand = mapper.MapToInternalCommand(@event); if (internalCommand is not null) return internalCommand; } return null; } } ================================================ FILE: src/BuildingBlocks/Core/Event/EventType.cs ================================================ namespace BuildingBlocks.Core.Event; [Flags] public enum EventType { DomainEvent = 1, IntegrationEvent = 2, InternalCommand = 4 } ================================================ FILE: src/BuildingBlocks/Core/Event/IDomainEvent.cs ================================================ namespace BuildingBlocks.Core.Event; public interface IDomainEvent : IEvent { } ================================================ FILE: src/BuildingBlocks/Core/Event/IEvent.cs ================================================ using MediatR; namespace BuildingBlocks.Core.Event; using global::MassTransit; public interface IEvent : INotification { Guid EventId => NewId.NextGuid(); public DateTime OccurredOn => DateTime.Now; public string EventType => GetType().AssemblyQualifiedName; } ================================================ FILE: src/BuildingBlocks/Core/Event/IHaveIntegrationEvent.cs ================================================ namespace BuildingBlocks.Core.Event; public interface IHaveIntegrationEvent { } ================================================ FILE: src/BuildingBlocks/Core/Event/IIntegrationEvent.cs ================================================ using MassTransit; namespace BuildingBlocks.Core.Event; [ExcludeFromTopology] public interface IIntegrationEvent : IEvent { } ================================================ FILE: src/BuildingBlocks/Core/Event/IInternalCommand.cs ================================================ namespace BuildingBlocks.Core.Event; public interface IInternalCommand : IEvent { } ================================================ FILE: src/BuildingBlocks/Core/Event/InternalCommand.cs ================================================ using BuildingBlocks.Core.CQRS; namespace BuildingBlocks.Core.Event; public record InternalCommand : IInternalCommand, ICommand; ================================================ FILE: src/BuildingBlocks/Core/Event/MessageEnvelope.cs ================================================ using Google.Protobuf; namespace BuildingBlocks.Core.Event; public class MessageEnvelope { public MessageEnvelope(object? message, IDictionary? headers = null) { Message = message; Headers = headers ?? new Dictionary(); } public object? Message { get; init; } public IDictionary Headers { get; init; } } public class MessageEnvelope : MessageEnvelope where TMessage : class, IMessage { public MessageEnvelope(TMessage message, IDictionary header) : base(message, header) { Message = message; } public new TMessage? Message { get; } } ================================================ FILE: src/BuildingBlocks/Core/EventDispatcher.cs ================================================ using System.Security.Claims; using BuildingBlocks.Core.Event; using BuildingBlocks.PersistMessageProcessor; using BuildingBlocks.Web; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MessageEnvelope = BuildingBlocks.Core.Event.MessageEnvelope; namespace BuildingBlocks.Core; public sealed class EventDispatcher( IServiceScopeFactory serviceScopeFactory, IEventMapper eventMapper, ILogger logger, IPersistMessageProcessor persistMessageProcessor, IHttpContextAccessor httpContextAccessor ) : IEventDispatcher { public async Task SendAsync(IReadOnlyList events, Type type = null, CancellationToken cancellationToken = default) where T : IEvent { if (events.Count > 0) { var eventType = type != null && type.IsAssignableTo(typeof(IInternalCommand)) ? EventType.InternalCommand : EventType.DomainEvent; async Task PublishIntegrationEvent(IReadOnlyList integrationEvents) { foreach (var integrationEvent in integrationEvents) { await persistMessageProcessor.PublishMessageAsync( new MessageEnvelope(integrationEvent, SetHeaders()), cancellationToken); } } switch (events) { case IReadOnlyList domainEvents: { var integrationEvents = await MapDomainEventToIntegrationEventAsync(domainEvents) .ConfigureAwait(false); await PublishIntegrationEvent(integrationEvents); break; } case IReadOnlyList integrationEvents: await PublishIntegrationEvent(integrationEvents); break; } if (type != null && eventType == EventType.InternalCommand) { var internalMessages = await MapDomainEventToInternalCommandAsync(events as IReadOnlyList) .ConfigureAwait(false); foreach (var internalMessage in internalMessages) { await persistMessageProcessor.AddInternalMessageAsync(internalMessage, cancellationToken); } } } } public async Task SendAsync(T @event, Type type = null, CancellationToken cancellationToken = default) where T : IEvent => await SendAsync(new[] { @event }, type, cancellationToken); private Task> MapDomainEventToIntegrationEventAsync( IReadOnlyList events) { logger.LogTrace("Processing integration events start..."); var wrappedIntegrationEvents = GetWrappedIntegrationEvents(events.ToList())?.ToList(); if (wrappedIntegrationEvents?.Count > 0) return Task.FromResult>(wrappedIntegrationEvents); var integrationEvents = new List(); using var scope = serviceScopeFactory.CreateScope(); foreach (var @event in events) { var eventType = @event.GetType(); logger.LogTrace($"Handling domain event: {eventType.Name}"); var integrationEvent = eventMapper.MapToIntegrationEvent(@event); if (integrationEvent is null) continue; integrationEvents.Add(integrationEvent); } logger.LogTrace("Processing integration events done..."); return Task.FromResult>(integrationEvents); } private Task> MapDomainEventToInternalCommandAsync( IReadOnlyList events) { logger.LogTrace("Processing internal message start..."); var internalCommands = new List(); using var scope = serviceScopeFactory.CreateScope(); foreach (var @event in events) { var eventType = @event.GetType(); logger.LogTrace($"Handling domain event: {eventType.Name}"); var integrationEvent = eventMapper.MapToInternalCommand(@event); if (integrationEvent is null) continue; internalCommands.Add(integrationEvent); } logger.LogTrace("Processing internal message done..."); return Task.FromResult>(internalCommands); } private IEnumerable GetWrappedIntegrationEvents(IReadOnlyList domainEvents) { foreach (var domainEvent in domainEvents.Where(x => x is IHaveIntegrationEvent)) { var genericType = typeof(IntegrationEventWrapper<>) .MakeGenericType(domainEvent.GetType()); var domainNotificationEvent = (IIntegrationEvent)Activator .CreateInstance(genericType, domainEvent); yield return domainNotificationEvent; } } private IDictionary SetHeaders() { var headers = new Dictionary(); headers.Add("CorrelationId", httpContextAccessor?.HttpContext?.GetCorrelationId()); headers.Add("UserId", httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier)); headers.Add("UserName", httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.Name)); return headers; } } ================================================ FILE: src/BuildingBlocks/Core/IEventDispatcher.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Core; public interface IEventDispatcher { public Task SendAsync(IReadOnlyList events, Type type = null, CancellationToken cancellationToken = default) where T : IEvent; public Task SendAsync(T @event, Type type = null, CancellationToken cancellationToken = default) where T : IEvent; } ================================================ FILE: src/BuildingBlocks/Core/IEventMapper.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Core; public interface IEventMapper { IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event); IInternalCommand? MapToInternalCommand(IDomainEvent @event); } ================================================ FILE: src/BuildingBlocks/Core/IntegrationEventWrapper.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Core; public record IntegrationEventWrapper(TDomainEventType DomainEvent) : IIntegrationEvent where TDomainEventType : IDomainEvent; ================================================ FILE: src/BuildingBlocks/Core/Model/Aggregate.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Core.Model; public abstract record Aggregate : Entity, IAggregate { private readonly List _domainEvents = new(); public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); public void AddDomainEvent(IDomainEvent domainEvent) { _domainEvents.Add(domainEvent); } public IEvent[] ClearDomainEvents() { IEvent[] dequeuedEvents = _domainEvents.ToArray(); _domainEvents.Clear(); return dequeuedEvents; } } ================================================ FILE: src/BuildingBlocks/Core/Model/Entity.cs ================================================ namespace BuildingBlocks.Core.Model; public abstract record Entity : IEntity { public T Id { get; set; } public DateTime? CreatedAt { get; set; } public long? CreatedBy { get; set; } public DateTime? LastModified { get; set; } public long? LastModifiedBy { get; set; } public bool IsDeleted { get; set; } public long Version { get; set; } } ================================================ FILE: src/BuildingBlocks/Core/Model/IAggregate.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.Core.Model; public interface IAggregate : IAggregate, IEntity { } public interface IAggregate : IEntity { IReadOnlyList DomainEvents { get; } IEvent[] ClearDomainEvents(); } ================================================ FILE: src/BuildingBlocks/Core/Model/IEntity.cs ================================================ namespace BuildingBlocks.Core.Model; public interface IEntity : IEntity { public T Id { get; set; } } public interface IEntity : IVersion { public DateTime? CreatedAt { get; set; } public long? CreatedBy { get; set; } public DateTime? LastModified { get; set; } public long? LastModifiedBy { get; set; } public bool IsDeleted { get; set; } } ================================================ FILE: src/BuildingBlocks/Core/Model/IVersion.cs ================================================ namespace BuildingBlocks.Core.Model; // For handling optimistic concurrency public interface IVersion { long Version { get; set; } } ================================================ FILE: src/BuildingBlocks/Core/Pagination/Extensions.cs ================================================ namespace BuildingBlocks.Core.Pagination; using Sieve.Models; using Sieve.Services; public static class Extensions { public static async Task> ApplyPagingAsync( this IQueryable queryable, IPageRequest pageRequest, ISieveProcessor sieveProcessor, CancellationToken cancellationToken = default ) where TEntity : class { var sieveModel = new SieveModel { PageSize = pageRequest.PageSize, Page = pageRequest.PageNumber, Sorts = pageRequest.SortOrder, Filters = pageRequest.Filters }; // https://github.com/Biarity/Sieve/issues/34#issuecomment-403817573 var result = sieveProcessor.Apply(sieveModel, queryable, applyPagination: false); var total = result.Count(); result = sieveProcessor.Apply(sieveModel, queryable, applyFiltering: false, applySorting: false); // Only applies pagination var items = await result .ToAsyncEnumerable() .ToListAsync(cancellationToken: cancellationToken); return PageList.Create(items.AsReadOnly(), pageRequest.PageNumber, pageRequest.PageSize, total); } } ================================================ FILE: src/BuildingBlocks/Core/Pagination/IPageList.cs ================================================ namespace BuildingBlocks.Core.Pagination; public interface IPageList where T : class { int CurrentPageSize { get; } int CurrentStartIndex { get; } int CurrentEndIndex { get; } int TotalPages { get; } bool HasPrevious { get; } bool HasNext { get; } IReadOnlyList Items { get; init; } int TotalCount { get; init; } int PageNumber { get; init; } int PageSize { get; init; } } ================================================ FILE: src/BuildingBlocks/Core/Pagination/IPageQuery.cs ================================================ namespace BuildingBlocks.Core.Pagination; using MediatR; public interface IPageQuery : IPageRequest, IRequest where TResponse : class { } ================================================ FILE: src/BuildingBlocks/Core/Pagination/IPageRequest.cs ================================================ namespace BuildingBlocks.Core.Pagination; public interface IPageRequest { int PageNumber { get; init; } int PageSize { get; init; } string? Filters { get; init; } string? SortOrder { get; init; } } ================================================ FILE: src/BuildingBlocks/Core/Pagination/PageList.cs ================================================ namespace BuildingBlocks.Core.Pagination; public record PageList(IReadOnlyList Items, int PageNumber, int PageSize, int TotalCount) : IPageList where T : class { public int CurrentPageSize => Items.Count; public int CurrentStartIndex => TotalCount == 0 ? 0 : ((PageNumber - 1) * PageSize) + 1; public int CurrentEndIndex => TotalCount == 0 ? 0 : CurrentStartIndex + CurrentPageSize - 1; public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); public bool HasPrevious => PageNumber > 1; public bool HasNext => PageNumber < TotalPages; public static PageList Empty => new(Enumerable.Empty().ToList(), 0, 0, 0); public static PageList Create(IReadOnlyList items, int pageNumber, int pageSize, int totalItems) { return new PageList(items, pageNumber, pageSize, totalItems); } } ================================================ FILE: src/BuildingBlocks/EFCore/AppDbContextBase.cs ================================================ using System.Collections.Immutable; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; using BuildingBlocks.Web; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; using IsolationLevel = System.Data.IsolationLevel; namespace BuildingBlocks.EFCore; public abstract class AppDbContextBase : DbContext, IDbContext { private readonly ICurrentUserProvider? _currentUserProvider; private readonly ILogger? _logger; private IDbContextTransaction _currentTransaction; protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider? currentUserProvider = null, ILogger? logger = null) : base(options) { _currentUserProvider = currentUserProvider; _logger = logger; } protected override void OnModelCreating(ModelBuilder builder) { } 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(); } // ref: https://www.meziantou.net/entity-framework-core-generate-tracking-columns.htm // ref: https://www.meziantou.net/entity-framework-core-soft-delete-using-query-filters.htm private void OnBeforeSaving() { try { foreach (var entry in ChangeTracker.Entries()) { var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate)); var userId = _currentUserProvider?.GetCurrentUserId() ?? 0; if (isAuditable) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedBy = userId; entry.Entity.CreatedAt = DateTime.Now; break; case EntityState.Modified: entry.Entity.LastModifiedBy = userId; entry.Entity.LastModified = DateTime.Now; entry.Entity.Version++; break; case EntityState.Deleted: entry.State = EntityState.Modified; entry.Entity.LastModifiedBy = userId; entry.Entity.LastModified = DateTime.Now; entry.Entity.IsDeleted = true; entry.Entity.Version++; break; } } } } catch (System.Exception ex) { throw new System.Exception("try for find IAggregate", ex); } } } ================================================ FILE: src/BuildingBlocks/EFCore/DesignTimeDbContextFactoryBase.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; namespace BuildingBlocks.EFCore { public abstract class DesignTimeDbContextFactoryBase : IDesignTimeDbContextFactory where TContext : DbContext { public TContext CreateDbContext(string[] args) { return Create(Directory.GetCurrentDirectory(), Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")); } protected abstract TContext CreateNewInstance(DbContextOptions options); public TContext Create() { var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); var basePath = AppContext.BaseDirectory; return Create(basePath, environmentName); } private TContext Create(string basePath, string environmentName) { var builder = new ConfigurationBuilder() .SetBasePath(basePath) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{environmentName}.json", true) .AddEnvironmentVariables(); var config = builder.Build(); var connstr = config.GetConnectionString("DefaultConnection"); if (string.IsNullOrWhiteSpace(connstr)) { throw new InvalidOperationException( "Could not find a connection string named 'Default'."); } return Create(connstr); } private TContext Create(string connectionString) { if (string.IsNullOrEmpty(connectionString)) throw new ArgumentException( $"{nameof(connectionString)} is null or empty.", nameof(connectionString)); var optionsBuilder = new DbContextOptionsBuilder(); Console.WriteLine("DesignTimeDbContextFactory.Create(string): Connection string: {0}", connectionString); optionsBuilder.UseSqlServer(connectionString); var options = optionsBuilder.Options; return CreateNewInstance(options); } } } ================================================ FILE: src/BuildingBlocks/EFCore/EfTxBehavior.cs ================================================ using System.Text.Json; using System.Transactions; using BuildingBlocks.Core; using BuildingBlocks.PersistMessageProcessor; using BuildingBlocks.Polly; using MediatR; using Microsoft.Extensions.Logging; namespace BuildingBlocks.EFCore; public class EfTxBehavior( ILogger> logger, IDbContext dbContextBase, IPersistMessageDbContext persistMessageDbContext, IEventDispatcher eventDispatcher ) : IPipelineBehavior where TRequest : notnull, IRequest where TResponse : notnull { public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { logger.LogInformation( "{Prefix} Handled command {MediatrRequest}", nameof(EfTxBehavior), typeof(TRequest).FullName); logger.LogDebug( "{Prefix} Handled command {MediatrRequest} with content {RequestContent}", nameof(EfTxBehavior), typeof(TRequest).FullName, JsonSerializer.Serialize(request)); logger.LogInformation( "{Prefix} Open the transaction for {MediatrRequest}", nameof(EfTxBehavior), typeof(TRequest).FullName); //ref: https://learn.microsoft.com/en-us/ef/core/saving/transactions#using-systemtransactions using var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }, TransactionScopeAsyncFlowOption.Enabled); var response = await next(); logger.LogInformation( "{Prefix} Executed the {MediatrRequest} request", nameof(EfTxBehavior), typeof(TRequest).FullName); while (true) { var domainEvents = dbContextBase.GetDomainEvents(); if (domainEvents is null || !domainEvents.Any()) { return response; } await eventDispatcher.SendAsync(domainEvents.ToArray(), typeof(TRequest), cancellationToken); // Save data to database with some retry policy in distributed transaction await dbContextBase.RetryOnFailure(async () => { await dbContextBase.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; } } } ================================================ FILE: src/BuildingBlocks/EFCore/Extensions.cs ================================================ using System.Linq.Expressions; using BuildingBlocks.Core.Model; using BuildingBlocks.Web; using Humanizer; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace BuildingBlocks.EFCore; public static class Extensions { public static IServiceCollection AddCustomDbContext(this WebApplicationBuilder builder, string? connectionName = "") where TContext : DbContext, IDbContext { AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); builder.Services.AddValidateOptions(); builder.Services.AddDbContext( (sp, options) => { var aspireConnectionString = builder.Configuration.GetConnectionString(connectionName.Kebaberize()); var connectionString = aspireConnectionString ?? sp.GetRequiredService().ConnectionString; ArgumentException.ThrowIfNullOrEmpty(connectionString); options.UseNpgsql( connectionString, dbOptions => { dbOptions.MigrationsAssembly(typeof(TContext).Assembly.GetName().Name); }) .UseSnakeCaseNamingConvention(); // Suppress warnings for pending model changes options.ConfigureWarnings( w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); }); builder.Services.AddScoped(); builder.Services.AddScoped(sp => sp.GetRequiredService()); return builder.Services; } public static IApplicationBuilder UseMigration(this IApplicationBuilder app) where TContext : DbContext, IDbContext { MigrateAsync(app.ApplicationServices).GetAwaiter().GetResult(); SeedAsync(app.ApplicationServices).GetAwaiter().GetResult(); return app; } // ref: https://github.com/pdevito3/MessageBusTestingInMemHarness/blob/main/RecipeManagement/src/RecipeManagement/Databases/RecipesDbContext.cs public static void FilterSoftDeletedProperties(this ModelBuilder modelBuilder) { Expression> filterExpr = e => !e.IsDeleted; foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes() .Where(m => m.ClrType.IsAssignableTo(typeof(IEntity)))) { // modify expression to handle correct child type var parameter = Expression.Parameter(mutableEntityType.ClrType); var body = ReplacingExpressionVisitor .Replace(filterExpr.Parameters.First(), parameter, filterExpr.Body); var lambdaExpression = Expression.Lambda(body, parameter); // set filter mutableEntityType.SetQueryFilter(lambdaExpression); } } // ref: https://andrewlock.net/customising-asp-net-core-identity-ef-core-naming-conventions-for-postgresql/ public static void ToSnakeCaseTables(this ModelBuilder modelBuilder) { foreach (var entity in modelBuilder.Model.GetEntityTypes()) { // Replace table names entity.SetTableName(entity.GetTableName()?.Underscore()); var tableObjectIdentifier = StoreObjectIdentifier.Table( entity.GetTableName()?.Underscore()!, entity.GetSchema()); // Replace column names foreach (var property in entity.GetProperties()) { property.SetColumnName(property.GetColumnName(tableObjectIdentifier)?.Underscore()); } foreach (var key in entity.GetKeys()) { key.SetName(key.GetName()?.Underscore()); } foreach (var key in entity.GetForeignKeys()) { key.SetConstraintName(key.GetConstraintName()?.Underscore()); } } } private static async Task MigrateAsync(IServiceProvider serviceProvider) where TContext : DbContext, IDbContext { await using var scope = serviceProvider.CreateAsyncScope(); var context = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService>(); var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); if (pendingMigrations.Any()) { logger.LogInformation("Applying {Count} pending migrations...", pendingMigrations.Count()); await context.Database.MigrateAsync(); logger.LogInformation("Migrations applied successfully."); } } private static async Task SeedAsync(IServiceProvider serviceProvider) { await using var scope = serviceProvider.CreateAsyncScope(); var seedersManager = scope.ServiceProvider.GetRequiredService(); await seedersManager.ExecuteSeedAsync(); } } ================================================ FILE: src/BuildingBlocks/EFCore/IDataSeeder.cs ================================================ namespace BuildingBlocks.EFCore { public interface IDataSeeder { Task SeedAllAsync(); } public interface ITestDataSeeder { Task SeedAllAsync(); } } ================================================ FILE: src/BuildingBlocks/EFCore/IDbContext.cs ================================================ using BuildingBlocks.Core.Event; using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.EFCore; using Microsoft.EntityFrameworkCore.Storage; public interface IDbContext { DbSet Set() where TEntity : class; IReadOnlyList GetDomainEvents(); Task SaveChangesAsync(CancellationToken cancellationToken = default); Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task CommitTransactionAsync(CancellationToken cancellationToken = default); Task RollbackTransactionAsync(CancellationToken cancellationToken = default); IExecutionStrategy CreateExecutionStrategy(); Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default); } ================================================ FILE: src/BuildingBlocks/EFCore/ISeedManager.cs ================================================ namespace BuildingBlocks.EFCore; public interface ISeedManager { Task ExecuteSeedAsync(); Task ExecuteTestSeedAsync(); } ================================================ FILE: src/BuildingBlocks/EFCore/PostgresOptions.cs ================================================ namespace BuildingBlocks.EFCore; public class PostgresOptions { public string ConnectionString { get; set; } } ================================================ FILE: src/BuildingBlocks/EFCore/SeedManagers.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace BuildingBlocks.EFCore; public class SeedManager( ILogger logger, IWebHostEnvironment env, IServiceProvider serviceProvider ) : ISeedManager { public async Task ExecuteSeedAsync() { if (!env.IsEnvironment("test")) { await using var scope = serviceProvider.CreateAsyncScope(); var dataSeeders = scope.ServiceProvider.GetServices(); foreach (var seeder in dataSeeders) { logger.LogInformation("Seed {SeederName} is started.", seeder.GetType().Name); await seeder.SeedAllAsync(); logger.LogInformation("Seed {SeederName} is completed.", seeder.GetType().Name); } } } public async Task ExecuteTestSeedAsync() { await using var scope = serviceProvider.CreateAsyncScope(); var testDataSeeders = scope.ServiceProvider.GetServices(); foreach (var testSeeder in testDataSeeders) { logger.LogInformation("Seed {SeederName} is started.", testSeeder.GetType().Name); await testSeeder.SeedAllAsync(); logger.LogInformation("Seed {SeederName} is completed.", testSeeder.GetType().Name); } } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/BackgroundWorkers/BackgroundWorker.cs ================================================ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace BuildingBlocks.EventStoreDB.BackgroundWorkers; public class BackgroundWorker : BackgroundService { private readonly ILogger logger; private readonly Func perform; public BackgroundWorker( ILogger logger, Func perform ) { this.logger = logger; this.perform = perform; } protected override Task ExecuteAsync(CancellationToken stoppingToken) => Task.Run(async () => { await Task.Yield(); logger.LogInformation("Background worker stopped"); await perform(stoppingToken); logger.LogInformation("Background worker stopped"); }, stoppingToken); } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Config.cs ================================================ using System.Reflection; using BuildingBlocks.EventStoreDB.BackgroundWorkers; using BuildingBlocks.EventStoreDB.Projections; using BuildingBlocks.EventStoreDB.Repository; using BuildingBlocks.EventStoreDB.Subscriptions; using EventStore.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace BuildingBlocks.EventStoreDB; using Web; public class EventStoreOptions { public string ConnectionString { get; set; } = default!; } public record EventStoreDBOptions( bool UseInternalCheckpointing = true ); public static class EventStoreDBConfigExtensions { public static IServiceCollection AddEventStoreDB(this IServiceCollection services, IConfiguration configuration, EventStoreDBOptions? options = null) { services .AddSingleton(x => { var aspireConnectionString = configuration.GetConnectionString("eventstore"); var eventStoreOptions = services.GetOptions(nameof(EventStoreOptions)); return new EventStoreClient(EventStoreClientSettings.Create(aspireConnectionString ?? eventStoreOptions.ConnectionString)); }) .AddScoped(typeof(IEventStoreDBRepository<>), typeof(EventStoreDBRepository<>)) .AddTransient(); if (options?.UseInternalCheckpointing != false) services.AddTransient(); return services; } public static IServiceCollection AddEventStoreDBSubscriptionToAll( this IServiceCollection services, EventStoreDBSubscriptionToAllOptions? subscriptionOptions = null, bool checkpointToEventStoreDB = true) { if (checkpointToEventStoreDB) services.AddTransient(); return services.AddHostedService(serviceProvider => { var logger = serviceProvider.GetRequiredService>(); var eventStoreDBSubscriptionToAll = serviceProvider.GetRequiredService(); return new BackgroundWorker( logger, ct => eventStoreDBSubscriptionToAll.SubscribeToAll( subscriptionOptions ?? new EventStoreDBSubscriptionToAllOptions(), ct ) ); } ); } public static IServiceCollection AddProjections(this IServiceCollection services, params Assembly[] assembliesToScan) { services.AddSingleton(); RegisterProjections(services, assembliesToScan!); return services; } private static void RegisterProjections(IServiceCollection services, Assembly[] assembliesToScan) { services.Scan(scan => scan .FromAssemblies(assembliesToScan) .AddClasses(classes => classes.AssignableTo()) // Filter classes .AsImplementedInterfaces() .WithTransientLifetime()); } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/AggregateEventSourcing.cs ================================================ using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; namespace BuildingBlocks.EventStoreDB.Events { public abstract record AggregateEventSourcing : Entity, IAggregateEventSourcing { private readonly List _domainEvents = new(); public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); public void AddDomainEvent(IDomainEvent domainEvent) { _domainEvents.Add(domainEvent); } public IDomainEvent[] ClearDomainEvents() { var dequeuedEvents = _domainEvents.ToArray(); _domainEvents.Clear(); return dequeuedEvents; } public virtual void When(object @event) { } } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs ================================================ using BuildingBlocks.EventStoreDB.Serialization; using EventStore.Client; namespace BuildingBlocks.EventStoreDB.Events; public static class AggregateStreamExtensions { public static async Task AggregateStream( this EventStoreClient eventStore, Guid id, CancellationToken cancellationToken, ulong? fromVersion = null ) where T : class, IProjection { var readResult = eventStore.ReadStreamAsync( Direction.Forwards, StreamNameMapper.ToStreamId(id), fromVersion ?? StreamPosition.Start, cancellationToken: cancellationToken ); // TODO: consider adding extension method for the aggregation and deserialization var aggregate = (T)Activator.CreateInstance(typeof(T), true)!; if (await readResult.ReadState == ReadState.StreamNotFound) { return null; } await foreach (var @event in readResult) { var eventData = @event.Deserialize(); aggregate.When(eventData!); } return aggregate; } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/EventTypeMapper.cs ================================================ using System.Collections.Concurrent; using BuildingBlocks.Utils; namespace BuildingBlocks.EventStoreDB.Events; public class EventTypeMapper { private static readonly EventTypeMapper Instance = new(); private readonly ConcurrentDictionary typeMap = new(); private readonly ConcurrentDictionary typeNameMap = new(); public static void AddCustomMap(string mappedEventTypeName) => AddCustomMap(typeof(T), mappedEventTypeName); public static void AddCustomMap(Type eventType, string mappedEventTypeName) { Instance.typeNameMap.AddOrUpdate(eventType, mappedEventTypeName, (_, _) => mappedEventTypeName); Instance.typeMap.AddOrUpdate(mappedEventTypeName, eventType, (_, _) => eventType); } public static string ToName() => ToName(typeof(TEventType)); public static string ToName(Type eventType) => Instance.typeNameMap.GetOrAdd(eventType, _ => { var eventTypeName = eventType.FullName!.Replace(".", "_", StringComparison.CurrentCulture); Instance.typeMap.AddOrUpdate(eventTypeName, eventType, (_, _) => eventType); return eventTypeName; }); public static Type? ToType(string eventTypeName) => Instance.typeMap.GetOrAdd(eventTypeName, _ => { var type = TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(eventTypeName.Replace("_", ".", StringComparison.CurrentCulture)); if (type == null) return null; Instance.typeNameMap.AddOrUpdate(type, eventTypeName, (_, _) => eventTypeName); return type; }); } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/IAggregateEventSourcing.cs ================================================ using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; namespace BuildingBlocks.EventStoreDB.Events { public interface IAggregateEventSourcing : IProjection, IEntity { IReadOnlyList DomainEvents { get; } IDomainEvent[] ClearDomainEvents(); } public interface IAggregateEventSourcing : IAggregateEventSourcing, IEntity { } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs ================================================ using BuildingBlocks.Core.Event; using MediatR; namespace BuildingBlocks.EventStoreDB.Events; public interface IEventHandler : INotificationHandler where TEvent : IEvent { } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.EventStoreDB.Events; public interface IExternalEvent : IEvent { } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/IProjection.cs ================================================ namespace BuildingBlocks.EventStoreDB.Events; public interface IProjection { void When(object @event); } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs ================================================ using BuildingBlocks.Core.Event; namespace BuildingBlocks.EventStoreDB.Events; public record EventMetadata( ulong StreamRevision, ulong LogPosition ); public class StreamEvent : IEvent { public object Data { get; } public EventMetadata Metadata { get; } public StreamEvent(object data, EventMetadata metadata) { Data = data; Metadata = metadata; } } public class StreamEvent : StreamEvent where T : notnull { public new T Data => (T)base.Data; public StreamEvent(T data, EventMetadata metadata) : base(data, metadata) { } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/StreamEventExtensions.cs ================================================ using System.Diagnostics.Eventing.Reader; using BuildingBlocks.EventStoreDB.Serialization; using EventStore.Client; namespace BuildingBlocks.EventStoreDB.Events; public static class StreamEventExtensions { public static StreamEvent? ToStreamEvent(this ResolvedEvent resolvedEvent) { var eventData = resolvedEvent.Deserialize(); if (eventData == null) return null; var metaData = new EventMetadata(resolvedEvent.Event.EventNumber.ToUInt64(), resolvedEvent.Event.Position.CommitPosition); var type = typeof(StreamEvent<>).MakeGenericType(eventData.GetType()); return (StreamEvent)Activator.CreateInstance(type, eventData, metaData)!; } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Events/StreamNameMapper.cs ================================================ using System.Collections.Concurrent; namespace BuildingBlocks.EventStoreDB.Events; public class StreamNameMapper { private static readonly StreamNameMapper Instance = new(); private readonly ConcurrentDictionary TypeNameMap = new(); public static void AddCustomMap(string mappedStreamName) => AddCustomMap(typeof(TStream), mappedStreamName); public static void AddCustomMap(Type streamType, string mappedStreamName) { Instance.TypeNameMap.AddOrUpdate(streamType, mappedStreamName, (_, _) => mappedStreamName); } public static string ToStreamId(object aggregateId, object? tenantId = null) => ToStreamId(typeof(TStream), aggregateId); public static string ToStreamId(Type streamType, object aggregateId, object? tenantId = null) { var tenantPrefix = tenantId != null ? $"{tenantId}_" : ""; return $"{tenantPrefix}{streamType.Name}-{aggregateId}"; } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Extensions.cs ================================================ using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.EventStoreDB; using Web; public static class Extensions { // ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce public static IServiceCollection AddEventStore( this IServiceCollection services, IConfiguration configuration, params Assembly[] assemblies ) { services.AddValidateOptions(); var assembliesToScan = assemblies.Length > 0 ? assemblies : new[] { Assembly.GetEntryAssembly()! }; return services .AddEventStoreDB(configuration) .AddProjections(assembliesToScan); } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Projections/IProjectionProcessor.cs ================================================ using BuildingBlocks.EventStoreDB.Events; using MediatR; namespace BuildingBlocks.EventStoreDB.Projections; public interface IProjectionProcessor { Task ProcessEventAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) where T : INotification; } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Projections/IProjectionPublisher.cs ================================================ using BuildingBlocks.EventStoreDB.Events; using MediatR; namespace BuildingBlocks.EventStoreDB.Projections; public interface IProjectionPublisher { Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) where T : INotification; Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default); } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Projections/ProjectionPublisher.cs ================================================ using BuildingBlocks.EventStoreDB.Events; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.EventStoreDB.Projections; public class ProjectionPublisher : IProjectionPublisher { private readonly IServiceProvider _serviceProvider; public ProjectionPublisher(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) where T : INotification { using var scope = _serviceProvider.CreateScope(); var projectionsProcessors = scope.ServiceProvider.GetRequiredService>(); foreach (var projectionProcessor in projectionsProcessors) { await projectionProcessor.ProcessEventAsync(streamEvent, cancellationToken); } } public Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) { var streamData = streamEvent.Data.GetType(); var method = typeof(IProjectionPublisher) .GetMethods() .Single(m => m.Name == nameof(PublishAsync) && m.GetGenericArguments().Any()) .MakeGenericMethod(streamData); return (Task)method .Invoke(this, new object[] { streamEvent, cancellationToken })!; } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Repository/EventStoreDBRepository.cs ================================================ using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.EventStoreDB.Serialization; using EventStore.Client; namespace BuildingBlocks.EventStoreDB.Repository; public interface IEventStoreDBRepository where T : class, IAggregateEventSourcing { Task Find(Guid id, CancellationToken cancellationToken); Task Add(T aggregate, CancellationToken cancellationToken); Task Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default); Task Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default); } public class EventStoreDBRepository : IEventStoreDBRepository where T : class, IAggregateEventSourcing { private static readonly long _currentUserId; private readonly EventStoreClient eventStore; public EventStoreDBRepository(EventStoreClient eventStore) { this.eventStore = eventStore ?? throw new ArgumentNullException(nameof(eventStore)); } public Task Find(Guid id, CancellationToken cancellationToken) { return eventStore.AggregateStream( id, cancellationToken ); } public async Task Add(T aggregate, CancellationToken cancellationToken = default) { var result = await eventStore.AppendToStreamAsync( StreamNameMapper.ToStreamId(aggregate.Id), StreamState.NoStream, GetEventsToStore(aggregate), cancellationToken: cancellationToken ); return result.NextExpectedStreamRevision; } public async Task Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default) { var nextVersion = expectedRevision ?? aggregate.Version; var result = await eventStore.AppendToStreamAsync( StreamNameMapper.ToStreamId(aggregate.Id), (ulong)nextVersion, GetEventsToStore(aggregate), cancellationToken: cancellationToken ); return result.NextExpectedStreamRevision; } public Task Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default) { return Update(aggregate, expectedRevision, cancellationToken); } private static IEnumerable GetEventsToStore(T aggregate) { var events = aggregate.ClearDomainEvents(); return events .Select(EventStoreDBSerializer.ToJsonEventData); } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs ================================================ using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.Exception; namespace BuildingBlocks.EventStoreDB.Repository; public static class RepositoryExtensions { public static async Task Get( this IEventStoreDBRepository repository, Guid id, CancellationToken cancellationToken ) where T : class, IAggregateEventSourcing { var entity = await repository.Find(id, cancellationToken); return entity ?? throw AggregateNotFoundException.For(id); } public static async Task GetAndUpdate( this IEventStoreDBRepository repository, Guid id, Action action, long? expectedVersion = null, CancellationToken cancellationToken = default ) where T : class, IAggregateEventSourcing { var entity = await repository.Get(id, cancellationToken); action(entity); return await repository.Update(entity, expectedVersion, cancellationToken); } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Serialization/EventStoreDBSerializer.cs ================================================ using System.Text; using BuildingBlocks.EventStoreDB.Events; using EventStore.Client; using Newtonsoft.Json; namespace BuildingBlocks.EventStoreDB.Serialization; public static class EventStoreDBSerializer { private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings().WithNonDefaultConstructorContractResolver(); public static T? Deserialize(this ResolvedEvent resolvedEvent) where T : class => Deserialize(resolvedEvent) as T; public static object? Deserialize(this ResolvedEvent resolvedEvent) { // get type var eventType = EventTypeMapper.ToType(resolvedEvent.Event.EventType); if (eventType == null) return null; // deserialize event return JsonConvert.DeserializeObject( Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span), eventType, SerializerSettings )!; } public static EventData ToJsonEventData(this object @event) => new( Uuid.NewUuid(), EventTypeMapper.ToName(@event.GetType()), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(@event)), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { })) ); } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Serialization/JsonObjectContractProvider.cs ================================================ using System.Collections.Concurrent; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace BuildingBlocks.EventStoreDB.Serialization; public static class JsonObjectContractProvider { private static readonly Type ConstructorAttributeType = typeof(JsonConstructorAttribute); private static readonly ConcurrentDictionary Constructors = new(); public static JsonObjectContract UsingNonDefaultConstructor( JsonObjectContract contract, Type objectType, Func> createConstructorParameters) => Constructors.GetOrAdd(objectType.AssemblyQualifiedName!, _ => { var nonDefaultConstructor = GetNonDefaultConstructor(objectType); if (nonDefaultConstructor == null) return contract; contract.OverrideCreator = GetObjectConstructor(nonDefaultConstructor); contract.CreatorParameters.Clear(); foreach (var constructorParameter in createConstructorParameters(nonDefaultConstructor, contract.Properties)) { contract.CreatorParameters.Add(constructorParameter); } return contract; }); private static ObjectConstructor GetObjectConstructor(MethodBase method) { var c = method as ConstructorInfo; if (c == null) return a => method.Invoke(null, a)!; if (!c.GetParameters().Any()) return _ => c.Invoke(Array.Empty()); return a => c.Invoke(a); } private static ConstructorInfo? GetNonDefaultConstructor(Type objectType) { // Use default contract for non-object types. if (objectType.IsPrimitive || objectType.IsEnum) return null; return GetAttributeConstructor(objectType) ?? GetTheMostSpecificConstructor(objectType); } private static ConstructorInfo? GetAttributeConstructor(Type objectType) { // Use default contract for non-object types. if (objectType.IsPrimitive || objectType.IsEnum) return null; var constructors = objectType .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(c => c.GetCustomAttributes().Any(a => a.GetType() == ConstructorAttributeType)).ToList(); return constructors.Count switch { 1 => constructors[0], > 1 => throw new JsonException($"Multiple constructors with a {ConstructorAttributeType.Name}."), _ => null }; } private static ConstructorInfo? GetTheMostSpecificConstructor(Type objectType) => objectType .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .OrderByDescending(e => e.GetParameters().Length) .FirstOrDefault(); } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Serialization/NonDefaultConstructorContractResolver.cs ================================================ using Newtonsoft.Json.Serialization; namespace BuildingBlocks.EventStoreDB.Serialization; public class NonDefaultConstructorContractResolver : DefaultContractResolver { protected override JsonObjectContract CreateObjectContract(Type objectType) { return JsonObjectContractProvider.UsingNonDefaultConstructor( base.CreateObjectContract(objectType), objectType, base.CreateConstructorParameters ); } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Serialization/SerializationExtensions.cs ================================================ using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace BuildingBlocks.EventStoreDB.Serialization; public static class SerializationExtensions { public static JsonSerializerSettings WithDefaults(this JsonSerializerSettings settings) { settings.WithNonDefaultConstructorContractResolver() .Converters.Add(new StringEnumConverter()); return settings; } public static JsonSerializerSettings WithNonDefaultConstructorContractResolver(this JsonSerializerSettings settings) { settings.ContractResolver = new NonDefaultConstructorContractResolver(); return settings; } /// /// Deserialize object from json with JsonNet /// /// Type of the deserialized object /// json string /// deserialized object public static T FromJson(this string json) { return JsonConvert.DeserializeObject(json, new JsonSerializerSettings().WithNonDefaultConstructorContractResolver())!; } /// /// Deserialize object from json with JsonNet /// /// Type of the deserialized object /// json string /// object type /// deserialized object public static object FromJson(this string json, Type type) { return JsonConvert.DeserializeObject(json, type, new JsonSerializerSettings().WithNonDefaultConstructorContractResolver())!; } /// /// Serialize object to json with JsonNet /// /// object to serialize /// json string public static string ToJson(this object obj) { return JsonConvert.SerializeObject(obj); } /// /// Serialize object to json with JsonNet /// /// object to serialize /// json string public static StringContent ToJsonStringContent(this object obj) { return new StringContent(obj.ToJson(), Encoding.UTF8, "application/json"); } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs ================================================ using BuildingBlocks.Core.Event; using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.EventStoreDB.Serialization; using EventStore.Client; namespace BuildingBlocks.EventStoreDB.Subscriptions; public record CheckpointStored(string SubscriptionId, ulong? Position, DateTime CheckpointedAt) : IEvent; public class EventStoreDBSubscriptionCheckpointRepository : ISubscriptionCheckpointRepository { private readonly EventStoreClient eventStoreClient; public EventStoreDBSubscriptionCheckpointRepository( EventStoreClient eventStoreClient) { this.eventStoreClient = eventStoreClient ?? throw new ArgumentNullException(nameof(eventStoreClient)); } public async ValueTask Load(string subscriptionId, CancellationToken ct) { var streamName = GetCheckpointStreamName(subscriptionId); var result = eventStoreClient.ReadStreamAsync(Direction.Backwards, streamName, StreamPosition.End, 1, cancellationToken: ct); if (await result.ReadState == ReadState.StreamNotFound) { return null; } ResolvedEvent? @event = await result.FirstOrDefaultAsync(ct); return @event?.Deserialize()?.Position; } public async ValueTask Store(string subscriptionId, ulong position, CancellationToken ct) { var @event = new CheckpointStored(subscriptionId, position, DateTime.UtcNow); var eventToAppend = new[] { @event.ToJsonEventData() }; var streamName = GetCheckpointStreamName(subscriptionId); try { // store new checkpoint expecting stream to exist await eventStoreClient.AppendToStreamAsync( streamName, StreamState.StreamExists, eventToAppend, cancellationToken: ct ); } catch (WrongExpectedVersionException) { // WrongExpectedVersionException means that stream did not exist // Set the checkpoint stream to have at most 1 event // using stream metadata $maxCount property await eventStoreClient.SetStreamMetadataAsync( streamName, StreamState.NoStream, new StreamMetadata(1), cancellationToken: ct ); // append event again expecting stream to not exist await eventStoreClient.AppendToStreamAsync( streamName, StreamState.NoStream, eventToAppend, cancellationToken: ct ); } } private static string GetCheckpointStreamName(string subscriptionId) => $"checkpoint_{subscriptionId}"; } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionToAll.cs ================================================ using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.EventStoreDB.Projections; using BuildingBlocks.Utils; using EventStore.Client; using Grpc.Core; using MediatR; using Microsoft.Extensions.Logging; namespace BuildingBlocks.EventStoreDB.Subscriptions; public class EventStoreDBSubscriptionToAllOptions { public string SubscriptionId { get; set; } = "default"; public SubscriptionFilterOptions FilterOptions { get; set; } = new(EventTypeFilter.ExcludeSystemEvents()); public Action? ConfigureOperation { get; set; } public UserCredentials? Credentials { get; set; } public bool ResolveLinkTos { get; set; } public bool IgnoreDeserializationErrors { get; set; } = true; } public class EventStoreDBSubscriptionToAll { private readonly IProjectionPublisher projectionPublisher; private readonly EventStoreClient eventStoreClient; private readonly IMediator _mediator; private readonly ISubscriptionCheckpointRepository checkpointRepository; private readonly ILogger logger; private EventStoreDBSubscriptionToAllOptions subscriptionOptions = default!; private string SubscriptionId => subscriptionOptions.SubscriptionId; private readonly object resubscribeLock = new(); private CancellationToken cancellationToken; public EventStoreDBSubscriptionToAll( EventStoreClient eventStoreClient, IMediator mediator, IProjectionPublisher projectionPublisher, ISubscriptionCheckpointRepository checkpointRepository, ILogger logger ) { this.projectionPublisher = projectionPublisher; this.eventStoreClient = eventStoreClient ?? throw new ArgumentNullException(nameof(eventStoreClient)); _mediator = mediator; this.checkpointRepository = checkpointRepository ?? throw new ArgumentNullException(nameof(checkpointRepository)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task SubscribeToAll(EventStoreDBSubscriptionToAllOptions subscriptionOptions, CancellationToken ct) { // see: https://github.com/dotnet/runtime/issues/36063 await Task.Yield(); this.subscriptionOptions = subscriptionOptions; cancellationToken = ct; logger.LogInformation("Subscription to all '{SubscriptionId}'", subscriptionOptions.SubscriptionId); var checkpoint = await checkpointRepository.Load(SubscriptionId, ct); await eventStoreClient.SubscribeToAllAsync( checkpoint == null ? FromAll.Start : FromAll.After(new Position(checkpoint.Value, checkpoint.Value)), HandleEvent, subscriptionOptions.ResolveLinkTos, HandleDrop, subscriptionOptions.FilterOptions, subscriptionOptions.Credentials, ct ); logger.LogInformation("Subscription to all '{SubscriptionId}' started", SubscriptionId); } private async Task HandleEvent(StreamSubscription subscription, ResolvedEvent resolvedEvent, CancellationToken ct) { try { if (IsEventWithEmptyData(resolvedEvent) || IsCheckpointEvent(resolvedEvent)) return; var streamEvent = resolvedEvent.ToStreamEvent(); if (streamEvent == null) { // That can happen if we're sharing database between modules. // If we're subscribing to all and not filtering out events from other modules, // then we might get events that are from other module and we might not be able to deserialize them. // In that case it's safe to ignore deserialization error. // You may add more sophisticated logic checking if it should be ignored or not. logger.LogWarning("Couldn't deserialize event with id: {EventId}", resolvedEvent.Event.EventId); if (!subscriptionOptions.IgnoreDeserializationErrors) throw new InvalidOperationException($"Unable to deserialize event {resolvedEvent.Event.EventType} with id: {resolvedEvent.Event.EventId}"); return; } // publish event to internal event bus await _mediator.Publish(streamEvent, ct); await projectionPublisher.PublishAsync(streamEvent, ct); await checkpointRepository.Store(SubscriptionId, resolvedEvent.Event.Position.CommitPosition, ct); } catch (System.Exception e) { logger.LogError("Error consuming message: {ExceptionMessage}{ExceptionStackTrace}", e.Message, e.StackTrace); // if you're fine with dropping some events instead of stopping subscription // then you can add some logic if error should be ignored throw; } } private void HandleDrop(StreamSubscription _, SubscriptionDroppedReason reason, System.Exception? exception) { logger.LogError( exception, "Subscription to all '{SubscriptionId}' dropped with '{Reason}'", SubscriptionId, reason ); if (exception is RpcException { StatusCode: StatusCode.Cancelled }) return; Resubscribe(); } private void Resubscribe() { // You may consider adding a max resubscribe count if you want to fail process // instead of retrying until database is up while (true) { var resubscribed = false; try { Monitor.Enter(resubscribeLock); // No synchronization context is needed to disable synchronization context. // That enables running asynchronous method not causing deadlocks. // As this is a background process then we don't need to have async context here. using (NoSynchronizationContextScope.Enter()) { SubscribeToAll(subscriptionOptions, cancellationToken).Wait(cancellationToken); } resubscribed = true; } catch (System.Exception exception) { logger.LogWarning(exception, "Failed to resubscribe to all '{SubscriptionId}' dropped with '{ExceptionMessage}{ExceptionStackTrace}'", SubscriptionId, exception.Message, exception.StackTrace); } finally { Monitor.Exit(resubscribeLock); } if (resubscribed) break; // Sleep between reconnections to not flood the database or not kill the CPU with infinite loop // Randomness added to reduce the chance of multiple subscriptions trying to reconnect at the same time Thread.Sleep(1000 + new Random((int)DateTime.UtcNow.Ticks).Next(1000)); } } private bool IsEventWithEmptyData(ResolvedEvent resolvedEvent) { if (resolvedEvent.Event.Data.Length != 0) return false; logger.LogInformation("Event without data received"); return true; } private bool IsCheckpointEvent(ResolvedEvent resolvedEvent) { if (resolvedEvent.Event.EventType != EventTypeMapper.ToName()) return false; logger.LogInformation("Checkpoint event - ignoring"); return true; } } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Subscriptions/ISubscriptionCheckpointRepository.cs ================================================ namespace BuildingBlocks.EventStoreDB.Subscriptions; public interface ISubscriptionCheckpointRepository { ValueTask Load(string subscriptionId, CancellationToken ct); ValueTask Store(string subscriptionId, ulong position, CancellationToken ct); } ================================================ FILE: src/BuildingBlocks/EventStoreDB/Subscriptions/InMemorySubscriptionCheckpointRepository.cs ================================================ using System.Collections.Concurrent; namespace BuildingBlocks.EventStoreDB.Subscriptions; public class InMemorySubscriptionCheckpointRepository : ISubscriptionCheckpointRepository { private readonly ConcurrentDictionary checkpoints = new(); public ValueTask Load(string subscriptionId, CancellationToken ct) { return new(checkpoints.TryGetValue(subscriptionId, out var checkpoint) ? checkpoint : null); } public ValueTask Store(string subscriptionId, ulong position, CancellationToken ct) { checkpoints.AddOrUpdate(subscriptionId, position, (_, _) => position); return ValueTask.CompletedTask; } } ================================================ FILE: src/BuildingBlocks/Exception/AggregateNotFoundException.cs ================================================ namespace BuildingBlocks.Exception; public class AggregateNotFoundException : System.Exception { public AggregateNotFoundException(string typeName, Guid id) : base($"{typeName} with id '{id}' was not found") { } public static AggregateNotFoundException For(Guid id) { return new AggregateNotFoundException(typeof(T).Name, id); } } ================================================ FILE: src/BuildingBlocks/Exception/AppException.cs ================================================ using System.Net; namespace BuildingBlocks.Exception; public class AppException : CustomException { public AppException(string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest, int? code = null) : base(message, statusCode, code: code) { } public AppException(string message, System.Exception innerException, HttpStatusCode statusCode = HttpStatusCode.BadRequest, int? code = null) : base(message, innerException, statusCode, code) { } } ================================================ FILE: src/BuildingBlocks/Exception/BadRequestException.cs ================================================ using System; using System.Net; namespace BuildingBlocks.Exception { public class BadRequestException : CustomException { public BadRequestException(string message, int? code = null) : base(message, HttpStatusCode.BadRequest, code: code) { } } } ================================================ FILE: src/BuildingBlocks/Exception/ConflictException.cs ================================================ using System.Net; namespace BuildingBlocks.Exception { public class ConflictException : CustomException { public ConflictException(string message, int? code = null) : base(message, HttpStatusCode.Conflict, code: code) { } } } ================================================ FILE: src/BuildingBlocks/Exception/CustomException.cs ================================================ using System.Net; namespace BuildingBlocks.Exception; public class CustomException : System.Exception { public CustomException( string message, HttpStatusCode statusCode = HttpStatusCode.InternalServerError, int? code = null) : base(message) { StatusCode = statusCode; Code = code; } public CustomException( string message, System.Exception innerException, HttpStatusCode statusCode = HttpStatusCode.InternalServerError, int? code = null) : base(message, innerException) { StatusCode = statusCode; Code = code; } public CustomException( HttpStatusCode statusCode = HttpStatusCode.InternalServerError, int? code = null) : base() { StatusCode = statusCode; Code = code; } public HttpStatusCode StatusCode { get; } public int? Code { get; } } ================================================ FILE: src/BuildingBlocks/Exception/DomainException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace SmartCharging.Infrastructure.Exceptions { public class DomainException : CustomException { public DomainException(string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest) : base(message, statusCode) { } public DomainException(string message, Exception innerException, HttpStatusCode statusCode = HttpStatusCode.BadRequest, int? code = null) : base(message, innerException, statusCode, code) { } } } ================================================ FILE: src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs ================================================ using Grpc.Core; using Grpc.Core.Interceptors; using Microsoft.Extensions.Logging; namespace BuildingBlocks.Exception; public class GrpcExceptionInterceptor : Interceptor { public override async Task UnaryServerHandler( TRequest request, ServerCallContext context, UnaryServerMethod continuation) { try { return await continuation(request, context); } catch (System.Exception exception) { throw new RpcException(new Status(StatusCode.Internal, exception.Message)); } } } ================================================ FILE: src/BuildingBlocks/Exception/InternalServerException.cs ================================================ using System.Globalization; using System.Net; namespace BuildingBlocks.Exception { public class InternalServerException : CustomException { public InternalServerException() : base() { } public InternalServerException(string message, int? code) : base(message, code: code) { } public InternalServerException(string message, int? code = null, params object[] args) : base(message: String.Format(CultureInfo.CurrentCulture, message, args, HttpStatusCode.InternalServerError, code)) { } } } ================================================ FILE: src/BuildingBlocks/Exception/NotFoundException.cs ================================================ using System.Net; namespace BuildingBlocks.Exception { public class NotFoundException : CustomException { public NotFoundException(string message, int? code = null) : base(message, HttpStatusCode.NotFound, code: code) { } } } ================================================ FILE: src/BuildingBlocks/Exception/ProblemDetailsWithCode.cs ================================================ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; namespace BuildingBlocks.Exception; using ProblemDetails = Microsoft.AspNetCore.Mvc.ProblemDetails; public class ProblemDetailsWithCode : ProblemDetails { [JsonPropertyName("code")] public int? Code { get; set; } } ================================================ FILE: src/BuildingBlocks/Exception/ValidationException.cs ================================================ using System.Net; namespace BuildingBlocks.Exception { public class ValidationException : CustomException { public ValidationException(string message, int? code = null) : base(message, HttpStatusCode.BadRequest, code: code) { } } } ================================================ FILE: src/BuildingBlocks/HealthCheck/Extensions.cs ================================================ using BuildingBlocks.EFCore; using BuildingBlocks.EventStoreDB; using BuildingBlocks.MassTransit; using BuildingBlocks.Mongo; using BuildingBlocks.Web; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; using MongoDB.Driver; using RabbitMQ.Client; namespace BuildingBlocks.HealthCheck; public static class Extensions { private const string HealthEndpointPath = "/health"; private const string AlivenessEndpointPath = "/alive"; public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services) { var healthOptions = services.GetOptions(nameof(HealthOptions)); if (healthOptions.Enabled) { var appOptions = services.GetOptions(nameof(AppOptions)); var postgresOptions = services.GetOptions(nameof(PostgresOptions)); var rabbitMqOptions = services.GetOptions(nameof(RabbitMqOptions)); var eventStoreOptions = services.GetOptions(nameof(EventStoreOptions)); var mongoOptions = services.GetOptions(nameof(MongoOptions)); var healthChecksBuilder = services.AddHealthChecks() // Add a default liveness check to ensure app is responsive .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]) .AddRabbitMQ( serviceProvider => { var factory = new ConnectionFactory { Uri = new Uri($"amqp://{rabbitMqOptions.UserName}:{rabbitMqOptions.Password}@{rabbitMqOptions.HostName}"), }; return factory.CreateConnectionAsync(); }); if (!string.IsNullOrEmpty(mongoOptions.ConnectionString)) { healthChecksBuilder.AddMongoDb( clientFactory: _ => new MongoClient(mongoOptions.ConnectionString), name: "MongoDB-Health", failureStatus: HealthStatus.Unhealthy, timeout: TimeSpan.FromSeconds(10)); } if (!string.IsNullOrEmpty(postgresOptions.ConnectionString)) healthChecksBuilder.AddNpgSql(postgresOptions.ConnectionString); if (!string.IsNullOrEmpty(eventStoreOptions.ConnectionString)) healthChecksBuilder.AddEventStore(eventStoreOptions.ConnectionString); services.AddHealthChecksUI(setup => { setup.SetEvaluationTimeInSeconds(60); // time in seconds between check setup.AddHealthCheckEndpoint($"Self Check - {appOptions.Name}", HealthEndpointPath); }).AddInMemoryStorage(); } services.AddHealthChecks().AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return services; } public static WebApplication UseCustomHealthCheck(this WebApplication app) { var healthOptions = app.Configuration.GetOptions(nameof(HealthOptions)); if (app.Environment.IsDevelopment()) { app.MapHealthChecks(HealthEndpointPath); app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live"), }); } if (healthOptions.Enabled) app.MapHealthChecksUI(options => options.UIPath = "/health-ui"); return app; } } ================================================ FILE: src/BuildingBlocks/HealthCheck/HealthOptions.cs ================================================ namespace BuildingBlocks.HealthCheck; public class HealthOptions { public bool Enabled { get; set; } = true; } ================================================ FILE: src/BuildingBlocks/Jwt/AuthHeaderHandler.cs ================================================ using System.Net.Http.Headers; using Microsoft.AspNetCore.Http; namespace BuildingBlocks.Jwt; public class AuthHeaderHandler : DelegatingHandler { private readonly IHttpContextAccessor _httpContext; public AuthHeaderHandler(IHttpContextAccessor httpContext) { _httpContext = httpContext; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = (_httpContext?.HttpContext?.Request.Headers["Authorization"])?.ToString(); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "", StringComparison.CurrentCulture)); return base.SendAsync(request, cancellationToken); } } ================================================ FILE: src/BuildingBlocks/Jwt/JwtExtensions.cs ================================================ using BuildingBlocks.Constants; using BuildingBlocks.Web; using Duende.IdentityServer.EntityFramework.Entities; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace BuildingBlocks.Jwt { public static class JwtExtensions { public static IServiceCollection AddJwt(this IServiceCollection services) { // Bind Jwt settings from configuration var jwtOptions = services.GetOptions("Jwt"); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = jwtOptions.Authority; options.Audience = jwtOptions.Audience; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuers = [jwtOptions.Authority], ValidateAudience = true, ValidAudiences = [jwtOptions.Audience], ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(2), // Reduce default clock skew // For IdentityServer4/Duende, we should also validate the signing key ValidateIssuerSigningKey = true, NameClaimType = "name", // Map "name" claim to User.Identity.Name RoleClaimType = "role", // Map "role" claim to User.IsInRole() }; // Preserve ALL claims from the token (including "sub") options.MapInboundClaims = false; }); services.AddAuthorization( options => { options.AddPolicy( nameof(ApiScope), policy => { policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", jwtOptions.Audience); }); // Role-based policies options.AddPolicy( IdentityConstant.Role.Admin, x => { x.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); x.RequireRole(IdentityConstant.Role.Admin); } ); options.AddPolicy( IdentityConstant.Role.User, x => { x.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); x.RequireRole(IdentityConstant.Role.User); } ); }); return services; } } } ================================================ FILE: src/BuildingBlocks/Logging/LoggingBehavior.cs ================================================ using System.Diagnostics; using MediatR; using Microsoft.Extensions.Logging; namespace BuildingBlocks.Logging; public class LoggingBehavior : IPipelineBehavior where TRequest : notnull, IRequest where TResponse : notnull { private readonly ILogger> _logger; public LoggingBehavior(ILogger> logger) { _logger = logger; } public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { const string prefix = nameof(LoggingBehavior); _logger.LogInformation("[{Prefix}] Handle request={X-RequestData} and response={X-ResponseData}", prefix, typeof(TRequest).Name, typeof(TResponse).Name); var timer = new Stopwatch(); timer.Start(); var response = await next(); timer.Stop(); var timeTaken = timer.Elapsed; if (timeTaken.Seconds > 3) // if the request is greater than 3 seconds, then log the warnings _logger.LogWarning("[{Perf-Possible}] The request {X-RequestData} took {TimeTaken} seconds.", prefix, typeof(TRequest).Name, timeTaken.Seconds); _logger.LogInformation("[{Prefix}] Handled {X-RequestData}", prefix, typeof(TRequest).Name); return response; } } ================================================ FILE: src/BuildingBlocks/Mapster/Extensions.cs ================================================ using System.Reflection; using Mapster; using MapsterMapper; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.Mapster; public static class Extensions { public static IServiceCollection AddCustomMapster(this IServiceCollection services, params Assembly[] assemblies) { var typeAdapterConfig = TypeAdapterConfig.GlobalSettings; typeAdapterConfig.Scan(assemblies); var mapperConfig = new Mapper(typeAdapterConfig); services.AddSingleton(mapperConfig); return services; } } ================================================ FILE: src/BuildingBlocks/MassTransit/ConsumeFilter.cs ================================================ using BuildingBlocks.Core.Event; using BuildingBlocks.PersistMessageProcessor; using MassTransit; namespace BuildingBlocks.MassTransit; // Handle inbox messages with masstransit pipeline public class ConsumeFilter : IFilter> where T : class { private readonly IPersistMessageProcessor _persistMessageProcessor; public ConsumeFilter(IPersistMessageProcessor persistMessageProcessor) { _persistMessageProcessor = persistMessageProcessor; } public async Task Send(ConsumeContext context, IPipe> next) { var id = await _persistMessageProcessor.AddReceivedMessageAsync( new MessageEnvelope( context.Message, context.Headers.ToDictionary(x => x.Key, x => x.Value)) ); var message = await _persistMessageProcessor.ExistMessageAsync(id); if (message is null) { await next.Send(context); await _persistMessageProcessor.ProcessInboxAsync(id); } } public void Probe(ProbeContext context) { } } ================================================ FILE: src/BuildingBlocks/MassTransit/Extensions.cs ================================================ using System.Reflection; using BuildingBlocks.Web; using MassTransit; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace BuildingBlocks.MassTransit; using Exception; public static class Extensions { public static IServiceCollection AddCustomMassTransit( this IServiceCollection services, IWebHostEnvironment env, TransportType transportType, params Assembly[] assembly ) { services.AddValidateOptions(); if (env.IsEnvironment("test")) { services.AddMassTransitTestHarness( configure => { SetupMasstransitConfigurations(services, configure, transportType, assembly); }); } else { services.AddMassTransit( configure => { SetupMasstransitConfigurations(services, configure, transportType, assembly); }); } return services; } private static void SetupMasstransitConfigurations( IServiceCollection services, IBusRegistrationConfigurator configure, TransportType transportType, params Assembly[] assembly ) { configure.AddConsumers(assembly); configure.AddSagaStateMachines(assembly); configure.AddSagas(assembly); configure.AddActivities(assembly); switch (transportType) { case TransportType.RabbitMq: configure.UsingRabbitMq( (context, configurator) => { var configuration = context.GetRequiredService(); var aspireConnectionString = configuration.GetConnectionString("rabbitmq"); if (!string.IsNullOrEmpty(aspireConnectionString)) { configurator.Host(new Uri(aspireConnectionString)); } else { var rabbitMqOptions = services.GetOptions(nameof(RabbitMqOptions)); ArgumentNullException.ThrowIfNull(rabbitMqOptions); configurator.Host( rabbitMqOptions?.HostName, rabbitMqOptions?.Port ?? 5672, "/", h => { h.Username(rabbitMqOptions.UserName); h.Password(rabbitMqOptions.Password); }); } configurator.ConfigureEndpoints(context); configurator.UseMessageRetry(AddRetryConfiguration); }); break; case TransportType.InMemory: configure.UsingInMemory( (context, configurator) => { configurator.ConfigureEndpoints(context); configurator.UseMessageRetry(AddRetryConfiguration); }); break; default: throw new ArgumentOutOfRangeException( nameof(transportType), transportType, message: null); } } private static void AddRetryConfiguration(IRetryConfigurator retryConfigurator) { retryConfigurator.Exponential( 3, TimeSpan.FromMilliseconds(200), TimeSpan.FromMinutes(120), TimeSpan.FromMilliseconds(200)) .Ignore< ValidationException>(); // don't retry if we have invalid data and message goes to _error queue masstransit } } ================================================ FILE: src/BuildingBlocks/MassTransit/RabbitMqOptions.cs ================================================ namespace BuildingBlocks.MassTransit; public class RabbitMqOptions { public string HostName { get; set; } public string ExchangeName { get; set; } public string UserName { get; set; } public string Password { get; set; } public ushort? Port { get; set; } } ================================================ FILE: src/BuildingBlocks/MassTransit/TransportType.cs ================================================ namespace BuildingBlocks.MassTransit; public enum TransportType { RabbitMq, InMemory } ================================================ FILE: src/BuildingBlocks/Mongo/Extensions.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.Mongo { using Web; public static class Extensions { public static IServiceCollection AddMongoDbContext( this WebApplicationBuilder builder, Action? configurator = null) where TContext : MongoDbContext { return builder.Services.AddMongoDbContext(builder.Configuration, configurator); } public static IServiceCollection AddMongoDbContext( this IServiceCollection services, IConfiguration configuration, Action? configurator = null) where TContextService : IMongoDbContext where TContextImplementation : MongoDbContext, TContextService { // Configure MongoOptions with Aspire-aware defaults services.AddOptions() .Bind(configuration.GetSection(nameof(MongoOptions))) .PostConfigure(options => { var aspireConnectionString = configuration.GetConnectionString("mongo"); options.ConnectionString = aspireConnectionString ?? options.ConnectionString; }); if (configurator is { }) { services.Configure(nameof(MongoOptions), configurator); } else { services.AddValidateOptions(); } services.AddScoped(typeof(TContextService), typeof(TContextImplementation)); services.AddScoped(typeof(TContextImplementation)); services.AddScoped(sp => sp.GetRequiredService()); services.AddTransient(typeof(IMongoRepository<,>), typeof(MongoRepository<,>)); services.AddTransient(typeof(IMongoUnitOfWork<>), typeof(MongoUnitOfWork<>)); return services; } } } ================================================ FILE: src/BuildingBlocks/Mongo/IMongoDbContext.cs ================================================ using MongoDB.Driver; namespace BuildingBlocks.Mongo; public interface IMongoDbContext : IDisposable { IMongoCollection GetCollection(string? name = null); Task SaveChangesAsync(CancellationToken cancellationToken = default); Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task CommitTransactionAsync(CancellationToken cancellationToken = default); Task RollbackTransaction(CancellationToken cancellationToken = default); void AddCommand(Func func); } ================================================ FILE: src/BuildingBlocks/Mongo/IMongoRepository.cs ================================================ using BuildingBlocks.Core.Model; namespace BuildingBlocks.Mongo; public interface IMongoRepository : IRepository where TEntity : class, IAggregate { } ================================================ FILE: src/BuildingBlocks/Mongo/IMongoUnitOfWork.cs ================================================ namespace BuildingBlocks.Mongo; public interface IMongoUnitOfWork : IUnitOfWork where TContext : class, IMongoDbContext { } ================================================ FILE: src/BuildingBlocks/Mongo/IRepository.cs ================================================ using System.Linq.Expressions; using BuildingBlocks.Core.Model; namespace BuildingBlocks.Mongo; public interface IReadRepository where TEntity : class, IAggregate { Task FindByIdAsync(TId id, CancellationToken cancellationToken = default); Task FindOneAsync( Expression> predicate, CancellationToken cancellationToken = default); Task> FindAsync( Expression> predicate, CancellationToken cancellationToken = default); Task> GetAllAsync(CancellationToken cancellationToken = default); public Task> RawQuery( string query, CancellationToken cancellationToken = default, params object[] queryParams); } public interface IWriteRepository where TEntity : class, IAggregate { Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default); Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default); Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default); Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default); } public interface IRepository : IReadRepository, IWriteRepository, IDisposable where TEntity : class, IAggregate { } public interface IRepository : IRepository where TEntity : class, IAggregate { } ================================================ FILE: src/BuildingBlocks/Mongo/ITransactionAble.cs ================================================ namespace BuildingBlocks.Mongo; public interface ITransactionAble { Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task RollbackTransactionAsync(CancellationToken cancellationToken = default); Task CommitTransactionAsync(CancellationToken cancellationToken = default); } ================================================ FILE: src/BuildingBlocks/Mongo/IUnitOfWork.cs ================================================ namespace BuildingBlocks.Mongo; public interface IUnitOfWork : IDisposable { Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task CommitAsync(CancellationToken cancellationToken = default); } public interface IUnitOfWork : IUnitOfWork where TContext : class { TContext Context { get; } } ================================================ FILE: src/BuildingBlocks/Mongo/ImmutablePocoConvention.cs ================================================ using System.Reflection; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; namespace BuildingBlocks.Mongo { /// /// A convention that map all read only properties for which a matching constructor is found. /// Also matching constructors are mapped. /// public class ImmutablePocoConvention : ConventionBase, IClassMapConvention { private readonly BindingFlags _bindingFlags; public ImmutablePocoConvention() : this(BindingFlags.Instance | BindingFlags.Public) { } public ImmutablePocoConvention(BindingFlags bindingFlags) { _bindingFlags = bindingFlags | BindingFlags.DeclaredOnly; } public void Apply(BsonClassMap classMap) { var readOnlyProperties = classMap.ClassType.GetTypeInfo() .GetProperties(_bindingFlags) .Where(p => IsReadOnlyProperty(classMap, p)) .ToList(); foreach (var constructor in classMap.ClassType.GetConstructors()) { // If we found a matching constructor then we map it and all the readonly properties var matchProperties = GetMatchingProperties(constructor, readOnlyProperties); if (matchProperties.Any()) { // Map constructor classMap.MapConstructor(constructor); // Map properties foreach (var p in matchProperties) classMap.MapMember(p); } } } private static List GetMatchingProperties( ConstructorInfo constructor, List properties) { var matchProperties = new List(); var ctorParameters = constructor.GetParameters(); foreach (var ctorParameter in ctorParameters) { var matchProperty = properties.FirstOrDefault(p => ParameterMatchProperty(ctorParameter, p)); if (matchProperty == null) return new List(); matchProperties.Add(matchProperty); } return matchProperties; } private static bool ParameterMatchProperty(ParameterInfo parameter, PropertyInfo property) { return string.Equals(property.Name, parameter.Name, StringComparison.OrdinalIgnoreCase) && parameter.ParameterType == property.PropertyType; } private static bool IsReadOnlyProperty(BsonClassMap classMap, PropertyInfo propertyInfo) { // we can't read if (!propertyInfo.CanRead) return false; // we can write (already handled by the default convention...) if (propertyInfo.CanWrite) return false; // skip indexers if (propertyInfo.GetIndexParameters().Length != 0) return false; // skip overridden properties (they are already included by the base class) var getMethodInfo = propertyInfo.GetMethod; if (getMethodInfo.IsVirtual && getMethodInfo.GetBaseDefinition().DeclaringType != classMap.ClassType) return false; return true; } } } ================================================ FILE: src/BuildingBlocks/Mongo/MicroBootstrap.Persistence.Mongo.csproj ================================================ net6.0 enable enable ================================================ FILE: src/BuildingBlocks/Mongo/MongoDbContext.cs ================================================ using System.Globalization; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; namespace BuildingBlocks.Mongo; // https://www.thecodebuzz.com/mongodb-repository-implementation-unit-testing-net-core-example/ public class MongoDbContext : IMongoDbContext { public IClientSessionHandle? Session { get; set; } public IMongoDatabase Database { get; } public IMongoClient MongoClient { get; } protected readonly IList> _commands; private static readonly bool _isSerializerRegisterd; static MongoDbContext() { BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); } public MongoDbContext(IOptions options) { RegisterConventions(); MongoClient = new MongoClient(options.Value.ConnectionString); var databaseName = options.Value.DatabaseName; Database = MongoClient.GetDatabase(databaseName); // Every command will be stored and it'll be processed at SaveChanges _commands = new List>(); } private static void RegisterConventions() { ConventionRegistry.Register( "conventions", new ConventionPack { new CamelCaseElementNameConvention(), new IgnoreExtraElementsConvention(true), new EnumRepresentationConvention(BsonType.String), new IgnoreIfDefaultConvention(false), new ImmutablePocoConvention() }, _ => true); } public IMongoCollection GetCollection(string? name = null) { return Database.GetCollection(name ?? typeof(T).Name.ToLower(CultureInfo.CurrentCulture)); } public void Dispose() { while (Session is { IsInTransaction: true }) Thread.Sleep(TimeSpan.FromMilliseconds(100)); GC.SuppressFinalize(this); } public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var result = _commands.Count; using (Session = await MongoClient.StartSessionAsync(cancellationToken: cancellationToken)) { Session.StartTransaction(); try { var commandTasks = _commands.Select(c => c()); await Task.WhenAll(commandTasks); await Session.CommitTransactionAsync(cancellationToken); } catch (System.Exception ex) { await Session.AbortTransactionAsync(cancellationToken); _commands.Clear(); throw; } } _commands.Clear(); return result; } public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { Session = await MongoClient.StartSessionAsync(cancellationToken: cancellationToken); Session.StartTransaction(); } public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) { if (Session is { IsInTransaction: true }) await Session.CommitTransactionAsync(cancellationToken); Session?.Dispose(); } public async Task RollbackTransaction(CancellationToken cancellationToken = default) { await Session?.AbortTransactionAsync(cancellationToken)!; } public void AddCommand(Func func) { _commands.Add(func); } public async Task ExecuteTransactionalAsync(Func action, CancellationToken cancellationToken = default) { await BeginTransactionAsync(cancellationToken); try { await action(); await CommitTransactionAsync(cancellationToken); } catch { await RollbackTransaction(cancellationToken); throw; } } public async Task ExecuteTransactionalAsync( Func> action, CancellationToken cancellationToken = default) { await BeginTransactionAsync(cancellationToken); try { var result = await action(); await CommitTransactionAsync(cancellationToken); return result; } catch { await RollbackTransaction(cancellationToken); throw; } } } ================================================ FILE: src/BuildingBlocks/Mongo/MongoOptions.cs ================================================ namespace BuildingBlocks.Mongo; public class MongoOptions { public string ConnectionString { get; set; } = null!; public string DatabaseName { get; set; } = null!; public static Guid UniqueId { get; set; } = Guid.NewGuid(); } ================================================ FILE: src/BuildingBlocks/Mongo/MongoRepository.cs ================================================ using System.Linq.Expressions; using BuildingBlocks.Core.Model; using MongoDB.Driver; using MongoDB.Driver.Linq; namespace BuildingBlocks.Mongo; public class MongoRepository : IMongoRepository where TEntity : class, IAggregate { private readonly IMongoDbContext _context; protected readonly IMongoCollection DbSet; public MongoRepository(IMongoDbContext context) { _context = context; DbSet = _context.GetCollection(); } public void Dispose() { _context?.Dispose(); } public Task FindByIdAsync(TId id, CancellationToken cancellationToken = default) { return FindOneAsync(e => e.Id.Equals(id), cancellationToken); } public Task FindOneAsync( Expression> predicate, CancellationToken cancellationToken = default) { return DbSet.Find(predicate).SingleOrDefaultAsync(cancellationToken: cancellationToken)!; } public async Task> FindAsync( Expression> predicate, CancellationToken cancellationToken = default) { return await DbSet.Find(predicate).ToListAsync(cancellationToken: cancellationToken)!; } public async Task> GetAllAsync(CancellationToken cancellationToken = default) { return await DbSet.AsQueryable().ToListAsync(cancellationToken); } public Task> RawQuery( string query, CancellationToken cancellationToken = default, params object[] queryParams) { throw new NotImplementedException(); } public async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) { await DbSet.InsertOneAsync(entity, new InsertOneOptions(), cancellationToken); return entity; } public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) { await DbSet.ReplaceOneAsync(e => e.Id.Equals(entity.Id), entity, new ReplaceOptions(), cancellationToken); return entity; } public Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default) { return DbSet.DeleteOneAsync(e => entities.Any(i => e.Id.Equals(i.Id)), cancellationToken); } public Task DeleteAsync( Expression> predicate, CancellationToken cancellationToken = default) => DbSet.DeleteOneAsync(predicate, cancellationToken); public Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default) { return DbSet.DeleteOneAsync(e => e.Id.Equals(entity.Id), cancellationToken); } public Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default) { return DbSet.DeleteOneAsync(e => e.Id.Equals(id), cancellationToken); } } ================================================ FILE: src/BuildingBlocks/Mongo/MongoUnitOfWork.cs ================================================ namespace BuildingBlocks.Mongo; public class MongoUnitOfWork : IMongoUnitOfWork, ITransactionAble where TContext : MongoDbContext { public MongoUnitOfWork(TContext context) => Context = context; public TContext Context { get; } public async Task CommitAsync(CancellationToken cancellationToken = default) { await Context.SaveChangesAsync(cancellationToken); } public Task BeginTransactionAsync(CancellationToken cancellationToken = default) { return Context.BeginTransactionAsync(cancellationToken); } public Task RollbackTransactionAsync(CancellationToken cancellationToken = default) { return Context.RollbackTransaction(cancellationToken); } public Task CommitTransactionAsync(CancellationToken cancellationToken = default) { return Context.CommitTransactionAsync(cancellationToken); } public void Dispose() => Context.Dispose(); } ================================================ FILE: src/BuildingBlocks/OpenApi/Extensions.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Scalar.AspNetCore; namespace BuildingBlocks.OpenApi { public static class Extensions { // ref: https://github.com/dotnet/eShop/blob/main/src/eShop.ServiceDefaults/OpenApi.Extensions.cs public static IServiceCollection AddAspnetOpenApi(this IServiceCollection services) { string[] versions = ["v1"]; foreach (var description in versions) { services.AddOpenApi( description, options => { options.AddDocumentTransformer(); }); } return services; } public static IApplicationBuilder UseAspnetOpenApi(this WebApplication app) { app.MapOpenApi(); app.UseSwaggerUI( options => { var descriptions = app.DescribeApiVersions(); // build a swagger endpoint for each discovered API version foreach (var description in descriptions) { var openApiUrl = $"/openapi/{description.GroupName}.json"; var name = description.GroupName.ToUpperInvariant(); options.SwaggerEndpoint(openApiUrl, name); } }); // Add scalar ui app.MapScalarApiReference( redocOptions => { redocOptions.WithOpenApiRoutePattern("/openapi/{documentName}.json"); }); return app; } } } ================================================ FILE: src/BuildingBlocks/OpenApi/SecuritySchemeDocumentTransformer.cs ================================================ using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi; public class SecuritySchemeDocumentTransformer : IOpenApiDocumentTransformer { public Task TransformAsync( OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken ) { document.Components ??= new OpenApiComponents(); // Initialize with the correct interface type document.Components.SecuritySchemes ??= new Dictionary(); var securitySchemes = new Dictionary { ["Bearer"] = new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "Enter 'Bearer' [space] and your token in the text input below.\n\nExample: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'", }, ["ApiKey"] = new OpenApiSecurityScheme { Name = "X-API-KEY", Type = SecuritySchemeType.ApiKey, In = ParameterLocation.Header, Description = "Enter your API key in the text input below.\n\nExample: '12345-abcdef'", }, }; foreach (var (key, scheme) in securitySchemes) { if (!document.Components.SecuritySchemes.ContainsKey(key)) { document.Components.SecuritySchemes.Add(key, scheme); } } return Task.CompletedTask; } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/ActivityExtensions.cs ================================================ using System.Diagnostics; using System.Globalization; namespace BuildingBlocks.OpenTelemetryCollector; internal static class ActivityExtensions { /// /// Retrieves the tags from the parent of the current Activity, if available. /// /// The current Activity. /// A dictionary containing the parent tags, or an empty dictionary if no parent tags are available. public static Dictionary GetParentTags(this Activity activity) { ArgumentNullException.ThrowIfNull(activity); var parentTags = new Dictionary(); // Check if the current activity has a parent var parentActivity = activity.Parent; if (parentActivity != null) { foreach (var tag in parentActivity.Tags) { parentTags[tag.Key] = tag.Value; } } else { // If no parent Activity is available, check for links foreach (var link in activity.Links) { // Extract tags from the first link's context (assuming it's the parent-like context) if (link.Tags != null) { foreach (var tag in link.Tags) { parentTags[tag.Key] = tag.Value; } } // Break after processing the first link, as there should only be one parent context. break; } } return parentTags; } /// /// Extracts important information from an Activity into an ActivityInfo object. /// /// The Activity from which to extract information. /// An ActivityInfo object containing the extracted information. public static ActivityInfo ExtractImportantInformation(this Activity activity) { ArgumentNullException.ThrowIfNull(activity); var activityInfo = new ActivityInfo { Name = activity.DisplayName, StartTime = activity.StartTimeUtc, Duration = activity.Duration, Status = activity.Tags.FirstOrDefault(tag => tag.Key == TelemetryTags.Tracing.Otel.StatusCode).Value ?? "Unknown", StatusDescription = activity .Tags.FirstOrDefault(tag => tag.Key == TelemetryTags.Tracing.Otel.StatusDescription) .Value, Tags = activity.Tags.ToDictionary(tag => tag.Key, tag => tag.Value), Events = activity .Events.Select(e => new ActivityEventInfo { Name = e.Name, Timestamp = e.Timestamp, Attributes = e.Tags.ToDictionary(tag => tag.Key, tag => tag.Value), }) .ToList(), TraceId = activity.TraceId.ToString(), SpanId = activity.SpanId.ToString(), }; return activityInfo; } /// /// Sets an "OK" status on the provided Activity, indicating a successful operation. /// /// The Activity to update. /// An optional description of the successful operation. /// The updated Activity with the status and tags set. public static Activity SetOkStatus(this Activity activity, string? description = null) { ArgumentNullException.ThrowIfNull(activity); // Set the status of the activity to "OK" activity.SetStatus(ActivityStatusCode.Ok, description); // Add telemetry tags for status activity.SetTag( TelemetryTags.Tracing.Otel.StatusCode, nameof(ActivityStatusCode.Ok).ToUpper(CultureInfo.InvariantCulture) ); if (!string.IsNullOrEmpty(description)) activity.SetTag(TelemetryTags.Tracing.Otel.StatusDescription, description); return activity; } /// /// Sets an "Unset" status on the provided Activity, indicating no explicit status was applied. /// /// The Activity to update. /// An optional description of the unset status. /// The updated Activity with the status and tags set. public static Activity SetUnsetStatus(this Activity activity, string? description = null) { ArgumentNullException.ThrowIfNull(activity); // Set the status of the activity to "Unset" activity.SetStatus(ActivityStatusCode.Unset, description); // Add telemetry tags for status activity.SetTag( TelemetryTags.Tracing.Otel.StatusCode, nameof(ActivityStatusCode.Unset).ToUpper(CultureInfo.InvariantCulture) ); if (!string.IsNullOrEmpty(description)) activity.SetTag(TelemetryTags.Tracing.Otel.StatusDescription, description); return activity; } /// /// Sets an "Error" status on the provided Activity, indicating a failed operation. /// /// The Activity to update. /// The exception associated with the error, if available. /// An optional description of the error. /// The updated Activity with the status, error details, and tags set. public static Activity SetErrorStatus(this Activity activity, System.Exception? exception, string? description = null) { ArgumentNullException.ThrowIfNull(activity); // Add telemetry tags for status activity.SetTag( TelemetryTags.Tracing.Otel.StatusCode, nameof(ActivityStatusCode.Error).ToUpper(CultureInfo.InvariantCulture) ); if (!string.IsNullOrEmpty(description)) activity.SetTag(TelemetryTags.Tracing.Otel.StatusDescription, description); // Add detailed exception tags, if an exception is provided return activity.SetExceptionTags(exception); } // See https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/exceptions/ public static Activity SetExceptionTags(this Activity activity, System.Exception? ex) { if (ex is null) { return activity; } activity.SetStatus(ActivityStatusCode.Error); activity.AddException(ex); activity.AddTag(TelemetryTags.Tracing.Exception.Message, ex.Message); activity.AddTag(TelemetryTags.Tracing.Exception.Stacktrace, ex.ToString()); activity.AddTag(TelemetryTags.Tracing.Exception.Type, ex.GetType().FullName); return activity; } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/ActivityInfo.cs ================================================ using System.Diagnostics; namespace BuildingBlocks.OpenTelemetryCollector; public class ActivityInfo { public string Name { get; set; } = default!; public DateTime StartTime { get; set; } public TimeSpan Duration { get; set; } public string Status { get; set; } = default!; public string? StatusDescription { get; set; } public IDictionary Tags { get; set; } = new Dictionary(); public IList Events { get; set; } = new List(); public string TraceId { get; set; } = default!; public string SpanId { get; set; } = default!; public string? ParentId { get; set; } public ActivityContext? Parent { get; set; } public ActivityKind Kind { get; set; } } public class ActivityEventInfo { public string Name { get; set; } = default!; public DateTimeOffset Timestamp { get; set; } public IDictionary Attributes { get; set; } = new Dictionary(); } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/Behaviors/ObservabilityPipelineBehavior.cs ================================================ using BuildingBlocks.Core.CQRS; using BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Commands; using BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Query; using MediatR; namespace BuildingBlocks.OpenTelemetryCollector.Behaviors; public class ObservabilityPipelineBehavior( CommandHandlerActivity commandActivity, CommandHandlerMetrics commandMetrics, QueryHandlerActivity queryActivity, QueryHandlerMetrics queryMetrics ) : IPipelineBehavior where TRequest : IRequest where TResponse : notnull { public async Task Handle(TRequest message, RequestHandlerDelegate next, CancellationToken cancellationToken) { var isCommand = message is IQuery; var isQuery = message is ICommand; if (isCommand) { commandMetrics.StartExecuting(); } if (isQuery) { queryMetrics.StartExecuting(); } try { if (isCommand) { var commandResult = await commandActivity.Execute( async (activity, ct) => { var response = await next(); return response; }, cancellationToken ); commandMetrics.FinishExecuting(); return commandResult; } if (isQuery) { var queryResult = await queryActivity.Execute( async (activity, ct) => { var response = await next(); return response; }, cancellationToken ); queryMetrics.FinishExecuting(); return queryResult; } } catch (System.Exception) { if (isQuery) { queryMetrics.FailedCommand(); } if (isCommand) { commandMetrics.FailedCommand(); } throw; } return await next(); } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/CoreDiagnostics/Commands/CommandHandlerActivity.cs ================================================ using System.Diagnostics; using BuildingBlocks.Core.CQRS; using BuildingBlocks.OpenTelemetryCollector.DiagnosticsProvider; namespace BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Commands; public class CommandHandlerActivity(IDiagnosticsProvider diagnosticsProvider) { public async Task Execute( Func action, CancellationToken cancellationToken ) { var commandName = typeof(TCommand).Name; var handlerType = typeof(TCommand) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>) && i.GetGenericArguments()[0] == typeof(TCommand) ) ); var commandHandlerName = handlerType?.Name; // usually we use class/methodName var activityName = $"{ObservabilityConstant.Components.CommandHandler}.{commandHandlerName}/{commandName}"; await diagnosticsProvider.ExecuteActivityAsync( new CreateActivityInfo { Name = activityName, ActivityKind = ActivityKind.Consumer, Tags = new Dictionary { { TelemetryTags.Tracing.Application.Commands.Command, commandName }, { TelemetryTags.Tracing.Application.Commands.CommandType, typeof(TCommand).FullName }, { TelemetryTags.Tracing.Application.Commands.CommandHandler, commandHandlerName }, { TelemetryTags.Tracing.Application.Commands.CommandHandlerType, handlerType?.FullName }, }, }, action, cancellationToken ); } public async Task Execute( Func> action, CancellationToken cancellationToken ) { var commandName = typeof(TCommand).Name; var handlerType = typeof(TCommand) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>) && i.GetGenericArguments()[0] == typeof(TCommand) ) ); var commandHandlerName = handlerType?.Name; // usually we use class/methodName var activityName = $"{ObservabilityConstant.Components.CommandHandler}.{commandHandlerName}/{commandName}"; return await diagnosticsProvider.ExecuteActivityAsync( new CreateActivityInfo { Name = activityName, ActivityKind = ActivityKind.Consumer, Tags = new Dictionary { { TelemetryTags.Tracing.Application.Commands.Command, commandName }, { TelemetryTags.Tracing.Application.Commands.CommandType, typeof(TCommand).FullName }, { TelemetryTags.Tracing.Application.Commands.CommandHandler, commandHandlerName }, { TelemetryTags.Tracing.Application.Commands.CommandHandlerType, handlerType?.FullName }, }, }, action, cancellationToken ); } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/CoreDiagnostics/Commands/CommandHandlerMetrics.cs ================================================ using System.Diagnostics; using System.Diagnostics.Metrics; using BuildingBlocks.Core.CQRS; using BuildingBlocks.OpenTelemetryCollector; using BuildingBlocks.OpenTelemetryCollector.DiagnosticsProvider; namespace BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Commands; public class CommandHandlerMetrics { private readonly UpDownCounter _activeCommandsCounter; private readonly Counter _totalCommandsNumber; private readonly Counter _successCommandsNumber; private readonly Counter _failedCommandsNumber; private readonly Histogram _handlerDuration; private Stopwatch _timer; public CommandHandlerMetrics(IDiagnosticsProvider diagnosticsProvider) { _activeCommandsCounter = diagnosticsProvider.Meter.CreateUpDownCounter( TelemetryTags.Metrics.Application.Commands.ActiveCount, unit: "{active_commands}", description: "Number of commands currently being handled" ); _totalCommandsNumber = diagnosticsProvider.Meter.CreateCounter( TelemetryTags.Metrics.Application.Commands.TotalExecutedCount, unit: "{total_commands}", description: "Total number of executed command that sent to command handlers" ); _successCommandsNumber = diagnosticsProvider.Meter.CreateCounter( TelemetryTags.Metrics.Application.Commands.SuccessCount, unit: "{success_commands}", description: "Number commands that handled successfully" ); _failedCommandsNumber = diagnosticsProvider.Meter.CreateCounter( TelemetryTags.Metrics.Application.Commands.FaildCount, unit: "{failed_commands}", description: "Number commands that handled with errors" ); _handlerDuration = diagnosticsProvider.Meter.CreateHistogram( TelemetryTags.Metrics.Application.Commands.HandlerDuration, unit: "s", description: "Measures the duration of command handler" ); } public void StartExecuting() { var commandName = typeof(TCommand).Name; var handlerType = typeof(TCommand) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>) && i.GetGenericArguments()[0] == typeof(TCommand) ) ); var commandHandlerName = handlerType?.Name; var tags = new TagList { { TelemetryTags.Tracing.Application.Commands.Command, commandName }, { TelemetryTags.Tracing.Application.Commands.CommandType, typeof(TCommand).FullName }, { TelemetryTags.Tracing.Application.Commands.CommandHandler, commandHandlerName }, { TelemetryTags.Tracing.Application.Commands.CommandHandlerType, handlerType?.FullName }, }; if (_activeCommandsCounter.Enabled) { _activeCommandsCounter.Add(1, tags); } if (_totalCommandsNumber.Enabled) { _totalCommandsNumber.Add(1, tags); } _timer = Stopwatch.StartNew(); } public void FinishExecuting() { var commandName = typeof(TCommand).Name; var handlerType = typeof(TCommand) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>) && i.GetGenericArguments()[0] == typeof(TCommand) ) ); var commandHandlerName = handlerType?.Name; var tags = new TagList { { TelemetryTags.Tracing.Application.Commands.Command, commandName }, { TelemetryTags.Tracing.Application.Commands.CommandType, typeof(TCommand).FullName }, { TelemetryTags.Tracing.Application.Commands.CommandHandler, commandHandlerName }, { TelemetryTags.Tracing.Application.Commands.CommandHandlerType, handlerType?.FullName }, }; if (_activeCommandsCounter.Enabled) { _activeCommandsCounter.Add(-1, tags); } if (!_handlerDuration.Enabled) return; var elapsedTimeSeconds = _timer.Elapsed.Seconds; _handlerDuration.Record(elapsedTimeSeconds, tags); if (_successCommandsNumber.Enabled) { _successCommandsNumber.Add(1, tags); } } public void FailedCommand() { var commandName = typeof(TCommand).Name; var handlerType = typeof(TCommand) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>) && i.GetGenericArguments()[0] == typeof(TCommand) ) ); var commandHandlerName = handlerType?.Name; var tags = new TagList { { TelemetryTags.Tracing.Application.Commands.Command, commandName }, { TelemetryTags.Tracing.Application.Commands.CommandType, typeof(TCommand).FullName }, { TelemetryTags.Tracing.Application.Commands.CommandHandler, commandHandlerName }, { TelemetryTags.Tracing.Application.Commands.CommandHandlerType, handlerType?.FullName }, }; if (_failedCommandsNumber.Enabled) { _failedCommandsNumber.Add(1, tags); } } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/CoreDiagnostics/Query/QueryHandlerActivity.cs ================================================ using System.Diagnostics; using BuildingBlocks.Core.CQRS; using BuildingBlocks.OpenTelemetryCollector.DiagnosticsProvider; namespace BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Query; public class QueryHandlerActivity(IDiagnosticsProvider diagnosticsProvider) { public async Task Execute( Func> action, CancellationToken cancellationToken ) { var queryName = typeof(TQuery).Name; var handlerType = typeof(TQuery) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>) && i.GetGenericArguments()[0] == typeof(TQuery) ) ); var queryHandlerName = handlerType?.Name; // usually we use class/methodName var activityName = $"{ObservabilityConstant.Components.QueryHandler}.{queryHandlerName}/{queryName}"; return await diagnosticsProvider.ExecuteActivityAsync( new CreateActivityInfo { Name = activityName, ActivityKind = ActivityKind.Consumer, Tags = new Dictionary { { TelemetryTags.Tracing.Application.Queries.Query, queryName }, { TelemetryTags.Tracing.Application.Queries.QueryType, typeof(TQuery).FullName }, { TelemetryTags.Tracing.Application.Queries.QueryHandler, queryHandlerName }, { TelemetryTags.Tracing.Application.Queries.QueryHandlerType, handlerType?.FullName }, }, }, action, cancellationToken ); } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/CoreDiagnostics/Query/QueryHandlerMetrics.cs ================================================ using System.Diagnostics; using System.Diagnostics.Metrics; using BuildingBlocks.Core.CQRS; using BuildingBlocks.OpenTelemetryCollector.DiagnosticsProvider; namespace BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Query; public class QueryHandlerMetrics { private readonly UpDownCounter _activeQueriesCounter; private readonly Counter _totalQueriesNumber; private readonly Counter _successQueriesNumber; private readonly Counter _failedQueriesNumber; private readonly Histogram _handlerDuration; private Stopwatch _timer; public QueryHandlerMetrics(IDiagnosticsProvider diagnosticsProvider) { _activeQueriesCounter = diagnosticsProvider.Meter.CreateUpDownCounter( TelemetryTags.Metrics.Application.Commands.ActiveCount, unit: "{active_queries}", description: "Number of queries currently being handled" ); _totalQueriesNumber = diagnosticsProvider.Meter.CreateCounter( TelemetryTags.Metrics.Application.Commands.TotalExecutedCount, unit: "{total_queries}", description: "Total number of executed query that sent to query handlers" ); _successQueriesNumber = diagnosticsProvider.Meter.CreateCounter( TelemetryTags.Metrics.Application.Commands.SuccessCount, unit: "{success_queries}", description: "Number queries that handled successfully" ); _failedQueriesNumber = diagnosticsProvider.Meter.CreateCounter( TelemetryTags.Metrics.Application.Commands.FaildCount, unit: "{failed_queries}", description: "Number queries that handled with errors" ); _handlerDuration = diagnosticsProvider.Meter.CreateHistogram( TelemetryTags.Metrics.Application.Commands.HandlerDuration, unit: "s", description: "Measures the duration of query handler" ); } public void StartExecuting() { var queryName = typeof(TQuery).Name; var handlerType = typeof(TQuery) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>) && i.GetGenericArguments()[0] == typeof(TQuery) ) ); var queryHandlerName = handlerType?.Name; var tags = new TagList { { TelemetryTags.Tracing.Application.Queries.Query, queryName }, { TelemetryTags.Tracing.Application.Queries.QueryType, typeof(TQuery).FullName }, { TelemetryTags.Tracing.Application.Queries.QueryHandler, queryHandlerName }, { TelemetryTags.Tracing.Application.Queries.QueryHandlerType, handlerType?.FullName }, }; if (_activeQueriesCounter.Enabled) { _activeQueriesCounter.Add(1, tags); } if (_totalQueriesNumber.Enabled) { _totalQueriesNumber.Add(1, tags); } _timer = Stopwatch.StartNew(); } public void FinishExecuting() { var queryName = typeof(TQuery).Name; var handlerType = typeof(TQuery) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>) && i.GetGenericArguments()[0] == typeof(TQuery) ) ); var queryHandlerName = handlerType?.Name; var tags = new TagList { { TelemetryTags.Tracing.Application.Queries.Query, queryName }, { TelemetryTags.Tracing.Application.Queries.QueryType, typeof(TQuery).FullName }, { TelemetryTags.Tracing.Application.Queries.QueryHandler, queryHandlerName }, { TelemetryTags.Tracing.Application.Queries.QueryHandlerType, handlerType?.FullName }, }; if (_activeQueriesCounter.Enabled) { _activeQueriesCounter.Add(-1, tags); } if (!_handlerDuration.Enabled) return; var elapsedTimeSeconds = _timer.Elapsed.Seconds; _handlerDuration.Record(elapsedTimeSeconds, tags); if (_successQueriesNumber.Enabled) { _successQueriesNumber.Add(1, tags); } } public void FailedCommand() { var queryName = typeof(TQuery).Name; var handlerType = typeof(TQuery) .Assembly.GetTypes() .FirstOrDefault(t => t.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>) && i.GetGenericArguments()[0] == typeof(TQuery) ) ); var queryHandlerName = handlerType?.Name; var tags = new TagList { { TelemetryTags.Tracing.Application.Queries.Query, queryName }, { TelemetryTags.Tracing.Application.Queries.QueryType, typeof(TQuery).FullName }, { TelemetryTags.Tracing.Application.Queries.QueryHandler, queryHandlerName }, { TelemetryTags.Tracing.Application.Queries.QueryHandlerType, handlerType?.FullName }, }; if (_failedQueriesNumber.Enabled) { _failedQueriesNumber.Add(1, tags); } } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/CreateActivityInfo.cs ================================================ using System.Diagnostics; namespace BuildingBlocks.OpenTelemetryCollector; public class CreateActivityInfo { public required string Name { get; set; } public IDictionary Tags { get; set; } = new Dictionary(); public string? ParentId { get; set; } public ActivityContext? Parent { get; set; } public required ActivityKind ActivityKind = ActivityKind.Internal; } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/DiagnosticsProvider/CustomeDiagnosticsProvider.cs ================================================ using System.Diagnostics; using System.Diagnostics.Metrics; using System.Reflection; using Microsoft.Extensions.Options; namespace BuildingBlocks.OpenTelemetryCollector.DiagnosticsProvider; public class CustomeDiagnosticsProvider(IMeterFactory meterFactory, IOptions options) : IDiagnosticsProvider { private readonly Version? _version = Assembly.GetCallingAssembly().GetName().Version; private ActivitySource? _activitySource; private ActivityListener? _listener; private Meter? _meter; public string InstrumentationName { get; } = options.Value.InstrumentationName ?? throw new ArgumentException("InstrumentationName cannot be null or empty."); // https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs public ActivitySource ActivitySource { get { if (_activitySource != null) return _activitySource; _activitySource = new(InstrumentationName, _version?.ToString()); _listener = new ActivityListener { ShouldListenTo = x => true, Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, }; ActivitySource.AddActivityListener(_listener); return _activitySource; } } // https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-instrumentation public Meter Meter { get { if (_meter != null) return _meter; _meter = meterFactory.Create(InstrumentationName, _version?.ToString()); return _meter; } } public async Task ExecuteActivityAsync( CreateActivityInfo createActivityInfo, Func action, CancellationToken cancellationToken = default ) { if (!options.Value.TracingEnabled) { await action(null, cancellationToken); return; } using var activity = ActivitySource .CreateActivity( name: $"{InstrumentationName}.{createActivityInfo.Name}", kind: createActivityInfo.ActivityKind, parentContext: createActivityInfo.Parent ?? default, idFormat: ActivityIdFormat.W3C, tags: createActivityInfo.Tags ) ?.Start() ?? Activity.Current; try { await action(activity!, cancellationToken); activity?.SetOkStatus(); } catch (System.Exception ex) { activity?.SetErrorStatus(ex); throw; } } public async Task ExecuteActivityAsync( CreateActivityInfo createActivityInfo, Func> action, CancellationToken cancellationToken = default ) { if (!options.Value.TracingEnabled) { return await action(null, cancellationToken); } using var activity = ActivitySource .CreateActivity( name: $"{InstrumentationName}.{createActivityInfo.Name}", kind: createActivityInfo.ActivityKind, parentContext: createActivityInfo.Parent ?? default, idFormat: ActivityIdFormat.W3C, tags: createActivityInfo.Tags ) ?.Start() ?? Activity.Current; try { var result = await action(activity!, cancellationToken); activity?.SetOkStatus(); return result; } catch (System.Exception ex) { activity?.SetErrorStatus(ex); throw; } } public void Dispose() { _listener?.Dispose(); _meter?.Dispose(); _activitySource?.Dispose(); } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/DiagnosticsProvider/IDiagnosticsProvider.cs ================================================ using System.Diagnostics; using System.Diagnostics.Metrics; namespace BuildingBlocks.OpenTelemetryCollector.DiagnosticsProvider; public interface IDiagnosticsProvider : IDisposable { string InstrumentationName { get; } ActivitySource ActivitySource { get; } Meter Meter { get; } Task ExecuteActivityAsync( CreateActivityInfo createActivityInfo, Func action, CancellationToken cancellationToken = default ); Task ExecuteActivityAsync( CreateActivityInfo createActivityInfo, Func> action, CancellationToken cancellationToken = default ); } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/Extensions.cs ================================================ using System.Diagnostics; using System.Reflection; using BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Commands; using BuildingBlocks.OpenTelemetryCollector.CoreDiagnostics.Query; using BuildingBlocks.OpenTelemetryCollector.DiagnosticsProvider; using BuildingBlocks.Web; using Grafana.OpenTelemetry; using MassTransit.Logging; using MassTransit.Monitoring; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Npgsql; using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace BuildingBlocks.OpenTelemetryCollector; // https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-otlp-example // https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-prgrja-example // https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-prgrja-example // https://blog.codingmilitia.com/2023/09/05/observing-dotnet-microservices-with-opentelemetry-logs-traces-metrics/ public static class Extensions { private const string HealthEndpointPath = "/health"; private const string AlivenessEndpointPath = "/alive"; public static WebApplicationBuilder AddCustomObservability(this WebApplicationBuilder builder) { Activity.DefaultIdFormat = ActivityIdFormat.W3C; builder.Services.AddSingleton(); builder.AddCoreDiagnostics(); builder.Services.AddValidateOptions(); var observabilityOptions = builder.Services.GetOptions(nameof(ObservabilityOptions)); // InstrumentationName property option is mandatory and can't be empty ArgumentException.ThrowIfNullOrEmpty(observabilityOptions.InstrumentationName); ObservabilityConstant.InstrumentationName = observabilityOptions.InstrumentationName; if (observabilityOptions is { MetricsEnabled: false, TracingEnabled: false, LoggingEnabled: false }) { return builder; } void ConfigureResourceBuilder(ResourceBuilder resourceBuilder) { resourceBuilder.AddAttributes([new("service.environment", builder.Environment.EnvironmentName)]); resourceBuilder.AddService( serviceName: observabilityOptions.ServiceName ?? builder.Environment.ApplicationName, serviceVersion: Assembly.GetCallingAssembly().GetName().Version?.ToString() ?? "unknown", serviceInstanceId: Environment.MachineName ); } if (observabilityOptions.LoggingEnabled) { // logging // opentelemtry logging works with .net default logging providers and doesn't work for `serilog`, in serilog we should enable `WriteToProviders=true` builder.Logging.AddOpenTelemetry(options => { var resourceBuilder = ResourceBuilder.CreateDefault(); ConfigureResourceBuilder(resourceBuilder); options.SetResourceBuilder(resourceBuilder); options.IncludeFormattedMessage = true; options.IncludeScopes = true; // this allows the state value passed to the logger.Log method to be parsed, in case it isn't a collection of KeyValuePair, which is the case when we use things like logger.LogInformation. options.ParseStateValues = true; // which means the message wouldn't have the placeholders replaced options.IncludeFormattedMessage = true; // add some metadata to exported logs options.SetResourceBuilder( ResourceBuilder .CreateDefault() .AddService( observabilityOptions.ServiceName ?? builder.Environment.ApplicationName, serviceVersion: Assembly.GetCallingAssembly().GetName().Version?.ToString() ?? "unknown", serviceInstanceId: Environment.MachineName ) ); options.AddLoggingExporters(observabilityOptions); }); } if (observabilityOptions is { MetricsEnabled: false, TracingEnabled: false }) { return builder; } OpenTelemetryBuilder otel = null!; if (observabilityOptions.MetricsEnabled || observabilityOptions.TracingEnabled) { // metrics and tracing otel = builder.Services.AddOpenTelemetry(); otel.ConfigureResource(ConfigureResourceBuilder); } if (observabilityOptions.MetricsEnabled) { otel.WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddProcessInstrumentation() .AddRuntimeInstrumentation() .AddMeter(InstrumentationOptions.MeterName) .AddMeter(observabilityOptions.InstrumentationName) // metrics provides by ASP.NET Core in .NET 8 .AddView( "http.server.request.duration", new ExplicitBucketHistogramConfiguration { Boundaries = [0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10], } ) .AddMeter("System.Runtime") .AddMeter("Microsoft.AspNetCore.Hosting") .AddMeter("Microsoft.AspNetCore.Server.Kestrel"); AddMetricsExporter(observabilityOptions, metrics); }); } if (observabilityOptions.TracingEnabled) { otel.WithTracing(tracing => { if (builder.Environment.IsDevelopment()) { // We want to view all traces in development tracing.SetSampler(new AlwaysOnSampler()); } tracing .SetErrorStatusOnException() .AddAspNetCoreInstrumentation(options => { options.RecordException = true; // Don't trace requests to the health endpoint to avoid filling the dashboard with noise options.Filter = httpContext => !(httpContext.Request.Path.StartsWithSegments( HealthEndpointPath, StringComparison.OrdinalIgnoreCase) || httpContext.Request.Path.StartsWithSegments( AlivenessEndpointPath, StringComparison.OrdinalIgnoreCase )); }) .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(instrumentationOptions => { instrumentationOptions.RecordException = true; }) .AddEntityFrameworkCoreInstrumentation() .AddSource(DiagnosticHeaders.DefaultListenerName) .AddNpgsql() // `AddSource` for adding custom activity sources .AddSource(observabilityOptions.InstrumentationName) // metrics provides by ASP.NET Core in .NET 8 .AddSource("Microsoft.AspNetCore.Hosting") .AddSource("Microsoft.AspNetCore.Server.Kestrel"); AddTracingExporter(observabilityOptions, tracing); }); } return builder; } public static WebApplication UseCustomObservability(this WebApplication app) { var options = app.Services.GetRequiredService>().Value; app.Use( async (context, next) => { var metricsFeature = context.Features.Get(); if (metricsFeature != null && context.Request.Path is { Value: "/metrics" or "/health" }) { metricsFeature.MetricsDisabled = true; } await next(context); } ); if (options.UsePrometheusExporter) { // export application metrics in `/metrics` endpoint and should scrape in the Prometheus config file and `scrape_configs` // https://github.com/open-telemetry/opentelemetry-dotnet/tree/e330e57b04fa3e51fe5d63b52bfff891fb5b7961/src/OpenTelemetry.Exporter.Prometheus.AspNetCore app.UseOpenTelemetryPrometheusScrapingEndpoint(); // http://localhost:4000/metrics } return app; } private static void AddTracingExporter(ObservabilityOptions observabilityOptions, TracerProviderBuilder tracing) { if (observabilityOptions.UseJaegerExporter) { ArgumentNullException.ThrowIfNull(observabilityOptions.JaegerOptions); // https://github.com/open-telemetry/opentelemetry-dotnet/tree/e330e57b04fa3e51fe5d63b52bfff891fb5b7961/docs/trace/getting-started-jaeger // `OpenTelemetry.Exporter.Jaeger` package and `AddJaegerExporter` to use Http endpoint (http://localhost:14268/api/traces) is deprecated, and we should use `OpenTelemetry.Exporter.OpenTelemetryProtocol` and `AddOtlpExporter` with OTLP port `4317` on Jaeger // tracing.AddJaegerExporter( // x => x.Endpoint = new Uri(observabilityOptions.JaegerOptions.HttpExporterEndpoint)); // http://localhost:14268/api/traces tracing.AddOtlpExporter(x => { x.Endpoint = new Uri(observabilityOptions.JaegerOptions.OTLPGrpcExporterEndpoint); x.Protocol = OtlpExportProtocol.Grpc; }); } if (observabilityOptions.UseZipkinExporter) { ArgumentNullException.ThrowIfNull(observabilityOptions.ZipkinOptions); // https://github.com/open-telemetry/opentelemetry-dotnet/tree/e330e57b04fa3e51fe5d63b52bfff891fb5b7961/src/OpenTelemetry.Exporter.Zipkin tracing.AddZipkinExporter(x => x.Endpoint = new Uri(observabilityOptions.ZipkinOptions.HttpExporterEndpoint) ); // "http://localhost:9411/api/v2/spans" } if (observabilityOptions.UseConsoleExporter) { tracing.AddConsoleExporter(); } if (observabilityOptions.UseOTLPExporter) { ArgumentNullException.ThrowIfNull(observabilityOptions.OTLPOptions); tracing.AddOtlpExporter(x => { x.Endpoint = new Uri(observabilityOptions.OTLPOptions.OTLPGrpcExporterEndpoint); x.Protocol = OtlpExportProtocol.Grpc; }); } if (observabilityOptions.UseAspireOTLPExporter) { // we can just one `AddOtlpExporter` and in development use `aspire-dashboard` OTLP endpoint address as `OTLPExporterEndpoint` and in production we can use `otel-collector` OTLP endpoint address ArgumentNullException.ThrowIfNull(observabilityOptions.OTLPOptions); tracing.AddOtlpExporter(x => { x.Endpoint = new Uri(observabilityOptions.AspireDashboardOTLPOptions.OTLPGrpcExporterEndpoint); x.Protocol = OtlpExportProtocol.Grpc; }); } if (observabilityOptions.UseGrafanaExporter) { // https://github.com/grafana/grafana-opentelemetry-dotnet/blob/main/docs/configuration.md#aspnet-core // https://github.com/grafana/grafana-opentelemetry-dotnet/ // https://github.com/grafana/grafana-opentelemetry-dotnet/blob/main/docs/configuration.md#sending-to-an-agent-or-collector-via-otlp // https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/instrument/dotnet/ tracing.UseGrafana(); } } private static void AddMetricsExporter(ObservabilityOptions observabilityOptions, MeterProviderBuilder metrics) { if (observabilityOptions.UsePrometheusExporter) { // https://github.com/open-telemetry/opentelemetry-dotnet/tree/e330e57b04fa3e51fe5d63b52bfff891fb5b7961/src/OpenTelemetry.Exporter.Prometheus.AspNetCore // for exporting app metrics to `/metrics` endpoint metrics.AddPrometheusExporter(o => o.DisableTotalNameSuffixForCounters = true); // http://localhost:4000/metrics } if (observabilityOptions.UseConsoleExporter) { metrics.AddConsoleExporter(); } if (observabilityOptions.UseOTLPExporter) { ArgumentNullException.ThrowIfNull(observabilityOptions.OTLPOptions); metrics.AddOtlpExporter(x => { x.Endpoint = new Uri(observabilityOptions.OTLPOptions.OTLPGrpcExporterEndpoint); x.Protocol = OtlpExportProtocol.Grpc; }); } if (observabilityOptions.UseAspireOTLPExporter) { // we can just one `AddOtlpExporter` and in development use `aspire-dashboard` OTLP endpoint address as `OTLPExporterEndpoint` and in production we can use `otel-collector` OTLP endpoint address ArgumentNullException.ThrowIfNull(observabilityOptions.OTLPOptions); metrics.AddOtlpExporter(x => { x.Endpoint = new Uri(observabilityOptions.AspireDashboardOTLPOptions.OTLPGrpcExporterEndpoint); x.Protocol = OtlpExportProtocol.Grpc; }); } if (observabilityOptions.UseGrafanaExporter) { // https://github.com/grafana/grafana-opentelemetry-dotnet/blob/main/docs/configuration.md#aspnet-core // https://github.com/grafana/grafana-opentelemetry-dotnet/ // https://github.com/grafana/grafana-opentelemetry-dotnet/blob/main/docs/configuration.md#sending-to-an-agent-or-collector-via-otlp // https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/instrument/dotnet/ metrics.UseGrafana(); } } private static void AddLoggingExporters( this OpenTelemetryLoggerOptions openTelemetryLoggerOptions, ObservabilityOptions observabilityOptions ) { if (observabilityOptions.UseOTLPExporter) { ArgumentNullException.ThrowIfNull(observabilityOptions.OTLPOptions); openTelemetryLoggerOptions.AddOtlpExporter(options => { options.Endpoint = new Uri(observabilityOptions.OTLPOptions.OTLPGrpcExporterEndpoint); options.Protocol = OtlpExportProtocol.Grpc; }); } if (observabilityOptions.UseAspireOTLPExporter) { // we can just one `AddOtlpExporter` and in development use `aspire-dashboard` OTLP endpoint address as `OTLPExporterEndpoint` and in production we can use `otel-collector` OTLP endpoint address ArgumentNullException.ThrowIfNull(observabilityOptions.OTLPOptions); openTelemetryLoggerOptions.AddOtlpExporter(x => { x.Endpoint = new Uri(observabilityOptions.AspireDashboardOTLPOptions.OTLPGrpcExporterEndpoint); x.Protocol = OtlpExportProtocol.Grpc; }); } if (observabilityOptions.UseGrafanaExporter) { // https://github.com/grafana/grafana-opentelemetry-dotnet/ // https://github.com/grafana/grafana-opentelemetry-dotnet/blob/main/docs/configuration.md#aspnet-core // https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/instrument/dotnet/ openTelemetryLoggerOptions.UseGrafana(); } if (observabilityOptions.UseConsoleExporter) { openTelemetryLoggerOptions.AddConsoleExporter(); } } private static WebApplicationBuilder AddCoreDiagnostics(this WebApplicationBuilder builder) { builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); return builder; } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/ObservabilityConstant.cs ================================================ namespace BuildingBlocks.OpenTelemetryCollector; public static class ObservabilityConstant { public static string InstrumentationName = default!; public static class Components { public const string CommandHandler = "CommandHandler"; public const string QueryHandler = "QueryHandler"; public const string EventStore = "EventStore"; public const string Producer = "Producer"; public const string Consumer = "Consumer"; public const string EventHandler = "EventHandler"; } } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/ObservabilityOptions.cs ================================================ namespace BuildingBlocks.OpenTelemetryCollector; public class ObservabilityOptions { public string InstrumentationName { get; set; } = default!; public string? ServiceName { get; set; } public bool MetricsEnabled { get; set; } = true; public bool TracingEnabled { get; set; } = true; public bool LoggingEnabled { get; set; } = true; public bool UsePrometheusExporter { get; set; } = true; public bool UseOTLPExporter { get; set; } = true; public bool UseAspireOTLPExporter { get; set; } = true; public bool UseGrafanaExporter { get; set; } public bool UseConsoleExporter { get; set; } public bool UseJaegerExporter { get; set; } public bool UseZipkinExporter { get; set; } public ZipkinOptions ZipkinOptions { get; set; } = default!; public JaegerOptions JaegerOptions { get; set; } = default!; public OTLPOptions OTLPOptions { get; set; } = default!; public AspireDashboardOTLPOptions AspireDashboardOTLPOptions { get; set; } = default!; } // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Zipkin/README.md public class ZipkinOptions { /// /// Gets or sets endpoint address to receive telemetry /// public string HttpExporterEndpoint { get; set; } = "http://localhost:9411/api/v2/spans"; } public class JaegerOptions { public string OTLPGrpcExporterEndpoint { get; set; } = "http://localhost:14317"; public string HttpExporterEndpoint { get; set; } = "http://localhost:14268/api/traces"; } public class OTLPOptions { public string OTLPGrpcExporterEndpoint { get; set; } = "http://localhost:4317"; public string OTLPHttpExporterEndpoint { get; set; } = "http://localhost:4318"; } public class AspireDashboardOTLPOptions { public string OTLPGrpcExporterEndpoint { get; set; } = "http://localhost:4319"; } ================================================ FILE: src/BuildingBlocks/OpenTelemetryCollector/TelemetryTags.cs ================================================ namespace BuildingBlocks.OpenTelemetryCollector; /// /// Telemetry tags use for adding tags to activities as tag name /// public static class TelemetryTags { // https://opentelemetry.io/docs/specs/semconv/general/trace/ // https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/ public static class Tracing { // https://opentelemetry.io/docs/specs/semconv/resource/#service // https://opentelemetry.io/docs/specs/semconv/attributes-registry/peer/#peer-attributes public static class Service { public const string PeerService = "peer.service"; public const string Name = "service.name"; public const string InstanceId = "service.instance.id"; public const string Version = "service.version"; public const string NameSpace = "service.namespace"; } // https://opentelemetry.io/docs/specs/semconv/attributes-registry/messaging/#general-messaging-attributes // https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/ public static class Messaging { // https://opentelemetry.io/docs/specs/semconv/attributes-registry/messaging/#messaging-operation-type public static class OperationType { public const string Key = "messaging.operation.type"; public const string Receive = "receive"; public const string Send = "send"; public const string Process = "process"; } // https://opentelemetry.io/docs/specs/semconv/attributes-registry/messaging/#messaging-system public static class System { public const string Key = "messaging.system"; public const string ActiveMQ = "activemq"; public const string RabbitMQ = "rabbitmq"; public const string AwsSqs = "aws_sqs"; public const string EventGrid = "eventgrid"; public const string EventHubs = "eventhubs"; public const string GcpPubSub = "gcp_pubsub"; public const string Kafka = "kafka"; public const string Pulsar = "pulsar"; public const string ServiceBus = "servicebus"; } public const string Destination = "messaging.destination"; public const string DestinationKind = "messaging.destination_kind"; public const string Url = "messaging.url"; public const string MessageId = "messaging.message_id"; public const string ConversationId = "messaging.conversation_id"; public const string CorrelationId = "messaging.correlation_id"; public const string CausationId = "messaging.causation_id"; public const string Operation = "messaging.operation"; public const string OperationName = "messaging.operation.name"; public const string DestinationName = "messaging.destination.name"; public const string ConsumerGroup = "messaging.consumer.group.name"; public const string DestinationPartition = "messaging.destination.partition.id"; // https://opentelemetry.io/docs/specs/semconv/attributes-registry/messaging/#rabbitmq-attributes // https://opentelemetry.io/docs/specs/semconv/messaging/rabbitmq/ public static class RabbitMQ { public const string RoutingKey = "messaging.rabbitmq.destination.routing_key"; public const string DeliveryTag = "messaging.rabbitmq.message.delivery_tag"; public static IDictionary ProducerTags( string serviceName, string topicName, string routingKey, string? deliveryTag = null ) => new Dictionary { { System.Key, System.Kafka }, { DeliveryTag, deliveryTag }, { Destination, topicName }, { OperationType.Key, OperationType.Send }, { Service.Name, serviceName }, { RoutingKey, routingKey }, }; public static IDictionary ConsumerTags( string serviceName, string topicName, string routingKey, string? consumerGroup = null ) => new Dictionary { { System.Key, System.Kafka }, { Destination, topicName }, { OperationType.Key, OperationType.Receive }, { Service.Name, serviceName }, { ConsumerGroup, consumerGroup }, { RoutingKey, routingKey }, }; } // https://opentelemetry.io/docs/specs/semconv/attributes-registry/messaging/#kafka-attributes // https://opentelemetry.io/docs/specs/semconv/messaging/kafka/ public static class Kafka { public const string MessageKey = "messaging.kafka.message.key"; public const string Tombstone = "messaging.kafka.message.tombstone"; public const string Offset = "messaging.kafka.offset"; public static IDictionary ProducerTags( string serviceName, string topicName, string messageKey ) => new Dictionary { { System.Key, System.Kafka }, { Destination, topicName }, { OperationType.Key, OperationType.Send }, { Service.Name, serviceName }, { MessageKey, messageKey }, }; public static IDictionary ConsumerTags( string serviceName, string topicName, string messageKey, string partitionName, string consumerGroup ) => new Dictionary { { System.Key, System.Kafka }, { Destination, topicName }, { OperationType.Key, OperationType.Receive }, { Service.Name, serviceName }, { MessageKey, messageKey }, { DestinationPartition, partitionName }, { ConsumerGroup, consumerGroup }, }; } } // https://opentelemetry.io/docs/specs/semconv/database/database-spans/#common-attributes // https://opentelemetry.io/docs/specs/semconv/database/postgresql/#attributes public static class Db { public const string System = "db.system"; public const string ConnectionString = "db.connection_string"; public const string User = "db.user"; public const string MsSqlInstanceName = "db.mssql.instance_name"; public const string Name = "db.name"; public const string Statement = "db.statement"; public const string Operation = "db.operation"; public const string Instance = "db.instance"; public const string Url = "db.url"; public const string CassandraKeyspace = "db.cassandra.keyspace"; public const string RedisDatabaseIndex = "db.redis.database_index"; public const string MongoDbCollection = "db.mongodb.collection"; } // https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/#exception-event public static class Exception { public const string EventName = "exception"; public const string Type = "exception.type"; public const string Message = "exception.message"; public const string Stacktrace = "exception.stacktrace"; } // https://opentelemetry.io/docs/specs/semconv/attributes-registry/otel/#otel-attributes public static class Otel { public const string StatusCode = "otel.status_code"; public const string StatusDescription = "otel.status_description"; } public static class Message { public const string Type = "message.type"; public const string Id = "message.id"; } public static class Application { public static string AppService = $"{ObservabilityConstant.InstrumentationName}.appservice"; public static string Consumer = $"{ObservabilityConstant.InstrumentationName}.consumer"; public static string Producer = $"{ObservabilityConstant.InstrumentationName}.producer"; public static class Commands { public static string Command = $"{ObservabilityConstant.InstrumentationName}.command"; public static string CommandType = $"{Command}.type"; public static string CommandHandler = $"{Command}.handler"; public static string CommandHandlerType = $"{CommandHandler}.type"; } public static class Queries { public static string Query = $"{ObservabilityConstant.InstrumentationName}.query"; public static string QueryType = $"{Query}.type"; public static string QueryHandler = $"{Query}.handler"; public static string QueryHandlerType = $"{QueryHandler}.type"; } public static class Events { public static string Event = $"{ObservabilityConstant.InstrumentationName}.event"; public static string EventType = $"{Event}.type"; public static string EventHandler = $"{Event}.handler"; public static string EventHandlerType = $"{EventHandler}.type"; } } } // https://opentelemetry.io/docs/specs/semconv/general/metrics/ // https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/ public static class Metrics { public static class Application { public static string AppService = $"{ObservabilityConstant.InstrumentationName}.appservice"; public static string Consumer = $"{ObservabilityConstant.InstrumentationName}.consumer"; public static string Producer = $"{ObservabilityConstant.InstrumentationName}.producer"; public static class Commands { public static string Command = $"{ObservabilityConstant.InstrumentationName}.command"; public static string CommandType = $"{Command}.type"; public static string CommandHandler = $"{Command}.handler"; public static string SuccessCount = $"{CommandHandler}.success.count"; public static string FaildCount = $"{CommandHandler}.failed.count"; public static string ActiveCount = $"{CommandHandler}.active.count"; public static string TotalExecutedCount = $"{CommandHandler}.total.count"; public static string HandlerDuration = $"{CommandHandler}.duration"; } public static class Queries { public static string Query = $"{ObservabilityConstant.InstrumentationName}.query"; public static string QueryType = $"{Query}.type"; public static string QueryHandler = $"{Query}.handler"; public static string SuccessCount = $"{QueryHandler}.success.count"; public static string FaildCount = $"{QueryHandler}.failed.count"; public static string ActiveCount = $"{QueryHandler}.active.count"; public static string TotalExecutedCount = $"{QueryHandler}.total.count"; public static string HandlerDuration = $"{QueryHandler}.duration"; } public static class Events { public static string Event = $"{ObservabilityConstant.InstrumentationName}.event"; public static string EventType = $"{Event}.type"; public static string EventHandler = $"{Event}.handler"; public static string SuccessCount = $"{EventHandler}.success.count"; public static string FaildCount = $"{EventHandler}.failed.count"; public static string ActiveCount = $"{EventHandler}.active.count"; public static string TotalExecutedCount = $"{EventHandler}.total.count"; public static string HandlerDuration = $"{EventHandler}.duration"; } } } } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/Extensions.cs ================================================ using BuildingBlocks.Web; using Humanizer; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.PersistMessageProcessor; public static class Extensions { public static IServiceCollection AddPersistMessageProcessor(this WebApplicationBuilder builder, string? connectionName = "persist-message") { AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); builder.Services.AddValidateOptions(); builder.Services.AddDbContext( (sp, options) => { var aspireConnectionString = builder.Configuration.GetConnectionString(connectionName.Kebaberize()); var connectionString = aspireConnectionString ?? sp.GetRequiredService().ConnectionString; ArgumentException.ThrowIfNullOrEmpty(connectionString); options.UseNpgsql( connectionString, dbOptions => { dbOptions.MigrationsAssembly( typeof(PersistMessageDbContext).Assembly.GetName().Name); }) // https://github.com/efcore/EFCore.NamingConventions .UseSnakeCaseNamingConvention(); // Todo: follow up the issues of .net 9 to use better approach taht will provide by .net! options.ConfigureWarnings( w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); }); builder.Services.AddScoped( provider => { var persistMessageDbContext = provider.GetRequiredService(); persistMessageDbContext.Database.EnsureCreated(); persistMessageDbContext.CreatePersistMessageTableIfNotExists(); return persistMessageDbContext; }); builder.Services.AddScoped(); builder.Services.AddHostedService(); return builder.Services; } } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs ================================================ using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.PersistMessageProcessor; public interface IPersistMessageDbContext { DbSet PersistMessage { get; } Task SaveChangesAsync(CancellationToken cancellationToken = default); Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default); } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/IPersistMessageProcessor.cs ================================================ using System.Linq.Expressions; using BuildingBlocks.Core.Event; namespace BuildingBlocks.PersistMessageProcessor; // Ref: http://www.kamilgrzybek.com/design/the-outbox-pattern/ // Ref: https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/ // Ref: https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/ // Ref: https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/subscribe-events#designing-atomicity-and-resiliency-when-publishing-to-the-event-bus // Ref: https://github.com/kgrzybek/modular-monolith-with-ddd#38-internal-processing public interface IPersistMessageProcessor { Task PublishMessageAsync( TMessageEnvelope messageEnvelope, CancellationToken cancellationToken = default) where TMessageEnvelope : MessageEnvelope; Task AddReceivedMessageAsync( TMessageEnvelope messageEnvelope, CancellationToken cancellationToken = default) where TMessageEnvelope : MessageEnvelope; Task AddInternalMessageAsync( TCommand internalCommand, CancellationToken cancellationToken = default) where TCommand : class, IInternalCommand; Task> GetByFilterAsync( Expression> predicate, CancellationToken cancellationToken = default); Task ExistMessageAsync( Guid messageId, CancellationToken cancellationToken = default); Task ProcessInboxAsync( Guid messageId, CancellationToken cancellationToken = default); Task ProcessAsync(Guid messageId, MessageDeliveryType deliveryType, CancellationToken cancellationToken = default); Task ProcessAllAsync(CancellationToken cancellationToken = default); } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/MessageDeliveryType.cs ================================================ namespace BuildingBlocks.PersistMessageProcessor; [Flags] public enum MessageDeliveryType { Unknown = 0, Outbox = 1, Inbox = 2, Internal = 3 } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/MessageStatus.cs ================================================ namespace BuildingBlocks.PersistMessageProcessor; public enum MessageStatus { Unknown = 0, InProgress = 1, Processed = 2 } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs ================================================ using BuildingBlocks.Core.Model; namespace BuildingBlocks.PersistMessageProcessor; public class PersistMessage : IVersion { public PersistMessage(Guid id, string dataType, string data, MessageDeliveryType deliveryType) { Id = id; DataType = dataType; Data = data; DeliveryType = deliveryType; Created = DateTime.Now; MessageStatus = MessageStatus.InProgress; RetryCount = 0; } public Guid Id { get; private set; } public string DataType { get; private set; } public string Data { get; private set; } public DateTime Created { get; private set; } public int RetryCount { get; private set; } public MessageStatus MessageStatus { get; private set; } public MessageDeliveryType DeliveryType { get; private set; } public long Version { get; set; } public void ChangeState(MessageStatus messageStatus) { MessageStatus = messageStatus; } public void IncreaseRetry() { RetryCount++; } } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/PersistMessageBackgroundService.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BuildingBlocks.PersistMessageProcessor; public class PersistMessageBackgroundService( ILogger logger, IServiceProvider serviceProvider, IOptions options ) : BackgroundService { private PersistMessageOptions _options = options.Value; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("PersistMessage Background Service Start"); await ProcessAsync(stoppingToken); } public override Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("PersistMessage Background Service Stop"); return base.StopAsync(cancellationToken); } private async Task ProcessAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await using (var scope = serviceProvider.CreateAsyncScope()) { var service = scope.ServiceProvider.GetRequiredService(); await service.ProcessAllAsync(stoppingToken); } var delay = _options.Interval is { } ? TimeSpan.FromSeconds((int)_options.Interval) : TimeSpan.FromSeconds(30); await Task.Delay(delay, stoppingToken); } } } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/PersistMessageDbContext.cs ================================================ using BuildingBlocks.Core.Model; using BuildingBlocks.EFCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using IsolationLevel = System.Data.IsolationLevel; namespace BuildingBlocks.PersistMessageProcessor; public class PersistMessageDbContext : DbContext, IPersistMessageDbContext { private readonly ILogger? _logger; public PersistMessageDbContext(DbContextOptions options, ILogger? logger = null) : base(options) { _logger = logger; } public DbSet PersistMessage => Set(); protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ToSnakeCaseTables(); } //ref: https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency#execution-strategies-and-transactions public Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default) { var strategy = Database.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 void CreatePersistMessageTableIfNotExists() { string createTableSql = @" create table if not exists persist_message ( id uuid not null, data_type text, data text, created timestamp with time zone not null, retry_count integer not null, message_status text not null default 'InProgress'::text, delivery_type text not null default 'Outbox'::text, version bigint not null, constraint pk_persist_message primary key (id) )"; Database.ExecuteSqlRaw(createTableSql); } 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 (System.Exception ex) { throw new System.Exception("try for find IVersion", ex); } } } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/PersistMessageOptions.cs ================================================ namespace BuildingBlocks.PersistMessageProcessor; public class PersistMessageOptions { public int? Interval { get; set; } = 30; public bool Enabled { get; set; } = true; public string? ConnectionString { get; set; } } ================================================ FILE: src/BuildingBlocks/PersistMessageProcessor/PersistMessageProcessor.cs ================================================ using System.Linq.Expressions; using System.Text.Json; using Ardalis.GuardClauses; using BuildingBlocks.Core.Event; using BuildingBlocks.Utils; using MassTransit; using MediatR; using Microsoft.Extensions.Logging; namespace BuildingBlocks.PersistMessageProcessor; using Microsoft.EntityFrameworkCore; public class PersistMessageProcessor : IPersistMessageProcessor { private readonly ILogger _logger; private readonly IMediator _mediator; private readonly IPersistMessageDbContext _persistMessageDbContext; private readonly IPublishEndpoint _publishEndpoint; public PersistMessageProcessor( ILogger logger, IMediator mediator, IPersistMessageDbContext persistMessageDbContext, IPublishEndpoint publishEndpoint) { _logger = logger; _mediator = mediator; _persistMessageDbContext = persistMessageDbContext; _publishEndpoint = publishEndpoint; } public async Task PublishMessageAsync( TMessageEnvelope messageEnvelope, CancellationToken cancellationToken = default) where TMessageEnvelope : MessageEnvelope { await SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Outbox, cancellationToken); } public Task AddReceivedMessageAsync(TMessageEnvelope messageEnvelope, CancellationToken cancellationToken = default) where TMessageEnvelope : MessageEnvelope { return SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Inbox, cancellationToken); } public async Task AddInternalMessageAsync(TCommand internalCommand, CancellationToken cancellationToken = default) where TCommand : class, IInternalCommand { await SavePersistMessageAsync(new MessageEnvelope(internalCommand), MessageDeliveryType.Internal, cancellationToken); } public async Task> GetByFilterAsync(Expression> predicate, CancellationToken cancellationToken = default) { return (await _persistMessageDbContext.PersistMessage.Where(predicate).ToListAsync(cancellationToken)) .AsReadOnly(); } public Task ExistMessageAsync(Guid messageId, CancellationToken cancellationToken = default) { return _persistMessageDbContext.PersistMessage.FirstOrDefaultAsync(x => x.Id == messageId && x.DeliveryType == MessageDeliveryType.Inbox && x.MessageStatus == MessageStatus.Processed, cancellationToken); } public async Task ProcessAsync( Guid messageId, MessageDeliveryType deliveryType, CancellationToken cancellationToken = default) { var message = await _persistMessageDbContext.PersistMessage.FirstOrDefaultAsync( x => x.Id == messageId && x.DeliveryType == deliveryType, cancellationToken); if (message is null) return; switch (deliveryType) { case MessageDeliveryType.Internal: var sentInternalMessage = await ProcessInternalAsync(message, cancellationToken); if (sentInternalMessage) { await ChangeMessageStatusAsync(message, cancellationToken); break; } else { return; } case MessageDeliveryType.Outbox: var sentOutbox = await ProcessOutboxAsync(message, cancellationToken); if (sentOutbox) { await ChangeMessageStatusAsync(message, cancellationToken); break; } else { return; } } } public async Task ProcessAllAsync(CancellationToken cancellationToken = default) { var messages = await _persistMessageDbContext.PersistMessage .Where(x => x.MessageStatus != MessageStatus.Processed) .ToListAsync(cancellationToken); foreach (var message in messages) { await ProcessAsync(message.Id, message.DeliveryType, cancellationToken); } } public async Task ProcessInboxAsync(Guid messageId, CancellationToken cancellationToken = default) { var message = await _persistMessageDbContext.PersistMessage.FirstOrDefaultAsync( x => x.Id == messageId && x.DeliveryType == MessageDeliveryType.Inbox && x.MessageStatus == MessageStatus.InProgress, cancellationToken); await ChangeMessageStatusAsync(message, cancellationToken); } private async Task ProcessOutboxAsync(PersistMessage message, CancellationToken cancellationToken) { var messageEnvelope = JsonSerializer.Deserialize(message.Data); if (messageEnvelope is null || messageEnvelope.Message is null) return false; var data = JsonSerializer.Deserialize(messageEnvelope.Message.ToString() ?? string.Empty, TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(message.DataType) ?? typeof(object)); if (data is not IEvent) return false; await _publishEndpoint.Publish(data, context => { foreach (var header in messageEnvelope.Headers) context.Headers.Set(header.Key, header.Value); }, cancellationToken); _logger.LogInformation( "Message with id: {MessageId} and delivery type: {DeliveryType} processed from the persistence message store.", message.Id, message.DeliveryType); return true; } private async Task ProcessInternalAsync(PersistMessage message, CancellationToken cancellationToken) { var messageEnvelope = JsonSerializer.Deserialize(message.Data); if (messageEnvelope is null || messageEnvelope.Message is null) return false; var data = JsonSerializer.Deserialize(messageEnvelope.Message.ToString() ?? string.Empty, TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(message.DataType) ?? typeof(object)); if (data is not IInternalCommand internalCommand) return false; await _mediator.Send(internalCommand, cancellationToken); _logger.LogInformation( "InternalCommand with id: {EventID} and delivery type: {DeliveryType} processed from the persistence message store.", message.Id, message.DeliveryType); return true; } private async Task SavePersistMessageAsync( MessageEnvelope messageEnvelope, MessageDeliveryType deliveryType, CancellationToken cancellationToken = default) { Guard.Against.Null(messageEnvelope.Message, nameof(messageEnvelope.Message)); Guid id; if (messageEnvelope.Message is IEvent message) id = message.EventId; else id = NewId.NextGuid(); await _persistMessageDbContext.PersistMessage.AddAsync( new PersistMessage( id, messageEnvelope.Message.GetType().ToString(), JsonSerializer.Serialize(messageEnvelope), deliveryType), cancellationToken); await _persistMessageDbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation( "Message with id: {MessageID} and delivery type: {DeliveryType} saved in persistence message store.", id, deliveryType.ToString()); return id; } private async Task ChangeMessageStatusAsync(PersistMessage message, CancellationToken cancellationToken) { message.ChangeState(MessageStatus.Processed); _persistMessageDbContext.PersistMessage.Update(message); await _persistMessageDbContext.SaveChangesAsync(cancellationToken); } } ================================================ FILE: src/BuildingBlocks/Polly/Extensions.cs ================================================ using Microsoft.Extensions.Logging; namespace BuildingBlocks.Polly; using global::Polly; using Exception = System.Exception; public static class Extensions { public static ILogger Logger { get; set; } = null!; public static T RetryOnFailure(this object retrySource, Func action, int retryCount = 3) { var retryPolicy = Policy .Handle() .Retry(retryCount, (exception, retryAttempt, context) => { Logger.LogInformation($"Retry attempt: {retryAttempt}"); Logger.LogError($"Exception: {exception.Message}"); }); return retryPolicy.Execute(action); } } ================================================ FILE: src/BuildingBlocks/ProblemDetails/Extensions.cs ================================================ namespace BuildingBlocks.ProblemDetails; using Exception; using Grpc.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; public static class Extensions { public static WebApplication UseCustomProblemDetails(this WebApplication app) { app.UseStatusCodePages(statusCodeHandlerApp => { statusCodeHandlerApp.Run(async context => { context.Response.ContentType = "application/problem+json"; if (context.RequestServices.GetService() is { } problemDetailsService) { await problemDetailsService.WriteAsync(new ProblemDetailsContext { HttpContext = context, ProblemDetails = { Detail = ReasonPhrases.GetReasonPhrase(context.Response.StatusCode), Status = context.Response.StatusCode } }); } }); }); app.UseExceptionHandler(exceptionHandlerApp => { exceptionHandlerApp.Run(async context => { context.Response.ContentType = "application/problem+json"; if (context.RequestServices.GetService() is { } problemDetailsService) { var exceptionHandlerFeature = context.Features.Get(); var exceptionType = exceptionHandlerFeature?.Error; if (exceptionType is not null) { (string Detail, string Title, int StatusCode) details = exceptionType switch { ConflictException => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = StatusCodes.Status409Conflict ), ValidationException validationException => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = (int)validationException.StatusCode ), BadRequestException => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = StatusCodes.Status400BadRequest ), NotFoundException => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = StatusCodes.Status404NotFound ), AppException => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = StatusCodes.Status400BadRequest ), DbUpdateConcurrencyException => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = StatusCodes.Status409Conflict ), RpcException => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = StatusCodes.Status400BadRequest ), _ => ( exceptionType.Message, exceptionType.GetType().Name, context.Response.StatusCode = StatusCodes.Status500InternalServerError ) }; var problem = new ProblemDetailsContext { HttpContext = context, ProblemDetails = { Title = details.Title, Detail = details.Detail, Status = details.StatusCode } }; if (app.Environment.IsDevelopment()) { problem.ProblemDetails.Extensions.Add("exception", exceptionHandlerFeature?.Error.ToString()); } await problemDetailsService.WriteAsync(problem); } } }); }); return app; } } ================================================ FILE: src/BuildingBlocks/TestBase/TestBase.cs ================================================ using System.Net; using System.Security.Claims; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; using BuildingBlocks.EFCore; using BuildingBlocks.Mongo; using BuildingBlocks.PersistMessageProcessor; using BuildingBlocks.Web; using Duende.IdentityServer.EntityFramework.Entities; using EasyNetQ.Management.Client; using Grpc.Net.Client; using MassTransit; using MassTransit.Testing; using MediatR; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MongoDB.Driver; using NSubstitute; using Respawn; using WebMotions.Fake.Authentication.JwtBearer; using Xunit; using Xunit.Abstractions; namespace BuildingBlocks.TestBase; using System.Globalization; using Npgsql; using Testcontainers.EventStoreDb; using Testcontainers.MongoDb; using Testcontainers.PostgreSql; using Testcontainers.RabbitMq; public class TestFixture : IAsyncLifetime where TEntryPoint : class { private readonly WebApplicationFactory _factory; private int Timeout => 120; // Second public ITestHarness TestHarness => ServiceProvider?.GetTestHarness(); private Action TestRegistrationServices { get; set; } private PostgreSqlContainer PostgresTestcontainer; private PostgreSqlContainer PostgresPersistTestContainer; public RabbitMqContainer RabbitMqTestContainer; public MongoDbContainer MongoDbTestContainer; public EventStoreDbContainer EventStoreDbTestContainer; public CancellationTokenSource CancellationTokenSource; public PersistMessageBackgroundService PersistMessageBackgroundService => ServiceProvider.GetRequiredService(); public HttpClient HttpClient { get { var claims = new Dictionary { { ClaimTypes.Name, "test@sample.com" }, { ClaimTypes.Role, "admin" }, { "scope", "flight-api" }, }; var httpClient = _factory.CreateClient(); httpClient.SetFakeBearerToken(claims); // Uses FakeJwtBearer return httpClient; } } public GrpcChannel Channel => GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions { HttpClient = HttpClient }); public IServiceProvider ServiceProvider => _factory?.Services; public IConfiguration Configuration => _factory?.Services.GetRequiredService(); public ILogger Logger { get; set; } protected TestFixture() { _factory = new WebApplicationFactory().WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration(AddCustomAppSettings); builder.UseEnvironment("test"); builder.ConfigureServices(services => { TestRegistrationServices?.Invoke(services); services.ReplaceSingleton(AddHttpContextAccessorMock); services.AddSingleton(); services.RemoveHostedService(); // Register all ITestDataSeeder implementations dynamically services.Scan(scan => scan.FromApplicationDependencies() // Scan the current app and its dependencies .AddClasses(classes => classes.AssignableTo()) // Find classes that implement ITestDataSeeder .AsImplementedInterfaces() .WithScopedLifetime() ); // Add Fake JWT Authentication - we can use SetAdminUser method to set authenticate user to existing HttContextAccessor // https://github.com/webmotions/fake-authentication-jwtbearer // https://github.com/webmotions/fake-authentication-jwtbearer/issues/14 services .AddAuthentication(options => { options.DefaultAuthenticateScheme = FakeJwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = FakeJwtBearerDefaults.AuthenticationScheme; }) .AddFakeJwtBearer(); // Mock Authorization Policies services.AddAuthorization(options => { options.AddPolicy( nameof(ApiScope), policy => { policy.AddAuthenticationSchemes(FakeJwtBearerDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", "flight-api"); // Test-specific scope } ); }); }); }); } public async Task InitializeAsync() { CancellationTokenSource = new CancellationTokenSource(); await StartTestContainerAsync(); } public async Task DisposeAsync() { await StopTestContainerAsync(); await _factory.DisposeAsync(); await CancellationTokenSource.CancelAsync(); } public virtual void RegisterServices(Action services) { TestRegistrationServices += services; } // ref: https://github.com/trbenning/serilog-sinks-xunit public ILogger CreateLogger(ITestOutputHelper output) { if (output == null) return null; var loggerFactory = LoggerFactory.Create(builder => { builder.AddXunit(output); builder.SetMinimumLevel(LogLevel.Debug); }); return loggerFactory.CreateLogger("TestLogger"); } protected async Task ExecuteScopeAsync(Func action) { using var scope = ServiceProvider.CreateScope(); await action(scope.ServiceProvider); } protected async Task ExecuteScopeAsync(Func> action) { using var scope = ServiceProvider.CreateScope(); var result = await action(scope.ServiceProvider); return result; } public Task SendAsync(IRequest request) { return ExecuteScopeAsync(sp => { var mediator = sp.GetRequiredService(); return mediator.Send(request); }); } public Task SendAsync(IRequest request) { return ExecuteScopeAsync(sp => { var mediator = sp.GetRequiredService(); return mediator.Send(request); }); } public async Task Publish(TMessage message, CancellationToken cancellationToken = default) where TMessage : class, IEvent { // Use harness bus to ensure publish happens only after the bus is started. await TestHarness.Bus.Publish(message, cancellationToken); } public async Task WaitForPublishing(CancellationToken cancellationToken = default) where TMessage : class, IEvent { var result = await WaitUntilConditionMet( async () => { var published = await TestHarness.Published.Any(cancellationToken); return published; }, cancellationToken: cancellationToken ); return result; } public Task WaitUntilAsync( Func> condition, TimeSpan? timeout = null, TimeSpan? pollInterval = null, CancellationToken cancellationToken = default ) { var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(Timeout); var effectivePollInterval = pollInterval ?? TimeSpan.FromMilliseconds(200); return WaitUntilConditionMet( conditionToMet: async () => { cancellationToken.ThrowIfCancellationRequested(); return await condition(); }, timeoutSecond: (int)Math.Ceiling(effectiveTimeout.TotalSeconds), pollInterval: effectivePollInterval, cancellationToken: cancellationToken ); } // Ref: https://tech.energyhelpline.com/in-memory-testing-with-masstransit/ private async Task WaitUntilConditionMet( Func> conditionToMet, int? timeoutSecond = null, TimeSpan? pollInterval = null, CancellationToken cancellationToken = default ) { var time = timeoutSecond ?? Timeout; var delay = pollInterval ?? TimeSpan.FromMilliseconds(100); var startTime = DateTime.UtcNow; while (DateTime.UtcNow - startTime <= TimeSpan.FromSeconds(time)) { cancellationToken.ThrowIfCancellationRequested(); if (await conditionToMet.Invoke()) return true; await Task.Delay(delay, cancellationToken); } return false; } private async Task StartTestContainerAsync() { PostgresTestcontainer = TestContainers.PostgresTestContainer(); PostgresPersistTestContainer = TestContainers.PostgresPersistTestContainer(); RabbitMqTestContainer = TestContainers.RabbitMqTestContainer(); MongoDbTestContainer = TestContainers.MongoTestContainer(); EventStoreDbTestContainer = TestContainers.EventStoreTestContainer(); await MongoDbTestContainer.StartAsync(); await PostgresTestcontainer.StartAsync(); await PostgresPersistTestContainer.StartAsync(); await RabbitMqTestContainer.StartAsync(); await EventStoreDbTestContainer.StartAsync(); } private async Task StopTestContainerAsync() { await PostgresTestcontainer.StopAsync(); await PostgresPersistTestContainer.StopAsync(); await RabbitMqTestContainer.StopAsync(); await MongoDbTestContainer.StopAsync(); await EventStoreDbTestContainer.StopAsync(); } private void AddCustomAppSettings(IConfigurationBuilder configuration) { //todo: provide better approach for reading `PostgresOptions` configuration.AddInMemoryCollection( new KeyValuePair[] { new("PostgresOptions:ConnectionString", PostgresTestcontainer.GetConnectionString()), new("PostgresOptions:ConnectionString:Flight", PostgresTestcontainer.GetConnectionString()), new("PostgresOptions:ConnectionString:Identity", PostgresTestcontainer.GetConnectionString()), new("PostgresOptions:ConnectionString:Passenger", PostgresTestcontainer.GetConnectionString()), new("PersistMessageOptions:ConnectionString", PostgresPersistTestContainer.GetConnectionString()), new("RabbitMqOptions:HostName", "127.0.0.1"), new("RabbitMqOptions:UserName", TestContainers.RabbitMqContainerConfiguration.UserName), new("RabbitMqOptions:Password", TestContainers.RabbitMqContainerConfiguration.Password), new( "RabbitMqOptions:Port", RabbitMqTestContainer .GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration.Port) .ToString(NumberFormatInfo.InvariantInfo) ), new("MongoOptions:ConnectionString", MongoDbTestContainer.GetConnectionString()), new("MongoOptions:DatabaseName", TestContainers.MongoContainerConfiguration.Name), new("EventStoreOptions:ConnectionString", EventStoreDbTestContainer.GetConnectionString()), } ); } private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) { var httpContextAccessorMock = Substitute.For(); using var scope = serviceProvider.CreateScope(); httpContextAccessorMock.HttpContext = new DefaultHttpContext { RequestServices = scope.ServiceProvider }; httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012); httpContextAccessorMock.HttpContext.Request.Scheme = "http"; return httpContextAccessorMock; } } public class TestWriteFixture : TestFixture where TEntryPoint : class where TWContext : DbContext { public Task ExecuteDbContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public Task ExecuteDbContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService()).AsTask()); } public Task ExecuteDbContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService(), sp.GetRequiredService())); } public Task ExecuteDbContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public Task ExecuteDbContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService()).AsTask()); } public Task ExecuteDbContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService(), sp.GetRequiredService())); } public Task InsertAsync(params T[] entities) where T : class { return ExecuteDbContextAsync(db => { foreach (var entity in entities) { db.Set().Add(entity); } return db.SaveChangesAsync(); }); } public async Task InsertAsync(TEntity entity) where TEntity : class { await ExecuteDbContextAsync(db => { db.Set().Add(entity); return db.SaveChangesAsync(); }); } public Task InsertAsync(TEntity entity, TEntity2 entity2) where TEntity : class where TEntity2 : class { return ExecuteDbContextAsync(db => { db.Set().Add(entity); db.Set().Add(entity2); return db.SaveChangesAsync(); }); } public Task InsertAsync(TEntity entity, TEntity2 entity2, TEntity3 entity3) where TEntity : class where TEntity2 : class where TEntity3 : class { return ExecuteDbContextAsync(db => { db.Set().Add(entity); db.Set().Add(entity2); db.Set().Add(entity3); return db.SaveChangesAsync(); }); } public Task InsertAsync( TEntity entity, TEntity2 entity2, TEntity3 entity3, TEntity4 entity4 ) where TEntity : class where TEntity2 : class where TEntity3 : class where TEntity4 : class { return ExecuteDbContextAsync(db => { db.Set().Add(entity); db.Set().Add(entity2); db.Set().Add(entity3); db.Set().Add(entity4); return db.SaveChangesAsync(); }); } public Task FindAsync(TKey id) where T : class, IEntity { return ExecuteDbContextAsync(db => db.Set().FindAsync(id).AsTask()); } public Task FirstOrDefaultAsync() where T : class, IEntity { return ExecuteDbContextAsync(db => db.Set().FirstOrDefaultAsync()); } } public class TestReadFixture : TestFixture where TEntryPoint : class where TRContext : MongoDbContext { public Task ExecuteReadContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public Task ExecuteReadContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public async Task InsertMongoDbContextAsync(string collectionName, params T[] entities) where T : class { await ExecuteReadContextAsync(async db => { await db.GetCollection(collectionName).InsertManyAsync(entities.ToList()); }); } } public class TestFixture : TestWriteFixture where TEntryPoint : class where TWContext : DbContext where TRContext : MongoDbContext { public Task ExecuteReadContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public Task ExecuteReadContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public async Task InsertMongoDbContextAsync(string collectionName, params T[] entities) where T : class { await ExecuteReadContextAsync(async db => { await db.GetCollection(collectionName).InsertManyAsync(entities.ToList()); }); } } public class TestFixtureCore : IAsyncLifetime where TEntryPoint : class { private Respawner _reSpawnerDefaultDb; private Respawner _reSpawnerPersistDb; private NpgsqlConnection DefaultDbConnection { get; set; } private NpgsqlConnection PersistDbConnection { get; set; } private Type _dbContextType; public TestFixtureCore( TestFixture integrationTestFixture, ITestOutputHelper outputHelper, Type dbContextType = null ) { Fixture = integrationTestFixture; integrationTestFixture.RegisterServices(RegisterTestsServices); integrationTestFixture.Logger = integrationTestFixture.CreateLogger(outputHelper); _dbContextType = dbContextType; } public TestFixture Fixture { get; } public async Task InitializeAsync() { await InitPostgresAsync(); } public async Task DisposeAsync() { await ResetPostgresAsync(); await ResetMongoAsync(); await ResetRabbitMqAsync(); } private async Task InitPostgresAsync() { var postgresOptions = Fixture.ServiceProvider.GetService(); var persistOptions = Fixture.ServiceProvider.GetService(); if (!string.IsNullOrEmpty(persistOptions?.ConnectionString)) { PersistDbConnection = new NpgsqlConnection(persistOptions.ConnectionString); await PersistDbConnection.OpenAsync(); using var scope = Fixture.ServiceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.EnsureCreatedAsync(); await Fixture.PersistMessageBackgroundService.StartAsync(Fixture.CancellationTokenSource.Token); _reSpawnerPersistDb = await Respawner.CreateAsync( PersistDbConnection, new RespawnerOptions { DbAdapter = DbAdapter.Postgres } ); } if (!string.IsNullOrEmpty(postgresOptions?.ConnectionString) && _dbContextType != null) { DefaultDbConnection = new NpgsqlConnection(postgresOptions.ConnectionString); await DefaultDbConnection.OpenAsync(); using var scope = Fixture.ServiceProvider.CreateScope(); if (scope.ServiceProvider.GetRequiredService(_dbContextType) is DbContext dbContext) { await dbContext.Database.EnsureCreatedAsync(); } _reSpawnerDefaultDb = await Respawner.CreateAsync( DefaultDbConnection, new RespawnerOptions { DbAdapter = DbAdapter.Postgres, TablesToIgnore = ["__EFMigrationsHistory"] } ); await SeedDataAsync(); } } private async Task ResetPostgresAsync() { if (PersistDbConnection is not null) { await _reSpawnerPersistDb.ResetAsync(PersistDbConnection); await Fixture.PersistMessageBackgroundService.StopAsync(Fixture.CancellationTokenSource.Token); } if (DefaultDbConnection is not null) { await _reSpawnerDefaultDb.ResetAsync(DefaultDbConnection); } } private async Task ResetMongoAsync(CancellationToken cancellationToken = default) { //https://stackoverflow.com/questions/3366397/delete-everything-in-a-mongodb-database var dbClient = new MongoClient(Fixture.MongoDbTestContainer?.GetConnectionString()); var collections = await dbClient .GetDatabase(TestContainers.MongoContainerConfiguration.Name) .ListCollectionsAsync(cancellationToken: cancellationToken); foreach (var collection in collections.ToList()) { await dbClient .GetDatabase(TestContainers.MongoContainerConfiguration.Name) .DropCollectionAsync(collection["name"].AsString, cancellationToken); } } private async Task ResetRabbitMqAsync(CancellationToken cancellationToken = default) { var port = Fixture.RabbitMqTestContainer?.GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration.ApiPort) ?? TestContainers.RabbitMqContainerConfiguration.ApiPort; var managementClient = new ManagementClient( Fixture.RabbitMqTestContainer?.Hostname, TestContainers.RabbitMqContainerConfiguration?.UserName, TestContainers.RabbitMqContainerConfiguration?.Password, port ); var bd = await managementClient.GetBindingsAsync(cancellationToken); var bindings = bd.Where(x => !string.IsNullOrEmpty(x.Source) && !string.IsNullOrEmpty(x.Destination)); foreach (var binding in bindings) { await managementClient.DeleteBindingAsync(binding, cancellationToken); } var queues = await managementClient.GetQueuesAsync(cancellationToken: cancellationToken); foreach (var queue in queues) { await managementClient.PurgeAsync(queue, cancellationToken); } } protected virtual void RegisterTestsServices(IServiceCollection services) { } private async Task SeedDataAsync() { using var scope = Fixture.ServiceProvider.CreateScope(); var seedManager = scope.ServiceProvider.GetService(); await seedManager.ExecuteTestSeedAsync(); } } public abstract class TestReadBase : TestFixtureCore // ,IClassFixture> where TEntryPoint : class where TRContext : MongoDbContext { protected TestReadBase( TestReadFixture integrationTestFixture, ITestOutputHelper outputHelper = null ) : base(integrationTestFixture, outputHelper) { Fixture = integrationTestFixture; } public TestReadFixture Fixture { get; } } public abstract class TestWriteBase : TestFixtureCore //,IClassFixture> where TEntryPoint : class where TWContext : DbContext { protected TestWriteBase( TestWriteFixture integrationTestFixture, ITestOutputHelper outputHelper = null ) : base(integrationTestFixture, outputHelper, typeof(TWContext)) { Fixture = integrationTestFixture; } public TestWriteFixture Fixture { get; } } public abstract class TestBase : TestFixtureCore //,IClassFixture> where TEntryPoint : class where TWContext : DbContext where TRContext : MongoDbContext { protected TestBase( TestFixture integrationTestFixture, ITestOutputHelper outputHelper = null ) : base(integrationTestFixture, outputHelper, typeof(TWContext)) { Fixture = integrationTestFixture; } public TestFixture Fixture { get; } } ================================================ FILE: src/BuildingBlocks/TestBase/TestContainers.cs ================================================ using DotNet.Testcontainers.Builders; namespace BuildingBlocks.TestBase; using Testcontainers.EventStoreDb; using Testcontainers.MongoDb; using Testcontainers.PostgreSql; using Testcontainers.RabbitMq; using Web; public static class TestContainers { public static RabbitMqContainerOptions RabbitMqContainerConfiguration { get; } public static PostgresContainerOptions PostgresContainerConfiguration { get; } public static PostgresPersistContainerOptions PostgresPersistContainerConfiguration { get; } public static MongoContainerOptions MongoContainerConfiguration { get; } public static EventStoreContainerOptions EventStoreContainerConfiguration { get; } static TestContainers() { var configuration = ConfigurationHelper.GetConfiguration(); RabbitMqContainerConfiguration = configuration.GetOptions( nameof(RabbitMqContainerOptions) ); PostgresContainerConfiguration = configuration.GetOptions( nameof(PostgresContainerOptions) ); PostgresPersistContainerConfiguration = configuration.GetOptions( nameof(PostgresPersistContainerOptions) ); MongoContainerConfiguration = configuration.GetOptions(nameof(MongoContainerOptions)); EventStoreContainerConfiguration = configuration.GetOptions( nameof(EventStoreContainerOptions) ); } public static PostgreSqlContainer PostgresTestContainer() { var baseBuilder = new PostgreSqlBuilder() .WithUsername(PostgresContainerConfiguration.UserName) .WithPassword(PostgresContainerConfiguration.Password) .WithLabel("Key", "Value"); var builder = baseBuilder .WithImage(PostgresContainerConfiguration.ImageName) .WithName(PostgresContainerConfiguration.Name) .WithCommand(new string[2] { "-c", "max_prepared_transactions=10" }) .WithPortBinding(PostgresContainerConfiguration.Port, true) .Build(); return builder; } public static PostgreSqlContainer PostgresPersistTestContainer() { var baseBuilder = new PostgreSqlBuilder() .WithUsername(PostgresPersistContainerConfiguration.UserName) .WithPassword(PostgresPersistContainerConfiguration.Password) .WithLabel("Key", "Value"); var builder = baseBuilder .WithImage(PostgresPersistContainerConfiguration.ImageName) .WithName(PostgresPersistContainerConfiguration.Name) .WithCommand(new string[2] { "-c", "max_prepared_transactions=10" }) .WithPortBinding(PostgresPersistContainerConfiguration.Port, true) .Build(); return builder; } public static MongoDbContainer MongoTestContainer() { var baseBuilder = new MongoDbBuilder() .WithUsername(MongoContainerConfiguration.UserName) .WithPassword(MongoContainerConfiguration.Password) .WithLabel("Key", "Value"); var builder = baseBuilder .WithImage(MongoContainerConfiguration.ImageName) .WithName(MongoContainerConfiguration.Name) .WithPortBinding(MongoContainerConfiguration.Port, true) .Build(); return builder; } public static RabbitMqContainer RabbitMqTestContainer() { var baseBuilder = new RabbitMqBuilder() .WithUsername(RabbitMqContainerConfiguration.UserName) .WithPassword(RabbitMqContainerConfiguration.Password) .WithLabel("Key", "Value"); var builder = baseBuilder .WithImage(RabbitMqContainerConfiguration.ImageName) .WithName(RabbitMqContainerConfiguration.Name) .WithPortBinding(RabbitMqContainerConfiguration.ApiPort, true) .WithPortBinding(RabbitMqContainerConfiguration.Port, true) .Build(); return builder; } public static EventStoreDbContainer EventStoreTestContainer() { var baseBuilder = new EventStoreDbBuilder().WithLabel("Key", "Value"); var builder = baseBuilder .WithImage(EventStoreContainerConfiguration.ImageName) .WithName(EventStoreContainerConfiguration.Name) .Build(); return builder; } public sealed class RabbitMqContainerOptions { public string Name { get; set; } = "rabbitmq_" + Guid.NewGuid(); public int Port { get; set; } = 5672; public int ApiPort { get; set; } = 15672; public string ImageName { get; set; } = "rabbitmq:management"; public string UserName { get; set; } = "guest"; public string Password { get; set; } = "guest"; } public sealed class PostgresContainerOptions { public string Name { get; set; } = "postgreSql_" + Guid.NewGuid().ToString("D"); public int Port { get; set; } = 5432; public string ImageName { get; set; } = "postgres:latest"; public string UserName { get; set; } = Guid.NewGuid().ToString("D"); public string Password { get; set; } = Guid.NewGuid().ToString("D"); } public sealed class PostgresPersistContainerOptions { public string Name { get; set; } = "postgreSql_" + Guid.NewGuid().ToString("D"); public int Port { get; set; } = 5432; public string ImageName { get; set; } = "postgres:latest"; public string UserName { get; set; } = Guid.NewGuid().ToString("D"); public string Password { get; set; } = Guid.NewGuid().ToString("D"); } public sealed class MongoContainerOptions { public string Name { get; set; } = "mongo_" + Guid.NewGuid().ToString("D"); public int Port { get; set; } = 27017; public string ImageName { get; set; } = "mongo:latest"; public string UserName { get; set; } = Guid.NewGuid().ToString("D"); public string Password { get; set; } = Guid.NewGuid().ToString("D"); } public sealed class EventStoreContainerOptions { public string Name { get; set; } = "event_store_" + Guid.NewGuid().ToString("D"); public int Port { get; set; } = 2113; public string ImageName { get; set; } = "eventstore/eventstore:latest"; } } ================================================ FILE: src/BuildingBlocks/Utils/NoSynchronizationContextScope.cs ================================================ namespace BuildingBlocks.Utils; public static class NoSynchronizationContextScope { public static Disposable Enter() { var context = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); return new Disposable(context); } public struct Disposable : IDisposable { private readonly SynchronizationContext? synchronizationContext; public Disposable(SynchronizationContext? synchronizationContext) { this.synchronizationContext = synchronizationContext; } public void Dispose() => SynchronizationContext.SetSynchronizationContext(synchronizationContext); } } ================================================ FILE: src/BuildingBlocks/Utils/ServiceLocator.cs ================================================ using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.Utils; //ref: https://dotnetcoretutorials.com/2018/05/06/servicelocator-shim-for-net-core/ public class ServiceLocator { private IServiceProvider _currentServiceProvider; private static IServiceProvider _serviceProvider; public ServiceLocator(IServiceProvider currentServiceProvider) { _currentServiceProvider = currentServiceProvider; } public static ServiceLocator Current { get { return new ServiceLocator(_serviceProvider); } } public static void SetLocatorProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public object GetInstance(Type serviceType) { return _currentServiceProvider.GetService(serviceType); } public TService GetInstance() { return _currentServiceProvider.GetService(); } } ================================================ FILE: src/BuildingBlocks/Utils/TypeProvider.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Mvc.ApplicationParts; namespace BuildingBlocks.Utils; public static class TypeProvider { private static bool IsRecord(this Type objectType) { return objectType.GetMethod("$") != null || ((TypeInfo)objectType) .DeclaredProperties.FirstOrDefault(x => x.Name == "EqualityContract")? .GetMethod?.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) != null; } public static Type? GetTypeFromAnyReferencingAssembly(string typeName) { var referencedAssemblies = Assembly.GetEntryAssembly()? .GetReferencedAssemblies() .Select(a => a.FullName); if (referencedAssemblies == null) return null; return AppDomain.CurrentDomain.GetAssemblies() .Where(a => referencedAssemblies.Contains(a.FullName)) .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName)) .FirstOrDefault(); } public static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string typeName) { var result = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName)) .FirstOrDefault(); return result; } public static IReadOnlyList GetReferencedAssemblies(Assembly? rootAssembly) { var visited = new HashSet(); var queue = new Queue(); var listResult = new List(); var root = rootAssembly ?? Assembly.GetEntryAssembly(); queue.Enqueue(root); do { var asm = queue.Dequeue(); if (asm == null) break; listResult.Add(asm); foreach (var reference in asm.GetReferencedAssemblies()) { if (!visited.Contains(reference.FullName)) { // Load will add assembly into the application domain of the caller. loading assemblies explicitly to AppDomain, because assemblies are loaded lazily // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.assembly.load queue.Enqueue(Assembly.Load(reference)); visited.Add(reference.FullName); } } } while (queue.Count > 0); return listResult.Distinct().ToList().AsReadOnly(); } public static IReadOnlyList GetApplicationPartAssemblies(Assembly rootAssembly) { var rootNamespace = rootAssembly.GetName().Name!.Split('.').First(); var list = rootAssembly!.GetCustomAttributes() .Where(x => x.AssemblyName.StartsWith(rootNamespace, StringComparison.InvariantCulture)) .Select(name => Assembly.Load(name.AssemblyName)) .Distinct(); return list.ToList().AsReadOnly(); } } ================================================ FILE: src/BuildingBlocks/Validation/Extensions.cs ================================================ using BuildingBlocks.Exception; using FluentValidation; namespace BuildingBlocks.Validation { public static class Extensions { /// /// Ref https://www.jerriepelser.com/blog/validation-response-aspnet-core-webapi /// public static async Task HandleValidationAsync(this IValidator validator, TRequest request) { var validationResult = await validator.ValidateAsync(request); if (!validationResult.IsValid) { throw new Exception.ValidationException(validationResult.Errors?.First()?.ErrorMessage); } } } } ================================================ FILE: src/BuildingBlocks/Validation/ValidationBehavior.cs ================================================ using FluentValidation; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.Validation; public sealed class ValidationBehavior : IPipelineBehavior where TRequest : class, IRequest { private IValidator _validator; private readonly IServiceProvider _serviceProvider; public ValidationBehavior(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { _validator = _serviceProvider.GetService>(); if (_validator is null) return await next(); await _validator.HandleValidationAsync(request); return await next(); } } ================================================ FILE: src/BuildingBlocks/Validation/ValidationError.cs ================================================ namespace BuildingBlocks.Validation { public class ValidationError { public string Field { get; } public string Message { get; } public ValidationError(string field, string message) { Field = field != string.Empty ? field : null; Message = message; } } } ================================================ FILE: src/BuildingBlocks/Validation/ValidationResultModel.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.Json; using FluentValidation.Results; namespace BuildingBlocks.Validation { public class ValidationResultModel { public int StatusCode { get; set; } = (int)HttpStatusCode.BadRequest; public string Message { get; set; } = "Validation Failed."; public List Errors { get; set; } public override string ToString() { return JsonSerializer.Serialize(this); } } } ================================================ FILE: src/BuildingBlocks/Web/ApiVersioningExtensions.cs ================================================ using Asp.Versioning; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.Web; public static class ApiVersioningExtensions { public static void AddCustomVersioning( this IServiceCollection services, Action? configurator = null) { // https://www.meziantou.net/versioning-an-asp-net-core-api.htm // https://dotnetthoughts.net/aspnetcore-api-versioning-with-net-6-minimal-apis/ // https://im5tu.io/article/2022/10/asp.net-core-versioning-minimal-apis/ // https://www.youtube.com/watch?v=YRJGKyzjFlY // https://www.nuget.org/packages/Asp.Versioning.Http // Support versioning in minimal apis with (Asp.Versioning.Http) dll services.AddApiVersioning(options => { // Add the headers "api-supported-versions" and "api-deprecated-versions" // This is better for discoverability options.ReportApiVersions = true; // AssumeDefaultVersionWhenUnspecified should only be enabled when supporting legacy services that did not previously // support API versioning. Forcing existing clients to specify an explicit API version for an // existing service introduces a breaking change. Conceptually, clients in this situation are // bound to some API version of a service, but they don't know what it is and never explicit request it. options.AssumeDefaultVersionWhenUnspecified = true; options.DefaultApiVersion = new ApiVersion(1, 0); // Defines how an API version is read from the current HTTP request options.ApiVersionReader = ApiVersionReader.Combine( new HeaderApiVersionReader("api-version"), new UrlSegmentApiVersionReader()); configurator?.Invoke(options); }) .AddApiExplorer( options => { // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service // note: the specified format code will format the version as "'v'major[.minor][-status]" options.GroupNameFormat = "'v'VVV"; // note: this option is only necessary when versioning by url segment. the SubstitutionFormat // can also be used to control the format of the API version in route templates options.SubstituteApiVersionInUrl = true; }) // Support versioning in mvc with with (Asp.Versioning.Mvc.ApiExplorer) dll .AddMvc(); // https://www.nuget.org/packages/Asp.Versioning.Mvc.ApiExplorer } } ================================================ FILE: src/BuildingBlocks/Web/AppOptions.cs ================================================ namespace BuildingBlocks.Web; public class AppOptions { public string Name { get; set; } } ================================================ FILE: src/BuildingBlocks/Web/BaseController.cs ================================================ using Asp.Versioning; using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.Web; using MapsterMapper; [Route(BaseApiPath)] [ApiController] [ApiVersion("1.0")] public abstract class BaseController : ControllerBase { protected const string BaseApiPath = "api/v{version:apiVersion}"; private IMapper _mapper; private IMediator _mediator; protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService(); protected IMapper Mapper => _mapper ??= HttpContext.RequestServices.GetService(); } ================================================ FILE: src/BuildingBlocks/Web/ConfigurationExtensions.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace BuildingBlocks.Web; using MassTransit; using Microsoft.Extensions.Options; public static class ConfigurationExtensions { public static TModel GetOptions(this IConfiguration configuration, string section) where TModel : new() { var model = new TModel(); configuration.GetSection(section).Bind(model); return model; } public static TModel GetOptions(this IServiceCollection service, string section) where TModel : new() { var model = new TModel(); var configuration = service.BuildServiceProvider().GetService(); configuration?.GetSection(section).Bind(model); return model; } public static TModel GetOptions(this WebApplication app, string section) where TModel : new() { var model = new TModel(); app.Configuration?.GetSection(section).Bind(model); return model; } public static void AddValidateOptions(this IServiceCollection service) where TModel : class, new() { service.AddOptions() .BindConfiguration(typeof(TModel).Name) .ValidateDataAnnotations(); service.AddSingleton(x => x.GetRequiredService>().Value); } } ================================================ FILE: src/BuildingBlocks/Web/ConfigurationHelper.cs ================================================ using Microsoft.Extensions.Configuration; namespace BuildingBlocks.Web { public static class ConfigurationHelper { public static IConfiguration GetConfiguration(string basePath = null) { basePath ??= Directory.GetCurrentDirectory(); var environmentVariable = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); return new ConfigurationBuilder() .SetBasePath(basePath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{environmentVariable}.json", optional: true) .AddEnvironmentVariables() .Build(); } } } ================================================ FILE: src/BuildingBlocks/Web/CorrelationExtensions.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace BuildingBlocks.Web; public static class CorrelationExtensions { private const string CorrelationId = "correlationId"; public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app) { return app.Use(async (ctx, next) => { if (!ctx.Request.Headers.TryGetValue(CorrelationId, out var correlationId)) correlationId = Guid.NewGuid().ToString("N"); ctx.Items[CorrelationId] = correlationId.ToString(); await next(); }); } public static Guid GetCorrelationId(this HttpContext context) { context.Items.TryGetValue(CorrelationId, out var correlationId); return string.IsNullOrEmpty(correlationId?.ToString()) ? Guid.NewGuid() : new Guid(correlationId.ToString()!); } } ================================================ FILE: src/BuildingBlocks/Web/CurrentUserProvider.cs ================================================ using System.Security.Claims; using Microsoft.AspNetCore.Http; namespace BuildingBlocks.Web; public interface ICurrentUserProvider { long? GetCurrentUserId(); } public class CurrentUserProvider : ICurrentUserProvider { private readonly IHttpContextAccessor _httpContextAccessor; public CurrentUserProvider(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public long? GetCurrentUserId() { var nameIdentifier = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier); long.TryParse(nameIdentifier, out var userId); return userId; } } ================================================ FILE: src/BuildingBlocks/Web/EndpointConfig.cs ================================================ using Asp.Versioning.Builder; namespace BuildingBlocks.Web; public class EndpointConfig { public const string BaseApiPath = "api/v{version:apiVersion}"; public static ApiVersionSet VersionSet { get; private set; } = default!; } ================================================ FILE: src/BuildingBlocks/Web/IMinimalEndpoint.cs ================================================ using Microsoft.AspNetCore.Routing; namespace BuildingBlocks.Web; public interface IMinimalEndpoint { IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder); } ================================================ FILE: src/BuildingBlocks/Web/MinimalApiExtensions.cs ================================================ using System.Reflection; using BuildingBlocks.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Scrutor; namespace BuildingBlocks.Web; public static class MinimalApiExtensions { public static IServiceCollection AddMinimalEndpoints( this WebApplicationBuilder applicationBuilder, ServiceLifetime lifetime = ServiceLifetime.Scoped, params Assembly[] assemblies) { var scanAssemblies = assemblies.Any() ? assemblies : TypeProvider.GetReferencedAssemblies(Assembly.GetCallingAssembly()) .Concat(TypeProvider.GetApplicationPartAssemblies(Assembly.GetCallingAssembly())) .Distinct() .ToArray(); applicationBuilder.Services.Scan(scan => scan .FromAssemblies(scanAssemblies) .AddClasses(classes => classes.AssignableTo(typeof(IMinimalEndpoint))) .UsingRegistrationStrategy(RegistrationStrategy.Append) .As() .WithLifetime(lifetime)); return applicationBuilder.Services; } /// /// Map Minimal Endpoints /// /// builder. /// IEndpointRouteBuilder. public static IEndpointRouteBuilder MapMinimalEndpoints(this IEndpointRouteBuilder builder) { var scope = builder.ServiceProvider.CreateScope(); var endpoints = scope.ServiceProvider.GetServices(); foreach (var endpoint in endpoints) { endpoint.MapEndpoint(builder); } return builder; } } ================================================ FILE: src/BuildingBlocks/Web/ServiceCollectionExtensions.cs ================================================ using Microsoft.Extensions.DependencyInjection; using NSubstitute; namespace BuildingBlocks.Web; public static class ServiceCollectionExtensions { public static void ReplaceScoped(this IServiceCollection services) where TService : class where TImplementation : class, TService { services.Unregister(); services.AddScoped(); } public static void ReplaceScoped(this IServiceCollection services, Func implementationFactory) where TService : class { services.Unregister(); services.AddScoped(implementationFactory); } public static void ReplaceTransient(this IServiceCollection services) where TService : class where TImplementation : class, TService { services.Unregister(); services.AddTransient(); } public static void ReplaceTransient(this IServiceCollection services, Func implementationFactory) where TService : class { services.Unregister(); services.AddTransient(implementationFactory); } public static void ReplaceSingleton(this IServiceCollection services) where TService : class where TImplementation : class, TService { services.Unregister(); services.AddSingleton(); } public static void ReplaceSingleton(this IServiceCollection services, Func implementationFactory) where TService : class { services.Unregister(); services.AddSingleton(implementationFactory); } public static void Unregister(this IServiceCollection services) { var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(TService)); services.Remove(descriptor); } public static IServiceCollection ReplaceServiceWithSingletonMock(this IServiceCollection services) where TService : class { var service = services.FirstOrDefault(d => d.ServiceType == typeof(TService)); services.Remove(service); services.AddSingleton(_ => Substitute.For()); return services; } } ================================================ FILE: src/BuildingBlocks/Web/ServiceProviderExtensions.cs ================================================ using Microsoft.Extensions.Hosting; namespace BuildingBlocks.Web; public static class ServiceProviderExtensions { public static async Task StartTestHostedServices( this IServiceProvider serviceProvider, Type[] hostedServiceTypes, CancellationToken cancellationToken = default) { foreach (var hostedServiceType in hostedServiceTypes) { if (serviceProvider.GetService(hostedServiceType) is IHostedService hostedService) await hostedService.StartAsync(cancellationToken); } } public static async Task StopTestHostedServices( this IServiceProvider serviceProvider, Type[] hostedServiceTypes, CancellationToken cancellationToken = default) { foreach (var hostedServiceType in hostedServiceTypes) { if (serviceProvider.GetService(hostedServiceType) is IHostedService hostedService) await hostedService.StopAsync(cancellationToken); } } } ================================================ FILE: src/BuildingBlocks/Web/SlugifyParameterTransformer.cs ================================================ using System.Globalization; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Routing; namespace BuildingBlocks.Web; public class SlugifyParameterTransformer : IOutboundParameterTransformer { public string TransformOutbound(object value) { // Slugify value return value == null ? null : Regex.Replace(value.ToString() ?? string.Empty, "([a-z])([A-Z])", "$1-$2").ToLower(CultureInfo.CurrentCulture); } } ================================================ FILE: src/Services/Booking/Dockerfile ================================================ # ---------- Build Stage ---------- FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy solution-level files COPY .editorconfig . COPY global.json . COPY Directory.Build.props . # Copy project files first (for Docker layer caching) COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/ COPY src/Services/Booking/src/Booking/Booking.csproj src/Services/Booking/src/Booking/ COPY src/Services/Booking/src/Booking.Api/Booking.Api.csproj src/Services/Booking/src/Booking.Api/ COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/ # Restore dependencies RUN dotnet restore src/Services/Booking/src/Booking.Api/Booking.Api.csproj # Copy the rest of the source COPY src ./src # Publish (build included) RUN dotnet publish src/Services/Booking/src/Booking.Api/Booking.Api.csproj \ -c Release \ -o /app/publish \ --no-restore # ---------- Runtime Stage ---------- FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . ENV ASPNETCORE_URLS=http://+:80 ENV ASPNETCORE_ENVIRONMENT=docker EXPOSE 80 ENTRYPOINT ["dotnet", "Booking.Api.dll"] ================================================ FILE: src/Services/Booking/src/Booking/AssemblyInfo.cs ================================================ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Unit.Test")] [assembly: InternalsVisibleTo("Integration.Test")] [assembly: InternalsVisibleTo("EndToEnd.Test")] ================================================ FILE: src/Services/Booking/src/Booking/Booking/Dtos/CreateReservation.cs ================================================ namespace Booking.Booking.Dtos; public record BookingResponseDto(Guid Id, string Name, string FlightNumber, Guid AircraftId, decimal Price, DateTime FlightDate, string SeatNumber, Guid DepartureAirportId, Guid ArriveAirportId, string Description); ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/BookingAlreadyExistException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Booking.Booking.Exceptions; public class BookingAlreadyExistException : AppException { public BookingAlreadyExistException(int? code = default) : base("Booking already exist!", HttpStatusCode.Conflict, code) { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/FlightNotFoundException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Booking.Booking.Exceptions; public class FlightNotFoundException : AppException { public FlightNotFoundException() : base("Flight doesn't exist!", HttpStatusCode.NotFound) { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/InvalidAircraftIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class InvalidAircraftIdException : DomainException { public InvalidAircraftIdException(Guid aircraftId) : base($"aircraftId: '{aircraftId}' is invalid.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/InvalidArriveAirportIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class InvalidArriveAirportIdException : DomainException { public InvalidArriveAirportIdException(Guid arriveAirportId) : base($"arriveAirportId: '{arriveAirportId}' is invalid.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/InvalidDepartureAirportIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class InvalidDepartureAirportIdException : DomainException { public InvalidDepartureAirportIdException(Guid departureAirportId) : base($"departureAirportId: '{departureAirportId}' is invalid.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/InvalidFlightDateException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class InvalidFlightDateException : DomainException { public InvalidFlightDateException(DateTime flightDate) : base($"Flight Date: '{flightDate}' is invalid.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/InvalidFlightNumberException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class InvalidFlightNumberException : DomainException { public InvalidFlightNumberException(string flightNumber) : base($"Flight Number: '{flightNumber}' is invalid.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/InvalidPassengerNameException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class InvalidPassengerNameException : DomainException { public InvalidPassengerNameException(string passengerName) : base($"Passenger Name: '{passengerName}' is invalid.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/InvalidPriceException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class InvalidPriceException : DomainException { public InvalidPriceException(decimal price) : base($"Price: '{price}' must be grater than or equal 0.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Exceptions/SeatNumberException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Booking.Booking.Exceptions; public class SeatNumberException : DomainException { public SeatNumberException(string seatNumber) : base($"Seat Number: '{seatNumber}' is invalid.") { } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Features/BookingMappings.cs ================================================ using Booking.Booking.Dtos; using Mapster; namespace Booking.Booking.Features; using CreatingBook.V1; 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)); } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Features/CreatingBook/V1/CreateBooking.cs ================================================ namespace Booking.Booking.Features.CreatingBook.V1; using Ardalis.GuardClauses; using BuildingBlocks.Core; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; using BuildingBlocks.EventStoreDB.Repository; using BuildingBlocks.Web; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using Flight; using FluentValidation; using Mapster; using MapsterMapper; using MassTransit; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Passenger; using ValueObjects; 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(nameof(ApiScope)) .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 FlightGrpcService.FlightGrpcServiceClient _flightGrpcServiceClient; private readonly PassengerGrpcService.PassengerGrpcServiceClient _passengerGrpcServiceClient; public CreateBookingCommandHandler(IEventStoreDBRepository eventStoreDbRepository, ICurrentUserProvider currentUserProvider, IEventDispatcher eventDispatcher, FlightGrpcService.FlightGrpcServiceClient flightGrpcServiceClient, PassengerGrpcService.PassengerGrpcServiceClient passengerGrpcServiceClient) { _eventStoreDbRepository = eventStoreDbRepository; _currentUserProvider = currentUserProvider; _eventDispatcher = eventDispatcher; _flightGrpcServiceClient = flightGrpcServiceClient; _passengerGrpcServiceClient = passengerGrpcServiceClient; } public async Task Handle(CreateBooking command, CancellationToken cancellationToken) { Guard.Against.Null(command, nameof(command)); var flight = await _flightGrpcServiceClient.GetByIdAsync(new Flight.GetByIdRequest { Id = command.FlightId.ToString() }, cancellationToken: cancellationToken); if (flight is null) { throw new FlightNotFoundException(); } var passenger = await _passengerGrpcServiceClient.GetByIdAsync(new Passenger.GetByIdRequest { Id = command.PassengerId.ToString() }, cancellationToken: cancellationToken); var emptySeat = (await _flightGrpcServiceClient .GetAvailableSeatsAsync(new GetAvailableSeatsRequest { FlightId = command.FlightId.ToString() }, cancellationToken: cancellationToken) .ResponseAsync) ?.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, new Guid(flight.FlightDto.AircraftId), new Guid(flight.FlightDto.DepartureAirportId), new Guid(flight.FlightDto.ArriveAirportId), flight.FlightDto.FlightDate.ToDateTime(), (decimal)flight.FlightDto.Price, command.Description, emptySeat?.SeatNumber), false, _currentUserProvider.GetCurrentUserId()); await _eventDispatcher.SendAsync(aggrigate.DomainEvents, cancellationToken: cancellationToken); await _flightGrpcServiceClient.ReserveSeatAsync(new ReserveSeatRequest { FlightId = flight.FlightDto.Id, SeatNumber = emptySeat?.SeatNumber }, cancellationToken: cancellationToken); var result = await _eventStoreDbRepository.Add( aggrigate, cancellationToken); return new CreateBookingResult(result); } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Models/Booking.cs ================================================ using BuildingBlocks.EventStoreDB.Events; namespace Booking.Booking.Models; using Features.CreatingBook.V1; using ValueObjects; 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++; } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/Models/BookingReadModel.cs ================================================ namespace Booking.Booking.Models; using ValueObjects; 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; } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/ValueObjects/PassengerInfo.cs ================================================ namespace Booking.Booking.ValueObjects; using Exceptions; 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); } } ================================================ FILE: src/Services/Booking/src/Booking/Booking/ValueObjects/Trip.cs ================================================ namespace Booking.Booking.ValueObjects; using Exceptions; 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); } } ================================================ FILE: src/Services/Booking/src/Booking/Booking.csproj ================================================ all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/Services/Booking/src/Booking/BookingEventMapper.cs ================================================ using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Core; using BuildingBlocks.Core.Event; namespace Booking; using Booking.Features.CreatingBook.V1; 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 }; } } ================================================ FILE: src/Services/Booking/src/Booking/BookingProjection.cs ================================================ using Booking.Data; using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.EventStoreDB.Projections; using MediatR; using MongoDB.Driver; using MongoDB.Driver.Linq; namespace Booking; using Booking.Features.CreatingBook.V1; using Booking.Models; using MassTransit; 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); } } } ================================================ FILE: src/Services/Booking/src/Booking/BookingRoot.cs ================================================ namespace Booking; public class BookingRoot { } ================================================ FILE: src/Services/Booking/src/Booking/Configuration/GrpcOptions.cs ================================================ namespace Booking.Configuration; public class GrpcOptions { public string FlightAddress { get; set; } public string PassengerAddress { get; set; } } ================================================ FILE: src/Services/Booking/src/Booking/Data/BookingReadDbContext.cs ================================================ using BuildingBlocks.Mongo; using Humanizer; using Microsoft.Extensions.Options; using MongoDB.Driver; namespace Booking.Data; using Booking.Models; public class BookingReadDbContext : MongoDbContext { public BookingReadDbContext(IOptions options) : base(options) { Booking = GetCollection(nameof(Booking).Underscore()); } public IMongoCollection Booking { get; } } ================================================ FILE: src/Services/Booking/src/Booking/Extensions/Infrastructure/GrpcClientExtensions.cs ================================================ using Booking.Configuration; using BuildingBlocks.Web; using Flight; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Resilience; using Passenger; using Polly; namespace Booking.Extensions.Infrastructure; public static class GrpcClientExtensions { public static IServiceCollection AddGrpcClients(this IServiceCollection services) { var grpcOptions = services.GetOptions("Grpc"); var resilienceOptions = services.GetOptions( nameof(HttpStandardResilienceOptions) ); services .AddGrpcClient(o => { o.Address = new Uri(grpcOptions.FlightAddress); }) .AddResilienceHandler( "grpc-flight-resilience", options => { var timeSpan = TimeSpan.FromMinutes(1); options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 }); options.AddCircuitBreaker( new HttpCircuitBreakerStrategyOptions { SamplingDuration = timeSpan * 2 } ); options.AddTimeout(new HttpTimeoutStrategyOptions { Timeout = timeSpan * 3 }); } ); services .AddGrpcClient(o => { o.Address = new Uri(grpcOptions.PassengerAddress); }) .AddResilienceHandler( "grpc-passenger-resilience", options => { var timeSpan = TimeSpan.FromMinutes(1); options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 }); options.AddCircuitBreaker( new HttpCircuitBreakerStrategyOptions { SamplingDuration = timeSpan * 2 } ); options.AddTimeout(new HttpTimeoutStrategyOptions { Timeout = timeSpan * 3 }); } ); return services; } } ================================================ FILE: src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs ================================================ using Booking.Data; using BuildingBlocks.Core; using BuildingBlocks.EventStoreDB; using BuildingBlocks.HealthCheck; using BuildingBlocks.Jwt; 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 Figgle.Fonts; using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ServiceDefaults; namespace Booking.Extensions.Infrastructure; public static class InfrastructureExtensions { public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder builder) { var configuration = builder.Configuration; var env = builder.Environment; builder.AddServiceDefaults(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); var appOptions = builder.Services.GetOptions(nameof(AppOptions)); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); builder.AddPersistMessageProcessor(nameof(PersistMessage)); builder.AddMongoDbContext(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddJwt(); builder.Services.AddHttpContextAccessor(); builder.Services.AddAspnetOpenApi(); builder.Services.AddCustomVersioning(); builder.Services.AddCustomMediatR(); builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly); builder.Services.AddProblemDetails(); builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly); builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(BookingRoot).Assembly); builder.Services.AddTransient(); // ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce builder.Services.AddEventStore(configuration, typeof(BookingRoot).Assembly) .AddEventStoreDBSubscriptionToAll(); builder.Services.AddGrpcClients(); return builder; } public static WebApplication UseInfrastructure(this WebApplication app) { var env = app.Environment; var appOptions = app.GetOptions(nameof(AppOptions)); app.UseAuthentication(); app.UseAuthorization(); app.UseServiceDefaults(); app.UseCustomProblemDetails(); app.UseCorrelationId(); app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); if (env.IsDevelopment()) { app.UseAspnetOpenApi(); } return app; } } ================================================ FILE: src/Services/Booking/src/Booking/Extensions/Infrastructure/MediatRExtensions.cs ================================================ using BuildingBlocks.Logging; using BuildingBlocks.Validation; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace Booking.Extensions.Infrastructure; public static class MediatRExtensions { public static IServiceCollection AddCustomMediatR(this IServiceCollection services) { services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(BookingRoot).Assembly)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); return services; } } ================================================ FILE: src/Services/Booking/src/Booking/GrpcClient/Protos/flight.proto ================================================ syntax = "proto3"; package flight; import "google/protobuf/timestamp.proto"; service FlightGrpcService { rpc GetById (GetByIdRequest) returns (GetFlightByIdResult); rpc GetAvailableSeats (GetAvailableSeatsRequest) returns (GetAvailableSeatsResult); rpc ReserveSeat (ReserveSeatRequest) returns (ReserveSeatResult); } message GetByIdRequest { string Id = 1; } message GetFlightByIdResult{ FlightResponse FlightDto = 1; } message GetAvailableSeatsResult{ repeated SeatDtoResponse SeatDtos = 1; } message ReserveSeatResult{ string Id = 1; } message FlightResponse { string Id = 1; string FlightNumber = 2; string AircraftId = 3; string DepartureAirportId = 4; google.protobuf.Timestamp DepartureDate = 5; google.protobuf.Timestamp ArriveDate = 6; string ArriveAirportId = 7; double DurationMinutes = 8; google.protobuf.Timestamp FlightDate = 9; FlightStatus Status = 10; double Price = 11; string FlightId = 12; } message GetAvailableSeatsRequest { string FlightId = 1; } message SeatDtoResponse { string Id = 1; string SeatNumber = 2; SeatType Type = 3; SeatClass Class = 4; string FlightId = 5; } message ReserveSeatRequest { string FlightId = 1; string SeatNumber = 2; } enum FlightStatus { FLIGHT_STATUS_UNKNOWN = 0; FLIGHT_STATUS_FLYING = 1; FLIGHT_STATUS_DELAY = 2; FLIGHT_STATUS_CANCELED = 3; FLIGHT_STATUS_COMPLETED = 4; } enum SeatType { SEAT_TYPE_UNKNOWN = 0; SEAT_TYPE_WINDOW = 1; SEAT_TYPE_MIDDLE = 2; SEAT_TYPE_AISLE = 3; } enum SeatClass { SEAT_CLASS_UNKNOWN = 0; SEAT_CLASS_FIRST_CLASS = 1; SEAT_CLASS_BUSINESS = 2; SEAT_CLASS_ECONOMY = 3; } ================================================ FILE: src/Services/Booking/src/Booking/GrpcClient/Protos/passenger.proto ================================================ syntax = "proto3"; package passenger; service PassengerGrpcService { rpc GetById (GetByIdRequest) returns (GetPassengerByIdResult); } message GetByIdRequest { string Id = 1; } message GetPassengerByIdResult { PassengerResponse PassengerDto = 1; } message PassengerResponse { string Id = 1; string Name = 2; string PassportNumber = 3; PassengerType PassengerType = 4; int32 Age = 5; string Email = 6; } enum PassengerType { PASSENGER_TYPE_UNKNOWN = 0; PASSENGER_TYPE_MALE = 1; PASSENGER_TYPE_FEMALE = 2; PASSENGER_TYPE_BABY = 3; } ================================================ FILE: src/Services/Booking/src/Booking.Api/Booking.Api.csproj ================================================ ================================================ FILE: src/Services/Booking/src/Booking.Api/Program.cs ================================================ using Booking; using Booking.Extensions.Infrastructure; using BuildingBlocks.Web; var builder = WebApplication.CreateBuilder(args); builder.AddMinimalEndpoints(assemblies: typeof(BookingRoot).Assembly); builder.AddInfrastructure(); var app = builder.Build(); app.MapMinimalEndpoints(); app.UseInfrastructure(); app.Run(); namespace Booking.Api { public partial class Program { } } ================================================ FILE: src/Services/Booking/src/Booking.Api/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "Booking.Api": { "commandName": "Project", "dotnetRunMessages": true, "launchUrl": "swagger", "launchBrowser": true, "applicationUrl": "http://localhost:6010;https://localhost:5010", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/Services/Booking/src/Booking.Api/appsettings.Development.json ================================================ { } ================================================ FILE: src/Services/Booking/src/Booking.Api/appsettings.docker.json ================================================ { "App": "Booking-Service", "Logging": { "LogLevel": { "Default": "Information" } }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "EventStoreOptions": { "ConnectionString": "esdb://eventstore:2113?tls=false" }, "MongoOptions": { "ConnectionString": "mongodb://mongo:27017", "DatabaseName": "booking-db" }, "RabbitMqOptions": { "HostName": "rabbitmq", "ExchangeName": "booking", "UserName": "guest", "Password": "guest", "Port": 5672 }, "Jwt": { "Authority": "http://identity:80", "Audience": "booking-api" }, "Grpc": { "FlightAddress": "flight:5003", "PassengerAddress": "passenger:5003" }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Booking/src/Booking.Api/appsettings.json ================================================ { "AppOptions": { "Name": "Booking-Service" }, "Logging": { "LogLevel": { "Default": "Information" } }, "Jwt": { "Authority": "http://localhost:6005", "Audience": "booking-api" }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "booking", "UserName": "guest", "Password": "guest", "Port": 5672 }, "Grpc": { "FlightAddress": "https://localhost:5003", "PassengerAddress": "https://localhost:5012" }, "PolicyOptions": { "Retry": { "RetryCount": 3, "SleepDuration": 1 }, "CircuitBreaker": { "RetryCount": 5, "BreakDuration": 30 } }, "EventStoreOptions": { "ConnectionString": "esdb://localhost:2113?tls=false" }, "MongoOptions": { "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "booking-db" }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "HealthOptions": { "Enabled": false }, "ObservabilityOptions": { "InstrumentationName": "booking_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 Service" }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Booking/src/Booking.Api/appsettings.test.json ================================================ { "Logging": { "LogLevel": { "Default": "Information" } }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "booking", "UserName": "guest", "Password": "guest", "Port": 5672 }, "MongoOptions": { "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "booking-db-test" }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true" } } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/Booking/Features/CreateBookingTests.cs ================================================ using System.Threading.Tasks; using Booking.Api; using Booking.Data; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.TestBase; using Flight; using FluentAssertions; using Grpc.Core; using Grpc.Core.Testing; using Integration.Test.Fakes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; using Passenger; using Xunit; using GetByIdRequest = Flight.GetByIdRequest; namespace Integration.Test.Booking.Features { public class CreateBookingTests : BookingIntegrationTestBase { public CreateBookingTests(TestReadFixture integrationTestFixture) : base(integrationTestFixture) { } protected override void RegisterTestsServices(IServiceCollection services) { MockFlightGrpcServices(services); MockPassengerGrpcServices(services); } [Fact] public async Task should_create_booking_to_event_store_currectly() { // Arrange var command = new FakeCreateBookingCommand().Generate(); // Act var response = await Fixture.SendAsync(command); // Assert response?.Id.Should().BeGreaterThanOrEqualTo(0); (await Fixture.WaitForPublishing()).Should().Be(true); } private void MockPassengerGrpcServices(IServiceCollection services) { services.Replace( ServiceDescriptor.Singleton(x => { var mockPassenger = Substitute.For(); mockPassenger .GetByIdAsync(Arg.Any()) .Returns( TestCalls.AsyncUnaryCall( Task.FromResult(FakePassengerResponse.Generate()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { } ) ); return mockPassenger; }) ); } private void MockFlightGrpcServices(IServiceCollection services) { services.Replace( ServiceDescriptor.Singleton(x => { var mockFlight = Substitute.For(); mockFlight .GetByIdAsync(Arg.Any()) .Returns( TestCalls.AsyncUnaryCall( Task.FromResult(FakeFlightResponse.Generate()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { } ) ); mockFlight .GetAvailableSeatsAsync(Arg.Any()) .Returns( TestCalls.AsyncUnaryCall( Task.FromResult(FakeGetAvailableSeatsResponse.Generate()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { } ) ); mockFlight .ReserveSeatAsync(Arg.Any()) .Returns( TestCalls.AsyncUnaryCall( Task.FromResult(FakeReserveSeatResponse.Generate()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { } ) ); return mockFlight; }) ); } } } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/BookingIntegrationTestBase.cs ================================================ using Booking.Data; using BuildingBlocks.TestBase; using Xunit; namespace Integration.Test; [Collection(IntegrationTestCollection.Name)] public class BookingIntegrationTestBase : TestReadBase { public BookingIntegrationTestBase(TestReadFixture integrationTestFixture) : base(integrationTestFixture) { } } [CollectionDefinition(Name)] public class IntegrationTestCollection : ICollectionFixture> { public const string Name = "Booking Integration Test"; } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/Fakes/FakeCreateBookingCommand.cs ================================================ using AutoBogus; namespace Integration.Test.Fakes; using System; using global::Booking.Booking.Features.CreatingBook.V1; using MassTransit; public sealed class FakeCreateBookingCommand : AutoFaker { public FakeCreateBookingCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightId, _ => new Guid("3c5c0000-97c6-fc34-2eb9-08db322230c9")); RuleFor(r => r.PassengerId, _ => new Guid("4c5c8888-97c6-fc34-2eb9-18db322230c1")); } } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/Fakes/FakeFlightResponse.cs ================================================ using System; using AutoBogus; using Flight; using Google.Protobuf.WellKnownTypes; namespace Integration.Test.Fakes; public static class FakeFlightResponse { public static GetFlightByIdResult Generate() { var flightMock = new GetFlightByIdResult { FlightDto = new FlightResponse { Id = new Guid("3c5c0000-97c6-fc34-2eb9-08db322230c9").ToString(), Price = 100, Status = FlightStatus.Completed, AircraftId = new Guid("3c5c0000-97c6-fc34-fcd3-08db322230c8").ToString(), ArriveAirportId = new Guid("3c5c0000-97c6-fc34-a0cb-08db322230c8").ToString(), ArriveDate = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc).ToTimestamp(), DepartureDate = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc).ToTimestamp(), FlightDate = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc).ToTimestamp(), FlightNumber = "1500B", DepartureAirportId = new Guid("3c5c0000-97c6-fc34-fc3c-08db322230c8").ToString() } }; return flightMock; } } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/Fakes/FakeGetAvailableSeatsResponse.cs ================================================ using System.Collections.Generic; using Flight; namespace Integration.Test.Fakes; using System; using MassTransit; public static class FakeGetAvailableSeatsResponse { public static GetAvailableSeatsResult Generate() { var result = new GetAvailableSeatsResult(); result.SeatDtos.AddRange(new List { new SeatDtoResponse() { FlightId = new Guid("3c5c0000-97c6-fc34-2eb9-08db322230c9").ToString(), Class = SeatClass.Economy, Type = SeatType.Aisle, SeatNumber = "33F", Id = NewId.NextGuid().ToString() }, new SeatDtoResponse() { FlightId = new Guid("3c5c0000-97c6-fc34-2eb9-08db322230c9").ToString(), Class = SeatClass.Economy, Type = SeatType.Window, SeatNumber = "22D", Id = NewId.NextGuid().ToString() } }); return result; } } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/Fakes/FakePassengerResponse.cs ================================================ namespace Integration.Test.Fakes; using MassTransit; using Passenger; public static class FakePassengerResponse { public static GetPassengerByIdResult Generate() { var result = new GetPassengerByIdResult { PassengerDto = new PassengerResponse() { Id = NewId.NextGuid().ToString(), Name = "Test", PassportNumber = "121618" } }; return result; } } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/Fakes/FakeReserveSeatResponse.cs ================================================ namespace Integration.Test.Fakes; using Flight; using MassTransit; public static class FakeReserveSeatResponse { public static ReserveSeatResult Generate() { var result = new ReserveSeatResult(); result.Id = NewId.NextGuid().ToString(); return result; } } ================================================ FILE: src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj ================================================ PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: src/Services/Booking/tests/IntegrationTest/xunit.runner.json ================================================ { "parallelizeAssembly": false, "parallelizeTestCollections": false } ================================================ FILE: src/Services/Booking/tests/PerformanceTest/.openapi-generator/FILES ================================================ .openapi-generator-ignore README.md script.js ================================================ FILE: src/Services/Booking/tests/PerformanceTest/.openapi-generator/VERSION ================================================ 6.6.0-SNAPSHOT ================================================ FILE: src/Services/Booking/tests/PerformanceTest/.openapi-generator-ignore ================================================ # OpenAPI Generator Ignore # Generated by openapi-generator https://github.com/openapitools/openapi-generator # Use this file to prevent files from being overwritten by the generator. # The patterns follow closely to .gitignore or .dockerignore. # As an example, the C# client generator defines ApiClient.cs. # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: #ApiClient.cs # You can match any string of characters against a directory, file or extension with a single asterisk (*): #foo/*/qux # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux # You can recursively match patterns against a directory, file or extension with a double asterisk (**): #foo/**/qux # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux # You can also negate patterns with an exclamation (!). # For example, you can ignore all files in a docs folder with the file extension .md: #docs/*.md # Then explicitly reverse the ignore rule for a single file: #!docs/README.md ================================================ FILE: src/Services/Booking/tests/PerformanceTest/README.md ================================================ # Generated k6 script The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. If the Swagger/OpenAPI specification used as the input spec contains examples at parameter level, those will be extracted and utilized as parameter values. The `handleParamValue` custom Mustache lambda registered for use in the K6 `script.mustache` template handles the conditional checks, formatting, and outputting of parameter values. If a given parameter has value specified – either in `example` or `examples` field, defined at the parameter level – that value will be used. For list (`examples`), entire list will be output in the generated script and the first element from that list will be assigned as parameter value. If a given parameter does not have an example defined, a placeholder value with `TODO_EDIT_THE_` prefix will be generated for that parameter, and you will have to assign a value before you can run the script. In other words, you can now generate K6 test scripts which are ready to run, provided the Swagger/OpenAPI specification used as the input spec contains examples for all of the path/query parameters; see `modules/openapi-generator/src/test/resources/3_0/examples.yaml` for an example of such specification, and https://swagger.io/docs/specification/adding-examples/ for more information about adding examples. k6 specific parameters are in the [`params`](https://docs.k6.io/docs/params-k6http) object, and `body` contains the [request](https://docs.k6.io/docs/http-requests) body which is in the form of `identifier: type`, which the `type` should be substituted by a proper value. Then goes the request and the check. [Check](https://docs.k6.io/docs/checks) are like asserts but differ in that they don't halt execution, instead they just store the result of the check, pass or fail, and let the script execution continue. Each request is always followed by a 0.1 second [sleep](https://docs.k6.io/docs/sleep-t-1) to prevent the script execution from flooding the system with too many requests simultaneously. Note that the default iteration count and VU count is 1. So each request in each group will be executed once. For more information, see the [k6 options](https://docs.k6.io/docs/options). ================================================ FILE: src/Services/Booking/tests/PerformanceTest/script.js ================================================ /* * APIs * An example application with OpenAPI, Swashbuckle, and API versioning. * * OpenAPI spec version: 1.0 * Contact: * * NOTE: This class is auto generated by OpenAPI Generator. * https://github.com/OpenAPITools/openapi-generator * * OpenAPI generator version: 6.6.0-SNAPSHOT */ import http from "k6/http"; import { group, check, sleep } from "k6"; const BASE_URL = "/"; // Sleep duration between successive requests. // You might want to edit the value of this variable or remove calls to the sleep function on the script. const SLEEP_DURATION = 0.1; // Global variables should be initialized. export default function() { group("/api/v1/booking", () => { // Request No. 1: CreateBooking { let url = BASE_URL + `/api/v1/booking`; // TODO: edit the parameters of the request body. let body = {"passengerId": "uuid", "flightId": "uuid", "description": "string"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 200 }); } }); } ================================================ FILE: src/Services/Booking/tests/tests.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "IntegrationTest\Integration.Test.csproj", "{AC2806BF-F80B-4075-8240-0D3E87E41FEA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {AC2806BF-F80B-4075-8240-0D3E87E41FEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC2806BF-F80B-4075-8240-0D3E87E41FEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC2806BF-F80B-4075-8240-0D3E87E41FEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC2806BF-F80B-4075-8240-0D3E87E41FEA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal ================================================ FILE: src/Services/Flight/Dockerfile ================================================ # ---------- Build Stage ---------- FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy solution-level files COPY .editorconfig . COPY global.json . COPY Directory.Build.props . # Copy project files first (better layer caching) COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/ COPY src/Services/Flight/src/Flight/Flight.csproj src/Services/Flight/src/Flight/ COPY src/Services/Flight/src/Flight.Api/Flight.Api.csproj src/Services/Flight/src/Flight.Api/ COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/ # Restore dependencies RUN dotnet restore src/Services/Flight/src/Flight.Api/Flight.Api.csproj # Copy remaining source code COPY src ./src # Publish (build included) RUN dotnet publish src/Services/Flight/src/Flight.Api/Flight.Api.csproj \ -c Release \ -o /app/publish \ --no-restore # ---------- Runtime Stage ---------- FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . ENV ASPNETCORE_URLS=http://+:80 ENV ASPNETCORE_ENVIRONMENT=docker EXPOSE 80 ENTRYPOINT ["dotnet", "Flight.Api.dll"] ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Dtos/AircraftDto.cs ================================================ namespace Flight.Aircrafts.Dtos; public record AircraftDto(long Id, string Name, string Model, int ManufacturingYear); ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Flight.Aircrafts.Exceptions; public class AircraftAlreadyExistException : AppException { public AircraftAlreadyExistException() : base("Aircraft already exist!", HttpStatusCode.Conflict) { } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Exceptions/InvalidAircraftIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Aircrafts.Exceptions; public class InvalidAircraftIdException : DomainException { public InvalidAircraftIdException(Guid aircraftId) : base($"AircraftId: '{aircraftId}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Exceptions/InvalidManufacturingYearException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Aircrafts.Exceptions; public class InvalidManufacturingYearException : DomainException { public InvalidManufacturingYearException() : base("ManufacturingYear must be greater than 1900") { } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Exceptions/InvalidModelException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Aircrafts.Exceptions; public class InvalidModelException : DomainException { public InvalidModelException() : base("Model cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Exceptions/InvalidNameException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Aircrafts.Exceptions; public class InvalidNameException : DomainException { public InvalidNameException() : base("Name cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Features/AircraftMappings.cs ================================================ using Flight.Aircrafts.Models; using Mapster; namespace Flight.Aircrafts.Features; using CreatingAircraft.V1; using MassTransit; using ValueObjects; 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)); } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs ================================================ namespace Flight.Aircrafts.Features.CreatingAircraft.V1; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Data; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using Flight.Aircrafts.ValueObjects; 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; using Models; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs ================================================ namespace Flight.Aircrafts.Features.CreatingAircraft.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Data; using Exceptions; using MapsterMapper; using MediatR; using Models; using MongoDB.Driver; using MongoDB.Driver.Linq; using ValueObjects; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs ================================================ using BuildingBlocks.Core.Model; namespace Flight.Aircrafts.Models; using Features.CreatingAircraft.V1; using ValueObjects; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/Models/AircraftReadModel.cs ================================================ namespace Flight.Aircrafts.Models; using System; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/ValueObjects/AircraftId.cs ================================================ namespace Flight.Aircrafts.ValueObjects; using System; using Flight.Aircrafts.Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/ValueObjects/ManufacturingYear.cs ================================================ namespace Flight.Aircrafts.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/ValueObjects/Model.cs ================================================ namespace Flight.Aircrafts.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Aircrafts/ValueObjects/Name.cs ================================================ namespace Flight.Aircrafts.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Dtos/AirportDto.cs ================================================ namespace Flight.Airports.Dtos; public record AirportDto(long Id, string Name, string Address, string Code); ================================================ FILE: src/Services/Flight/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Flight.Airports.Exceptions; public class AirportAlreadyExistException : AppException { public AirportAlreadyExistException(int? code = default) : base("Airport already exist!", HttpStatusCode.Conflict, code) { } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Exceptions/InvalidAddressException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Airports.Exceptions; public class InvalidAddressException : DomainException { public InvalidAddressException() : base("Address cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Exceptions/InvalidAirportIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Airports.Exceptions; public class InvalidAirportIdException : DomainException { public InvalidAirportIdException(Guid airportId) : base($"airportId: '{airportId}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Exceptions/InvalidCodeException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Airports.Exceptions; public class InvalidCodeException : DomainException { public InvalidCodeException() : base("Code cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Exceptions/InvalidNameException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Airports.Exceptions; public class InvalidNameException : DomainException { public InvalidNameException() : base("Name cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Features/AirportMappings.cs ================================================ namespace Flight.Airports.Features; using CreatingAirport.V1; using Mapster; using MassTransit; using Models; 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)); } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirport.cs ================================================ namespace Flight.Airports.Features.CreatingAirport.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Data; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; 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; using ValueObjects; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs ================================================ namespace Flight.Airports.Features.CreatingAirport.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Data; using Exceptions; using MapsterMapper; using MediatR; using Models; using MongoDB.Driver; using MongoDB.Driver.Linq; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Models/Airport.cs ================================================ using BuildingBlocks.Core.Model; namespace Flight.Airports.Models; using Features.CreatingAirport.V1; using ValueObjects; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/Models/AirportReadModel.cs ================================================ namespace Flight.Airports.Models; using System; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/ValueObjects/Address.cs ================================================ namespace Flight.Airports.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/ValueObjects/AirportId.cs ================================================ namespace Flight.Airports.ValueObjects; using System; using Flight.Airports.Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/ValueObjects/Code.cs ================================================ namespace Flight.Airports.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Airports/ValueObjects/Name.cs ================================================ namespace Flight.Airports.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/AssemblyInfo.cs ================================================ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Unit.Test")] [assembly: InternalsVisibleTo("Integration.Test")] [assembly: InternalsVisibleTo("EndToEnd.Test")] ================================================ FILE: src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs ================================================ using Flight.Aircrafts.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Flight.Data.Configurations; using System; using Aircrafts.ValueObjects; 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(); } ); } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs ================================================ using System; using Flight.Airports.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Flight.Data.Configurations; using Airports.ValueObjects; 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(); } ); } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs ================================================ using Flight.Aircrafts.Models; using Flight.Airports.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Flight.Data.Configurations; using System; using Flights.Models; using Flights.ValueObjects; public class FlightConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(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(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(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(Flight.Price)) .HasMaxLength(10) .IsRequired(); } ); builder.OwnsOne( x => x.ArriveDate, a => { a.Property(p => p.Value) .HasColumnName(nameof(Flight.ArriveDate)) .IsRequired(); } ); builder.OwnsOne( x => x.DepartureDate, a => { a.Property(p => p.Value) .HasColumnName(nameof(Flight.DepartureDate)) .IsRequired(); } ); builder.OwnsOne( x => x.FlightDate, a => { a.Property(p => p.Value) .HasColumnName(nameof(Flight.FlightDate)) .IsRequired(); } ); } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs ================================================ using Flight.Seats.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Flight.Data.Configurations; using System; using Seats.ValueObjects; 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 => (Flight.Seats.Enums.SeatClass)Enum.Parse(typeof(Flight.Seats.Enums.SeatClass), x)); builder.Property(x => x.Type) .HasDefaultValue(Seats.Enums.SeatType.Unknown) .HasConversion( x => x.ToString(), x => (Flight.Seats.Enums.SeatType)Enum.Parse(typeof(Flight.Seats.Enums.SeatType), x)); } } ================================================ FILE: src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; namespace Flight.Data { public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory { public FlightDbContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder(); builder.UseNpgsql("Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true") .UseSnakeCaseNamingConvention(); return new FlightDbContext(builder.Options); } } } ================================================ FILE: src/Services/Flight/src/Flight/Data/FlightDbContext.cs ================================================ using BuildingBlocks.EFCore; using Flight.Aircrafts.Models; using Flight.Airports.Models; using Flight.Seats.Models; using Microsoft.EntityFrameworkCore; namespace Flight.Data; using BuildingBlocks.Web; using Microsoft.Extensions.Logging; 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); builder.ApplyConfigurationsFromAssembly(typeof(FlightRoot).Assembly); builder.FilterSoftDeletedProperties(); builder.ToSnakeCaseTables(); } } ================================================ FILE: src/Services/Flight/src/Flight/Data/FlightReadDbContext.cs ================================================ using BuildingBlocks.Mongo; using Humanizer; using Microsoft.Extensions.Options; using MongoDB.Driver; namespace Flight.Data; using Aircrafts.Models; using Airports.Models; using Flights.Models; using Seats.Models; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Migrations/20230611230948_initial.Designer.cs ================================================ // using System; using 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 Flight.Data.Migrations { [DbContext(typeof(FlightDbContext))] [Migration("20230611230948_initial")] partial class initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "7.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => { b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AircraftId") .HasConstraintName("fk_aircraft_aircraft_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AircraftId") .HasConstraintName("fk_aircraft_aircraft_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AircraftId") .HasConstraintName("fk_aircraft_aircraft_id"); }); b.Navigation("ManufacturingYear") .IsRequired(); b.Navigation("Model") .IsRequired(); b.Navigation("Name") .IsRequired(); }); modelBuilder.Entity("Flight.Airports.Models.Airport", b => { b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AirportId") .HasConstraintName("fk_airport_airport_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AirportId") .HasConstraintName("fk_airport_airport_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AirportId") .HasConstraintName("fk_airport_airport_id"); }); b.Navigation("Address") .IsRequired(); b.Navigation("Code") .IsRequired(); b.Navigation("Name") .IsRequired(); }); modelBuilder.Entity("Flight.Flights.Models.Flight", b => { b.HasOne("Flight.Aircrafts.Models.Aircraft", null) .WithMany() .HasForeignKey("AircraftId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_flight_aircraft_aircraft_id"); b.HasOne("Flight.Airports.Models.Airport", null) .WithMany() .HasForeignKey("ArriveAirportId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_flight_airport_arrive_airport_id"); b.HasOne("Flight.Airports.Models.Airport", null) .WithMany() .HasForeignKey("DepartureAirportId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_flight_airport_departure_airport_id"); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); 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("Flight.Seats.Models.Seat", b => { b.HasOne("Flight.Flights.Models.Flight", null) .WithMany() .HasForeignKey("FlightId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_seat_flight_flight_id"); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("SeatId") .HasConstraintName("fk_seat_seat_id"); }); b.Navigation("SeatNumber") .IsRequired(); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Migrations/20230611230948_initial.cs ================================================ using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Flight.Data.Migrations { /// public partial class initial : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "aircraft", 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), manufacturingyear = table.Column(name: "manufacturing_year", type: "integer", maxLength: 5, nullable: false), createdat = table.Column(name: "created_at", type: "timestamp with time zone", nullable: true), createdby = table.Column(name: "created_by", type: "bigint", nullable: true), lastmodified = table.Column(name: "last_modified", type: "timestamp with time zone", nullable: true), lastmodifiedby = table.Column(name: "last_modified_by", type: "bigint", nullable: true), isdeleted = table.Column(name: "is_deleted", type: "boolean", nullable: false), version = table.Column(type: "bigint", nullable: false) }, constraints: table => { table.PrimaryKey("pk_aircraft", x => x.id); }); migrationBuilder.CreateTable( name: "airport", 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), createdat = table.Column(name: "created_at", type: "timestamp with time zone", nullable: true), createdby = table.Column(name: "created_by", type: "bigint", nullable: true), lastmodified = table.Column(name: "last_modified", type: "timestamp with time zone", nullable: true), lastmodifiedby = table.Column(name: "last_modified_by", type: "bigint", nullable: true), isdeleted = table.Column(name: "is_deleted", type: "boolean", nullable: false), version = table.Column(type: "bigint", nullable: false) }, constraints: table => { table.PrimaryKey("pk_airport", x => x.id); }); migrationBuilder.CreateTable( name: "flight", columns: table => new { id = table.Column(type: "uuid", nullable: false), flightnumber = table.Column(name: "flight_number", type: "character varying(50)", maxLength: 50, nullable: false), aircraftid = table.Column(name: "aircraft_id", type: "uuid", nullable: false), departureairportid = table.Column(name: "departure_airport_id", type: "uuid", nullable: false), arriveairportid = table.Column(name: "arrive_airport_id", type: "uuid", nullable: false), durationminutes = table.Column(name: "duration_minutes", type: "numeric", maxLength: 50, nullable: false), status = table.Column(type: "text", nullable: false, defaultValue: "Unknown"), price = table.Column(type: "numeric", maxLength: 10, nullable: false), arrivedate = table.Column(name: "arrive_date", type: "timestamp with time zone", nullable: false), departuredate = table.Column(name: "departure_date", type: "timestamp with time zone", nullable: false), flightdate = table.Column(name: "flight_date", type: "timestamp with time zone", nullable: false), createdat = table.Column(name: "created_at", type: "timestamp with time zone", nullable: true), createdby = table.Column(name: "created_by", type: "bigint", nullable: true), lastmodified = table.Column(name: "last_modified", type: "timestamp with time zone", nullable: true), lastmodifiedby = table.Column(name: "last_modified_by", type: "bigint", nullable: true), isdeleted = table.Column(name: "is_deleted", 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.aircraftid, principalTable: "aircraft", principalColumn: "id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "fk_flight_airport_arrive_airport_id", column: x => x.arriveairportid, principalTable: "airport", principalColumn: "id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "fk_flight_airport_departure_airport_id", column: x => x.departureairportid, principalTable: "airport", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "seat", columns: table => new { id = table.Column(type: "uuid", nullable: false), seatnumber = table.Column(name: "seat_number", 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"), flightid = table.Column(name: "flight_id", type: "uuid", nullable: false), createdat = table.Column(name: "created_at", type: "timestamp with time zone", nullable: true), createdby = table.Column(name: "created_by", type: "bigint", nullable: true), lastmodified = table.Column(name: "last_modified", type: "timestamp with time zone", nullable: true), lastmodifiedby = table.Column(name: "last_modified_by", type: "bigint", nullable: true), isdeleted = table.Column(name: "is_deleted", 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.flightid, principalTable: "flight", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( name: "ix_flight_aircraft_id", table: "flight", column: "aircraft_id"); migrationBuilder.CreateIndex( name: "ix_flight_arrive_airport_id", table: "flight", column: "arrive_airport_id"); migrationBuilder.CreateIndex( name: "ix_flight_departure_airport_id", table: "flight", column: "departure_airport_id"); migrationBuilder.CreateIndex( name: "ix_seat_flight_id", table: "seat", column: "flight_id"); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "seat"); migrationBuilder.DropTable( name: "flight"); migrationBuilder.DropTable( name: "aircraft"); migrationBuilder.DropTable( name: "airport"); } } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs ================================================ // using System; using Flight.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Flight.Data.Migrations { [DbContext(typeof(FlightDbContext))] partial class FlightDbContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => { b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AircraftId") .HasConstraintName("fk_aircraft_aircraft_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AircraftId") .HasConstraintName("fk_aircraft_aircraft_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AircraftId") .HasConstraintName("fk_aircraft_aircraft_id"); }); b.Navigation("ManufacturingYear") .IsRequired(); b.Navigation("Model") .IsRequired(); b.Navigation("Name") .IsRequired(); }); modelBuilder.Entity("Flight.Airports.Models.Airport", b => { b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AirportId") .HasConstraintName("fk_airport_airport_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AirportId") .HasConstraintName("fk_airport_airport_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("AirportId") .HasConstraintName("fk_airport_airport_id"); }); b.Navigation("Address") .IsRequired(); b.Navigation("Code") .IsRequired(); b.Navigation("Name") .IsRequired(); }); modelBuilder.Entity("Flight.Flights.Models.Flight", b => { b.HasOne("Flight.Aircrafts.Models.Aircraft", null) .WithMany() .HasForeignKey("AircraftId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_flight_aircraft_aircraft_id"); b.HasOne("Flight.Airports.Models.Airport", null) .WithMany() .HasForeignKey("ArriveAirportId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_flight_airport_arrive_airport_id"); b.HasOne("Flight.Airports.Models.Airport", null) .WithMany() .HasForeignKey("DepartureAirportId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_flight_airport_departure_airport_id"); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("FlightId") .HasConstraintName("fk_flight_flight_id"); }); b.OwnsOne("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"); 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("Flight.Seats.Models.Seat", b => { b.HasOne("Flight.Flights.Models.Flight", null) .WithMany() .HasForeignKey("FlightId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_seat_flight_flight_id"); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("SeatId") .HasConstraintName("fk_seat_seat_id"); }); b.Navigation("SeatNumber") .IsRequired(); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Seed/FlightDataSeeder.cs ================================================ using BuildingBlocks.EFCore; using Flight.Aircrafts.Models; using Flight.Airports.Models; using Flight.Flights.Models; using Flight.Seats.Models; using MapsterMapper; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; using MongoDB.Driver.Linq; namespace 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)); } } } } ================================================ FILE: src/Services/Flight/src/Flight/Data/Seed/InitialData.cs ================================================ namespace Flight.Data.Seed; using System; using System.Collections.Generic; using System.Linq; using Aircrafts.Models; using Airports.Models; using Airports.ValueObjects; using Flight.Aircrafts.ValueObjects; using Flights.Models; using Flights.ValueObjects; using MassTransit; using Seats.Models; using Seats.ValueObjects; 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.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)), global::Flight.Flights.Enums.FlightStatus.Completed, Price.Of(8000)) }; Seats = new List { Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of( "12A"), global::Flight.Seats.Enums.SeatType.Window, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid)Flights.First().Id)), Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12B"), global::Flight.Seats.Enums.SeatType.Window, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid)Flights.First().Id)), Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12C"), global::Flight.Seats.Enums.SeatType.Middle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)), Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12D"), global::Flight.Seats.Enums.SeatType.Middle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)), Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12E"), global::Flight.Seats.Enums.SeatType.Aisle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)), Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12F"), global::Flight.Seats.Enums.SeatType.Aisle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)) }; } } ================================================ FILE: src/Services/Flight/src/Flight/Data/readme.md ================================================ dotnet ef migrations add initial --context FlightDbContext -o "Data\Migrations" dotnet ef database update --context FlightDbContext ================================================ FILE: src/Services/Flight/src/Flight/Extensions/Infrastructure/InfrastructureExtensions.cs ================================================ using BuildingBlocks.Core; using BuildingBlocks.EFCore; using BuildingBlocks.Exception; using BuildingBlocks.HealthCheck; using BuildingBlocks.Jwt; 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 Figgle.Fonts; using Flight.Data; using Flight.Data.Seed; using Flight.GrpcServer.Services; using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ServiceDefaults; namespace Flight.Extensions.Infrastructure; public static class InfrastructureExtensions { public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder builder) { var configuration = builder.Configuration; var env = builder.Environment; builder.AddServiceDefaults(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); builder.Services.AddCustomMediatR(); builder.Services.AddProblemDetails(); var appOptions = builder.Services.GetOptions(nameof(AppOptions)); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); builder.AddCustomDbContext(nameof(Flight)); builder.Services.AddScoped(); builder.AddMongoDbContext(); builder.AddPersistMessageProcessor(nameof(PersistMessage)); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddJwt(); builder.Services.AddAspnetOpenApi(); builder.Services.AddCustomVersioning(); builder.Services.AddValidatorsFromAssembly(typeof(FlightRoot).Assembly); builder.Services.AddCustomMapster(typeof(FlightRoot).Assembly); builder.Services.AddHttpContextAccessor(); builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(FlightRoot).Assembly); builder.Services.AddGrpc(options => { options.Interceptors.Add(); }); builder.Services.AddEasyCaching(options => { options.UseInMemory(configuration, "mem"); }); return builder; } public static WebApplication UseInfrastructure(this WebApplication app) { var env = app.Environment; var appOptions = app.GetOptions(nameof(AppOptions)); app.UseAuthentication(); app.UseAuthorization(); app.UseServiceDefaults(); app.UseCustomProblemDetails(); app.UseCorrelationId(); app.UseMigration(); app.MapGrpcService(); app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); if (env.IsDevelopment()) { app.UseAspnetOpenApi(); } return app; } } ================================================ FILE: src/Services/Flight/src/Flight/Extensions/Infrastructure/MediatRExtensions.cs ================================================ using BuildingBlocks.Caching; using BuildingBlocks.EFCore; using BuildingBlocks.Logging; using BuildingBlocks.Validation; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace Flight.Extensions.Infrastructure; public static class MediatRExtensions { public static IServiceCollection AddCustomMediatR(this IServiceCollection services) { services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(FlightRoot).Assembly)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(InvalidateCachingBehavior<,>)); return services; } } ================================================ FILE: src/Services/Flight/src/Flight/Flight.csproj ================================================ all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/Services/Flight/src/Flight/FlightEventMapper.cs ================================================ using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Core; using BuildingBlocks.Core.Event; namespace Flight; using Aircrafts.Features.CreatingAircraft.V1; using Aircrafts.ValueObjects; using Airports.Features.CreatingAirport.V1; using Flights.Features.CreatingFlight.V1; using Flights.Features.DeletingFlight.V1; using Flights.Features.UpdatingFlight.V1; using Seats.Features.CreatingSeat.V1; using Seats.Features.ReservingSeat.V1; // 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 }; } } ================================================ FILE: src/Services/Flight/src/Flight/FlightRoot.cs ================================================ namespace Flight; public class FlightRoot { } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Dtos/FlightDto.cs ================================================ using System; namespace 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); ================================================ FILE: src/Services/Flight/src/Flight/Flights/Enums/FlightStatus.cs ================================================ namespace Flight.Flights.Enums; public enum FlightStatus { Unknown = 0, Flying = 1, Delay = 2, Canceled = 3, Completed = 4 } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Flight.Flights.Exceptions; public class FlightAlreadyExistException : AppException { public FlightAlreadyExistException(int? code = default) : base("Flight already exist!", HttpStatusCode.Conflict, code) { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/FlightNotFountException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Flight.Flights.Exceptions; public class FlightNotFountException : AppException { public FlightNotFountException() : base("Flight not found!", HttpStatusCode.NotFound) { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/InvalidArriveDateException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Flights.Exceptions; public class InvalidArriveDateException : DomainException { public InvalidArriveDateException(DateTime arriveDate) : base($"Arrive Date: '{arriveDate}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/InvalidDepartureDateException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Flights.Exceptions; public class InvalidDepartureDateException : DomainException { public InvalidDepartureDateException(DateTime departureDate) : base($"Departure Date: '{departureDate}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/InvalidDurationException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Flights.Exceptions; public class InvalidDurationException : DomainException { public InvalidDurationException() : base("Duration cannot be negative.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/InvalidFlightDateException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Flights.Exceptions; public class InvalidFlightDateException : DomainException { public InvalidFlightDateException(DateTime flightDate) : base($"Flight Date: '{flightDate}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/InvalidFlightIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Flights.Exceptions; public class InvalidFlightIdException : DomainException { public InvalidFlightIdException(Guid flightId) : base($"flightId: '{flightId}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/InvalidFlightNumberException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Flights.Exceptions; public class InvalidFlightNumberException : DomainException { public InvalidFlightNumberException(string flightNumber) : base($"Flight Number: '{flightNumber}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Exceptions/InvalidPriceException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Flights.Exceptions; public class InvalidPriceException : DomainException { public InvalidPriceException() : base($"Price Cannot be negative.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlight.cs ================================================ namespace Flight.Flights.Features.CreatingFlight.V1; using System; using System.Threading; using System.Threading.Tasks; using Aircrafts.ValueObjects; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Data; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using Flight.Airports.ValueObjects; 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; using ValueObjects; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/CreatingFlight/V1/CreateFlightMongo.cs ================================================ namespace Flight.Flights.Features.CreatingFlight.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Data; using Exceptions; using MapsterMapper; using MediatR; using Models; using MongoDB.Driver; using MongoDB.Driver.Linq; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlight.cs ================================================ using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Duende.IdentityServer.EntityFramework.Entities; using Flight.Data; using Flight.Flights.Exceptions; using FluentValidation; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; namespace 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/DeletingFlight/V1/DeleteFlightMongo.cs ================================================ namespace Flight.Flights.Features.DeletingFlight.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Data; using Exceptions; using MapsterMapper; using MediatR; using Models; using MongoDB.Driver; using MongoDB.Driver.Linq; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs ================================================ using Mapster; namespace Flight.Flights.Features; using CreatingFlight.V1; using DeletingFlight.V1; using MassTransit; using Models; using UpdatingFlight.V1; 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)); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/GettingAvailableFlights/V1/GetAvailableFlights.cs ================================================ using MongoDB.Driver.Linq; namespace Flight.Flights.Features.GettingAvailableFlights.V1; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Caching; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Web; using Data; using Dtos; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using Mapster; using MapsterMapper; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using MongoDB.Driver; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/GettingFlightById/V1/GetFlightById.cs ================================================ namespace Flight.Flights.Features.GettingFlightById.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Web; using Data; using Dtos; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; 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; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlight.cs ================================================ namespace Flight.Flights.Features.UpdatingFlight.V1; using System; using System.Threading; using System.Threading.Tasks; using Aircrafts.ValueObjects; using Ardalis.GuardClauses; using BuildingBlocks.Caching; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Data; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using Flight.Airports.ValueObjects; using Flight.Flights.Features.CreatingFlight.V1; using FluentValidation; using MapsterMapper; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using ValueObjects; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Features/UpdatingFlight/V1/UpdateFlightMongo.cs ================================================ namespace Flight.Flights.Features.UpdatingFlight.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Data; using Exceptions; using MapsterMapper; using MediatR; using Models; using MongoDB.Driver; using MongoDB.Driver.Linq; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Models/Flight.cs ================================================ using BuildingBlocks.Core.Model; namespace Flight.Flights.Models; using Aircrafts.ValueObjects; using Airports.ValueObjects; using Features.CreatingFlight.V1; using Features.DeletingFlight.V1; using Features.UpdatingFlight.V1; using ValueObjects; 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); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/Models/FlightReadModel.cs ================================================ namespace Flight.Flights.Models; using System; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/ValueObjects/ArriveDate.cs ================================================ namespace Flight.Flights.ValueObjects; using System; using Flight.Flights.Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/ValueObjects/DepartureDate.cs ================================================ namespace Flight.Flights.ValueObjects; using System; using Flight.Flights.Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/ValueObjects/DurationMinutes.cs ================================================ namespace Flight.Flights.ValueObjects; using Exceptions; 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); } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/ValueObjects/FlightDate.cs ================================================ namespace Flight.Flights.ValueObjects; using System; using Flight.Flights.Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/ValueObjects/FlightId.cs ================================================ namespace Flight.Flights.ValueObjects; using System; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/ValueObjects/FlightNumber.cs ================================================ namespace Flight.Flights.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Flights/ValueObjects/Price.cs ================================================ namespace Flight.Flights.ValueObjects; using Flight.Flights.Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/GrpcServer/Protos/flight.proto ================================================ syntax = "proto3"; package flight; import "google/protobuf/timestamp.proto"; service FlightGrpcService { rpc GetById (GetByIdRequest) returns (GetFlightByIdResult); rpc GetAvailableSeats (GetAvailableSeatsRequest) returns (GetAvailableSeatsResult); rpc ReserveSeat (ReserveSeatRequest) returns (ReserveSeatResult); } message GetByIdRequest { string Id = 1; } message GetFlightByIdResult{ FlightResponse FlightDto = 1; } message GetAvailableSeatsResult{ repeated SeatDtoResponse SeatDtos = 1; } message ReserveSeatResult{ string Id = 1; } message FlightResponse { string Id = 1; string FlightNumber = 2; string AircraftId = 3; string DepartureAirportId = 4; google.protobuf.Timestamp DepartureDate = 5; google.protobuf.Timestamp ArriveDate = 6; string ArriveAirportId = 7; double DurationMinutes = 8; google.protobuf.Timestamp FlightDate = 9; FlightStatus Status = 10; double Price = 11; string FlightId = 12; } message GetAvailableSeatsRequest { string FlightId = 1; } message SeatDtoResponse { string Id = 1; string SeatNumber = 2; SeatType Type = 3; SeatClass Class = 4; string FlightId = 5; } message ReserveSeatRequest { string FlightId = 1; string SeatNumber = 2; } enum FlightStatus { FLIGHT_STATUS_UNKNOWN = 0; FLIGHT_STATUS_FLYING = 1; FLIGHT_STATUS_DELAY = 2; FLIGHT_STATUS_CANCELED = 3; FLIGHT_STATUS_COMPLETED = 4; } enum SeatType { SEAT_TYPE_UNKNOWN = 0; SEAT_TYPE_WINDOW = 1; SEAT_TYPE_MIDDLE = 2; SEAT_TYPE_AISLE = 3; } enum SeatClass { SEAT_CLASS_UNKNOWN = 0; SEAT_CLASS_FIRST_CLASS = 1; SEAT_CLASS_BUSINESS = 2; SEAT_CLASS_ECONOMY = 3; } ================================================ FILE: src/Services/Flight/src/Flight/GrpcServer/Services/FlightGrpcServices.cs ================================================ using System.Threading.Tasks; using Grpc.Core; using Mapster; using MediatR; namespace Flight.GrpcServer.Services; using System; using Flights.Features.GettingFlightById.V1; using Seats.Features.GettingAvailableSeats.V1; using Seats.Features.ReservingSeat.V1; using GetAvailableSeatsResult = GetAvailableSeatsResult; using GetFlightByIdResult = GetFlightByIdResult; using ReserveSeatResult = ReserveSeatResult; public class FlightGrpcServices : FlightGrpcService.FlightGrpcServiceBase { private readonly IMediator _mediator; public FlightGrpcServices(IMediator mediator) { _mediator = mediator; } public override async Task GetById(GetByIdRequest request, ServerCallContext context) { var result = await _mediator.Send(new GetFlightById(new Guid(request.Id))); return result.Adapt(); } public override async Task GetAvailableSeats(GetAvailableSeatsRequest request, ServerCallContext context) { var result = new GetAvailableSeatsResult(); var availableSeats = await _mediator.Send(new GetAvailableSeats(new Guid(request.FlightId))); if (availableSeats?.SeatDtos == null) { return result; } foreach (var availableSeat in availableSeats.SeatDtos) { result.SeatDtos.Add(availableSeat.Adapt()); } return result; } public override async Task ReserveSeat(ReserveSeatRequest request, ServerCallContext context) { var result = await _mediator.Send(new ReserveSeat(new Guid(request.FlightId), request.SeatNumber)); return result.Adapt(); } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Dtos/SeatDto.cs ================================================ namespace Flight.Seats.Dtos; using System; public record SeatDto(Guid Id, string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, Guid FlightId); ================================================ FILE: src/Services/Flight/src/Flight/Seats/Enums/SeatClass.cs ================================================ namespace Flight.Seats.Enums; public enum SeatClass { Unknown = 0, FirstClass, Business, Economy } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Enums/SeatType.cs ================================================ namespace Flight.Seats.Enums; public enum SeatType { Unknown = 0, Window, Middle, Aisle } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Exceptions/AllSeatsFullException.cs ================================================ using BuildingBlocks.Exception; namespace Flight.Seats.Exceptions; public class AllSeatsFullException : AppException { public AllSeatsFullException() : base("All seats are full!") { } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Exceptions/InvalidSeatIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Seats.Exceptions; public class InvalidSeatIdException : DomainException { public InvalidSeatIdException(Guid seatId) : base($"seatId: '{seatId}' is invalid.") { } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Exceptions/InvalidSeatNumberException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Flight.Seats.Exceptions; public class InvalidSeatNumberException : DomainException { public InvalidSeatNumberException() : base("SeatNumber Cannot be null or negative") { } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Flight.Seats.Exceptions; public class SeatAlreadyExistException : AppException { public SeatAlreadyExistException(int? code = default) : base("Seat already exist!", HttpStatusCode.Conflict, code) { } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs ================================================ using BuildingBlocks.Exception; namespace Flight.Seats.Exceptions; public class SeatNumberIncorrectException : AppException { public SeatNumberIncorrectException() : base("Seat number is incorrect!") { } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeat.cs ================================================ namespace Flight.Seats.Features.CreatingSeat.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Data; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using Flights.ValueObjects; 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; using Models; using ValueObjects; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Features/CreatingSeat/V1/CreateSeatMongo.cs ================================================ namespace Flight.Seats.Features.CreatingSeat.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Data; using Exceptions; using MapsterMapper; using MediatR; using Models; using MongoDB.Driver; using MongoDB.Driver.Linq; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Features/GettingAvailableSeats/V1/GetAvailableSeats.cs ================================================ using MongoDB.Driver.Linq; namespace Flight.Seats.Features.GettingAvailableSeats.V1; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Web; using Data; using Dtos; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using FluentValidation; using Mapster; using MapsterMapper; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using MongoDB.Driver; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeat.cs ================================================ namespace Flight.Seats.Features.ReservingSeat.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Duende.IdentityServer.EntityFramework.Entities; using Flight.Data; using Flight.Seats.Exceptions; using FluentValidation; using Mapster; using MapsterMapper; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Features/ReservingSeat/V1/ReserveSeatMongo.cs ================================================ namespace Flight.Seats.Features.ReservingSeat.V1; using System; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Flight.Data; using Flight.Seats.Models; using MapsterMapper; using MediatR; using MongoDB.Driver; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Features/SeatMappings.cs ================================================ using Flight.Seats.Dtos; using Flight.Seats.Models; using Mapster; namespace Flight.Seats.Features; using CreatingSeat.V1; using MassTransit; using ReservingSeat.V1; 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)); } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Models/Seat.cs ================================================ using System; using BuildingBlocks.Core.Model; namespace Flight.Seats.Models; using Features.CreatingSeat.V1; using Features.ReservingSeat.V1; using Flight.Flights.ValueObjects; using ValueObjects; 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); } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/Models/SeatReadModel.cs ================================================ namespace Flight.Seats.Models; using System; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/ValueObjects/SeatId.cs ================================================ namespace Flight.Seats.ValueObjects; using System; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight/Seats/ValueObjects/SeatNumber.cs ================================================ namespace Flight.Seats.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Flight/src/Flight.Api/Flight.Api.csproj ================================================ ================================================ FILE: src/Services/Flight/src/Flight.Api/Program.cs ================================================ using BuildingBlocks.Web; using Flight; using Flight.Extensions.Infrastructure; var builder = WebApplication.CreateBuilder(args); builder.AddMinimalEndpoints(assemblies: typeof(FlightRoot).Assembly); builder.AddInfrastructure(); var app = builder.Build(); // ref: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-7.0#routing-basics app.MapMinimalEndpoints(); app.UseInfrastructure(); app.Run(); namespace Flight.Api { public partial class Program { } } ================================================ FILE: src/Services/Flight/src/Flight.Api/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "Flight.Api": { "commandName": "Project", "dotnetRunMessages": true, "launchUrl": "swagger", "launchBrowser": true, "applicationUrl": "http://localhost:5004;https://localhost:5003", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/Services/Flight/src/Flight.Api/appsettings.Development.json ================================================ { } ================================================ FILE: src/Services/Flight/src/Flight.Api/appsettings.docker.json ================================================ { "App": "Flight-Service", "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=postgres;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true" }, "Jwt": { "Authority": "http://identity:80", "Audience": "flight-api" }, "RabbitMqOptions": { "HostName": "rabbitmq", "ExchangeName": "flight", "UserName": "guest", "Password": "guest", "Port": 5672 }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "MongoOptions": { "ConnectionString": "mongodb://mongo:27017", "DatabaseName": "flight-db" }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Flight/src/Flight.Api/appsettings.json ================================================ { "AppOptions": { "Name": "Flight-Service" }, "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true" }, "MongoOptions": { "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "flight-db" }, "Jwt": { "Authority": "http://localhost:6005", "Audience": "flight-api" }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "flight", "UserName": "guest", "Password": "guest", "Port": 5672 }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "HealthOptions": { "Enabled": false }, "ObservabilityOptions": { "InstrumentationName": "flight_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": "Flight Service" }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Flight/src/Flight.Api/appsettings.test.json ================================================ { "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=localhost;Port=5432;Database=flight_test;User Id=postgres;Password=postgres;Include Error Detail=true" }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "flight", "UserName": "guest", "Password": "guest", "Port": 5672 }, "PersistMessageOptions": { "Interval": 2, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true" } } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/EndToEnd.Test.csproj ================================================ PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: src/Services/Flight/tests/EndToEndTest/Fakes/FakeCreateFlightCommand.cs ================================================ using AutoBogus; using Flight.Flights.Enums; namespace EndToEnd.Test.Fakes; using global::Flight.Data.Seed; using global::Flight.Flights.Features.CreatingFlight.V1; using MassTransit; public sealed class FakeCreateFlightCommand : AutoFaker { public FakeCreateFlightCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightNumber, r => "12FF"); RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id); RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id); RuleFor(r => r.Status, _ => FlightStatus.Flying); RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id); } } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/Fakes/FakeCreateFlightMongoCommand.cs ================================================ namespace EndToEnd.Test.Fakes; using AutoBogus; using global::Flight.Data.Seed; using global::Flight.Flights.Enums; using global::Flight.Flights.Features.CreatingFlight.V1; using MassTransit; public sealed class FakeCreateFlightMongoCommand : AutoFaker { public FakeCreateFlightMongoCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightNumber, r => "12FF"); RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id); RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id); RuleFor(r => r.Status, _ => FlightStatus.Flying); RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id); RuleFor(r => r.IsDeleted, _ => false); } } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/Flight/Features/CreateFlightTests.cs ================================================ using System.Net; using System.Net.Http.Json; using BuildingBlocks.TestBase; using EndToEnd.Test.Fakes; using EndToEnd.Test.Routes; using Flight.Api; using Flight.Data; using FluentAssertions; using Xunit; namespace EndToEnd.Test.Flight.Features; public class CreateFlightTests : FlightEndToEndTestBase { public CreateFlightTests(TestFixture integrationTestFixture) : base(integrationTestFixture) { } [Fact] public async Task should_create_new_flight_to_db_and_publish_message_to_broker() { //Arrange var command = new FakeCreateFlightCommand().Generate(); // Act var route = ApiRoutes.Flight.CreateFlight; var result = await Fixture.HttpClient.PostAsJsonAsync(route, command); // Assert result.StatusCode.Should().Be(HttpStatusCode.Created); } } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/Flight/Features/GetFlightByIdTests.cs ================================================ using System.Net; using BuildingBlocks.TestBase; using EndToEnd.Test.Fakes; using EndToEnd.Test.Routes; using Flight.Api; using Flight.Data; using FluentAssertions; using Xunit; namespace EndToEnd.Test.Flight.Features; public class GetFlightByIdTests : FlightEndToEndTestBase { public GetFlightByIdTests(TestFixture integrationTestFixture) : base(integrationTestFixture) { } [Fact] public async Task should_retrive_a_flight_by_id_currectly() { //Arrange var command = new FakeCreateFlightMongoCommand().Generate(); await Fixture.SendAsync(command); // Act var route = ApiRoutes.Flight.GetFlightById.Replace(ApiRoutes.Flight.Id, command.Id.ToString(), StringComparison.CurrentCulture); var result = await Fixture.HttpClient.GetAsync(route); // Assert result.StatusCode.Should().Be(HttpStatusCode.OK); } } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/FlightEndToEndTestBase.cs ================================================ using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using Xunit; namespace EndToEnd.Test; [Collection(EndToEndTestCollection.Name)] public class FlightEndToEndTestBase : TestBase { public FlightEndToEndTestBase(TestFixture integrationTestFixture) : base(integrationTestFixture) { } } [CollectionDefinition(Name)] public class EndToEndTestCollection : ICollectionFixture> { public const string Name = "Flight EndToEnd Test"; } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/FlightTestDataSeeder.cs ================================================ using BuildingBlocks.EFCore; using Flight.Aircrafts.Models; using Flight.Airports.Models; using Flight.Data; using Flight.Data.Seed; using Flight.Flights.Models; using Flight.Seats.Models; using MapsterMapper; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; using MongoDB.Driver.Linq; namespace EndToEnd.Test; public class FlightTestDataSeeder( FlightDbContext flightDbContext, FlightReadDbContext flightReadDbContext, IMapper mapper ) : ITestDataSeeder { public async Task SeedAllAsync() { 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)); } } } } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/Routes/ApiRoutes.cs ================================================ namespace EndToEnd.Test.Routes; public static class ApiRoutes { private const string BaseApiPath = "api/v1.0"; public static class Flight { public const string Id = "{id}"; public const string GetFlightById = $"{BaseApiPath}/flight/{Id}"; public const string CreateFlight = $"{BaseApiPath}/flight"; } } ================================================ FILE: src/Services/Flight/tests/EndToEndTest/xunit.runner.json ================================================ { "parallelizeAssembly": false, "parallelizeTestCollections": false } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Aircraft/Features/CreateAircraftTests.cs ================================================ using System.Threading.Tasks; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Aircraft.Features; public class CreateAircraftTests : FlightIntegrationTestBase { public CreateAircraftTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_create_new_aircraft_to_db_and_publish_message_to_broker() { // Arrange var command = new FakeCreateAircraftCommand().Generate(); // Act var response = await Fixture.SendAsync(command); // Assert response?.Id.Value.Should().Be(command.Id); (await Fixture.WaitForPublishing()).Should().Be(true); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Airport/Features/CreateAirportTests.cs ================================================ using System.Threading.Tasks; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Airport.Features; public class CreateAirportTests : FlightIntegrationTestBase { public CreateAirportTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_create_new_airport_to_db_and_publish_message_to_broker() { // Arrange var command = new FakeCreateAirportCommand().Generate(); // Act var response = await Fixture.SendAsync(command); // Assert response?.Id.Should().Be(command.Id); (await Fixture.WaitForPublishing()).Should().Be(true); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateAircraftCommand.cs ================================================ using AutoBogus; namespace Integration.Test.Fakes; using global::Flight.Aircrafts.Features.CreatingAircraft.V1; using MassTransit; public class FakeCreateAircraftCommand : AutoFaker { public FakeCreateAircraftCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.ManufacturingYear, _ => 2000); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateAirportCommand.cs ================================================ using AutoBogus; namespace Integration.Test.Fakes; using global::Flight.Airports.Features.CreatingAirport.V1; using MassTransit; public class FakeCreateAirportCommand : AutoFaker { public FakeCreateAirportCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateFlightCommand.cs ================================================ using AutoBogus; using Flight.Flights.Enums; namespace Integration.Test.Fakes; using System.Linq; using global::Flight.Data.Seed; using global::Flight.Flights.Features.CreatingFlight.V1; using MassTransit; public sealed class FakeCreateFlightCommand : AutoFaker { public FakeCreateFlightCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightNumber, r => r.Random.Number(1000, 2000).ToString()); RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id); RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id); RuleFor(r => r.Status, _ => FlightStatus.Flying); RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateFlightMongoCommand.cs ================================================ namespace Integration.Test.Fakes; using System.Linq; using AutoBogus; using global::Flight.Data.Seed; using global::Flight.Flights.Enums; using global::Flight.Flights.Features.CreatingFlight.V1; using MassTransit; public sealed class FakeCreateFlightMongoCommand : AutoFaker { public FakeCreateFlightMongoCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightNumber, r => "12FF"); RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id); RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id); RuleFor(r => r.Status, _ => FlightStatus.Flying); RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id); RuleFor(r => r.IsDeleted, _ => false); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateSeatCommand.cs ================================================ using AutoBogus; using Flight.Seats.Enums; namespace Integration.Test.Fakes; using System; using global::Flight.Seats.Features.CreatingSeat.V1; using MassTransit; public class FakeCreateSeatCommand : AutoFaker { public FakeCreateSeatCommand(Guid flightId) { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightId, _ => flightId); RuleFor(r => r.Class, _ => SeatClass.Economy); RuleFor(r => r.Type, _ => SeatType.Middle); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateSeatMongoCommand.cs ================================================ namespace Integration.Test.Fakes; using System; using AutoBogus; using global::Flight.Seats.Enums; using global::Flight.Seats.Features.CreatingSeat.V1; using MassTransit; public class FakeCreateSeatMongoCommand : AutoFaker { public FakeCreateSeatMongoCommand(Guid flightId) { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightId, _ => flightId); RuleFor(r => r.Class, _ => SeatClass.Economy); RuleFor(r => r.Type, _ => SeatType.Middle); RuleFor(r => r.IsDeleted, _ => false); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Fakes/FakeUpdateFlightCommand.cs ================================================ using AutoBogus; namespace Integration.Test.Fakes; using global::Flight.Flights.Features.UpdatingFlight.V1; public class FakeUpdateFlightCommand : AutoFaker { public FakeUpdateFlightCommand(global::Flight.Flights.Models.Flight flight) { RuleFor(r => r.Id, _ => flight.Id); RuleFor(r => r.DepartureAirportId, _ => flight.DepartureAirportId); RuleFor(r => r.ArriveAirportId, _ => flight.ArriveAirportId); RuleFor(r => r.AircraftId, _ => flight.AircraftId); RuleFor(r => r.FlightNumber, r => r.Random.Number(1000, 2000).ToString()); RuleFor(r => r.Price, _ => 800); RuleFor(r => r.Status, _ => flight.Status); RuleFor(r => r.ArriveDate, _ => flight.ArriveDate); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Flight/Features/CreateFlightTests.cs ================================================ using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Flight.Features; public class CreateFlightTests : FlightIntegrationTestBase { public CreateFlightTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_create_new_flight_to_db_and_publish_message_to_broker() { //Arrange var command = new FakeCreateFlightCommand().Generate(); // Act var response = await Fixture.SendAsync(command); // Assert response.Should().NotBeNull(); response?.Id.Should().Be(command.Id); (await Fixture.WaitForPublishing()).Should().Be(true); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Flight/Features/DeleteFlightTests.cs ================================================ using System.Linq; using System.Threading.Tasks; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Xunit; namespace Integration.Test.Flight.Features; using global::Flight.Data.Seed; using global::Flight.Flights.Features.DeletingFlight.V1; using global::Flight.Flights.Models; using global::Flight.Flights.ValueObjects; public class DeleteFlightTests : FlightIntegrationTestBase { public DeleteFlightTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_delete_flight_from_db() { // Arrange var flightEntity = await Fixture.FindAsync(InitialData.Flights.First().Id); var command = new DeleteFlight(flightEntity.Id.Value); // Act await Fixture.SendAsync(command); var deletedFlight = (await Fixture.ExecuteDbContextAsync(db => db.Flights .Where(x => x.Id == FlightId.Of(command.Id)) .IgnoreQueryFilters() .ToListAsync()) ).FirstOrDefault(); // Assert deletedFlight?.IsDeleted.Should().BeTrue(); (await Fixture.WaitForPublishing()).Should().Be(true); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Flight/Features/GetAvailableFlightsTests.cs ================================================ using System.Linq; using System.Threading.Tasks; using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Flight.Features; using global::Flight.Flights.Features.CreatingFlight.V1; using global::Flight.Flights.Features.GettingAvailableFlights.V1; public class GetAvailableFlightsTests : FlightIntegrationTestBase { public GetAvailableFlightsTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_return_available_flights() { // Arrange var command = new FakeCreateFlightMongoCommand().Generate(); await Fixture.SendAsync(command); var query = new GetAvailableFlights(); // Act var response = (await Fixture.SendAsync(query))?.FlightDtos?.ToList(); // Assert response?.Should().NotBeNull(); response?.Count.Should().BeGreaterThanOrEqualTo(2); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Flight/Features/GetFlightByIdTests.cs ================================================ using System.Threading.Tasks; using BuildingBlocks.TestBase; using Flight; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Flight.Features; using global::Flight.Flights.Features.GettingFlightById.V1; public class GetFlightByIdTests : FlightIntegrationTestBase { public GetFlightByIdTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_retrive_a_flight_by_id_currectly() { //Arrange var command = new FakeCreateFlightMongoCommand().Generate(); await Fixture.SendAsync(command); var query = new GetFlightById(command.Id); // Act var response = await Fixture.SendAsync(query); // Assert response.Should().NotBeNull(); response?.FlightDto?.Id.Should().Be(command.Id); } [Fact] public async Task should_retrive_a_flight_by_id_from_grpc_service() { //Arrange var command = new FakeCreateFlightMongoCommand().Generate(); await Fixture.SendAsync(command); var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(Fixture.Channel); // Act var response = await flightGrpcClient.GetByIdAsync(new GetByIdRequest { Id = command.Id.ToString() }).ResponseAsync; // Assert response?.Should().NotBeNull(); response?.FlightDto.Id.Should().Be(command.Id.ToString()); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Flight/Features/UpdateFlightTests.cs ================================================ using System.Threading.Tasks; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Flight.Features; using System.Linq; using global::Flight.Data.Seed; using global::Flight.Flights.Models; using global::Flight.Flights.ValueObjects; public class UpdateFlightTests : FlightIntegrationTestBase { public UpdateFlightTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_update_flight_to_db_and_publish_message_to_broker() { // Arrange var flightEntity = await Fixture.FindAsync(InitialData.Flights.First().Id); var command = new FakeUpdateFlightCommand(flightEntity).Generate(); // Act var response = await Fixture.SendAsync(command); // Assert response.Should().NotBeNull(); response?.Id.Should().Be(flightEntity.Id); (await Fixture.WaitForPublishing()).Should().Be(true); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/FlightIntegrationTestBase.cs ================================================ using BuildingBlocks.TestBase; using Flight.Api; using Flight.Data; using Xunit; namespace Integration.Test; [Collection(IntegrationTestCollection.Name)] public class FlightIntegrationTestBase : TestBase { public FlightIntegrationTestBase(TestFixture integrationTestFixture) : base(integrationTestFixture) { } } [CollectionDefinition(Name)] public class IntegrationTestCollection : ICollectionFixture> { public const string Name = "Flight Integration Test"; } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/FlightTestDataSeeder.cs ================================================ using BuildingBlocks.EFCore; using Flight.Aircrafts.Models; using Flight.Airports.Models; using Flight.Data; using Flight.Data.Seed; using Flight.Flights.Models; using Flight.Seats.Models; using MapsterMapper; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; using MongoDB.Driver.Linq; namespace Integration.Test; public class FlightTestDataSeeder( FlightDbContext flightDbContext, FlightReadDbContext flightReadDbContext, IMapper mapper ) : ITestDataSeeder { public async Task SeedAllAsync() { 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)); } } } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Integration.Test.csproj ================================================ PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Seat/Features/GetAvailableSeatsTests.cs ================================================ using System.Threading.Tasks; using BuildingBlocks.TestBase; using Flight; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Seat.Features; using global::Flight.Flights.Features.CreatingFlight.V1; using global::Flight.Seats.Features.CreatingSeat.V1; public class GetAvailableSeatsTests : FlightIntegrationTestBase { public GetAvailableSeatsTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_return_available_seats_from_grpc_service() { // Arrange var flightCommand = new FakeCreateFlightMongoCommand().Generate(); await Fixture.SendAsync(flightCommand); var seatCommand = new FakeCreateSeatMongoCommand(flightCommand.Id).Generate(); await Fixture.SendAsync(seatCommand); var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(Fixture.Channel); // Act var response = await flightGrpcClient.GetAvailableSeatsAsync(new GetAvailableSeatsRequest { FlightId = flightCommand.Id.ToString() }); // Assert response?.Should().NotBeNull(); response?.SeatDtos?.Count.Should().BeGreaterThanOrEqualTo(1); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/Seat/Features/ReserveSeatTests.cs ================================================ using System.Threading.Tasks; using BuildingBlocks.TestBase; using Flight; using Flight.Api; using Flight.Data; using FluentAssertions; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Seat.Features; public class ReserveSeatTests : FlightIntegrationTestBase { public ReserveSeatTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_return_valid_reserve_seat_from_grpc_service() { // Arrange var flightCommand = new FakeCreateFlightCommand().Generate(); await Fixture.SendAsync(flightCommand); var seatCommand = new FakeCreateSeatCommand(flightCommand.Id).Generate(); await Fixture.SendAsync(seatCommand); var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(Fixture.Channel); // Act var response = await flightGrpcClient.ReserveSeatAsync(new ReserveSeatRequest() { FlightId = seatCommand.FlightId.ToString(), SeatNumber = seatCommand.SeatNumber }); // Assert response?.Should().NotBeNull(); response?.Id.Should().Be(seatCommand?.Id.ToString()); } } ================================================ FILE: src/Services/Flight/tests/IntegrationTest/xunit.runner.json ================================================ { "parallelizeAssembly": false, "parallelizeTestCollections": false } ================================================ FILE: src/Services/Flight/tests/PerformanceTest/.openapi-generator/FILES ================================================ README.md script.js ================================================ FILE: src/Services/Flight/tests/PerformanceTest/.openapi-generator/VERSION ================================================ 6.6.0-SNAPSHOT ================================================ FILE: src/Services/Flight/tests/PerformanceTest/.openapi-generator-ignore ================================================ # OpenAPI Generator Ignore # Generated by openapi-generator https://github.com/openapitools/openapi-generator # Use this file to prevent files from being overwritten by the generator. # The patterns follow closely to .gitignore or .dockerignore. # As an example, the C# client generator defines ApiClient.cs. # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: #ApiClient.cs # You can match any string of characters against a directory, file or extension with a single asterisk (*): #foo/*/qux # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux # You can recursively match patterns against a directory, file or extension with a double asterisk (**): #foo/**/qux # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux # You can also negate patterns with an exclamation (!). # For example, you can ignore all files in a docs folder with the file extension .md: #docs/*.md # Then explicitly reverse the ignore rule for a single file: #!docs/README.md ================================================ FILE: src/Services/Flight/tests/PerformanceTest/README.md ================================================ # Generated k6 script The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. If the Swagger/OpenAPI specification used as the input spec contains examples at parameter level, those will be extracted and utilized as parameter values. The `handleParamValue` custom Mustache lambda registered for use in the K6 `script.mustache` template handles the conditional checks, formatting, and outputting of parameter values. If a given parameter has value specified – either in `example` or `examples` field, defined at the parameter level – that value will be used. For list (`examples`), entire list will be output in the generated script and the first element from that list will be assigned as parameter value. If a given parameter does not have an example defined, a placeholder value with `TODO_EDIT_THE_` prefix will be generated for that parameter, and you will have to assign a value before you can run the script. In other words, you can now generate K6 test scripts which are ready to run, provided the Swagger/OpenAPI specification used as the input spec contains examples for all of the path/query parameters; see `modules/openapi-generator/src/test/resources/3_0/examples.yaml` for an example of such specification, and https://swagger.io/docs/specification/adding-examples/ for more information about adding examples. k6 specific parameters are in the [`params`](https://docs.k6.io/docs/params-k6http) object, and `body` contains the [request](https://docs.k6.io/docs/http-requests) body which is in the form of `identifier: type`, which the `type` should be substituted by a proper value. Then goes the request and the check. [Check](https://docs.k6.io/docs/checks) are like asserts but differ in that they don't halt execution, instead they just store the result of the check, pass or fail, and let the script execution continue. Each request is always followed by a 0.1 second [sleep](https://docs.k6.io/docs/sleep-t-1) to prevent the script execution from flooding the system with too many requests simultaneously. Note that the default iteration count and VU count is 1. So each request in each group will be executed once. For more information, see the [k6 options](https://docs.k6.io/docs/options). ================================================ FILE: src/Services/Flight/tests/PerformanceTest/script.js ================================================ /* * APIs * An example application with OpenAPI, Swashbuckle, and API versioning. * * OpenAPI spec version: 1.0 * Contact: * * NOTE: This class is auto generated by OpenAPI Generator. * https://github.com/OpenAPITools/openapi-generator * * OpenAPI generator version: 6.6.0-SNAPSHOT */ import http from "k6/http"; import { group, check, sleep } from "k6"; const BASE_URL = "/"; // Sleep duration between successive requests. // You might want to edit the value of this variable or remove calls to the sleep function on the script. const SLEEP_DURATION = 0.1; // Global variables should be initialized. export default function() { group("/api/v1/flight/seat", () => { // Request No. 1: CreateSeat { let url = BASE_URL + `/api/v1/flight/seat`; // TODO: edit the parameters of the request body. let body = {"seatNumber": "string", "type": "seattype", "class": "seatclass", "flightId": "uuid"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 200 }); } }); group("/api/v1/flight/get-available-flights", () => { // Request No. 1: GetAvailableFlights { let url = BASE_URL + `/api/v1/flight/get-available-flights`; let request = http.get(url); check(request, { "Success": (r) => r.status === 200 }); } }); group("/api/v1/flight/airport", () => { // Request No. 1: CreateAirport { let url = BASE_URL + `/api/v1/flight/airport`; // TODO: edit the parameters of the request body. let body = {"name": "string", "address": "string", "code": "string"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 200 }); } }); group("/api/v1/flight/get-available-seats/{id}", () => { let id = 'TODO_EDIT_THE_ID'; // specify value as there is no example value for this parameter in OpenAPI spec // Request No. 1: GetAvailableSeats { let url = BASE_URL + `/api/v1/flight/get-available-seats/${id}`; let request = http.get(url); check(request, { "Success": (r) => r.status === 200 }); } }); group("/api/v1/flight", () => { // Request No. 1: UpdateFlight { let url = BASE_URL + `/api/v1/flight`; // TODO: edit the parameters of the request body. let body = {"id": "uuid", "flightNumber": "string", "aircraftId": "uuid", "departureAirportId": "uuid", "departureDate": "date", "arriveDate": "date", "arriveAirportId": "uuid", "durationMinutes": "double", "flightDate": "date", "status": "flightstatus", "price": "double", "isDeleted": "boolean"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/problem+json"}}; let request = http.put(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 204 }); sleep(SLEEP_DURATION); } // Request No. 2: CreateFlight { let url = BASE_URL + `/api/v1/flight`; // TODO: edit the parameters of the request body. let body = {"flightNumber": "string", "aircraftId": "uuid", "departureAirportId": "uuid", "departureDate": "date", "arriveDate": "date", "arriveAirportId": "uuid", "durationMinutes": "double", "flightDate": "date", "status": "flightstatus", "price": "double"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 201 }); } }); group("/api/v1/flight/{id}", () => { let id = 'TODO_EDIT_THE_ID'; // specify value as there is no example value for this parameter in OpenAPI spec // Request No. 1: GetFlightById { let url = BASE_URL + `/api/v1/flight/${id}`; let request = http.get(url); check(request, { "Success": (r) => r.status === 200 }); sleep(SLEEP_DURATION); } // Request No. 2: DeleteFlight { let url = BASE_URL + `/api/v1/flight/${id}`; let request = http.del(url); check(request, { "Success": (r) => r.status === 204 }); } }); group("/api/v1/flight/reserve-seat", () => { // Request No. 1: ReserveSeat { let url = BASE_URL + `/api/v1/flight/reserve-seat`; // TODO: edit the parameters of the request body. let body = {"flightId": "uuid", "seatNumber": "string"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 200 }); } }); group("/api/v1/flight/aircraft", () => { // Request No. 1: CreateAircraft { let url = BASE_URL + `/api/v1/flight/aircraft`; // TODO: edit the parameters of the request body. let body = {"name": "string", "model": "string", "manufacturingYear": "integer"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 200 }); } }); } ================================================ FILE: src/Services/Flight/tests/UnitTest/Aircraft/Features/CreateAircraftTests/CreateAircraftCommandHandlerTests.cs ================================================ using FluentAssertions; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; namespace Unit.Test.Aircraft.Features.CreateAircraftTests; using global::Flight.Aircrafts.Features.CreatingAircraft.V1; [Collection(nameof(UnitTestFixture))] public class CreateAircraftCommandHandlerTests { private readonly UnitTestFixture _fixture; private readonly CreateAircraftHandler _handler; public Task Act(CreateAircraft command, CancellationToken cancellationToken) => _handler.Handle(command, cancellationToken); public CreateAircraftCommandHandlerTests(UnitTestFixture fixture) { _fixture = fixture; _handler = new CreateAircraftHandler(_fixture.DbContext); } [Fact] public async Task handler_with_valid_command_should_create_new_aircraft_and_return_currect_aircraft_dto() { // Arrange var command = new FakeCreateAircraftCommand().Generate(); // Act var response = await Act(command, CancellationToken.None); // Assert var entity = await _fixture.DbContext.Aircraft.FindAsync(response?.Id); entity?.Should().NotBeNull(); response?.Id.Should().Be(entity.Id); } [Fact] public async Task handler_with_null_command_should_throw_argument_exception() { // Arrange CreateAircraft command = null; // Act Func act = async () => { await Act(command, CancellationToken.None); }; // Assert await act.Should().ThrowAsync(); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Aircraft/Features/CreateAircraftTests/CreateAircraftCommandValidatorTests.cs ================================================ using FluentAssertions; using FluentValidation.TestHelper; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; namespace Unit.Test.Aircraft.Features.CreateAircraftTests; using global::Flight.Aircrafts.Features.CreatingAircraft.V1; [Collection(nameof(UnitTestFixture))] public class CreateAircraftCommandValidatorTests { [Fact] public void is_valid_should_be_false_when_have_invalid_parameter() { // Arrange var command = new FakeValidateCreateAircraftCommand().Generate(); var validator = new CreateAircraftValidator(); // Act var result = validator.TestValidate(command); // Assert result.IsValid.Should().BeFalse(); result.ShouldHaveValidationErrorFor(x => x.Model); result.ShouldHaveValidationErrorFor(x => x.ManufacturingYear); result.ShouldHaveValidationErrorFor(x => x.Name); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Airport/Features/CreateAirportTests/CreateAirportCommandHandlerTests.cs ================================================ using FluentAssertions; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; namespace Unit.Test.Airport.Features.CreateAirportTests; using global::Flight.Airports.Features.CreatingAirport.V1; using global::Flight.Airports.ValueObjects; [Collection(nameof(UnitTestFixture))] public class CreateAirportCommandHandlerTests { private readonly UnitTestFixture _fixture; private readonly CreateAirportHandler _handler; public CreateAirportCommandHandlerTests(UnitTestFixture fixture) { _fixture = fixture; _handler = new CreateAirportHandler(_fixture.DbContext); } public Task Act(CreateAirport command, CancellationToken cancellationToken) => _handler.Handle(command, cancellationToken); [Fact] public async Task handler_with_valid_command_should_create_new_airport_and_return_currect_airport_dto() { // Arrange var command = new FakeCreateAirportCommand().Generate(); // Act var response = await Act(command, CancellationToken.None); // Assert var entity = await _fixture.DbContext.Airports.FindAsync(AirportId.Of(response.Id)); entity?.Should().NotBeNull(); response?.Id.Should().Be(entity.Id); } [Fact] public async Task handler_with_null_command_should_throw_argument_exception() { // Arrange CreateAirport command = null; // Act var act = async () => { await Act(command, CancellationToken.None); }; // Assert await act.Should().ThrowAsync(); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Airport/Features/CreateAirportTests/CreateAirportCommandValidatorTests.cs ================================================ using FluentAssertions; using FluentValidation.TestHelper; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; namespace Unit.Test.Airport.Features.CreateAirportTests; using global::Flight.Airports.Features.CreatingAirport.V1; [Collection(nameof(UnitTestFixture))] public class CreateAirportCommandValidatorTests { [Fact] public void is_valid_should_be_false_when_have_invalid_parameter() { // Arrange var command = new FakeValidateCreateAirportCommand().Generate(); var validator = new CreateAirportValidator(); // Act var result = validator.TestValidate(command); // Assert result.IsValid.Should().BeFalse(); result.ShouldHaveValidationErrorFor(x => x.Code); result.ShouldHaveValidationErrorFor(x => x.Address); result.ShouldHaveValidationErrorFor(x => x.Name); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs ================================================ using Flight.Data; using Flight.Flights.Enums; using Flight.Seats.Enums; using Microsoft.EntityFrameworkCore; namespace Unit.Test.Common; using global::Flight.Aircrafts.ValueObjects; using global::Flight.Airports.ValueObjects; using global::Flight.Flights.ValueObjects; using global::Flight.Seats.ValueObjects; using MassTransit; using AirportName = global::Flight.Airports.ValueObjects.Name; using Name = global::Flight.Aircrafts.ValueObjects.Name; public static class DbContextFactory { private static readonly Guid _airportId1 = NewId.NextGuid(); private static readonly Guid _airportId2 = NewId.NextGuid(); private static readonly Guid _aircraft1 = NewId.NextGuid(); private static readonly Guid _aircraft2 = NewId.NextGuid(); private static readonly Guid _aircraft3 = NewId.NextGuid(); private static readonly Guid _flightId1 = NewId.NextGuid(); public static FlightDbContext Create() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options; var context = new FlightDbContext(options, currentUserProvider: null, null); // Seed our data FlightDataSeeder(context); return context; } private static void FlightDataSeeder(FlightDbContext context) { var airports = new List { global::Flight.Airports.Models.Airport.Create(AirportId.Of(_airportId1), AirportName.Of("Lisbon International Airport"), Address.Of("LIS"), Code.Of("12988")), global::Flight.Airports.Models.Airport.Create(AirportId.Of(_airportId2), AirportName.Of("Sao Paulo International Airport"), Address.Of("BRZ"), Code.Of("11200")) }; context.Airports.AddRange(airports); var aircrafts = new List { global::Flight.Aircrafts.Models.Aircraft.Create(AircraftId.Of(_aircraft1), Name.Of("Boeing 737"), Model.Of("B737"), ManufacturingYear.Of(2005)), global::Flight.Aircrafts.Models.Aircraft.Create(AircraftId.Of(_aircraft2), Name.Of("Airbus 300"), Model.Of("A300"), ManufacturingYear.Of(2000)), global::Flight.Aircrafts.Models.Aircraft.Create(AircraftId.Of(_aircraft3), Name.Of("Airbus 320"), Model.Of("A320"), ManufacturingYear.Of(2003)) }; context.Aircraft.AddRange(aircrafts); var flights = new List { global::Flight.Flights.Models.Flight.Create(FlightId.Of(_flightId1), FlightNumber.Of( "BD467"), AircraftId.Of(_aircraft1), AirportId.Of( _airportId1), DepartureDate.Of( new DateTime(2022, 1, 31, 12, 0, 0)), ArriveDate.Of( new DateTime(2022, 1, 31, 14, 0, 0)), AirportId.Of( _airportId2), DurationMinutes.Of(120m), FlightDate.Of( new DateTime(2022, 1, 31)), FlightStatus.Completed, Price.Of(8000)) }; context.Flights.AddRange(flights); var seats = new List { global::Flight.Seats.Models.Seat.Create(SeatId.Of( NewId.NextGuid()), SeatNumber.Of("12A"), SeatType.Window, SeatClass.Economy, FlightId.Of(_flightId1)), global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12B"), SeatType.Window, SeatClass.Economy, FlightId.Of(_flightId1)), global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12C"), SeatType.Middle, SeatClass.Economy, FlightId.Of(_flightId1)), global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12D"), SeatType.Middle, SeatClass.Economy, FlightId.Of(_flightId1)), global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12E"), SeatType.Aisle, SeatClass.Economy, FlightId.Of(_flightId1)), global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12F"), SeatType.Aisle, SeatClass.Economy, FlightId.Of(_flightId1)) }; context.Seats.AddRange(seats); context.SaveChanges(); } public static void Destroy(FlightDbContext context) { context.Database.EnsureDeleted(); context.Dispose(); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Common/MapperFactory.cs ================================================ using Flight; using Mapster; using MapsterMapper; namespace Unit.Test.Common { public static class MapperFactory { public static IMapper Create() { var typeAdapterConfig = TypeAdapterConfig.GlobalSettings; typeAdapterConfig.Scan(typeof(FlightRoot).Assembly); IMapper instance = new Mapper(typeAdapterConfig); return instance; } } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Common/UnitTestFixture.cs ================================================ using Flight.Data; using MapsterMapper; using Xunit; namespace Unit.Test.Common { [CollectionDefinition(nameof(UnitTestFixture))] public class FixtureCollection : ICollectionFixture { } public class UnitTestFixture : IDisposable { public UnitTestFixture() { Mapper = MapperFactory.Create(); DbContext = DbContextFactory.Create(); } public IMapper Mapper { get; } public FlightDbContext DbContext { get; } public void Dispose() { DbContextFactory.Destroy(DbContext); } } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeCreateAircraftCommand.cs ================================================ using AutoBogus; namespace Unit.Test.Fakes; using global::Flight.Aircrafts.Features.CreatingAircraft.V1; using MassTransit; public class FakeCreateAircraftCommand : AutoFaker { public FakeCreateAircraftCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.ManufacturingYear, _ => 2000); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeCreateAirportCommand.cs ================================================ using AutoBogus; namespace Unit.Test.Fakes; using global::Flight.Airports.Features.CreatingAirport.V1; using MassTransit; public class FakeCreateAirportCommand : AutoFaker { public FakeCreateAirportCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeCreateFlightCommand.cs ================================================ using AutoBogus; namespace Unit.Test.Fakes; using System.Linq; using global::Flight.Data.Seed; using global::Flight.Flights.Features.CreatingFlight.V1; using MassTransit; public sealed class FakeCreateFlightCommand : AutoFaker { public FakeCreateFlightCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightNumber, r => r.Random.Number(1000, 2000).ToString()); RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id); RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id); RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeCreateSeatCommand.cs ================================================ using AutoBogus; using Flight.Seats.Enums; namespace Unit.Test.Fakes; using System.Linq; using global::Flight.Data.Seed; using global::Flight.Seats.Features.CreatingSeat.V1; using MassTransit; public class FakeCreateSeatCommand : AutoFaker { public FakeCreateSeatCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.FlightId, _ => InitialData.Flights.First().Id); RuleFor(r => r.SeatNumber, _ => "F99"); RuleFor(r => r.Type, _ => SeatType.Window); RuleFor(r => r.Class, _ => SeatClass.Economy); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeFlightCreate.cs ================================================ namespace Unit.Test.Fakes; using global::Flight.Aircrafts.ValueObjects; using global::Flight.Airports.ValueObjects; using global::Flight.Flights.ValueObjects; public static class FakeFlightCreate { public static global::Flight.Flights.Models.Flight Generate() { var command = new FakeCreateFlightCommand().Generate(); return global::Flight.Flights.Models.Flight.Create(FlightId.Of(command.Id), FlightNumber.Of(command.FlightNumber), AircraftId.Of(command.AircraftId), AirportId.Of(command.DepartureAirportId), DepartureDate.Of(command.DepartureDate), ArriveDate.Of(command.ArriveDate), AirportId.Of(command.ArriveAirportId), DurationMinutes.Of(command.DurationMinutes), FlightDate.Of(command.FlightDate), command.Status, Price.Of(command.Price)); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeFlightUpdate.cs ================================================ namespace Unit.Test.Fakes; using global::Flight.Flights.Models; using global::Flight.Flights.ValueObjects; public static class FakeFlightUpdate { public static void Generate(Flight flight) { flight.Update(flight.Id, flight.FlightNumber, flight.AircraftId, flight.DepartureAirportId, flight.DepartureDate, flight.ArriveDate, flight.ArriveAirportId, flight.DurationMinutes, flight.FlightDate, flight.Status, Price.Of(1000), flight.IsDeleted); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeValidateCreateAircraftCommand.cs ================================================ using AutoBogus; namespace Unit.Test.Fakes; using global::Flight.Aircrafts.Features.CreatingAircraft.V1; public class FakeValidateCreateAircraftCommand : AutoFaker { public FakeValidateCreateAircraftCommand() { RuleFor(r => r.ManufacturingYear, _ => 0); RuleFor(r => r.Name, _ => null); RuleFor(r => r.Model, _ => null); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeValidateCreateAirportCommand.cs ================================================ using AutoBogus; namespace Unit.Test.Fakes; using global::Flight.Airports.Features.CreatingAirport.V1; public class FakeValidateCreateAirportCommand : AutoFaker { public FakeValidateCreateAirportCommand() { RuleFor(r => r.Code, _ => null); RuleFor(r => r.Name, _ => null); RuleFor(r => r.Address, _ => null); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeValidateCreateFlightCommand.cs ================================================ using System; using AutoBogus; using Flight.Flights.Enums; namespace Unit.Test.Fakes; using global::Flight.Flights.Features.CreatingFlight.V1; public class FakeValidateCreateFlightCommand : AutoFaker { public FakeValidateCreateFlightCommand() { RuleFor(r => r.Price, _ => -10); RuleFor(r => r.Status, _ => (FlightStatus)10); RuleFor(r => r.AircraftId, _ => Guid.Empty); RuleFor(r => r.DepartureAirportId, _ => Guid.Empty); RuleFor(r => r.ArriveAirportId, _ => Guid.Empty); RuleFor(r => r.DurationMinutes, _ => 0); RuleFor(r => r.FlightDate, _ => new DateTime()); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Fakes/FakeValidateCreateSeatCommand.cs ================================================ using AutoBogus; using Flight.Seats.Enums; namespace Unit.Test.Fakes; using System; using global::Flight.Seats.Features.CreatingSeat.V1; public class FakeValidateCreateSeatCommand : AutoFaker { public FakeValidateCreateSeatCommand() { RuleFor(r => r.SeatNumber, _ => null); RuleFor(r => r.FlightId, _ => Guid.Empty); RuleFor(r => r.Class, _ => (SeatClass)10); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Flight/Features/Domains/CreateFlightTests.cs ================================================ namespace Unit.Test.Flight.Features.Domains { using System.Linq; using FluentAssertions; using global::Flight.Flights.Features.CreatingFlight.V1; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; [Collection(nameof(UnitTestFixture))] public class CreateFlightTests { [Fact] public void can_create_valid_flight() { // Arrange + Act var fakeFlight = FakeFlightCreate.Generate(); // Assert fakeFlight.Should().NotBeNull(); } [Fact] public void queue_domain_event_on_create() { // Arrange + Act var fakeFlight = FakeFlightCreate.Generate(); // Assert fakeFlight.DomainEvents.Count.Should().Be(1); fakeFlight.DomainEvents.FirstOrDefault().Should().BeOfType(typeof(FlightCreatedDomainEvent)); } } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Flight/Features/Domains/UpdateFlightTests.cs ================================================ namespace Unit.Test.Flight.Features.Domains; using System.Linq; using FluentAssertions; using global::Flight.Flights.Features.UpdatingFlight.V1; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; [Collection(nameof(UnitTestFixture))] public class UpdateFlightTests { [Fact] public void can_update_valid_flight() { // Arrange var fakeFlight = FakeFlightCreate.Generate(); // Act FakeFlightUpdate.Generate(fakeFlight); // Assert fakeFlight.Price.Value.Should().Be(1000); } [Fact] public void queue_domain_event_on_update() { // Arrange var fakeFlight = FakeFlightCreate.Generate(); fakeFlight.ClearDomainEvents(); // Act FakeFlightUpdate.Generate(fakeFlight); // Assert fakeFlight.DomainEvents.Count.Should().Be(1); fakeFlight.DomainEvents.FirstOrDefault().Should().BeOfType(typeof(FlightUpdatedDomainEvent)); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Flight/Features/Handlers/CreateFlight/CreateFlightCommandHandlerTests.cs ================================================ namespace Unit.Test.Flight.Features.Handlers.CreateFlight; using System; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using global::Flight.Flights.Features.CreatingFlight.V1; using global::Flight.Flights.ValueObjects; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; [Collection(nameof(UnitTestFixture))] public class CreateFlightCommandHandlerTests { private readonly UnitTestFixture _fixture; private readonly CreateFlightHandler _handler; public Task Act(CreateFlight command, CancellationToken cancellationToken) => _handler.Handle(command, cancellationToken); public CreateFlightCommandHandlerTests(UnitTestFixture fixture) { _fixture = fixture; _handler = new CreateFlightHandler(fixture.DbContext); } [Fact] public async Task handler_with_valid_command_should_create_new_flight_and_return_currect_flight_dto() { // Arrange var command = new FakeCreateFlightCommand().Generate(); // Act var response = await Act(command, CancellationToken.None); // Assert var entity = await _fixture.DbContext.Flights.FindAsync(FlightId.Of(response.Id)); entity?.Should().NotBeNull(); response?.Id.Should().Be(entity.Id); } [Fact] public async Task handler_with_null_command_should_throw_argument_exception() { // Arrange CreateFlight command = null; // Act Func act = async () => { await Act(command, CancellationToken.None); }; // Assert await act.Should().ThrowAsync(); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Flight/Features/Handlers/CreateFlight/CreateFlightCommandValidatorTests.cs ================================================ namespace Unit.Test.Flight.Features.Handlers.CreateFlight; using FluentAssertions; using FluentValidation.TestHelper; using global::Flight.Flights.Features.CreatingFlight.V1; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; [Collection(nameof(UnitTestFixture))] public class CreateFlightCommandValidatorTests { [Fact] public void is_valid_should_be_false_when_have_invalid_parameter() { // Arrange var command = new FakeValidateCreateFlightCommand().Generate(); var validator = new CreateFlightValidator(); // Act var result = validator.TestValidate(command); // Assert result.IsValid.Should().BeFalse(); result.ShouldHaveValidationErrorFor(x => x.Price); result.ShouldHaveValidationErrorFor(x => x.Status); result.ShouldHaveValidationErrorFor(x => x.AircraftId); result.ShouldHaveValidationErrorFor(x => x.DepartureAirportId); result.ShouldHaveValidationErrorFor(x => x.ArriveAirportId); result.ShouldHaveValidationErrorFor(x => x.DurationMinutes); result.ShouldHaveValidationErrorFor(x => x.FlightDate); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Flight/FlightMappingTests.cs ================================================ using Flight.Flights.Dtos; using MapsterMapper; using Unit.Test.Common; using Xunit; namespace Unit.Test.Flight; [Collection(nameof(UnitTestFixture))] public class FlightMappingTests { private readonly IMapper _mapper; public FlightMappingTests(UnitTestFixture fixture) { _mapper = fixture.Mapper; } public static IEnumerable Data { get { yield return new object[] { // these types will instantiate with reflection in the future typeof(global::Flight.Flights.Models.FlightReadModel), typeof(FlightDto) }; } } [Theory] [MemberData(nameof(Data))] public void should_support_mapping_from_source_to_destination(Type source, Type destination, params object[] parameters) { var instance = Activator.CreateInstance(source, parameters); _mapper.Map(instance, source, destination); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Seat/Features/CreateSeatCommandHandlerTests.cs ================================================ using FluentAssertions; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; namespace Unit.Test.Seat.Features; using global::Flight.Seats.Features.CreatingSeat.V1; using global::Flight.Seats.ValueObjects; [Collection(nameof(UnitTestFixture))] public class CreateSeatCommandHandlerTests { private readonly UnitTestFixture _fixture; private readonly CreateSeatCommandHandler _handler; public CreateSeatCommandHandlerTests(UnitTestFixture fixture) { _fixture = fixture; _handler = new CreateSeatCommandHandler(_fixture.DbContext); } public Task Act(CreateSeat command, CancellationToken cancellationToken) { return _handler.Handle(command, cancellationToken); } [Fact] public async Task handler_with_valid_command_should_create_new_seat_and_return_currect_seat_dto() { // Arrange var command = new FakeCreateSeatCommand().Generate(); // Act var response = await Act(command, CancellationToken.None); // Assert var entity = await _fixture.DbContext.Seats.FindAsync(SeatId.Of(response.Id)); entity?.Should().NotBeNull(); response?.Id.Should().Be(entity.Id); } [Fact] public async Task handler_with_null_command_should_throw_argument_exception() { // Arrange CreateSeat command = null; // Act var act = async () => { await Act(command, CancellationToken.None); }; // Assert await act.Should().ThrowAsync(); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Seat/Features/CreateSeatCommandValidatorTests.cs ================================================ using FluentAssertions; using FluentValidation.TestHelper; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; namespace Unit.Test.Seat.Features; using global::Flight.Seats.Features.CreatingSeat.V1; [Collection(nameof(UnitTestFixture))] public class CreateSeatCommandValidatorTests { [Fact] public void is_valid_should_be_false_when_have_invalid_parameter() { // Arrange var command = new FakeValidateCreateSeatCommand().Generate(); var validator = new CreateSeatValidator(); // Act var result = validator.TestValidate(command); // Assert result.IsValid.Should().BeFalse(); result.ShouldHaveValidationErrorFor(x => x.SeatNumber); result.ShouldHaveValidationErrorFor(x => x.FlightId); result.ShouldHaveValidationErrorFor(x => x.Class); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Seat/SeatMappingTests.cs ================================================ using Flight.Seats.Dtos; using MapsterMapper; using Unit.Test.Common; using Xunit; namespace Unit.Test.Seat; [Collection(nameof(UnitTestFixture))] public class SeatMappingTests { private readonly UnitTestFixture _fixture; private readonly IMapper _mapper; public SeatMappingTests(UnitTestFixture fixture) { _mapper = fixture.Mapper; } public static IEnumerable Data { get { yield return new object[] { // these types will instantiate with reflection in the future typeof(global::Flight.Seats.Models.SeatReadModel), typeof(SeatDto) }; } } [Theory] [MemberData(nameof(Data))] public void should_support_mapping_from_source_to_destination(Type source, Type destination, params object[] parameters) { var instance = Activator.CreateInstance(source, parameters); _mapper.Map(instance, source, destination); } } ================================================ FILE: src/Services/Flight/tests/UnitTest/Unit.Test.csproj ================================================ PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: src/Services/Flight/tests/UnitTest/xunit.runner.json ================================================ { "parallelizeAssembly": false, "parallelizeTestCollections": false } ================================================ FILE: src/Services/Flight/tests/tests.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndToEnd.Test", "EndToEndTest\EndToEnd.Test.csproj", "{EA6ECE87-A111-4F2A-8034-8CAEADE7F491}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "IntegrationTest\Integration.Test.csproj", "{815ED633-1B3E-4F1A-B5FB-A674AB326150}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unit.Test", "UnitTest\Unit.Test.csproj", "{7472C957-FA4A-48E1-87DE-474216B35F87}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {EA6ECE87-A111-4F2A-8034-8CAEADE7F491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA6ECE87-A111-4F2A-8034-8CAEADE7F491}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA6ECE87-A111-4F2A-8034-8CAEADE7F491}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA6ECE87-A111-4F2A-8034-8CAEADE7F491}.Release|Any CPU.Build.0 = Release|Any CPU {815ED633-1B3E-4F1A-B5FB-A674AB326150}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {815ED633-1B3E-4F1A-B5FB-A674AB326150}.Debug|Any CPU.Build.0 = Debug|Any CPU {815ED633-1B3E-4F1A-B5FB-A674AB326150}.Release|Any CPU.ActiveCfg = Release|Any CPU {815ED633-1B3E-4F1A-B5FB-A674AB326150}.Release|Any CPU.Build.0 = Release|Any CPU {7472C957-FA4A-48E1-87DE-474216B35F87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7472C957-FA4A-48E1-87DE-474216B35F87}.Debug|Any CPU.Build.0 = Debug|Any CPU {7472C957-FA4A-48E1-87DE-474216B35F87}.Release|Any CPU.ActiveCfg = Release|Any CPU {7472C957-FA4A-48E1-87DE-474216B35F87}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal ================================================ FILE: src/Services/Identity/Dockerfile ================================================ # ---------- Build Stage ---------- FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy solution-level files COPY .editorconfig . COPY global.json . COPY Directory.Build.props . # Copy project files first (for better Docker layer caching) COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/ COPY src/Services/Identity/src/Identity/Identity.csproj src/Services/Identity/src/Identity/ COPY src/Services/Identity/src/Identity.Api/Identity.Api.csproj src/Services/Identity/src/Identity.Api/ COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/ # Restore dependencies RUN dotnet restore src/Services/Identity/src/Identity.Api/Identity.Api.csproj # Copy remaining source code COPY src ./src # Publish (build included, no separate build step needed) RUN dotnet publish src/Services/Identity/src/Identity.Api/Identity.Api.csproj \ -c Release \ -o /app/publish \ --no-restore # ---------- Runtime Stage ---------- FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . ENV ASPNETCORE_URLS=http://+:80 ENV ASPNETCORE_ENVIRONMENT=docker EXPOSE 80 ENTRYPOINT ["dotnet", "Identity.Api.dll"] ================================================ FILE: src/Services/Identity/src/Identity/AssemblyInfo.cs ================================================ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Unit.Test")] [assembly: InternalsVisibleTo("Integration.Test")] [assembly: InternalsVisibleTo("EndToEnd.Test")] ================================================ FILE: src/Services/Identity/src/Identity/Configurations/AuthOptions.cs ================================================ namespace Identity.Configurations; public class AuthOptions { public string IssuerUri { get; set; } } ================================================ FILE: src/Services/Identity/src/Identity/Configurations/Config.cs ================================================ using Duende.IdentityServer; using Duende.IdentityServer.Models; using Identity.Identity.Constants; using IdentityModel; namespace Identity.Configurations; public static class Config { public static IEnumerable IdentityResources => new List { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() }; public static IEnumerable ApiScopes => new List { new(Constants.StandardScopes.FlightApi), new(Constants.StandardScopes.PassengerApi), new(Constants.StandardScopes.BookingApi), new(Constants.StandardScopes.IdentityApi), new(JwtClaimTypes.Role, new List {"role"}) }; public static IList ApiResources => new List { new(Constants.StandardScopes.FlightApi) { Scopes = { Constants.StandardScopes.FlightApi } }, new(Constants.StandardScopes.PassengerApi) { Scopes = { Constants.StandardScopes.PassengerApi } }, new(Constants.StandardScopes.BookingApi) { Scopes = { Constants.StandardScopes.BookingApi } }, new(Constants.StandardScopes.IdentityApi) { Scopes = { Constants.StandardScopes.IdentityApi } }, }; public static IEnumerable Clients => new List { new() { ClientId = "client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, JwtClaimTypes.Role, // Include roles scope Constants.StandardScopes.FlightApi, Constants.StandardScopes.PassengerApi, Constants.StandardScopes.BookingApi, Constants.StandardScopes.IdentityApi, }, AccessTokenLifetime = 3600, // authorize the client to access protected resources IdentityTokenLifetime = 3600, // authenticate the user, AlwaysIncludeUserClaimsInIdToken = true // Include claims in ID token } }; } ================================================ FILE: src/Services/Identity/src/Identity/Configurations/UserValidator.cs ================================================ namespace Identity.Configurations; using System.Security.Claims; using System.Threading.Tasks; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Identity.Models; using Microsoft.AspNetCore.Identity; 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"); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Configurations/RoleClaimConfiguration.cs ================================================ namespace Identity.Data.Configurations; using Identity.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class RoleClaimConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(nameof(RoleClaim)); // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api builder.Property(r => r.Version).IsConcurrencyToken(); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Configurations/RoleConfiguration.cs ================================================ namespace Identity.Data.Configurations; using Identity.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class RoleConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(nameof(Role)); // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api builder.Property(r => r.Version).IsConcurrencyToken(); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Configurations/UserClaimConfiguration.cs ================================================ namespace Identity.Data.Configurations; using Identity.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class UserClaimConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(nameof(UserClaim)); // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api builder.Property(r => r.Version).IsConcurrencyToken(); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Configurations/UserConfiguration.cs ================================================ namespace Identity.Data.Configurations; using Identity.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(nameof(User)); // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api builder.Property(r => r.Version).IsConcurrencyToken(); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Configurations/UserLoginConfiguration.cs ================================================ namespace Identity.Data.Configurations; using Identity.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class UserLoginConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(nameof(UserLogin)); // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api builder.Property(r => r.Version).IsConcurrencyToken(); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Configurations/UserRoleConfiguration.cs ================================================ namespace Identity.Data.Configurations; using Identity.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class UserRoleConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(nameof(UserRole)); // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api builder.Property(r => r.Version).IsConcurrencyToken(); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Configurations/UserTokenConfiguration.cs ================================================ namespace Identity.Data.Configurations; using Identity.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class UserTokenConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(nameof(UserToken)); // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api builder.Property(r => r.Version).IsConcurrencyToken(); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; namespace Identity.Data; public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory { public IdentityContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder(); builder.UseNpgsql("Server=localhost;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true") .UseSnakeCaseNamingConvention(); return new IdentityContext(builder.Options); } } ================================================ FILE: src/Services/Identity/src/Identity/Data/IdentityContext.cs ================================================ using System.Collections.Generic; using System.Collections.Immutable; using System.Data; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; using BuildingBlocks.EFCore; using Identity.Identity.Models; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Identity.Data; using System; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; 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) { builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 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); } } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Migrations/20230331193410_initial.Designer.cs ================================================ // using System; using 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 Identity.Data.Migrations { [DbContext(typeof(IdentityContext))] [Migration("20230331193410_initial")] partial class initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "7.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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") .HasColumnType("text") .HasColumnName("first_name"); b.Property("LastName") .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") .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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.Models.RoleClaim", b => { b.HasOne("Identity.Identity.Models.Role", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserClaim", b => { b.HasOne("Identity.Identity.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserLogin", b => { b.HasOne("Identity.Identity.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserRole", b => { b.HasOne("Identity.Identity.Models.Role", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); b.HasOne("Identity.Identity.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserToken", b => { b.HasOne("Identity.Identity.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 } } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Migrations/20230331193410_initial.cs ================================================ using System; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Identity.Data.Migrations { /// public partial class initial : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "asp_net_roles", 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), normalizedname = table.Column(name: "normalized_name", type: "character varying(256)", maxLength: 256, nullable: true), concurrencystamp = table.Column(name: "concurrency_stamp", type: "text", nullable: true) }, constraints: table => { table.PrimaryKey("pk_asp_net_roles", x => x.id); }); migrationBuilder.CreateTable( name: "asp_net_users", columns: table => new { id = table.Column(type: "uuid", nullable: false), firstname = table.Column(name: "first_name", type: "text", nullable: true), lastname = table.Column(name: "last_name", type: "text", nullable: true), passportnumber = table.Column(name: "pass_port_number", type: "text", nullable: true), version = table.Column(type: "bigint", nullable: false), username = table.Column(name: "user_name", type: "character varying(256)", maxLength: 256, nullable: true), normalizedusername = table.Column(name: "normalized_user_name", type: "character varying(256)", maxLength: 256, nullable: true), email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), normalizedemail = table.Column(name: "normalized_email", type: "character varying(256)", maxLength: 256, nullable: true), emailconfirmed = table.Column(name: "email_confirmed", type: "boolean", nullable: false), passwordhash = table.Column(name: "password_hash", type: "text", nullable: true), securitystamp = table.Column(name: "security_stamp", type: "text", nullable: true), concurrencystamp = table.Column(name: "concurrency_stamp", type: "text", nullable: true), phonenumber = table.Column(name: "phone_number", type: "text", nullable: true), phonenumberconfirmed = table.Column(name: "phone_number_confirmed", type: "boolean", nullable: false), twofactorenabled = table.Column(name: "two_factor_enabled", type: "boolean", nullable: false), lockoutend = table.Column(name: "lockout_end", type: "timestamp with time zone", nullable: true), lockoutenabled = table.Column(name: "lockout_enabled", type: "boolean", nullable: false), accessfailedcount = table.Column(name: "access_failed_count", type: "integer", nullable: false) }, constraints: table => { table.PrimaryKey("pk_asp_net_users", x => x.id); }); migrationBuilder.CreateTable( name: "asp_net_role_claims", columns: table => new { id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), version = table.Column(type: "bigint", nullable: false), roleid = table.Column(name: "role_id", type: "uuid", nullable: false), claimtype = table.Column(name: "claim_type", type: "text", nullable: true), claimvalue = table.Column(name: "claim_value", 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.roleid, principalTable: "asp_net_roles", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "asp_net_user_claims", columns: table => new { id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), version = table.Column(type: "bigint", nullable: false), userid = table.Column(name: "user_id", type: "uuid", nullable: false), claimtype = table.Column(name: "claim_type", type: "text", nullable: true), claimvalue = table.Column(name: "claim_value", 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.userid, principalTable: "asp_net_users", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "asp_net_user_logins", columns: table => new { loginprovider = table.Column(name: "login_provider", type: "text", nullable: false), providerkey = table.Column(name: "provider_key", type: "text", nullable: false), version = table.Column(type: "bigint", nullable: false), providerdisplayname = table.Column(name: "provider_display_name", type: "text", nullable: true), userid = table.Column(name: "user_id", type: "uuid", nullable: false) }, constraints: table => { table.PrimaryKey("pk_asp_net_user_logins", x => new { x.loginprovider, x.providerkey }); table.ForeignKey( name: "fk_asp_net_user_logins_asp_net_users_user_id", column: x => x.userid, principalTable: "asp_net_users", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "asp_net_user_roles", columns: table => new { userid = table.Column(name: "user_id", type: "uuid", nullable: false), roleid = table.Column(name: "role_id", type: "uuid", nullable: false), version = table.Column(type: "bigint", nullable: false) }, constraints: table => { table.PrimaryKey("pk_asp_net_user_roles", x => new { x.userid, x.roleid }); table.ForeignKey( name: "fk_asp_net_user_roles_asp_net_roles_role_id", column: x => x.roleid, 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.userid, principalTable: "asp_net_users", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "asp_net_user_tokens", columns: table => new { userid = table.Column(name: "user_id", type: "uuid", nullable: false), loginprovider = table.Column(name: "login_provider", 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.userid, x.loginprovider, x.name }); table.ForeignKey( name: "fk_asp_net_user_tokens_asp_net_users_user_id", column: x => x.userid, principalTable: "asp_net_users", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( name: "ix_asp_net_role_claims_role_id", table: "asp_net_role_claims", column: "role_id"); migrationBuilder.CreateIndex( name: "RoleNameIndex", table: "asp_net_roles", column: "normalized_name", unique: true); migrationBuilder.CreateIndex( name: "ix_asp_net_user_claims_user_id", table: "asp_net_user_claims", column: "user_id"); migrationBuilder.CreateIndex( name: "ix_asp_net_user_logins_user_id", table: "asp_net_user_logins", column: "user_id"); migrationBuilder.CreateIndex( name: "ix_asp_net_user_roles_role_id", table: "asp_net_user_roles", column: "role_id"); migrationBuilder.CreateIndex( name: "EmailIndex", table: "asp_net_users", column: "normalized_email"); migrationBuilder.CreateIndex( name: "UserNameIndex", table: "asp_net_users", column: "normalized_user_name", unique: true); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "asp_net_role_claims"); migrationBuilder.DropTable( name: "asp_net_user_claims"); migrationBuilder.DropTable( name: "asp_net_user_logins"); migrationBuilder.DropTable( name: "asp_net_user_roles"); migrationBuilder.DropTable( name: "asp_net_user_tokens"); migrationBuilder.DropTable( name: "asp_net_roles"); migrationBuilder.DropTable( name: "asp_net_users"); } } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs ================================================ // using System; using Identity.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Identity.Data.Migrations { [DbContext(typeof(IdentityContext))] partial class IdentityContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "7.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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") .HasColumnType("text") .HasColumnName("first_name"); b.Property("LastName") .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") .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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.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", (string)null); }); modelBuilder.Entity("Identity.Identity.Models.RoleClaim", b => { b.HasOne("Identity.Identity.Models.Role", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserClaim", b => { b.HasOne("Identity.Identity.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserLogin", b => { b.HasOne("Identity.Identity.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserRole", b => { b.HasOne("Identity.Identity.Models.Role", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); b.HasOne("Identity.Identity.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); }); modelBuilder.Entity("Identity.Identity.Models.UserToken", b => { b.HasOne("Identity.Identity.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 } } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Seed/IdentityDataSeeder.cs ================================================ using System; using System.Threading.Tasks; using BuildingBlocks.Constants; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Core; using BuildingBlocks.EFCore; using Identity.Identity.Constants; using Identity.Identity.Models; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace Identity.Data.Seed; using System.Linq; 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 _roleManager.RoleExistsAsync(IdentityConstant.Role.Admin) == false) { await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.Admin }); } if (await _roleManager.RoleExistsAsync(IdentityConstant.Role.User) == false) { await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.User }); } } private async Task SeedUsers() { 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(), IdentityConstant.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(), IdentityConstant.Role.User); await _eventDispatcher.SendAsync(new UserCreated(InitialData.Users.Last().Id, InitialData.Users.Last().FirstName + " " + InitialData.Users.Last().LastName, InitialData.Users.Last().PassPortNumber)); } } } } ================================================ FILE: src/Services/Identity/src/Identity/Data/Seed/InitialData.cs ================================================ namespace Identity.Data.Seed; using System; using System.Collections.Generic; using Identity.Models; using MassTransit; 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 = "12345678", Email = "sam@test.com", SecurityStamp = Guid.NewGuid().ToString() }, new User { Id = NewId.NextGuid(), FirstName = "Sam2", LastName = "H2", UserName = "samh2", PassPortNumber = "87654321", Email = "sam2@test.com", SecurityStamp = Guid.NewGuid().ToString() } }; } } ================================================ FILE: src/Services/Identity/src/Identity/Data/readme.md ================================================ dotnet ef migrations add initial --context IdentityContext -o "Data\Migrations" dotnet ef database update --context IdentityContext ================================================ FILE: src/Services/Identity/src/Identity/Extensions/Infrastructure/IdentityServerExtensions.cs ================================================ using BuildingBlocks.Web; using Identity.Data; using Identity.Identity.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; namespace Identity.Extensions.Infrastructure; using Configurations; 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(); builder.Services.ConfigureApplicationCookie(options => { options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; }; options.Events.OnRedirectToAccessDenied = context => { context.Response.StatusCode = StatusCodes.Status403Forbidden; return Task.CompletedTask; }; }); return builder; } } ================================================ FILE: src/Services/Identity/src/Identity/Extensions/Infrastructure/InfrastructureExtensions.cs ================================================ using BuildingBlocks.Core; using BuildingBlocks.EFCore; using BuildingBlocks.Mapster; using BuildingBlocks.MassTransit; using BuildingBlocks.OpenApi; using BuildingBlocks.PersistMessageProcessor; using BuildingBlocks.ProblemDetails; using BuildingBlocks.Web; using Figgle; using Figgle.Fonts; using FluentValidation; using Identity.Data; using Identity.Data.Seed; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ServiceDefaults; namespace Identity.Extensions.Infrastructure; public static class InfrastructureExtensions { public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder builder) { var configuration = builder.Configuration; var env = builder.Environment; builder.AddServiceDefaults(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); var appOptions = builder.Services.GetOptions(nameof(AppOptions)); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddControllers(); builder.AddPersistMessageProcessor(); builder.AddCustomDbContext(nameof(Identity)); builder.Services.AddScoped(); builder.Services.AddAspnetOpenApi(); builder.Services.AddCustomVersioning(); builder.Services.AddCustomMediatR(); builder.Services.AddValidatorsFromAssembly(typeof(IdentityRoot).Assembly); builder.Services.AddProblemDetails(); builder.Services.AddCustomMapster(typeof(IdentityRoot).Assembly); builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(IdentityRoot).Assembly); builder.AddCustomIdentityServer(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); return builder; } public static WebApplication UseInfrastructure(this WebApplication app) { var env = app.Environment; var appOptions = app.GetOptions(nameof(AppOptions)); app.UseAuthentication(); app.UseAuthorization(); app.UseServiceDefaults(); app.UseForwardedHeaders(); app.UseCustomProblemDetails(); app.UseCorrelationId(); app.UseMigration(); app.UseIdentityServer(); app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); if (env.IsDevelopment()) { app.UseAspnetOpenApi(); } return app; } } ================================================ FILE: src/Services/Identity/src/Identity/Extensions/Infrastructure/MediatRExtensions.cs ================================================ using BuildingBlocks.Logging; using BuildingBlocks.Validation; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace Identity.Extensions.Infrastructure; using Configurations; public static class MediatRExtensions { public static IServiceCollection AddCustomMediatR(this IServiceCollection services) { services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(IdentityRoot).Assembly)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); return services; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Constants/Constants.cs ================================================ namespace Identity.Identity.Constants; public static class Constants { 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"; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Exceptions/RegisterIdentityUserException.cs ================================================ using BuildingBlocks.Exception; namespace Identity.Identity.Exceptions; public class RegisterIdentityUserException : AppException { public RegisterIdentityUserException(string message) : base(message) { } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Features/IdentityMappings.cs ================================================ using Mapster; namespace Identity.Identity.Features; using RegisteringNewUser.V1; 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)); } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Features/RegisteringNewUser/V1/RegisterNewUser.cs ================================================ using BuildingBlocks.Constants; namespace Identity.Identity.Features.RegisteringNewUser.V1; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Core; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Web; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; using FluentValidation; using Mapster; using MapsterMapper; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Routing; using Models; 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, IdentityConstant.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); } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Models/Role.cs ================================================ namespace Identity.Identity.Models; using System; using BuildingBlocks.Core.Model; using Microsoft.AspNetCore.Identity; public class Role : IdentityRole, IVersion { public long Version { get; set; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Models/RoleClaim.cs ================================================ namespace Identity.Identity.Models; using System; using BuildingBlocks.Core.Model; using Microsoft.AspNetCore.Identity; public class RoleClaim : IdentityRoleClaim, IVersion { public long Version { get; set; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Models/User.cs ================================================ using Microsoft.AspNetCore.Identity; namespace Identity.Identity.Models; using System; using BuildingBlocks.Core.Model; 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; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Models/UserClaim.cs ================================================ namespace Identity.Identity.Models; using System; using BuildingBlocks.Core.Model; using Microsoft.AspNetCore.Identity; public class UserClaim : IdentityUserClaim, IVersion { public long Version { get; set; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Models/UserLogin.cs ================================================ namespace Identity.Identity.Models; using System; using BuildingBlocks.Core.Model; using Microsoft.AspNetCore.Identity; public class UserLogin : IdentityUserLogin, IVersion { public long Version { get; set; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Models/UserRole.cs ================================================ namespace Identity.Identity.Models; using System; using BuildingBlocks.Core.Model; using Microsoft.AspNetCore.Identity; public class UserRole : IdentityUserRole, IVersion { public long Version { get; set; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity/Models/UserToken.cs ================================================ namespace Identity.Identity.Models; using System; using BuildingBlocks.Core.Model; using Microsoft.AspNetCore.Identity; public class UserToken : IdentityUserToken, IVersion { public long Version { get; set; } } ================================================ FILE: src/Services/Identity/src/Identity/Identity.csproj ================================================ all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/Services/Identity/src/Identity/IdentityEventMapper.cs ================================================ using BuildingBlocks.Core; using BuildingBlocks.Core.Event; namespace Identity; public sealed class IdentityEventMapper : IEventMapper { public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event) { return @event switch { _ => null }; } public IInternalCommand? MapToInternalCommand(IDomainEvent @event) { return @event switch { _ => null }; } } ================================================ FILE: src/Services/Identity/src/Identity/IdentityRoot.cs ================================================ namespace Identity; public class IdentityRoot { } ================================================ FILE: src/Services/Identity/src/Identity.Api/Identity.Api.csproj ================================================ ================================================ FILE: src/Services/Identity/src/Identity.Api/Program.cs ================================================ using BuildingBlocks.Web; using Identity; using Identity.Extensions.Infrastructure; var builder = WebApplication.CreateBuilder(args); builder.AddMinimalEndpoints(assemblies: typeof(IdentityRoot).Assembly); builder.AddInfrastructure(); var app = builder.Build(); app.MapMinimalEndpoints(); app.UseInfrastructure(); app.Run(); namespace Identity.Api { public partial class Program { } } ================================================ FILE: src/Services/Identity/src/Identity.Api/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "Identity.Api": { "commandName": "Project", "dotnetRunMessages": true, "launchUrl": "swagger", "launchBrowser": true, "applicationUrl": "http://localhost:6005;https://localhost:5005", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/Services/Identity/src/Identity.Api/appsettings.Development.json ================================================ { } ================================================ FILE: src/Services/Identity/src/Identity.Api/appsettings.docker.json ================================================ { "App": "Identity-Service", "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=postgres;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true" }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "AuthOptions": { "IssuerUri": "http://identity:80" }, "RabbitMqOptions": { "HostName": "rabbitmq", "ExchangeName": "identity", "UserName": "guest", "Password": "guest", "Port": 5672 }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Identity/src/Identity.Api/appsettings.json ================================================ { "AppOptions": { "Name": "Identity-Service" }, "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=localhost;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true" }, "AuthOptions": { "IssuerUri": "http://localhost:6005" }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "identity", "UserName": "guest", "Password": "guest", "Port": 5672 }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "HealthOptions": { "Enabled": false }, "ObservabilityOptions": { "InstrumentationName": "identity_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": "Identity Service" }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Identity/src/Identity.Api/appsettings.test.json ================================================ { "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=localhost;Port=5432;Database=identity_test;User Id=postgres;Password=postgres;Include Error Detail=true" }, "AuthOptions": { "IssuerUri": "http://localhost:6005" }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "identity", "UserName": "guest", "Password": "guest", "Port": 5672 }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true" } } ================================================ FILE: src/Services/Identity/src/Identity.Api/keys/is-signing-key-0AC3347A09AA5E44E947F3E30ED54871.json ================================================ {"Version":1,"Id":"0AC3347A09AA5E44E947F3E30ED54871","Created":"2023-02-22T21:06:15.7901014Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Pno3caweVxDrMdMtoqjLU9W7bGGdPAEm8VDpWIg5fNfBATZdniIo9r5lhBSMZ3OFJya6SlNGIFe9JkbzJBa5fwNa-omLt15Kjlu3QNX4bAWI3V87P70NAfKDKe4xBVF12B3ooXFXKJ2U5QzFBY8U_1tBXkeUTQKfioQ1lTPxIbQtmSyD-uSc5anbDjtqIWhDq1OlujSJpVHRJV9EzRCms4yZdUc506aWg_KrkpVITZyywj1X1cz-SaUGUqy01wJSXgEj70OdVo2DoPIEYTDAlymi40-R2jQsnseFEG3W7XQlYX29joTygtkumnVED2WF9y9Boy80Kj5XN5XZvL1opV-qtxA8aekkp3XuwMJtBmnMeslHK8TyvBl5JjdZwULdYJFo-rGSDEuI4bswDzMWUgv6oAxgqE6e7SQiFlHuLgWHkmEis4-r-EphCl7B_lNOMQViifon6L8t0lUGdPFhFfN5Fo7cj_lZPx5uqymwVUwIEPrEisZtkwsPYZeKuVdSaRnl5xDlU6AgbQvcNeTtg9_kyB6Z43rEPTvtwMkoBjVe5iTQ5ZtubByEGKVIugxNMfHzPc1E9FNTT3uW928Uq6RQiNah17cNBQsVmYAq_iRggDJwAeFnUgqnh0hFuve2_9T4Nt6XFSV-0OQ7aM8C8VPpEzROYhs_MMrzYuJAnV7VuvgUm4Zk9A2OFx9nGQXjL8WZ1-CcNV-TGESRUc9tfFizHPnd417h_qBYb8rJ-znet07KYs0Hwo_S8nbc4fAeG7OEj8cgn0nwgnDMa0_ROzvfFhmeWkkY5LdwtF0pgbO-_xdQwrx3mD2u6Nq5kABizd8ixH1urwvlfY5Z-MGwohf4HDjebiP49PsT96PAaj9ZFxz1HPfz4lOSCob7CBapIEt2exK0-OIhxkkU6i5_7iRvI_oFshzTvg2mSmR0zDWYfhiLZFoWGIlPi_B-pJvMEYgsyXW294yiAdVIJHXxs1nJRzgGBblY_FXM7IOzzwzpBtXH0odO8OPK6m8VBumDs9Q6iGiArFHwO5jKfvTxWYCM-PnTqW3ZselPIjZHSXbpGEU3o-HYg4rAGSIP9P_GoEJvSrQXqpYvhvPPbLh70OTwTtc8qG9GNW7XMoLpi6c8wUsapvrrYUBStlUWQ11UgvYSX1Nu4uCqTNTy6O1QHerbOHxWxfIOHzZyYfYnqQaF3bZZcMVJK3pOorcJKiMRCUp-hu5ismZ6T5Q2zfq93uqI3DpG2Wuov53o2s0QFmC4c5K6CCQVgLYBcib69twXjrWkVmGeM2CswLukOf8flUpGp_923M1TA4E89xkHxVhYESPfLB8FAULwNHOAW60U0tUksX-bcaAG1NUo3UEmaku2YfVbDJ7t5PJv8gc4zcPC0iXdBEL00G4FtSE2-r1TUhqK64iASEaAv3UTwefOMUR3qzxZC3HB7wH4iiUESH70s1RXvNIQoKz_ZirEa9WoR0tgk0MFyr4VDow-q-embX06vesnwyHhCdZBr8el_KT_yGWwlkR_xx18fR6mHT5G_BYuW-cK8ADvJe-Y_vns2yQprfGGgeVUQ5t4IKacpqnNNB9J2lLMisQ9fjYJCRhMP41CB7vk7oUXu49PgvJ_LkqtM5LAngo5f5DVdrzgX1-9I_KtyamJ9ic8nLqVsI5Z9iof9u9cRyDKhwQ4KPpD3uovwGnK7if25F9UaHFLvAJk2iOQjkA1rQyjCIJzi8dqpdw3W_L3Gc2m9Gr7HS1D0ZtWIk8z-TMNtR2PQcmlWiTJ9ZhLyMs67LWnLZri0jchEw4TJ46EAAUHE1hGhbs8xsmtta1yiu9VaZwCjDb5OJwnnl_2tA1ZkCE5Mh9NGSAZSCYLJCSRaCE53KypCYZBtJhR7Jcl1cz0OY9nNNUpzpPrcmCEz1cLw4z2H1OtujncZHDiyKqh57zQOTs--q9Nx8ytyhg7hBYIcqJFP8Gb0xQPjSkN1oO5ScAPIyPNY54VdIR7sk-99995Y-U6pnLQ_1JdIJZRYYnaYa4c5cqQaRE1n_G-L4_g20aBCX65SKkck5bwsAMJL371t4CqCBfPjzFugoNxR37DWicLUd9sJGBUMjj_lhWsk709om6shSJAy4loiz1qViwPi1K5XcFI7oyH735wg8X05Qxe1tby83X9FSd1FlTpVPRe1btEHddVubt9oeaZ0kOiN5BagaEF4wjIiQttnnzBfjKqcpuyd0zMhDTL_CqlmrTE09HH01VSnZ6lVllzCXGYI2hZKJFeOL7quHV4cDSoEVHOBL7SbChhm62iWOVTe2ho8V1NGnGFMmMfeplVWKorv-k2-L0H-K8rtg9d2vUdt-s5bxKkesUjxJhScBVOlFVcX6GdDa0o2R1W6qszC7D-_UILhIEtkQwnEg9SR-dDMulOotrHp5_LrsMCJLCgqGfzrjVymq7vkHbkh-8XIUzopyXgS4yr__N1MT2lI-7sFBG8fTa-GbN","DataProtected":true} ================================================ FILE: src/Services/Identity/src/Identity.Api/keys/is-signing-key-A57781A0405849BDE786A79636460E49.json ================================================ {"Version":1,"Id":"A57781A0405849BDE786A79636460E49","Created":"2023-05-11T21:58:42.4707649Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Hnjqxp4tLpCov-J-Y0BVs7wR0CVu4eqUuKwsY0rf0WJyME4aNzhKfD2Z2K08hK8_8svrjSptVpC1ttyCPtXtPVMsQQRukJef7SjEUNlC-US2KuwQGOLMYIDNHkTHrnXN7GYHoFtZ1SQWdL61GJ43LYCyFY7_L_Sn9SFepnDcT9H3aA9-M3ZjDJNU8_qknPTEUhSjV1i7AMkYnCHHow0ttlYhcQGwXoiuMMI9oqu26yr79i7sG6m_BAK1f1eg_1cBGozNe4yRPHsRk4qCLKgG-JRaNfV8sXHM-3SpEeFx7iUYHLHl7SiQwsqrODIujk7sz_vJaDbiFqwSVxUTDBEBLPpmPULLGKzjGoPrGtpmvgHkghEsHtvXY6aTzWwDLBmOT4ao8IjImv8D3_ub_KgNC5rg5pBzYA-gKAhtE_RznEKP_DVR836v5Xpw7zMVxbYVNwLrJ5-QtD503G5QhywFUHO9pRiL-dtDHJTpSmq5esy-OKBl2QJ12WxfeeMUQ7tIkTSsj0ALMyJS-a7uVUkITxOkXxrBI5rF6OIK8GZwnmsxJK5L3Urbn76EgiFKVoqgyhBRs0GvRl5-WJyc69m3ltuhp1z7hYOh0BYjlecDd9WOqNbFE68BCYUi7O--mSE0Bv94K4eUs81ccSb3lwC0AtPWPWjG31E9cg8Sd-A1gXhAcYuLmeWSoAP2Issoi5blxeafWUadjbjdCtGI_lA1trpg9LpClEDZZt5uJ5DQa5yUVzdz7l-RnlF7C8ReBtM8qBxJYnaCnc4WC7WE5cKMYG6mGJY55o_EAR4urjsxy2Bdjh34cFIIEQwfh-tnex3uBvYFUzoCPp8KJY-eRhUXEjtZzeds51OVeLKkeyFtSVEZsFTCnmc3tTsOAtkpa7sjsU0Xe0sgMG_E1XS8P_BqfIm9RR8rPblmA6JytNt_Gs2f00ND28PAnj_UuLeOaFpl2sipa228-1JimzFxKnVN7ZtU_ztZtUCGCSELoNbana8ZyLuFRGjFxQnkVZarwEFVyl7w_4scMujShb3ttaJDul51tsSJJTY43nC-Vlo1UBtYUjjNKuexpmOKGXaYRPygy_g4EXsqiN4FTc3mcG7jRoHmd5mUcvXfI66WbDmpXn4DB4x1VrWs2MVrLJw1ioON42IgjhoUCklrYJx4cxEWrIC3EURILE_jTy2ThAhGF24Z6jdTM8iVN9W7wwrnUxoFJCflr_-jiWwCToUWxPfolVu4mpQDhWJ1K9vuTVhQNrbMbc95TH7-Z_HFKiZkw23U0Xvjd4dqBJol2POIdQkDDg_L2omPtlMdNwo8eJ9rLEycbqQzcFzbyZBr__BU7J16wpu2lwZZVGgYfiofb6E01Iq_2_VQw8WH7d6onBJNCRq6QIDfZ3VdEMjdlm3kGrcY-_kzsfM9x9H_a-EpzT0_PwZO6gWUvfEfI84fNgPhM0Gq4vkv4l0NSpvIpivmgU9g3J2yQpO4QXVyQGOcXSUvUGKDIBRMKch1IOIEB6qYi34muu8Et0pbHsDsXECCnkfs5FAnKlG-zuf6W2cNRsF7QyaoUpDSpRD8u_R_mcQEVA2VJb4NWPSV1zgJ6-ne7HO_DL1mW7F-0jl2yHqOJ5z8GMTNdl7Nh4LfvHEB7xs0M2fgPByq3JImX1M-KUZDyIxfqxUM8L44zPyjiHvpMgXDvaA21aytu7G4r3vlgFyuMnSPKJ-fV8LBYDUKZ7TSOtTL2yCV2D5Z_VE3uZzBrpmJzzPG1_OXgmqV57KsPnDdwVefeu9UbCaDzG0R_xGReJveQcf5be8iJtTRDwqB2qlYm01VuIUSxabFTb_MypnKoIFaOxonW1faOgZ0zV8ocbgDfKO-8T21BBAJryw76yHxEVm59_X7Oa1VS_TM7i2sB67DWQadn4i4VMrF34LQyyhwPguKPSey1k7DcsHMxJ9zwUbC22HURwqVRn9zzxz9vHHiCUskytu3bItRkSpwDD2z_ngL9hVUF3C4QM3d4vgoL4sWS7X1One8z-vyZpZH_BDUg4nwEx95KClv2XieWXTHep8_3rdSIyTPrLLqErhCur6U69boZlvoCmoYtgbYN7dA1VkdTzdnzU-zSRLRkOY_hsa2gEElHqdtjHHTdt9g3kNhnvBMmgTQcEsAKZFjnwSbC-BogTSYNiZYCUu9eYOmenwlF4X4Yti-bVpMCEn6_6H8dWjumdBb7hgsvm47Sbuna5vAnlfnPKZBgji-38Y5lPE3NRF0ImhrsaURNUFktoNhp2gwFh8R0nLRzoLr3jWF2pBMMbvKGGzEBxOGN0_3nodoSyjFJFyAHgTwNcxB0AeYImM_-mxFtjiCDESqZoZGWtg6MvXmXPnat_uaI-jwl6TFBUChTjWujfil5doh32BtImNFO8VQakhrpTCaVm6yBKH77eF6aCX50iTuVVBuMec3IJZNPdnsc_65R4VGpqLwdscWiJqtmaRt_Umj-eJ","DataProtected":true} ================================================ FILE: src/Services/Identity/src/Identity.Api/keys/is-signing-key-B3C31EEE2718D3C5004C6E85AD74F26C.json ================================================ {"Version":1,"Id":"B3C31EEE2718D3C5004C6E85AD74F26C","Created":"2025-04-08T16:45:11.9802139Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8KNpFTgHKl5Nl-o13RQ8rse7iqNX7fzIuFQawPXYLwTCbj1HOrmv2WHbV51cOwFkSjECACMh5ad32vYQMXHgU7-l9gMNU0QwhV5gdoDScJLFzFf-_gBCqNPsUWJxNebIFDHwSn12y9Kg56DzaRq9o4XO_wGEc3KsQjDjHuh0p-3n8qiliIxl_sBXTs8UCXhWMBqU5srioHUKwFu6nyAlhkDlsA9J6Syb9w6_WESzli6DpR9UphBKbjoV85ajrkuZzjd9GItRJ6C-OICEKUUoFXh5meVGuV-JmWhE1_nzASToml0vI6GZt8kxrJQkICt1MI7K2ZIQtHcoKhC09rWu7RpuOvKY8sLm0b8gbJQ1dVMNuZEnOczi-E3b6kKrYFwV_I3TFK4UZP7I90-cojBGpaWwX3IzkB8-Uwf-5qU2KXUZanO0tXEqw187ZVPDuvKkAUeAtphWFbV2ZNH12DGcXTvnvJfRxV5lRAuzH3N-Dhgz10F3VOQZtgoQyQ4wJE-B-04j3pqN_76NIh4Yx7GvArlWYMQTNBePMaLW33ycLlCrjov1nKK2BIhk4wykUeFT_rorXcf01PHJd3uqbtBDrP7ZoUqz8qWdY9-on3dvNi6cruyJ5rbmmE4hLAsWHLOqYLcceBF7LaZUthglcMtLlufU5C3wpXjoqpJZb1KiC7QR5sQE_KPfEiQsCO_hyXnFUvavOaGIiWWAhqADfGdD3wNma3qCAMwmV5T-_4_yW3wvYbrM8YFrHmQty6ks4W6qKJow1uwfAziDoobdV7bSLSxddWiO5zOuCeFNxIzsmMkk8PfxUyb-JySYFh_tA8D3Tn5kvTbFIRibfiBJXtB8Kk4ouvQxd6ydHCHOAHzuPgxs_nP6s3u0Xa12fmPpiCLaUjeNFHNP_wzh4l5Tn7GN4S07nOiFE1PyQUjT7xnYxcUuzySpiqLl2k9Bu98MMagGg6gqWnl5zV_Adxkq_aIvuvZF25JlE9SzAJkuJFBNqnXJ_ep-BCFogdHPgtjL_Y250A5DsiFnKrlJLuBIz88V_oLEj54iSi4hiJ66YT0H8lagPUIrL4h6cPCVWFeMiCf8yNeQVHCnRkIyXtID4CrSOr8KN5Qv_nKeAUxw_5BOgs5yEGxj4OGLLYViAobOCFOA1uCurA3OqyoI65CCyoPERAXm7dyKcIN8l0ChIHhCX2VBtwEoSG2QXlEjKLxmmMmHQrTuMMRNkd2_GlY6XNRw6MELf7W4SrxyZ-eWNY9nfye7z9Q-hv_dDkf5FGmd8qGpVUenIjH0GUz4JqWOQ6GmHb7b_9u5g5vNIqi5YHf59SuNGAUbE3PFpDGpIeh5YVrCV-msoaUEkYxXvsBcUsOIiC32kQy-u-fDPxYSg-aNCgBrKDnTK4IFFBNYjT1IIApIiz-Xq8mjTsJvgaYKOLsVfqPlWX_Fr63Fnnygt9zO1tNZjW6JSj34nZJ_6120mGZcNueXtUy66QJeF8DqVdt4DbXMWYk-rQcHvqk3lQ_r7u7r88hIwzXgE3YtFFlesUK_7DVcblSuTxNxDXO5M7U_2EO4KIJbDfQ6BYxovJrWwtPcSXb_Nq25St_X3wSMk3eSGO7GNHL89t_muN5tE97As0s89h8OM5cISROR72zLWyvki2aB4L5WFWpBg1xylymZBzWGF-VKbDpoIb09PsyuuWy502OONDxmsheeadUZCNZYlxiSkmGLXrjSgsAhQHmLr7BwZ2FmVJGHtWD-nUPMj32NljxYQ1Bzv0aYXmJhHT8lcmTcWxb8yQKhOk-S6oP7m3ftq-gzsmE4lATEAHFGtO9a_xJlNYivB0zMsOYfY7YJzRSYEgLCSxS4HiUysv3WYXYjAQaGJNoSJX1YMMvCDu1rt29oV17Td58wHWvnnsCFlckVah6y_DCfez9ucTpQPHsQMsW1Hwl-3xgndEyTuynTHZNgqCcxQWBFlo1Ay9HHl68ppFpxm7FvSScyTdjtMPD89qZ84P_V38Qlz9ye0J2LXWoH1np7jAnaRiEzc-_Dcra3Yv792ks_ZlLS3Tu9-m-iZiGILHxIaccR53BSbjjgKS7LXMPsWZ_kf4pBNspcIbnn1dm_L47sEKCeA_2J0BVQeG_x7ZXG38M100wreTadW_PE5WysYyFIbHy5_wvfVVIEPZ3cMFt66LjoFfAmn52kQUG2w65Nnr95bavMBNdKk9oo22QpRy3YJsrABp9DtUqh4CwCqZCu7KvD2yx5u6UKAOK-XWlU1UJ-dPxxcuoPsjeuG1ER8ySYexec7iXzg-tbKjeGGSnyj2z1fnTihIiQIw_CAiZwXuyD4e0zowAmjEF2RWRsWvacoU7n_TCFwx4dX5bEG41-J2KHGJVX1X9D2tBX7R963ICgP8xeoE5wxWEiQLrsED68PKsnMasLGW2oOtLR8DwERbydMYHRH2f63mexVE5gbdf_4UXpaYMC8bNUc3HWY3-Sb_Fbc46r","DataProtected":true} ================================================ FILE: src/Services/Identity/src/Identity.Api/keys/is-signing-key-E1668D5B7CCDD18C610506FCA7C5D194.json ================================================ {"Version":1,"Id":"E1668D5B7CCDD18C610506FCA7C5D194","Created":"2025-07-21T17:14:15.7672364Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Ai8D9hfDhJIplL3Rv2bStBd2WatjNqn9OUgTfnOqxduEkXmsnrgWiHbuP-AYTE2ZeIxoDSDbZWBD8dJbAMe5PfH-FC7E5njE18xMIRyRxxvLHBcINyJ3fAP7lk5-uTl_F2DhoY6S_DCtbgbXCoB57FPr8DJzMPN0q37yuC34ZQYhfgLbkfydesVGqW1DF2_a5pd2KAFK_m8_FVy1GfHvTFnLjuURASp8eKEDJrtii4683uplUkycVJRHfHHQta1PuJW3KOLcX9jSePWDEBnQkjsiHm2O3BQFDdsMHbr8F2cLGW9uMIYNd96QyiH7nUgVZ0i5R-sHpbnGzUZFnZvpWLJFZrT3269nBLL2-t-7Sh99laIIT54cdZsW_uGFZ7r1MqyEPt9MoHqXXuOuhrn22QQeQURGmyUt4YyxdKCozsEPs7MR4IR_1iY93Rv4ThEn24vq1-guK-58pu3DurgigbTB7hFToOtqBHEhiVyXKgoNGOXH7UW5bJLFj4JtOIN_2SaTohhw9pSf8yH7gf6Chp2aVGUQt0OnVLbLu__EbB9XzJblQHLi6Ddm8uXdrJYXW17iBdEUHL8xD4hOiE4SR-bhD_d8c9yD7ydSyIwdistrVFRYpeDc6n-kQLWC6r1Z1oxIQte7ph6n3ygAWSwyCzSkI1GKJoh6QdR6rrs4EN9jUdFN4NgjQqftlLaE6xKq3f82wbCTk5-Iy8jJlr-xdDue_myN6Z_XOJ01pyjDkX9u3Aa0UuNFlvGYde9bJa6R7VxE5ggVozjpB_3FNUc91_3merwjq3K5-3S3EnhU8Nxi44Yjsw6-pVLbyF0Tgbh3uGUWI4EVv_nUC94cLaftuutq6rApSs1lF1jq_RK2P9YBQyCNl23JYMWEkPscMGIrx0306eLpWelYu_TpLk2lj5JfM7ep4uQ714bQXMLAdNX9GttAOOKlTrb4--uz8kjHaOSaJ01tvutQpog-RrudUVHX46_EA43U0jazAVQJXX7uDC5CEiSfCXozk7Gvw56gk72Ha1Ygz_qfaSn1hv6Q4cHhuhHzRV_aGui5oztal-oSh-zd_6BobstL1iZHhukHrUq_gVcFhYnqNDgzKT1L21AV9d0LbqONkFZ6kBGZ7ZIE2ymIqRx4DgBCdkmgZZUAZRqK3wkYW3oKI18l1aF7dGrrUnRSFVg5Jr0BYSmVPBVpfGLUHb3VHpjPVxPMV2W-ak0JG01jL7OQBuKXUW115Qd89GE2rj_SnInDg6ljW94zDHbqb_8gH3WOSWawM6021yQlRpMe7xqv6bYVeAS6WZBphKPxLV3BtG0_23IOiEbvsreT6UvIn9hx-VkJ_CADmlKQ8uNFxUIDvop6vGq5zjPgQz2-kJ529MUrp-ayn-oXTOrRgrYzLsUAH-gzf_osIMKsGPQakQMmnL7jsyX0z7Ci2wKuZ7pgxw8DQk8PKrk_yNuTMRQN0etUvv-nSCtPbcHALqMN9VgcHgCEEZTTP4A0pUUNAWewd2hJoh12H4_JlyJirHFDK5ZyM_uWKjAfVbh4RFLnxPaEEdQsew-WgXXE4Ne965OG283Q7TCv2MUCh-UynOz2ySw1FKD5XeqkOYmIXlrGf7DuhXQcam1SW9_8N5k9_lIdG_QPW6cM7HR3amBiHXnR7I-MBmTf3CLirEu_CrP6fdcGjrWBtbpfHCOPXNUseltmEWc0Ph3FoIYSR7F8o7cuYHWm0cuK3yC_73OXSDuDWmkl79qhyy3FJHV-6RTlsiyIZiOZ6Sxg24NKr5lp8FoqEZ2iQIKPfIPIYuD4YGmRKkk4WTV_slY68upm06xojpzQLs_resmmSc-WmkkRMgmuXaOxQr5oqZEFFqC3XLU3LAQkmOjMfJk4DWYWBrT-WzkfRJLmXhi3_DH7xh4IQ6t6Patumr-DT70FLySntRsvGS-wcI9U_w39gfGupOU6ytZFN_0lcNrYWR_0K_IjD_lmEUPdywFNs1_EbrIFSPo9pKZRveo1HLcSEatZju_JGqIT0lUQ0nZDIximnODr7w217fA_Vq9pLoMZw3AvT42vQqeg3jqoROF_fkz43fMmCNdZlhLuRUQ_IjZ5qmgKEaodtM8ZtgvB0qJNn_5NzX7ArreV-XQpOf8TxOvIWwmM4C9Ra3-Uo0CDrgIwxxtW9QjnlLx__Nb_Pcvf63dTtvt8y6FLFki3eeA_Q0IC4v0oKRQAVd1BtybbTMYMojXsrc1WElijhfDvybG04y6oWvh0GWx97FYk2bx3bTTi5EB-tXmnD12V2Jigw1e1x4rWpFh28A5eO6o5ymX1KRFdgG2nV45__NmJoZxmgWFFmTkNQSpHgl05NRRzhbcIrAzfAFnMQxdzrOCg4omRzf3yygQEBvuzpfThoZr3eaDdMCtaUuS_eUABIFXAYsRQpI33nhK4ac11S_m7k_vUCc1OWZ7mVLodyVqF3b3vqDIHw-EFNppSUUlpUzeLN2Q","DataProtected":true} ================================================ FILE: src/Services/Identity/tests/IntegrationTest/Fakes/FakeRegisterNewUserCommand.cs ================================================ using AutoBogus; namespace Integration.Test.Fakes; using global::Identity.Identity.Features.RegisteringNewUser.V1; public class FakeRegisterNewUserCommand : AutoFaker { public FakeRegisterNewUserCommand() { RuleFor(r => r.Username, x => "TestMyUser"); RuleFor(r => r.Password, _ => "Password@123"); RuleFor(r => r.ConfirmPassword, _ => "Password@123"); RuleFor(r => r.Email, _ => "test@test.com"); } } ================================================ FILE: src/Services/Identity/tests/IntegrationTest/Identity/Features/RegisterNewUserTests.cs ================================================ using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.TestBase; using FluentAssertions; using Identity.Api; using Identity.Data; using Integration.Test.Fakes; using Xunit; namespace Integration.Test.Identity.Features; public class RegisterNewUserTests : IdentityIntegrationTestBase { public RegisterNewUserTests(TestWriteFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_create_new_user_to_db_and_publish_message_to_broker() { // Arrange var command = new FakeRegisterNewUserCommand().Generate(); // Act var response = await Fixture.SendAsync(command); // Assert response?.Should().NotBeNull(); response?.Username.Should().Be(command.Username); (await Fixture.WaitForPublishing()).Should().Be(true); } } ================================================ FILE: src/Services/Identity/tests/IntegrationTest/IdentityIntegrationTestBase.cs ================================================ using BuildingBlocks.TestBase; using Identity.Api; using Identity.Data; using Xunit; namespace Integration.Test; [Collection(IntegrationTestCollection.Name)] public class IdentityIntegrationTestBase : TestWriteBase { public IdentityIntegrationTestBase(TestWriteFixture integrationTestFactory) : base(integrationTestFactory) { } } [CollectionDefinition(Name)] public class IntegrationTestCollection : ICollectionFixture> { public const string Name = "Identity Integration Test"; } ================================================ FILE: src/Services/Identity/tests/IntegrationTest/IdentityTestDataSeeder.cs ================================================ using BuildingBlocks.Constants; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Core; using BuildingBlocks.EFCore; using Identity.Data.Seed; using Identity.Identity.Constants; using Identity.Identity.Models; using Microsoft.AspNetCore.Identity; namespace Integration.Test; public class IdentityTestDataSeeder( UserManager userManager, RoleManager roleManager, IEventDispatcher eventDispatcher ) : ITestDataSeeder { public async Task SeedAllAsync() { await SeedRoles(); await SeedUsers(); } private async Task SeedRoles() { if (await roleManager.RoleExistsAsync(IdentityConstant.Role.Admin) == false) { await roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.Admin }); } if (await roleManager.RoleExistsAsync(IdentityConstant.Role.User) == false) { await roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.User }); } } private async Task SeedUsers() { 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(), IdentityConstant.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(), IdentityConstant.Role.User); await eventDispatcher.SendAsync(new UserCreated(InitialData.Users.Last().Id, InitialData.Users.Last().FirstName + " " + InitialData.Users.Last().LastName, InitialData.Users.Last().PassPortNumber)); } } } } ================================================ FILE: src/Services/Identity/tests/IntegrationTest/Integration.Test.csproj ================================================ PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: src/Services/Identity/tests/IntegrationTest/xunit.runner.json ================================================ { "parallelizeAssembly": false, "parallelizeTestCollections": false } ================================================ FILE: src/Services/Identity/tests/PerformanceTest/.openapi-generator/FILES ================================================ .openapi-generator-ignore README.md script.js ================================================ FILE: src/Services/Identity/tests/PerformanceTest/.openapi-generator/VERSION ================================================ 6.6.0-SNAPSHOT ================================================ FILE: src/Services/Identity/tests/PerformanceTest/.openapi-generator-ignore ================================================ # OpenAPI Generator Ignore # Generated by openapi-generator https://github.com/openapitools/openapi-generator # Use this file to prevent files from being overwritten by the generator. # The patterns follow closely to .gitignore or .dockerignore. # As an example, the C# client generator defines ApiClient.cs. # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: #ApiClient.cs # You can match any string of characters against a directory, file or extension with a single asterisk (*): #foo/*/qux # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux # You can recursively match patterns against a directory, file or extension with a double asterisk (**): #foo/**/qux # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux # You can also negate patterns with an exclamation (!). # For example, you can ignore all files in a docs folder with the file extension .md: #docs/*.md # Then explicitly reverse the ignore rule for a single file: #!docs/README.md ================================================ FILE: src/Services/Identity/tests/PerformanceTest/README.md ================================================ # Generated k6 script The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. If the Swagger/OpenAPI specification used as the input spec contains examples at parameter level, those will be extracted and utilized as parameter values. The `handleParamValue` custom Mustache lambda registered for use in the K6 `script.mustache` template handles the conditional checks, formatting, and outputting of parameter values. If a given parameter has value specified – either in `example` or `examples` field, defined at the parameter level – that value will be used. For list (`examples`), entire list will be output in the generated script and the first element from that list will be assigned as parameter value. If a given parameter does not have an example defined, a placeholder value with `TODO_EDIT_THE_` prefix will be generated for that parameter, and you will have to assign a value before you can run the script. In other words, you can now generate K6 test scripts which are ready to run, provided the Swagger/OpenAPI specification used as the input spec contains examples for all of the path/query parameters; see `modules/openapi-generator/src/test/resources/3_0/examples.yaml` for an example of such specification, and https://swagger.io/docs/specification/adding-examples/ for more information about adding examples. k6 specific parameters are in the [`params`](https://docs.k6.io/docs/params-k6http) object, and `body` contains the [request](https://docs.k6.io/docs/http-requests) body which is in the form of `identifier: type`, which the `type` should be substituted by a proper value. Then goes the request and the check. [Check](https://docs.k6.io/docs/checks) are like asserts but differ in that they don't halt execution, instead they just store the result of the check, pass or fail, and let the script execution continue. Each request is always followed by a 0.1 second [sleep](https://docs.k6.io/docs/sleep-t-1) to prevent the script execution from flooding the system with too many requests simultaneously. Note that the default iteration count and VU count is 1. So each request in each group will be executed once. For more information, see the [k6 options](https://docs.k6.io/docs/options). ================================================ FILE: src/Services/Identity/tests/PerformanceTest/script.js ================================================ /* * APIs * An example application with OpenAPI, Swashbuckle, and API versioning. * * OpenAPI spec version: 1.0 * Contact: * * NOTE: This class is auto generated by OpenAPI Generator. * https://github.com/OpenAPITools/openapi-generator * * OpenAPI generator version: 6.6.0-SNAPSHOT */ import http from "k6/http"; import { group, check, sleep } from "k6"; const BASE_URL = "/"; // Sleep duration between successive requests. // You might want to edit the value of this variable or remove calls to the sleep function on the script. const SLEEP_DURATION = 0.1; // Global variables should be initialized. export default function() { group("/api/v1/identity/register-user", () => { // Request No. 1: RegisterUser { let url = BASE_URL + `/api/v1/identity/register-user`; // TODO: edit the parameters of the request body. let body = {"firstName": "string", "lastName": "string", "username": "string", "email": "string", "password": "string", "confirmPassword": "string", "passportNumber": "string"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 200 }); } }); } ================================================ FILE: src/Services/Identity/tests/tests.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "IntegrationTest\Integration.Test.csproj", "{76FE2BD0-B242-45DC-8361-084920CDEBB4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {76FE2BD0-B242-45DC-8361-084920CDEBB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {76FE2BD0-B242-45DC-8361-084920CDEBB4}.Debug|Any CPU.Build.0 = Debug|Any CPU {76FE2BD0-B242-45DC-8361-084920CDEBB4}.Release|Any CPU.ActiveCfg = Release|Any CPU {76FE2BD0-B242-45DC-8361-084920CDEBB4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal ================================================ FILE: src/Services/Passenger/Dockerfile ================================================ # ---------- Build Stage ---------- FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy solution-level files COPY .editorconfig . COPY global.json . COPY Directory.Build.props . # Copy project files first (better Docker layer caching) COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/ COPY src/Services/Passenger/src/Passenger/Passenger.csproj src/Services/Passenger/src/Passenger/ COPY src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj src/Services/Passenger/src/Passenger.Api/ COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/ # Restore dependencies RUN dotnet restore src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj # Copy remaining source code COPY src ./src # Publish (build included) RUN dotnet publish src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj \ -c Release \ -o /app/publish \ --no-restore # ---------- Runtime Stage ---------- FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . ENV ASPNETCORE_URLS=http://+:80 ENV ASPNETCORE_ENVIRONMENT=docker EXPOSE 80 ENTRYPOINT ["dotnet", "Passenger.Api.dll"] ================================================ FILE: src/Services/Passenger/src/Passenger/AssemblyInfo.cs ================================================ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Unit.Test")] [assembly: InternalsVisibleTo("Integration.Test")] [assembly: InternalsVisibleTo("EndToEnd.Test")] ================================================ FILE: src/Services/Passenger/src/Passenger/Data/Configurations/PassengerConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Passenger.Data.Configurations; using Passengers.ValueObjects; public class PassengerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable(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)); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; namespace Passenger.Data; public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory { public PassengerDbContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder(); builder.UseNpgsql("Server=localhost;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true") .UseSnakeCaseNamingConvention(); return new PassengerDbContext(builder.Options); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Data/Migrations/20230611213031_initial.Designer.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Passenger.Data; #nullable disable namespace Passenger.Data.Migrations { [DbContext(typeof(PassengerDbContext))] [Migration("20230611213031_initial")] partial class initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "7.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b => { b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("PassengerId") .HasConstraintName("fk_passenger_passenger_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("PassengerId") .HasConstraintName("fk_passenger_passenger_id"); }); b.OwnsOne("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"); 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 } } } ================================================ FILE: src/Services/Passenger/src/Passenger/Data/Migrations/20230611213031_initial.cs ================================================ using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Passenger.Data.Migrations { /// public partial class initial : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "passenger", columns: table => new { id = table.Column(type: "uuid", nullable: false), passportnumber = table.Column(name: "passport_number", type: "character varying(10)", maxLength: 10, nullable: false), name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), passengertype = table.Column(name: "passenger_type", type: "text", nullable: false, defaultValue: "Unknown"), age = table.Column(type: "integer", maxLength: 3, nullable: true), createdat = table.Column(name: "created_at", type: "timestamp with time zone", nullable: true), createdby = table.Column(name: "created_by", type: "bigint", nullable: true), lastmodified = table.Column(name: "last_modified", type: "timestamp with time zone", nullable: true), lastmodifiedby = table.Column(name: "last_modified_by", type: "bigint", nullable: true), isdeleted = table.Column(name: "is_deleted", 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"); } } } ================================================ FILE: src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Passenger.Data; #nullable disable namespace Passenger.Data.Migrations { [DbContext(typeof(PassengerDbContext))] partial class PassengerDbContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "7.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("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", (string)null); }); modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b => { b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("PassengerId") .HasConstraintName("fk_passenger_passenger_id"); }); b.OwnsOne("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"); b1.WithOwner() .HasForeignKey("PassengerId") .HasConstraintName("fk_passenger_passenger_id"); }); b.OwnsOne("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"); 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 } } } ================================================ FILE: src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs ================================================ using System.Reflection; using BuildingBlocks.EFCore; using BuildingBlocks.Web; using Microsoft.EntityFrameworkCore; namespace Passenger.Data; using Microsoft.Extensions.Logging; 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) { builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); base.OnModelCreating(builder); builder.FilterSoftDeletedProperties(); builder.ToSnakeCaseTables(); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Data/PassengerReadDbContext.cs ================================================ using BuildingBlocks.Mongo; using Humanizer; using Microsoft.Extensions.Options; using MongoDB.Driver; namespace Passenger.Data; using Passengers.Models; public class PassengerReadDbContext : MongoDbContext { public PassengerReadDbContext(IOptions options) : base(options) { Passenger = GetCollection(nameof(Passenger).Underscore()); } public IMongoCollection Passenger { get; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Data/readme.md ================================================ dotnet ef migrations add initial --context PassengerDbContext -o "Data\Migrations" dotnet ef database update --context PassengerDbContext ================================================ FILE: src/Services/Passenger/src/Passenger/Exceptions/InvalidAgeException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Passenger.Exceptions; public class InvalidAgeException : DomainException { public InvalidAgeException() : base("Age Cannot be null or negative") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Exceptions/InvalidNameException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Passenger.Exceptions; public class InvalidNameException : DomainException { public InvalidNameException() : base("Name cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Exceptions/InvalidPassengerIdException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Passenger.Exceptions; using System; public class InvalidPassengerIdException : DomainException { public InvalidPassengerIdException(Guid passengerId) : base($"PassengerId: '{passengerId}' is invalid.") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Exceptions/InvalidPassportNumberException.cs ================================================ using SmartCharging.Infrastructure.Exceptions; namespace Passenger.Exceptions; public class InvalidPassportNumberException : DomainException { public InvalidPassportNumberException() : base("Passport number cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Exceptions/PassengerAlreadyExist.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Passenger.Exceptions; public class PassengerNotExist : AppException { public PassengerNotExist() : base("Please register before!", HttpStatusCode.NotFound) { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Exceptions/PassengerNotFoundException.cs ================================================ using System.Net; using BuildingBlocks.Exception; namespace Passenger.Exceptions; public class PassengerNotFoundException : AppException { public PassengerNotFoundException() : base("Passenger not found!", HttpStatusCode.NotFound) { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Extensions/Infrastructure/InfrastructureExtensions.cs ================================================ using BuildingBlocks.Core; using BuildingBlocks.EFCore; using BuildingBlocks.Exception; using BuildingBlocks.HealthCheck; using BuildingBlocks.Jwt; 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 Figgle.Fonts; using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Passenger.Data; using Passenger.GrpcServer.Services; using ServiceDefaults; namespace Passenger.Extensions.Infrastructure; public static class InfrastructureExtensions { public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder builder) { var configuration = builder.Configuration; var env = builder.Environment; builder.AddServiceDefaults(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure( options => { options.SuppressModelStateInvalidFilter = true; }); var appOptions = builder.Services.GetOptions(nameof(AppOptions)); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); builder.AddPersistMessageProcessor(nameof(PersistMessage)); builder.AddCustomDbContext(nameof(Passenger)); builder.AddMongoDbContext(); builder.Services.AddJwt(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddAspnetOpenApi(); builder.Services.AddCustomVersioning(); builder.Services.AddCustomMediatR(); builder.Services.AddValidatorsFromAssembly(typeof(PassengerRoot).Assembly); builder.Services.AddProblemDetails(); builder.Services.AddCustomMapster(typeof(PassengerRoot).Assembly); builder.Services.AddHttpContextAccessor(); builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(PassengerRoot).Assembly); builder.Services.AddGrpc( options => { options.Interceptors.Add(); }); return builder; } public static WebApplication UseInfrastructure(this WebApplication app) { var env = app.Environment; var appOptions = app.GetOptions(nameof(AppOptions)); app.UseAuthentication(); app.UseAuthorization(); app.UseServiceDefaults(); app.UseCustomProblemDetails(); app.UseCorrelationId(); app.UseMigration(); app.MapGrpcService(); app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); if (env.IsDevelopment()) { app.UseAspnetOpenApi(); } return app; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Extensions/Infrastructure/MediatRExtensions.cs ================================================ using BuildingBlocks.EFCore; using BuildingBlocks.Logging; using BuildingBlocks.Validation; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace Passenger.Extensions.Infrastructure; public static class MediatRExtensions { public static IServiceCollection AddCustomMediatR(this IServiceCollection services) { services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(PassengerRoot).Assembly)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>)); return services; } } ================================================ FILE: src/Services/Passenger/src/Passenger/GrpcServer/Protos/passenger.proto ================================================ syntax = "proto3"; package passenger; service PassengerGrpcService { rpc GetById (GetByIdRequest) returns (GetPassengerByIdResult); } message GetByIdRequest { string Id = 1; } message GetPassengerByIdResult { PassengerResponse PassengerDto = 1; } message PassengerResponse { string Id = 1; string Name = 2; string PassportNumber = 3; PassengerType PassengerType = 4; int32 Age = 5; string Email = 6; } enum PassengerType { PASSENGER_TYPE_UNKNOWN = 0; PASSENGER_TYPE_MALE = 1; PASSENGER_TYPE_FEMALE = 2; PASSENGER_TYPE_BABY = 3; } ================================================ FILE: src/Services/Passenger/src/Passenger/GrpcServer/Services/PassengerGrpcServices.cs ================================================ using Grpc.Core; using MediatR; namespace Passenger.GrpcServer.Services; using Mapster; using Passengers.Features.GettingPassengerById.V1; using GetPassengerByIdResult = Passenger.GetPassengerByIdResult; public class PassengerGrpcServices : PassengerGrpcService.PassengerGrpcServiceBase { private readonly IMediator _mediator; public PassengerGrpcServices(IMediator mediator) { _mediator = mediator; } public override async Task GetById(GetByIdRequest request, ServerCallContext context) { var result = await _mediator.Send(new GetPassengerById(new Guid(request.Id))); return result?.Adapt(); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/PassengerCreatedDomainEvent.cs ================================================ namespace Passenger.Identity.Consumers.RegisteringNewUser.V1; using BuildingBlocks.Core.Event; public record PassengerCreatedDomainEvent(Guid Id, string Name, string PassportNumber, bool IsDeleted = false) : IDomainEvent; ================================================ FILE: src/Services/Passenger/src/Passenger/Identity/Consumers/RegisteringNewUser/V1/RegisterNewUser.cs ================================================ namespace Passenger.Identity.Consumers.RegisteringNewUser.V1; using Ardalis.GuardClauses; using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Core; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Data; using Humanizer; using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Passengers.ValueObjects; 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)); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passenger.csproj ================================================ all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/Services/Passenger/src/Passenger/PassengerEventMapper.cs ================================================ using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Core; using BuildingBlocks.Core.Event; namespace Passenger; using Identity.Consumers.RegisteringNewUser.V1; using Passengers.Features.CompletingRegisterPassenger.V1; 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 }; } } ================================================ FILE: src/Services/Passenger/src/Passenger/PassengerRoot.cs ================================================ namespace Passenger; public class PassengerRoot { } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Dtos/PassengerDto.cs ================================================ namespace Passenger.Passengers.Dtos; public record PassengerDto(Guid Id, string Name, string PassportNumber, Enums.PassengerType PassengerType, int Age); ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Enums/PassengerType.cs ================================================ namespace Passenger.Passengers.Enums; public enum PassengerType { Unknown = 0, Male, Female, Baby } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Exceptions/InvalidAgeException.cs ================================================ namespace Passenger.Passengers.Exceptions; using BuildingBlocks.Exception; public class InvalidAgeException : BadRequestException { public InvalidAgeException() : base("Age Cannot be null or negative") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Exceptions/InvalidNameException.cs ================================================ namespace Passenger.Passengers.Exceptions; using BuildingBlocks.Exception; public class InvalidNameException : BadRequestException { public InvalidNameException() : base("Name cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Exceptions/InvalidPassportNumberException.cs ================================================ namespace Passenger.Passengers.Exceptions; using BuildingBlocks.Exception; public class InvalidPassportNumberException : BadRequestException { public InvalidPassportNumberException() : base("Passport number cannot be empty or whitespace.") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs ================================================ namespace Passenger.Passengers.Exceptions; using BuildingBlocks.Exception; public class PassengerNotExist : BadRequestException { public PassengerNotExist(string code = default) : base("Please register before!") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs ================================================ namespace Passenger.Passengers.Exceptions; using BuildingBlocks.Exception; public class PassengerNotFoundException : NotFoundException { public PassengerNotFoundException(string code = default) : base("Passenger not found!") { } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassenger.cs ================================================ namespace Passenger.Passengers.Features.CompletingRegisterPassenger.V1; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using BuildingBlocks.Web; using Data; using Dtos; using Duende.IdentityServer.EntityFramework.Entities; using Exceptions; 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; using ValueObjects; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Features/CompletingRegisterPassenger/V1/CompleteRegisterPassengerMongo.cs ================================================ namespace Passenger.Passengers.Features.CompletingRegisterPassenger.V1; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.Event; using Data; using MapsterMapper; using MediatR; using Models; using MongoDB.Driver; using MongoDB.Driver.Linq; using ValueObjects; 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; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Features/GettingPassengerById/V1/GetPassengerById.cs ================================================ namespace Passenger.Passengers.Features.GettingPassengerById.V1; using Ardalis.GuardClauses; using BuildingBlocks.Core.CQRS; using BuildingBlocks.Web; using Duende.IdentityServer.EntityFramework.Entities; 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; using Passenger.Data; using Passenger.Passengers.Dtos; using Passenger.Passengers.Exceptions; 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(nameof(ApiScope)) .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); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Features/PassengerMappings.cs ================================================ using Mapster; namespace Passenger.Passengers.Features; using CompletingRegisterPassenger.V1; using Dtos; using MassTransit; using Models; using ValueObjects; 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)); } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs ================================================ using BuildingBlocks.Core.Model; namespace Passenger.Passengers.Models; using Features.CompletingRegisterPassenger.V1; using Identity.Consumers.RegisteringNewUser.V1; using ValueObjects; 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; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/Models/PassengerReadModel.cs ================================================ namespace 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; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/ValueObjects/Age.cs ================================================ namespace Passenger.Passengers.ValueObjects; using Exceptions; 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; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/ValueObjects/Name.cs ================================================ namespace Passenger.Passengers.ValueObjects; using Passenger.Passengers.Exceptions; 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; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/ValueObjects/PassengerId.cs ================================================ namespace Passenger.Passengers.ValueObjects; using System; using Passenger.Exceptions; 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; } } ================================================ FILE: src/Services/Passenger/src/Passenger/Passengers/ValueObjects/PassportNumber.cs ================================================ namespace Passenger.Passengers.ValueObjects; using Passenger.Passengers.Exceptions; 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; } } ================================================ FILE: src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj ================================================ ================================================ FILE: src/Services/Passenger/src/Passenger.Api/Program.cs ================================================ using BuildingBlocks.Web; using Passenger; using Passenger.Extensions.Infrastructure; var builder = WebApplication.CreateBuilder(args); builder.AddMinimalEndpoints(assemblies: typeof(PassengerRoot).Assembly); builder.AddInfrastructure(); var app = builder.Build(); app.MapMinimalEndpoints(); app.UseInfrastructure(); app.Run(); namespace Passenger.Api { public partial class Program { } } ================================================ FILE: src/Services/Passenger/src/Passenger.Api/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "Passenger.Api": { "commandName": "Project", "dotnetRunMessages": true, "launchUrl": "swagger", "launchBrowser": true, "applicationUrl": "http://localhost:6012;https://localhost:5012", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/Services/Passenger/src/Passenger.Api/appsettings.Development.json ================================================ { } ================================================ FILE: src/Services/Passenger/src/Passenger.Api/appsettings.docker.json ================================================ { "App": "Passenger-Service", "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=postgres;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true" }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "Jwt": { "Authority": "http://identity:80", "Audience": "passenger-api" }, "MongoOptions": { "ConnectionString": "mongodb://mongo:27017", "DatabaseName": "passenger-db" }, "RabbitMqOptions": { "HostName": "rabbitmq", "ExchangeName": "passenger", "UserName": "guest", "Password": "guest", "Port": 5672 }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Passenger/src/Passenger.Api/appsettings.json ================================================ { "AppOptions": { "Name": "Passenger-Service" }, "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=localhost;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true" }, "MongoOptions": { "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "passenger-db" }, "Jwt": { "Authority": "http://localhost:6005", "Audience": "passenger-api" }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "passenger", "UserName": "guest", "Password": "guest", "Port": 5672 }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true" }, "HealthOptions": { "Enabled": false }, "ObservabilityOptions": { "InstrumentationName": "passenger_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": "Passenger Service" }, "AllowedHosts": "*" } ================================================ FILE: src/Services/Passenger/src/Passenger.Api/appsettings.test.json ================================================ { "Logging": { "LogLevel": { "Default": "Information" } }, "PostgresOptions": { "ConnectionString": "Server=localhost;Port=5432;Database=passenger_test;User Id=postgres;Password=postgres;Include Error Detail=true" }, "RabbitMqOptions": { "HostName": "localhost", "ExchangeName": "passenger", "UserName": "guest", "Password": "guest", "Port": 5672 }, "PersistMessageOptions": { "Interval": 30, "Enabled": true, "ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true" } } ================================================ FILE: src/Services/Passenger/tests/IntegrationTest/Fakes/FakeCompleteRegisterPassengerCommand.cs ================================================ using AutoBogus; using Passenger.Passengers.Enums; namespace Integration.Test.Fakes; using global::Passenger.Passengers.Features.CompletingRegisterPassenger.V1; public sealed class FakeCompleteRegisterPassengerCommand : AutoFaker { public FakeCompleteRegisterPassengerCommand(string passportNumber, Guid passengerId) { RuleFor(r => r.Id, _ => passengerId); RuleFor(r => r.PassportNumber, _ => passportNumber); RuleFor(r => r.PassengerType, _ => PassengerType.Male); RuleFor(r => r.Age, _ => 30); } } ================================================ FILE: src/Services/Passenger/tests/IntegrationTest/Fakes/FakeCompleteRegisterPassengerMongoCommand.cs ================================================ namespace Integration.Test.Fakes; using AutoBogus; using global::Passenger.Passengers.Enums; using global::Passenger.Passengers.Features.CompletingRegisterPassenger.V1; using MassTransit; public class FakeCompleteRegisterPassengerMongoCommand : AutoFaker { public FakeCompleteRegisterPassengerMongoCommand() { RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.Name, _ => "Sam"); RuleFor(r => r.PassportNumber, _ => "123456789"); RuleFor(r => r.Age, _ => 30); RuleFor(r => r.IsDeleted, _ => false); RuleFor(r => r.PassengerType, _ => PassengerType.Male); } } ================================================ FILE: src/Services/Passenger/tests/IntegrationTest/Integration.Test.csproj ================================================ PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: src/Services/Passenger/tests/IntegrationTest/Passenger/Features/CompleteRegisterPassengerTests.cs ================================================ using BuildingBlocks.TestBase; using FluentAssertions; using Integration.Test.Fakes; using Passenger.Data; using Passenger.Passengers.ValueObjects; using Xunit; namespace Integration.Test.Passenger.Features; public class CompleteRegisterPassengerTests : PassengerIntegrationTestBase { public CompleteRegisterPassengerTests( TestFixture integrationTestFactory ) : base(integrationTestFactory) { } [Fact] public async Task should_complete_register_passenger_and_update_to_db() { // Arrange var passenger = global::Passenger.Passengers.Models.Passenger.Create( PassengerId.Of(Guid.CreateVersion7()), Name.Of("Sam"), PassportNumber.Of("123456789") ); await Fixture.InsertAsync(passenger); var command = new FakeCompleteRegisterPassengerCommand(passenger.PassportNumber, passenger.Id).Generate(); // Act var response = await Fixture.SendAsync(command); // Assert response.Should().NotBeNull(); response?.PassengerDto?.Name.Should().Be(passenger.Name); response?.PassengerDto?.PassportNumber.Should().Be(command.PassportNumber); response?.PassengerDto?.PassengerType.ToString().Should().Be(command.PassengerType.ToString()); response?.PassengerDto?.Age.Should().Be(command.Age); } } ================================================ FILE: src/Services/Passenger/tests/IntegrationTest/Passenger/Features/GetPassengerByIdTests.cs ================================================ using System.Threading.Tasks; using BuildingBlocks.TestBase; using FluentAssertions; using Integration.Test.Fakes; using Passenger; using Passenger.Api; using Passenger.Data; using Xunit; namespace Integration.Test.Passenger.Features; using global::Passenger.Passengers.Features.GettingPassengerById.V1; public class GetPassengerByIdTests : PassengerIntegrationTestBase { public GetPassengerByIdTests( TestFixture integrationTestFactory) : base(integrationTestFactory) { } [Fact] public async Task should_retrive_a_passenger_by_id_currectly() { // Arrange var command = new FakeCompleteRegisterPassengerMongoCommand().Generate(); await Fixture.SendAsync(command); var query = new GetPassengerById(command.Id); // Act var response = await Fixture.SendAsync(query); // Assert response.Should().NotBeNull(); response?.PassengerDto?.Id.Should().Be(command.Id); } [Fact] public async Task should_retrive_a_passenger_by_id_from_grpc_service() { // Arrange var command = new FakeCompleteRegisterPassengerMongoCommand().Generate(); await Fixture.SendAsync(command); var passengerGrpcClient = new PassengerGrpcService.PassengerGrpcServiceClient(Fixture.Channel); // Act var response = await passengerGrpcClient.GetByIdAsync(new GetByIdRequest { Id = command.Id.ToString() }); // Assert response?.Should().NotBeNull(); response?.PassengerDto?.Id.Should().Be(command.Id.ToString()); } } ================================================ FILE: src/Services/Passenger/tests/IntegrationTest/PassengerIntegrationTestBase.cs ================================================ using BuildingBlocks.TestBase; using Passenger.Api; using Passenger.Data; using Xunit; namespace Integration.Test; [Collection(IntegrationTestCollection.Name)] public class PassengerIntegrationTestBase : TestBase { public PassengerIntegrationTestBase(TestFixture integrationTestFactory) : base(integrationTestFactory) { } } [CollectionDefinition(Name)] public class IntegrationTestCollection : ICollectionFixture> { public const string Name = "Passenger Integration Test"; } ================================================ FILE: src/Services/Passenger/tests/IntegrationTest/xunit.runner.json ================================================ { "parallelizeAssembly": false, "parallelizeTestCollections": false } ================================================ FILE: src/Services/Passenger/tests/PerformanceTest/.openapi-generator/FILES ================================================ .openapi-generator-ignore README.md script.js ================================================ FILE: src/Services/Passenger/tests/PerformanceTest/.openapi-generator/VERSION ================================================ 6.6.0-SNAPSHOT ================================================ FILE: src/Services/Passenger/tests/PerformanceTest/.openapi-generator-ignore ================================================ # OpenAPI Generator Ignore # Generated by openapi-generator https://github.com/openapitools/openapi-generator # Use this file to prevent files from being overwritten by the generator. # The patterns follow closely to .gitignore or .dockerignore. # As an example, the C# client generator defines ApiClient.cs. # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: #ApiClient.cs # You can match any string of characters against a directory, file or extension with a single asterisk (*): #foo/*/qux # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux # You can recursively match patterns against a directory, file or extension with a double asterisk (**): #foo/**/qux # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux # You can also negate patterns with an exclamation (!). # For example, you can ignore all files in a docs folder with the file extension .md: #docs/*.md # Then explicitly reverse the ignore rule for a single file: #!docs/README.md ================================================ FILE: src/Services/Passenger/tests/PerformanceTest/README.md ================================================ # Generated k6 script The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. If the Swagger/OpenAPI specification used as the input spec contains examples at parameter level, those will be extracted and utilized as parameter values. The `handleParamValue` custom Mustache lambda registered for use in the K6 `script.mustache` template handles the conditional checks, formatting, and outputting of parameter values. If a given parameter has value specified – either in `example` or `examples` field, defined at the parameter level – that value will be used. For list (`examples`), entire list will be output in the generated script and the first element from that list will be assigned as parameter value. If a given parameter does not have an example defined, a placeholder value with `TODO_EDIT_THE_` prefix will be generated for that parameter, and you will have to assign a value before you can run the script. In other words, you can now generate K6 test scripts which are ready to run, provided the Swagger/OpenAPI specification used as the input spec contains examples for all of the path/query parameters; see `modules/openapi-generator/src/test/resources/3_0/examples.yaml` for an example of such specification, and https://swagger.io/docs/specification/adding-examples/ for more information about adding examples. k6 specific parameters are in the [`params`](https://docs.k6.io/docs/params-k6http) object, and `body` contains the [request](https://docs.k6.io/docs/http-requests) body which is in the form of `identifier: type`, which the `type` should be substituted by a proper value. Then goes the request and the check. [Check](https://docs.k6.io/docs/checks) are like asserts but differ in that they don't halt execution, instead they just store the result of the check, pass or fail, and let the script execution continue. Each request is always followed by a 0.1 second [sleep](https://docs.k6.io/docs/sleep-t-1) to prevent the script execution from flooding the system with too many requests simultaneously. Note that the default iteration count and VU count is 1. So each request in each group will be executed once. For more information, see the [k6 options](https://docs.k6.io/docs/options). ================================================ FILE: src/Services/Passenger/tests/PerformanceTest/script.js ================================================ /* * APIs * An example application with OpenAPI, Swashbuckle, and API versioning. * * OpenAPI spec version: 1.0 * Contact: * * NOTE: This class is auto generated by OpenAPI Generator. * https://github.com/OpenAPITools/openapi-generator * * OpenAPI generator version: 6.6.0-SNAPSHOT */ import http from "k6/http"; import { group, check, sleep } from "k6"; const BASE_URL = "/"; // Sleep duration between successive requests. // You might want to edit the value of this variable or remove calls to the sleep function on the script. const SLEEP_DURATION = 0.1; // Global variables should be initialized. export default function() { group("/api/v1/passenger/{id}", () => { let id = 'TODO_EDIT_THE_ID'; // specify value as there is no example value for this parameter in OpenAPI spec // Request No. 1: GetPassengerById { let url = BASE_URL + `/api/v1/passenger/${id}`; let request = http.get(url); check(request, { "Success": (r) => r.status === 200 }); } }); group("/api/v1/passenger/complete-registration", () => { // Request No. 1: CompleteRegisterPassenger { let url = BASE_URL + `/api/v1/passenger/complete-registration`; // TODO: edit the parameters of the request body. let body = {"passportNumber": "string", "passengerType": "passengertype", "age": "integer"}; let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; let request = http.post(url, JSON.stringify(body), params); check(request, { "Success": (r) => r.status === 200 }); } }); } ================================================ FILE: src/Services/Passenger/tests/tests.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "IntegrationTest\Integration.Test.csproj", "{3D9D0889-3A70-45FC-8FC0-0297F209AAB9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3D9D0889-3A70-45FC-8FC0-0297F209AAB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D9D0889-3A70-45FC-8FC0-0297F209AAB9}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D9D0889-3A70-45FC-8FC0-0297F209AAB9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D9D0889-3A70-45FC-8FC0-0297F209AAB9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal