diff --git a/2-modular-monolith-architecture-style/src/Api/src/Api.csproj b/2-modular-monolith-architecture-style/src/Api/src/Api.csproj
new file mode 100644
index 0000000..2e573a8
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/Api.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Api
+
+
+
+
+
+
+
+
+
+
diff --git a/2-modular-monolith-architecture-style/src/Api/src/Extensions/SharedInfrastructureExtensions.cs b/2-modular-monolith-architecture-style/src/Api/src/Extensions/SharedInfrastructureExtensions.cs
new file mode 100644
index 0000000..f7aca65
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/Extensions/SharedInfrastructureExtensions.cs
@@ -0,0 +1,122 @@
+using System.Threading.RateLimiting;
+using BuildingBlocks.Core;
+using BuildingBlocks.Exception;
+using BuildingBlocks.HealthCheck;
+using BuildingBlocks.Jwt;
+using BuildingBlocks.Logging;
+using BuildingBlocks.Mapster;
+using BuildingBlocks.MassTransit;
+using BuildingBlocks.OpenApi;
+using BuildingBlocks.OpenTelemetryCollector;
+using BuildingBlocks.PersistMessageProcessor;
+using BuildingBlocks.ProblemDetails;
+using BuildingBlocks.Web;
+using Figgle;
+using FluentValidation;
+using Microsoft.AspNetCore.Mvc;
+using Serilog;
+
+namespace Api.Extensions;
+
+public static class SharedInfrastructureExtensions
+{
+ public static WebApplicationBuilder AddSharedInfrastructure(this WebApplicationBuilder builder)
+ {
+ builder.Host.UseDefaultServiceProvider(
+ (context, options) =>
+ {
+ // Service provider validation
+ // ref: https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
+ options.ValidateScopes = context.HostingEnvironment.IsDevelopment() ||
+ context.HostingEnvironment.IsStaging() ||
+ context.HostingEnvironment.IsEnvironment("tests");
+
+ options.ValidateOnBuild = true;
+ });
+
+ var appOptions = builder.Services.GetOptions(nameof(AppOptions));
+ Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
+
+ builder.AddCustomSerilog(builder.Environment);
+ builder.Services.AddScoped();
+ builder.Services.AddJwt();
+ builder.Services.AddTransient();
+ builder.Services.AddPersistMessageProcessor();
+
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddControllers();
+ builder.Services.AddAspnetOpenApi();
+ builder.Services.AddCustomVersioning();
+ builder.Services.AddHttpContextAccessor();
+ builder.Services.AddScoped();
+
+ builder.Services.AddCustomMassTransit(
+ builder.Environment,
+ TransportType.InMemory,
+ AppDomain.CurrentDomain.GetAssemblies());
+
+ builder.Services.Configure(
+ options => options.SuppressModelStateInvalidFilter = true);
+
+ builder.Services.AddRateLimiter(
+ options =>
+ {
+ options.GlobalLimiter = PartitionedRateLimiter.Create(
+ httpContext =>
+ RateLimitPartition.GetFixedWindowLimiter(
+ partitionKey: httpContext.User.Identity?.Name ??
+ httpContext.Request.Headers.Host.ToString(),
+ factory: partition => new FixedWindowRateLimiterOptions
+ {
+ AutoReplenishment = true,
+ PermitLimit = 10,
+ QueueLimit = 0,
+ Window = TimeSpan.FromMinutes(1)
+ }));
+ });
+
+ builder.AddCustomObservability();
+ builder.Services.AddCustomHealthCheck();
+
+ builder.Services.AddGrpc(
+ options =>
+ {
+ options.Interceptors.Add();
+ });
+
+ builder.Services.AddEasyCaching(options => { options.UseInMemory(builder.Configuration, "mem"); });
+
+ builder.Services.AddValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies());
+ builder.Services.AddCustomMapster(AppDomain.CurrentDomain.GetAssemblies());
+ builder.Services.AddProblemDetails();
+
+ return builder;
+ }
+
+
+ public static WebApplication UserSharedInfrastructure(this WebApplication app)
+ {
+ var appOptions = app.Configuration.GetOptions(nameof(AppOptions));
+
+ app.UseCustomProblemDetails();
+ app.UseCustomObservability();
+ app.UseCustomHealthCheck();
+
+ app.UseSerilogRequestLogging(
+ options =>
+ {
+ options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;
+ });
+
+ app.UseCorrelationId();
+ app.UseRateLimiter();
+ app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));
+
+ if (app.Environment.IsDevelopment())
+ {
+ app.UseAspnetOpenApi();
+ }
+
+ return app;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Api/src/Program.cs b/2-modular-monolith-architecture-style/src/Api/src/Program.cs
new file mode 100644
index 0000000..9810f8e
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/Program.cs
@@ -0,0 +1,41 @@
+using Api.Extensions;
+using Booking.Extensions.Infrastructure;
+using BuildingBlocks.Web;
+using Flight.Extensions.Infrastructure;
+using Identity.Extensions.Infrastructure;
+using Passenger.Extensions.Infrastructure;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddMinimalEndpoints(assemblies: AppDomain.CurrentDomain.GetAssemblies());
+
+builder.AddSharedInfrastructure();
+
+builder.AddFlightModules();
+builder.AddIdentityModules();
+builder.AddPassengerModules();
+builder.AddBookingModules();
+
+
+var app = builder.Build();
+
+// ref: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-7.0#routing-basics
+app.UseAuthentication();
+app.UseAuthorization();
+app.MapMinimalEndpoints();
+app.UserSharedInfrastructure();
+
+app.UseFlightModules();
+app.UseIdentityModules();
+app.UsePassengerModules();
+app.UseBookingModules();
+
+
+app.Run();
+
+namespace Api
+{
+ public partial class Program
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Api/src/Properties/launchSettings.json b/2-modular-monolith-architecture-style/src/Api/src/Properties/launchSettings.json
new file mode 100644
index 0000000..c1dd2d1
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:17191",
+ "sslPort": 44352
+ }
+ },
+ "profiles": {
+ "Api": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchUrl": "swagger",
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:5000;http://localhost:5001",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Api/src/appsettings.Development.json b/2-modular-monolith-architecture-style/src/Api/src/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Api/src/appsettings.docker.json b/2-modular-monolith-architecture-style/src/Api/src/appsettings.docker.json
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/appsettings.docker.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/2-modular-monolith-architecture-style/src/Api/src/appsettings.json b/2-modular-monolith-architecture-style/src/Api/src/appsettings.json
new file mode 100644
index 0000000..5749172
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/appsettings.json
@@ -0,0 +1,71 @@
+{
+ "AppOptions": {
+ "Name": "Booking-Modular-Monolith"
+ },
+ "LogOptions": {
+ "Level": "information",
+ "LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
+ "File": {
+ "Enabled": false,
+ "Path": "logs/logs.txt",
+ "Interval": "day"
+ }
+ },
+ "PostgresOptions": {
+ "ConnectionString": {
+ "Flight": "Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true",
+ "Identity": "Server=localhost;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true",
+ "Passenger": "Server=localhost;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true"
+ }
+ },
+ "MongoOptions": {
+ "ConnectionString": "mongodb://localhost:27017",
+ "DatabaseName": "booking-read"
+ },
+ "EventStoreOptions": {
+ "ConnectionString": "esdb://localhost:2113?tls=false"
+ },
+ "Jwt": {
+ "Authority": "http://localhost:6005",
+ "Audience": "flight-api",
+ "RequireHttpsMetadata": false,
+ "MetadataAddress": "http://localhost:6005/.well-known/openid-configuration"
+ },
+ "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": "*"
+}
diff --git a/2-modular-monolith-architecture-style/src/Api/src/appsettings.test.json b/2-modular-monolith-architecture-style/src/Api/src/appsettings.test.json
new file mode 100644
index 0000000..7a73a41
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/appsettings.test.json
@@ -0,0 +1,2 @@
+{
+}
\ No newline at end of file
diff --git a/2-modular-monolith-architecture-style/src/Api/src/keys/is-signing-key-73D9025BDA857BF270C99C6594EE4246.json b/2-modular-monolith-architecture-style/src/Api/src/keys/is-signing-key-73D9025BDA857BF270C99C6594EE4246.json
new file mode 100644
index 0000000..5ca82b6
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Api/src/keys/is-signing-key-73D9025BDA857BF270C99C6594EE4246.json
@@ -0,0 +1 @@
+{"Version":1,"Id":"73D9025BDA857BF270C99C6594EE4246","Created":"2024-09-02T18:34:53.8631045Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8MEQ06y_es9CrKY4Ou9Uc7Hf97ujXcMcuawi9V5VrBvxWbAqbdjBlk5zKK_NzrDURTwaFvgOfLUQ__0r-sMvtTXBZLhWhNWJnLn1-KWhtnpvPt5jFTAf2f5mvrjVTTN8E-NTGMGk2yLOYm2TVE3QL7DuLRrazydXLJvIB71O2d_OrAfB7Pq62xrnwLgp4BErb_HphYUExsAEp7jn6uL34QJ5HI6zsb_ct2SHvOl0CzUH2yIjjrY_u-5GAgmqqpUyysGwUh8RpR_CDPCd8ZxgSOiKAlBkp3kQXxf86MF0C-kfBKU-iflrJRSJ1-5R8F5nNPK8FL29I4i7SYCugIkLvqez3wTewPUFv00bTpcfs3V_tmfqBFLyNWm4sppzpU9HGB3elZ0z5bBa_IGbtIrlHC_o4SDSxKKdD4OUZHGLE5X--xWBw5uV9GSgrSH6bcnHZ6rLd3qrt7b82BMr-rVIGyFzGIE6OTLVGkXSL6FdpJ9Lezp06qnZ6DtLFI86lVJGsYIuXR_AIVrz9U8uqCrK6jLwGCk9nR0adPtAUJfgcXGVVTQlvt_YgZv5k5_rYcVl0yLNdgd-BoidXeoqLPJxIJOCohwumVcqTPkf-gB_hjgNk6IEqXZxm0Tzl0d2jpyERDHdHxIvS5o1h4YUfkiMcliRfQMNkLoDxf0H2hFNXRYQKkQ1y6cwS9juq66Uj_-v-vN1etE-hK45ULFCfyppBvb2aTYfTuce5S0ps1t0ZIjvpm7Kvjg3doHEi97N-IqxYaf8r6n8gBcaUwlrfcHaYNPLRyWywX_varEmLm9qGK5KJj2itGNfw0wU0EygIeoV6V_PzqyAkf0JcVTC7lcog37TPdNU2AGzH0S8oXiAQEd49wPs2ZApjiOaiz26efr03I-hD5N91-R2_9ACGjENGPVHMyUtMVV5RPs3-pQMv9f_zweOuLQo7ZfhScqu4HmxGW70amuV4anMxGCQzbi3JWnkTmspptzClJyvE_MJVSTQ6SX04DaS4buSG3wZEc9Qy9SqTj-9CJ7XFGFs8XYmKUj_cIoQF1XuoSnWblsnnEC7EbNRF7y9fG8ZG-Sk3TEEYnalxRrcS-i26wVNdnuUBEmidz_HfsxFxCDKKmx7GHTvHxy72kI84ucMeQeVFjJI3ZDynGn--cL9xBiUbUKM8WDhJ-AgZ76wwh0qAPw5xJ6yHi-15moxySUkvFLjlNkP2Ad5j3_3ab_r6VIunM6zhsq7pSBWIg5povuV5ZwNVZQX0IeLqV9bHug443LaK5a57dTK8wy346AFftV-wc71i4Nt5MIFcOs3lxRPqYij1enbrPYvIV4-N8Sy6aaYj25Qn7VHrGeW72aZPAYY5W-czoPw_Oo6xYGjaPYFFsUSZVg6IQwCzwwAxUoc2gAL33FJQTNvNSnrYBJ5HN-Tqan23Pw_bEus7HHZu2N1daFjqtrl4-oOco46phsppUjH3LGhOPJnFSChr-W8tlk80coJ8IK_AsGludKB09WzId9JBtI5cp3Yu1J7N6nSL7nVTrT6Gw_0hitSoeu5ZLPSS9ooAynAXrvB_s0l0L9aFTRuc5IEhgt4bLzbeqimfQemRlBsNz09JGe04gmOOCmjWD52JHWUiVJQNMavrSGtW9Dy1-Z5h0D_BHzhpTia1S7wx7dSdItJ0-Pm1Au_TNkQGm4ffNFsVDQmNkCYyc8yFnYmZMYYEaPmbw7DvQTs1MHoe7aUzMM0DKcaqboSaxqQK9sxymgElvdoYqOMRWzS7s39UQ1O4TfPngfrWdtN2DogGUtyS-vPfNJpdS6jZvJAj8czgl7PU8buwWyPApE1-FVL32wC6a8dkHvJi4p7fbBjmTfFCnuW8G1KBX3VuToctJvidSjzoSUTX3vgKVni2QW-55Sh7DUYy91FGXGB_ui1yuxEnLmymtbWokcWYkIwcAsl8im70V4oK63ypNSYWea_gaDWMFXT_vANB1iAkr-_zE_ECocOXo93QqSR5UdLmfQFvLiDwjUeovkjFS5C2Z8AjEvHvFkedGWOIK5Bpam-0IEFip3Fvg6RgxwTinFXXa8PiRkcLSlt0J81b85ybrKsDj5WtUA-MFuK2Silyofn9BgD_lh9RU4HPFhVoqey7AuEJjHtGvqz4EnE_05y4A_mKgvJBAs4QiYjCopWtheeOGeeoUa636Ewmu30P66C5mimdAIx36-55xlyJBIM7DFFM6RAGvfAmpyNphjwT0y84B4pOhFEZeOQ2me2sfG-xRJbjjgDhP2SwBBEQ-hCLGeqOD-Xo74FZC4lCTvtn2Sbu1kIw0kz2P_vrq6d6SZwEIrhWYhfRVKTrT8nXj8i48Jdc1d1fyKdRL15USgLhAT-QSNcgVYHRLsVlQx5-b51tGg6Atx6vGCxtXBRSaTwZ3IxbdJs0T62H14K5U81EFu-2Vvf-cMwCm4gCQATxvvAsqToxElou9ZjIVMPt_FQUyAMtJke","DataProtected":true}
\ No newline at end of file
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking.csproj b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking.csproj
new file mode 100644
index 0000000..f9c7082
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Dtos/CreateReservation.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Dtos/CreateReservation.cs
new file mode 100644
index 0000000..56c4a89
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Dtos/CreateReservation.cs
@@ -0,0 +1,4 @@
+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);
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/BookingAlreadyExistException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/BookingAlreadyExistException.cs
new file mode 100644
index 0000000..0149537
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/BookingAlreadyExistException.cs
@@ -0,0 +1,10 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class BookingAlreadyExistException : ConflictException
+{
+ public BookingAlreadyExistException(int? code = default) : base("Booking already exist!", code)
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/FlightNotFoundException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/FlightNotFoundException.cs
new file mode 100644
index 0000000..af4b5e6
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/FlightNotFoundException.cs
@@ -0,0 +1,10 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class FlightNotFoundException : NotFoundException
+{
+ public FlightNotFoundException() : base("Flight doesn't exist!")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidAircraftIdException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidAircraftIdException.cs
new file mode 100644
index 0000000..e56f720
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidAircraftIdException.cs
@@ -0,0 +1,11 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class InvalidAircraftIdException : BadRequestException
+{
+ public InvalidAircraftIdException(Guid aircraftId)
+ : base($"aircraftId: '{aircraftId}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidArriveAirportIdException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidArriveAirportIdException.cs
new file mode 100644
index 0000000..33ac680
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidArriveAirportIdException.cs
@@ -0,0 +1,11 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class InvalidArriveAirportIdException : BadRequestException
+{
+ public InvalidArriveAirportIdException(Guid arriveAirportId)
+ : base($"arriveAirportId: '{arriveAirportId}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidDepartureAirportIdException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidDepartureAirportIdException.cs
new file mode 100644
index 0000000..47f0d56
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidDepartureAirportIdException.cs
@@ -0,0 +1,11 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class InvalidDepartureAirportIdException : BadRequestException
+{
+ public InvalidDepartureAirportIdException(Guid departureAirportId)
+ : base($"departureAirportId: '{departureAirportId}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidFlightDateException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidFlightDateException.cs
new file mode 100644
index 0000000..8b4fd48
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidFlightDateException.cs
@@ -0,0 +1,11 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class InvalidFlightDateException : BadRequestException
+{
+ public InvalidFlightDateException(DateTime flightDate)
+ : base($"Flight Date: '{flightDate}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidFlightNumberException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidFlightNumberException.cs
new file mode 100644
index 0000000..146a001
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidFlightNumberException.cs
@@ -0,0 +1,10 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+public class InvalidFlightNumberException : BadRequestException
+{
+ public InvalidFlightNumberException(string flightNumber)
+ : base($"Flight Number: '{flightNumber}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidPassengerNameException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidPassengerNameException.cs
new file mode 100644
index 0000000..9ff87eb
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidPassengerNameException.cs
@@ -0,0 +1,11 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class InvalidPassengerNameException : BadRequestException
+{
+ public InvalidPassengerNameException(string passengerName)
+ : base($"Passenger Name: '{passengerName}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidPriceException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidPriceException.cs
new file mode 100644
index 0000000..ece6249
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/InvalidPriceException.cs
@@ -0,0 +1,12 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class InvalidPriceException : BadRequestException
+{
+ public InvalidPriceException(decimal price)
+ : base($"Price: '{price}' must be grater than or equal 0.")
+ {
+ }
+}
+
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/SeatNumberException.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/SeatNumberException.cs
new file mode 100644
index 0000000..19678d7
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Exceptions/SeatNumberException.cs
@@ -0,0 +1,12 @@
+namespace Booking.Booking.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class SeatNumberException : BadRequestException
+{
+ public SeatNumberException(string seatNumber)
+ : base($"Seat Number: '{seatNumber}' is invalid.")
+ {
+ }
+}
+
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Features/BookingMappings.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Features/BookingMappings.cs
new file mode 100644
index 0000000..feaa98e
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Features/BookingMappings.cs
@@ -0,0 +1,23 @@
+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));
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Features/CreatingBook/V1/CreateBooking.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Features/CreatingBook/V1/CreateBooking.cs
new file mode 100644
index 0000000..bde40c0
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Features/CreatingBook/V1/CreateBooking.cs
@@ -0,0 +1,146 @@
+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);
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Models/Booking.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Models/Booking.cs
new file mode 100644
index 0000000..354f743
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Models/Booking.cs
@@ -0,0 +1,50 @@
+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++;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Models/BookingReadModel.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Models/BookingReadModel.cs
new file mode 100644
index 0000000..57d2018
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/Models/BookingReadModel.cs
@@ -0,0 +1,12 @@
+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; }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/ValueObjects/PassengerInfo.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/ValueObjects/PassengerInfo.cs
new file mode 100644
index 0000000..9cbedc6
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/ValueObjects/PassengerInfo.cs
@@ -0,0 +1,23 @@
+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);
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/ValueObjects/Trip.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/ValueObjects/Trip.cs
new file mode 100644
index 0000000..fb1b0d4
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Booking/ValueObjects/Trip.cs
@@ -0,0 +1,69 @@
+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);
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/BookingProjection.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/BookingProjection.cs
new file mode 100644
index 0000000..ba4d2db
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/BookingProjection.cs
@@ -0,0 +1,54 @@
+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);
+ }
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/BookingRoot.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/BookingRoot.cs
new file mode 100644
index 0000000..73eb27c
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/BookingRoot.cs
@@ -0,0 +1,6 @@
+namespace Booking;
+
+public class BookingRoot
+{
+
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Configuration/GrpcOptions.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Configuration/GrpcOptions.cs
new file mode 100644
index 0000000..4f4adf7
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Configuration/GrpcOptions.cs
@@ -0,0 +1,7 @@
+namespace Booking.Configuration;
+
+public class GrpcOptions
+{
+ public string FlightAddress { get; set; }
+ public string PassengerAddress { get; set; }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Data/BookingReadDbContext.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Data/BookingReadDbContext.cs
new file mode 100644
index 0000000..e35b256
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Data/BookingReadDbContext.cs
@@ -0,0 +1,18 @@
+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; }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/EventMapper.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/EventMapper.cs
new file mode 100644
index 0000000..2996d90
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/EventMapper.cs
@@ -0,0 +1,27 @@
+using BuildingBlocks.Contracts.EventBus.Messages;
+using BuildingBlocks.Core;
+using BuildingBlocks.Core.Event;
+
+namespace Booking;
+
+using Booking.Features.CreatingBook.V1;
+
+public sealed class EventMapper : IEventMapper
+{
+ public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event)
+ {
+ return @event switch
+ {
+ BookingCreatedDomainEvent e => new BookingCreated(e.Id),
+ _ => null
+ };
+ }
+
+ public IInternalCommand? MapToInternalCommand(IDomainEvent @event)
+ {
+ return @event switch
+ {
+ _ => null
+ };
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/GrpcClientExtensions.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/GrpcClientExtensions.cs
new file mode 100644
index 0000000..8c6d501
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/GrpcClientExtensions.cs
@@ -0,0 +1,33 @@
+using Booking.Configuration;
+using BuildingBlocks.Web;
+using Flight;
+using Microsoft.Extensions.DependencyInjection;
+using Passenger;
+
+namespace Booking.Extensions.Infrastructure;
+
+using BuildingBlocks.Polly;
+
+public static class GrpcClientExtensions
+{
+ public static IServiceCollection AddGrpcClients(this IServiceCollection services)
+ {
+ var grpcOptions = services.GetOptions("Grpc");
+
+ services.AddGrpcClient(o =>
+ {
+ o.Address = new Uri(grpcOptions.FlightAddress);
+ })
+ .AddGrpcRetryPolicyHandler()
+ .AddGrpcCircuitBreakerPolicyHandler();
+
+ services.AddGrpcClient(o =>
+ {
+ o.Address = new Uri(grpcOptions.PassengerAddress);
+ })
+ .AddGrpcRetryPolicyHandler()
+ .AddGrpcCircuitBreakerPolicyHandler();
+
+ return services;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/InfrastructureExtensions.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/InfrastructureExtensions.cs
new file mode 100644
index 0000000..87e6712
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/InfrastructureExtensions.cs
@@ -0,0 +1,33 @@
+using Booking.Data;
+using BuildingBlocks.Core;
+using BuildingBlocks.EventStoreDB;
+using BuildingBlocks.Mongo;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Booking.Extensions.Infrastructure;
+
+public static class InfrastructureExtensions
+{
+ public static WebApplicationBuilder AddBookingModules(this WebApplicationBuilder builder)
+ {
+ builder.Services.AddScoped();
+ builder.AddMongoDbContext();
+
+ // ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce
+ builder.Services.AddEventStore(builder.Configuration, typeof(BookingRoot).Assembly)
+ .AddEventStoreDBSubscriptionToAll();
+
+ builder.Services.AddGrpcClients();
+
+ builder.Services.AddCustomMediatR();
+
+ return builder;
+ }
+
+
+ public static WebApplication UseBookingModules(this WebApplication app)
+ {
+ return app;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/MediatRExtensions.cs b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/MediatRExtensions.cs
new file mode 100644
index 0000000..6b131a8
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/Extensions/Infrastructure/MediatRExtensions.cs
@@ -0,0 +1,21 @@
+using BuildingBlocks.Caching;
+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<,>));
+ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
+ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(InvalidateCachingBehavior<,>));
+
+ return services;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/GrpcClient/Protos/flight.proto b/2-modular-monolith-architecture-style/src/Modules/Booking/src/GrpcClient/Protos/flight.proto
new file mode 100644
index 0000000..aba77c4
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/GrpcClient/Protos/flight.proto
@@ -0,0 +1,85 @@
+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;
+ }
diff --git a/2-modular-monolith-architecture-style/src/Modules/Booking/src/GrpcClient/Protos/passenger.proto b/2-modular-monolith-architecture-style/src/Modules/Booking/src/GrpcClient/Protos/passenger.proto
new file mode 100644
index 0000000..bb98c29
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Booking/src/GrpcClient/Protos/passenger.proto
@@ -0,0 +1,34 @@
+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;
+ }
+
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Dtos/AircraftDto.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Dtos/AircraftDto.cs
new file mode 100644
index 0000000..89aeed8
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Dtos/AircraftDto.cs
@@ -0,0 +1,3 @@
+namespace Flight.Aircrafts.Dtos;
+
+public record AircraftDto(long Id, string Name, string Model, int ManufacturingYear);
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/AircraftAlreadyExistException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/AircraftAlreadyExistException.cs
new file mode 100644
index 0000000..241dcdb
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/AircraftAlreadyExistException.cs
@@ -0,0 +1,11 @@
+namespace Flight.Aircrafts.Exceptions;
+
+using System.Net;
+using BuildingBlocks.Exception;
+
+public class AircraftAlreadyExistException : AppException
+{
+ public AircraftAlreadyExistException() : base("Aircraft already exist!", HttpStatusCode.Conflict)
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidAircraftIdException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidAircraftIdException.cs
new file mode 100644
index 0000000..840f5e2
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidAircraftIdException.cs
@@ -0,0 +1,12 @@
+namespace Flight.Aircrafts.Exceptions;
+using System;
+using BuildingBlocks.Exception;
+
+
+public class InvalidAircraftIdException : BadRequestException
+{
+ public InvalidAircraftIdException(Guid aircraftId)
+ : base($"AircraftId: '{aircraftId}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidManufacturingYearException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidManufacturingYearException.cs
new file mode 100644
index 0000000..da58cce
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidManufacturingYearException.cs
@@ -0,0 +1,10 @@
+namespace Flight.Aircrafts.Exceptions;
+using BuildingBlocks.Exception;
+
+
+public class InvalidManufacturingYearException : BadRequestException
+{
+ public InvalidManufacturingYearException() : base("ManufacturingYear must be greater than 1900")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidModelException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidModelException.cs
new file mode 100644
index 0000000..276e733
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidModelException.cs
@@ -0,0 +1,10 @@
+namespace Flight.Aircrafts.Exceptions;
+using BuildingBlocks.Exception;
+
+
+public class InvalidModelException : BadRequestException
+{
+ public InvalidModelException() : base("Model cannot be empty or whitespace.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidNameException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidNameException.cs
new file mode 100644
index 0000000..d2a7354
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Exceptions/InvalidNameException.cs
@@ -0,0 +1,10 @@
+namespace Flight.Aircrafts.Exceptions;
+using BuildingBlocks.Exception;
+
+
+public class InvalidNameException : BadRequestException
+{
+ public InvalidNameException() : base("Name cannot be empty or whitespace.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/AircraftMappings.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/AircraftMappings.cs
new file mode 100644
index 0000000..81be420
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/AircraftMappings.cs
@@ -0,0 +1,25 @@
+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));
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs
new file mode 100644
index 0000000..ff9b266
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/CreatingAircraft/V1/CreateAircraft.cs
@@ -0,0 +1,108 @@
+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);
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs
new file mode 100644
index 0000000..e89ccb0
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Features/CreatingAircraft/V1/CreateAircraftMongo.cs
@@ -0,0 +1,52 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Models/Aircraft.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Models/Aircraft.cs
new file mode 100644
index 0000000..ab71aeb
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Models/Aircraft.cs
@@ -0,0 +1,35 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Models/AircraftReadModel.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Models/AircraftReadModel.cs
new file mode 100644
index 0000000..d80052e
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/Models/AircraftReadModel.cs
@@ -0,0 +1,13 @@
+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; }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/AircraftId.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/AircraftId.cs
new file mode 100644
index 0000000..cc7e241
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/AircraftId.cs
@@ -0,0 +1,29 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/ManufacturingYear.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/ManufacturingYear.cs
new file mode 100644
index 0000000..3ab80a5
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/ManufacturingYear.cs
@@ -0,0 +1,28 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/Model.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/Model.cs
new file mode 100644
index 0000000..eae73f5
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/Model.cs
@@ -0,0 +1,28 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/Name.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/Name.cs
new file mode 100644
index 0000000..63a0fa4
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Aircrafts/ValueObjects/Name.cs
@@ -0,0 +1,28 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Dtos/AirportDto.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Dtos/AirportDto.cs
new file mode 100644
index 0000000..6ff8797
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Dtos/AirportDto.cs
@@ -0,0 +1,3 @@
+namespace Flight.Airports.Dtos;
+
+public record AirportDto(long Id, string Name, string Address, string Code);
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/AirportAlreadyExistException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/AirportAlreadyExistException.cs
new file mode 100644
index 0000000..55cdb27
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/AirportAlreadyExistException.cs
@@ -0,0 +1,10 @@
+namespace Flight.Airports.Exceptions;
+
+using BuildingBlocks.Exception;
+
+public class AirportAlreadyExistException : ConflictException
+{
+ public AirportAlreadyExistException(int? code = default) : base("Airport already exist!", code)
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidAddressException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidAddressException.cs
new file mode 100644
index 0000000..1036a8b
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidAddressException.cs
@@ -0,0 +1,9 @@
+namespace Flight.Airports.Exceptions;
+using BuildingBlocks.Exception;
+
+public class InvalidAddressException : BadRequestException
+{
+ public InvalidAddressException() : base("Address cannot be empty or whitespace.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidAirportIdException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidAirportIdException.cs
new file mode 100644
index 0000000..0b4b90e
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidAirportIdException.cs
@@ -0,0 +1,11 @@
+namespace Flight.Airports.Exceptions;
+using System;
+using BuildingBlocks.Exception;
+
+public class InvalidAirportIdException : BadRequestException
+{
+ public InvalidAirportIdException(Guid airportId)
+ : base($"airportId: '{airportId}' is invalid.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidCodeException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidCodeException.cs
new file mode 100644
index 0000000..9b69b55
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidCodeException.cs
@@ -0,0 +1,10 @@
+namespace Flight.Airports.Exceptions;
+using BuildingBlocks.Exception;
+
+
+public class InvalidCodeException : BadRequestException
+{
+ public InvalidCodeException() : base("Code cannot be empty or whitespace.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidNameException.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidNameException.cs
new file mode 100644
index 0000000..310ffb8
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Exceptions/InvalidNameException.cs
@@ -0,0 +1,10 @@
+namespace Flight.Airports.Exceptions;
+using BuildingBlocks.Exception;
+
+
+public class InvalidNameException : BadRequestException
+{
+ public InvalidNameException() : base("Name cannot be empty or whitespace.")
+ {
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/AirportMappings.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/AirportMappings.cs
new file mode 100644
index 0000000..5c7b7cd
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/AirportMappings.cs
@@ -0,0 +1,23 @@
+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));
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/CreatingAirport/V1/CreateAirport.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/CreatingAirport/V1/CreateAirport.cs
new file mode 100644
index 0000000..580f9c7
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/CreatingAirport/V1/CreateAirport.cs
@@ -0,0 +1,105 @@
+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);
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs
new file mode 100644
index 0000000..ad1df5c
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Features/CreatingAirport/V1/CreateAirportMongo.cs
@@ -0,0 +1,51 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Models/Airport.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Models/Airport.cs
new file mode 100644
index 0000000..efe8120
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Models/Airport.cs
@@ -0,0 +1,35 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Models/AirportReadModel.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Models/AirportReadModel.cs
new file mode 100644
index 0000000..14d5a32
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/Models/AirportReadModel.cs
@@ -0,0 +1,13 @@
+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; }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Address.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Address.cs
new file mode 100644
index 0000000..04bbc7c
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Address.cs
@@ -0,0 +1,28 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/AirportId.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/AirportId.cs
new file mode 100644
index 0000000..d1fd8ae
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/AirportId.cs
@@ -0,0 +1,29 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Code.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Code.cs
new file mode 100644
index 0000000..f53c297
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Code.cs
@@ -0,0 +1,28 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Name.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Name.cs
new file mode 100644
index 0000000..c66e500
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Airports/ValueObjects/Name.cs
@@ -0,0 +1,28 @@
+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;
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/AircraftConfiguration.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/AircraftConfiguration.cs
new file mode 100644
index 0000000..cb0adfa
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/AircraftConfiguration.cs
@@ -0,0 +1,56 @@
+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();
+ }
+ );
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/AirportConfiguration.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/AirportConfiguration.cs
new file mode 100644
index 0000000..6570a2d
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/AirportConfiguration.cs
@@ -0,0 +1,57 @@
+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();
+ }
+ );
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/FlightConfiguration.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/FlightConfiguration.cs
new file mode 100644
index 0000000..18e75fe
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/FlightConfiguration.cs
@@ -0,0 +1,113 @@
+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();
+ }
+ );
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/SeatConfiguration.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/SeatConfiguration.cs
new file mode 100644
index 0000000..ff8b995
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Configurations/SeatConfiguration.cs
@@ -0,0 +1,50 @@
+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));
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/DesignTimeDbContextFactory.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/DesignTimeDbContextFactory.cs
new file mode 100644
index 0000000..b9a7a6c
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/DesignTimeDbContextFactory.cs
@@ -0,0 +1,17 @@
+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);
+ }
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/EfTxFlightBehavior.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/EfTxFlightBehavior.cs
new file mode 100644
index 0000000..33df7c6
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/EfTxFlightBehavior.cs
@@ -0,0 +1,100 @@
+using System.Text.Json;
+using System.Transactions;
+using BuildingBlocks.Core;
+using BuildingBlocks.PersistMessageProcessor;
+using BuildingBlocks.Polly;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Flight.Data;
+
+public class EfTxFlightBehavior : IPipelineBehavior
+where TRequest : notnull, IRequest
+where TResponse : notnull
+{
+ private readonly ILogger> _logger;
+ private readonly FlightDbContext _flightDbContext;
+ private readonly IPersistMessageDbContext _persistMessageDbContext;
+ private readonly IEventDispatcher _eventDispatcher;
+
+ public EfTxFlightBehavior(
+ ILogger> logger,
+ FlightDbContext flightDbContext,
+ IPersistMessageDbContext persistMessageDbContext,
+ IEventDispatcher eventDispatcher
+ )
+ {
+ _logger = logger;
+ _flightDbContext = flightDbContext;
+ _persistMessageDbContext = persistMessageDbContext;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken
+ )
+ {
+ _logger.LogInformation(
+ "{Prefix} Handled command {MediatrRequest}",
+ GetType().Name,
+ typeof(TRequest).FullName);
+
+ _logger.LogDebug(
+ "{Prefix} Handled command {MediatrRequest} with content {RequestContent}",
+ GetType().Name,
+ typeof(TRequest).FullName,
+ JsonSerializer.Serialize(request));
+
+ var response = await next();
+
+ _logger.LogInformation(
+ "{Prefix} Executed the {MediatrRequest} request",
+ GetType().Name,
+ typeof(TRequest).FullName);
+
+ while (true)
+ {
+ var domainEvents = _flightDbContext.GetDomainEvents();
+
+ if (domainEvents is null || !domainEvents.Any())
+ {
+ return response;
+ }
+
+ _logger.LogInformation(
+ "{Prefix} Open the transaction for {MediatrRequest}",
+ GetType().Name,
+ typeof(TRequest).FullName);
+
+ using var scope = new TransactionScope(
+ TransactionScopeOption.Required,
+ new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
+ TransactionScopeAsyncFlowOption.Enabled);
+
+ await _eventDispatcher.SendAsync(
+ domainEvents.ToArray(),
+ typeof(TRequest),
+ cancellationToken);
+
+ // Save data to database with some retry policy in distributed transaction
+ await _flightDbContext.RetryOnFailure(
+ async () =>
+ {
+ await _flightDbContext.SaveChangesAsync(cancellationToken);
+ });
+
+ // Save data to database with some retry policy in distributed transaction
+ await _persistMessageDbContext.RetryOnFailure(
+ async () =>
+ {
+ await _persistMessageDbContext.SaveChangesAsync(cancellationToken);
+ });
+
+ scope.Complete();
+
+ return response;
+ }
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightDbContext.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightDbContext.cs
new file mode 100644
index 0000000..8f9f77c
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightDbContext.cs
@@ -0,0 +1,32 @@
+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();
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightReadDbContext.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightReadDbContext.cs
new file mode 100644
index 0000000..0c63e27
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/FlightReadDbContext.cs
@@ -0,0 +1,27 @@
+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; }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/20230611230948_initial.Designer.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/20230611230948_initial.Designer.cs
new file mode 100644
index 0000000..4010517
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/20230611230948_initial.Designer.cs
@@ -0,0 +1,583 @@
+//
+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
+ }
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/20230611230948_initial.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/20230611230948_initial.cs
new file mode 100644
index 0000000..630f49f
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/20230611230948_initial.cs
@@ -0,0 +1,163 @@
+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");
+ }
+ }
+}
diff --git a/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/FlightDbContextModelSnapshot.cs b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/FlightDbContextModelSnapshot.cs
new file mode 100644
index 0000000..930ce7a
--- /dev/null
+++ b/2-modular-monolith-architecture-style/src/Modules/Flight/src/Data/Migrations/FlightDbContextModelSnapshot.cs
@@ -0,0 +1,580 @@
+//
+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