feat: add modular monolith source

This commit is contained in:
Meysam Hadeli 2025-04-06 17:55:28 +03:30
parent 0d4dfb3459
commit 27d25aa47d
224 changed files with 9533 additions and 60 deletions

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<RootNamespace>Api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Modules\Booking\src\Booking.csproj" />
<ProjectReference Include="..\..\Modules\Flight\src\Flight.csproj" />
<ProjectReference Include="..\..\Modules\Identity\src\Identity.csproj" />
<ProjectReference Include="..\..\Modules\Passenger\src\Passenger.csproj" />
</ItemGroup>
</Project>

View File

@ -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<AppOptions>(nameof(AppOptions));
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.AddCustomSerilog(builder.Environment);
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddJwt();
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services.AddPersistMessageProcessor();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddControllers();
builder.Services.AddAspnetOpenApi();
builder.Services.AddCustomVersioning();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IEventDispatcher, EventDispatcher>();
builder.Services.AddCustomMassTransit(
builder.Environment,
TransportType.InMemory,
AppDomain.CurrentDomain.GetAssemblies());
builder.Services.Configure<ApiBehaviorOptions>(
options => options.SuppressModelStateInvalidFilter = true);
builder.Services.AddRateLimiter(
options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
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<GrpcExceptionInterceptor>();
});
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<AppOptions>(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;
}
}

View File

@ -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
{
}
}

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -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": "*"
}

View File

@ -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}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.68.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="GrpcClient\Protos\flight.proto" GrpcServices="Both" />
<Protobuf Include="GrpcClient\Protos\passenger.proto" GrpcServices="Both" />
<Folder Include="GrpcClient\Protos" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\building-blocks\BuildingBlocks.csproj" />
</ItemGroup>
</Project>

View File

@ -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);

View File

@ -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)
{
}
}

View File

@ -0,0 +1,10 @@
namespace Booking.Booking.Exceptions;
using BuildingBlocks.Exception;
public class FlightNotFoundException : NotFoundException
{
public FlightNotFoundException() : base("Flight doesn't exist!")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Booking.Booking.Exceptions;
using BuildingBlocks.Exception;
public class InvalidAircraftIdException : BadRequestException
{
public InvalidAircraftIdException(Guid aircraftId)
: base($"aircraftId: '{aircraftId}' is invalid.")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Booking.Booking.Exceptions;
using BuildingBlocks.Exception;
public class InvalidArriveAirportIdException : BadRequestException
{
public InvalidArriveAirportIdException(Guid arriveAirportId)
: base($"arriveAirportId: '{arriveAirportId}' is invalid.")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Booking.Booking.Exceptions;
using BuildingBlocks.Exception;
public class InvalidDepartureAirportIdException : BadRequestException
{
public InvalidDepartureAirportIdException(Guid departureAirportId)
: base($"departureAirportId: '{departureAirportId}' is invalid.")
{
}
}

View File

@ -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.")
{
}
}

View File

@ -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.")
{
}
}

View File

@ -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.")
{
}
}

View File

@ -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.")
{
}
}

View File

@ -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.")
{
}
}

View File

@ -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<Models.Booking, BookingResponseDto>()
.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<CreateBookingRequestDto, CreateBooking>()
.ConstructUsing(x => new CreateBooking(x.PassengerId, x.FlightId, x.Description));
}
}

View File

@ -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<CreateBookingResult>
{
public Guid Id { get; init; } = NewId.NextGuid();
}
public record CreateBookingResult(ulong Id);
public record BookingCreatedDomainEvent(Guid Id, PassengerInfo PassengerInfo, Trip Trip) : Entity<Guid>, 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<CreateBooking>(request);
var result = await mediator.Send(command, cancellationToken);
var response = result.Adapt<CreateBookingResponseDto>();
return Results.Ok(response);
})
.RequireAuthorization(nameof(ApiScope))
.WithName("CreateBooking")
.WithApiVersionSet(builder.NewApiVersionSet("Booking").Build())
.Produces<CreateBookingResponseDto>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Create Booking")
.WithDescription("Create Booking")
.WithOpenApi()
.HasApiVersion(1.0);
return builder;
}
}
public class CreateBookingValidator : AbstractValidator<CreateBooking>
{
public CreateBookingValidator()
{
RuleFor(x => x.FlightId).NotNull().WithMessage("FlightId is required!");
RuleFor(x => x.PassengerId).NotNull().WithMessage("PassengerId is required!");
}
}
internal class CreateBookingCommandHandler : ICommandHandler<CreateBooking, CreateBookingResult>
{
private readonly IEventStoreDBRepository<Models.Booking> _eventStoreDbRepository;
private readonly ICurrentUserProvider _currentUserProvider;
private readonly IEventDispatcher _eventDispatcher;
private readonly FlightGrpcService.FlightGrpcServiceClient _flightGrpcServiceClient;
private readonly PassengerGrpcService.PassengerGrpcServiceClient _passengerGrpcServiceClient;
public CreateBookingCommandHandler(IEventStoreDBRepository<Models.Booking> eventStoreDbRepository,
ICurrentUserProvider currentUserProvider,
IEventDispatcher eventDispatcher,
FlightGrpcService.FlightGrpcServiceClient flightGrpcServiceClient,
PassengerGrpcService.PassengerGrpcServiceClient passengerGrpcServiceClient)
{
_eventStoreDbRepository = eventStoreDbRepository;
_currentUserProvider = currentUserProvider;
_eventDispatcher = eventDispatcher;
_flightGrpcServiceClient = flightGrpcServiceClient;
_passengerGrpcServiceClient = passengerGrpcServiceClient;
}
public async Task<CreateBookingResult> 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);
}
}

View File

@ -0,0 +1,50 @@
using BuildingBlocks.EventStoreDB.Events;
namespace Booking.Booking.Models;
using Features.CreatingBook.V1;
using ValueObjects;
public record Booking : AggregateEventSourcing<Guid>
{
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++;
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<T>(StreamEvent<T> 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);
}
}
}

View File

@ -0,0 +1,6 @@
namespace Booking;
public class BookingRoot
{
}

View File

@ -0,0 +1,7 @@
namespace Booking.Configuration;
public class GrpcOptions
{
public string FlightAddress { get; set; }
public string PassengerAddress { get; set; }
}

View File

@ -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<MongoOptions> options) : base(options)
{
Booking = GetCollection<BookingReadModel>(nameof(Booking).Underscore());
}
public IMongoCollection<BookingReadModel> Booking { get; }
}

View File

@ -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
};
}
}

View File

@ -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<GrpcOptions>("Grpc");
services.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
{
o.Address = new Uri(grpcOptions.FlightAddress);
})
.AddGrpcRetryPolicyHandler()
.AddGrpcCircuitBreakerPolicyHandler();
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
{
o.Address = new Uri(grpcOptions.PassengerAddress);
})
.AddGrpcRetryPolicyHandler()
.AddGrpcCircuitBreakerPolicyHandler();
return services;
}
}

View File

@ -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<IEventMapper, EventMapper>();
builder.AddMongoDbContext<BookingReadDbContext>();
// 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
namespace Flight.Aircrafts.Dtos;
public record AircraftDto(long Id, string Name, string Model, int ManufacturingYear);

View File

@ -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)
{
}
}

View File

@ -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.")
{
}
}

View File

@ -0,0 +1,10 @@
namespace Flight.Aircrafts.Exceptions;
using BuildingBlocks.Exception;
public class InvalidManufacturingYearException : BadRequestException
{
public InvalidManufacturingYearException() : base("ManufacturingYear must be greater than 1900")
{
}
}

View File

@ -0,0 +1,10 @@
namespace Flight.Aircrafts.Exceptions;
using BuildingBlocks.Exception;
public class InvalidModelException : BadRequestException
{
public InvalidModelException() : base("Model cannot be empty or whitespace.")
{
}
}

View File

@ -0,0 +1,10 @@
namespace Flight.Aircrafts.Exceptions;
using BuildingBlocks.Exception;
public class InvalidNameException : BadRequestException
{
public InvalidNameException() : base("Name cannot be empty or whitespace.")
{
}
}

View File

@ -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<CreateAircraftMongo, AircraftReadModel>()
.Map(d => d.Id, s => NewId.NextGuid())
.Map(d => d.AircraftId, s => AircraftId.Of(s.Id));
config.NewConfig<Aircraft, AircraftReadModel>()
.Map(d => d.Id, s => NewId.NextGuid())
.Map(d => d.AircraftId, s => AircraftId.Of(s.Id.Value));
config.NewConfig<CreateAircraftRequestDto, CreatingAircraft.V1.CreateAircraft>()
.ConstructUsing(x => new CreatingAircraft.V1.CreateAircraft(x.Name, x.Model, x.ManufacturingYear));
}
}

View File

@ -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<CreateAircraftResult>,
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<CreateAircraft>(request);
var result = await mediator.Send(command, cancellationToken);
var response = result.Adapt<CreateAircraftResponseDto>();
return Results.Ok(response);
})
.RequireAuthorization(nameof(ApiScope))
.WithName("CreateAircraft")
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.Produces<CreateAircraftResponseDto>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Create Aircraft")
.WithDescription("Create Aircraft")
.WithOpenApi()
.HasApiVersion(1.0);
return builder;
}
}
public class CreateAircraftValidator : AbstractValidator<CreateAircraft>
{
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<CreateAircraft, CreateAircraftResult>
{
private readonly FlightDbContext _flightDbContext;
public CreateAircraftHandler(FlightDbContext flightDbContext)
{
_flightDbContext = flightDbContext;
}
public async Task<CreateAircraftResult> 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);
}
}

View File

@ -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<CreateAircraftMongo>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public CreateAircraftMongoHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(CreateAircraftMongo request, CancellationToken cancellationToken)
{
Guard.Against.Null(request, nameof(request));
var aircraftReadModel = _mapper.Map<AircraftReadModel>(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;
}
}

View File

@ -0,0 +1,35 @@
using BuildingBlocks.Core.Model;
namespace Flight.Aircrafts.Models;
using Features.CreatingAircraft.V1;
using ValueObjects;
public record Aircraft : Aggregate<AircraftId>
{
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;
}
}

View File

@ -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; }
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
namespace Flight.Airports.Dtos;
public record AirportDto(long Id, string Name, string Address, string Code);

View File

@ -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)
{
}
}

View File

@ -0,0 +1,9 @@
namespace Flight.Airports.Exceptions;
using BuildingBlocks.Exception;
public class InvalidAddressException : BadRequestException
{
public InvalidAddressException() : base("Address cannot be empty or whitespace.")
{
}
}

View File

@ -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.")
{
}
}

View File

@ -0,0 +1,10 @@
namespace Flight.Airports.Exceptions;
using BuildingBlocks.Exception;
public class InvalidCodeException : BadRequestException
{
public InvalidCodeException() : base("Code cannot be empty or whitespace.")
{
}
}

View File

@ -0,0 +1,10 @@
namespace Flight.Airports.Exceptions;
using BuildingBlocks.Exception;
public class InvalidNameException : BadRequestException
{
public InvalidNameException() : base("Name cannot be empty or whitespace.")
{
}
}

View File

@ -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<CreateAirportMongo, AirportReadModel>()
.Map(d => d.Id, s => NewId.NextGuid())
.Map(d => d.AirportId, s => s.Id);
config.NewConfig<Airport, AirportReadModel>()
.Map(d => d.Id, s => NewId.NextGuid())
.Map(d => d.AirportId, s => s.Id.Value);
config.NewConfig<CreateAirportRequestDto, CreateAirport>()
.ConstructUsing(x => new CreateAirport(x.Name, x.Address, x.Code));
}
}

View File

@ -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<CreateAirportResult>, 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<CreateAirport>(request);
var result = await mediator.Send(command, cancellationToken);
var response = result.Adapt<CreateAirportResponseDto>();
return Results.Ok(response);
})
.RequireAuthorization(nameof(ApiScope))
.WithName("CreateAirport")
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.Produces<CreateAirportResponseDto>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Create Airport")
.WithDescription("Create Airport")
.WithOpenApi()
.HasApiVersion(1.0);
return builder;
}
}
public class CreateAirportValidator : AbstractValidator<CreateAirport>
{
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<CreateAirport, CreateAirportResult>
{
private readonly FlightDbContext _flightDbContext;
public CreateAirportHandler(FlightDbContext flightDbContext)
{
_flightDbContext = flightDbContext;
}
public async Task<CreateAirportResult> 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);
}
}

View File

@ -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<CreateAirportMongo>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public CreateAirportMongoHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(CreateAirportMongo request, CancellationToken cancellationToken)
{
Guard.Against.Null(request, nameof(request));
var airportReadModel = _mapper.Map<AirportReadModel>(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;
}
}

View File

@ -0,0 +1,35 @@
using BuildingBlocks.Core.Model;
namespace Flight.Airports.Models;
using Features.CreatingAirport.V1;
using ValueObjects;
public record Airport : Aggregate<AirportId>
{
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;
}
}

View File

@ -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; }
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<Aircraft>
{
public void Configure(EntityTypeBuilder<Aircraft> builder)
{
builder.ToTable(nameof(Aircraft));
builder.HasKey(r => r.Id);
builder.Property(r => r.Id).ValueGeneratedNever()
.HasConversion<Guid>(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();
}
);
}
}

View File

@ -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<Airport>
{
public void Configure(EntityTypeBuilder<Airport> builder)
{
builder.ToTable(nameof(Airport));
builder.HasKey(r => r.Id);
builder.Property(r => r.Id).ValueGeneratedNever()
.HasConversion<Guid>(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();
}
);
}
}

View File

@ -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<Flights.Models.Flight>
{
public void Configure(EntityTypeBuilder<Flights.Models.Flight> builder)
{
builder.ToTable(nameof(Flight));
builder.HasKey(r => r.Id);
builder.Property(r => r.Id).ValueGeneratedNever()
.HasConversion<Guid>(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<Aircraft>()
.WithMany()
.HasForeignKey(p => p.AircraftId)
.IsRequired();
builder
.HasOne<Airport>()
.WithMany()
.HasForeignKey(d => d.DepartureAirportId)
.IsRequired();
builder
.HasOne<Airport>()
.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();
}
);
}
}

View File

@ -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<Seat>
{
public void Configure(EntityTypeBuilder<Seat> builder)
{
builder.ToTable(nameof(Seat));
builder.HasKey(r => r.Id);
builder.Property(r => r.Id).ValueGeneratedNever()
.HasConversion<Guid>(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<Flights.Models.Flight>()
.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));
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Flight.Data
{
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<FlightDbContext>
{
public FlightDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<FlightDbContext>();
builder.UseNpgsql("Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true")
.UseSnakeCaseNamingConvention();
return new FlightDbContext(builder.Options);
}
}
}

View File

@ -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<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull, IRequest<TResponse>
where TResponse : notnull
{
private readonly ILogger<EfTxFlightBehavior<TRequest, TResponse>> _logger;
private readonly FlightDbContext _flightDbContext;
private readonly IPersistMessageDbContext _persistMessageDbContext;
private readonly IEventDispatcher _eventDispatcher;
public EfTxFlightBehavior(
ILogger<EfTxFlightBehavior<TRequest, TResponse>> logger,
FlightDbContext flightDbContext,
IPersistMessageDbContext persistMessageDbContext,
IEventDispatcher eventDispatcher
)
{
_logger = logger;
_flightDbContext = flightDbContext;
_persistMessageDbContext = persistMessageDbContext;
_eventDispatcher = eventDispatcher;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> 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;
}
}
}

View File

@ -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<FlightDbContext> options, ICurrentUserProvider? currentUserProvider = null,
ILogger<FlightDbContext>? logger = null) : base(
options, currentUserProvider, logger)
{
}
public DbSet<Flights.Models.Flight> Flights => Set<Flights.Models.Flight>();
public DbSet<Airport> Airports => Set<Airport>();
public DbSet<Aircraft> Aircraft => Set<Aircraft>();
public DbSet<Seat> Seats => Set<Seat>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurationsFromAssembly(typeof(FlightRoot).Assembly);
builder.FilterSoftDeletedProperties();
builder.ToSnakeCaseTables();
}
}

View File

@ -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<MongoOptions> options) : base(options)
{
Flight = GetCollection<FlightReadModel>(nameof(Flight).Underscore());
Aircraft = GetCollection<AircraftReadModel>(nameof(Aircraft).Underscore());
Airport = GetCollection<AirportReadModel>(nameof(Airport).Underscore());
Seat = GetCollection<SeatReadModel>(nameof(Seat).Underscore());
}
public IMongoCollection<FlightReadModel> Flight { get; }
public IMongoCollection<AircraftReadModel> Aircraft { get; }
public IMongoCollection<AirportReadModel> Airport { get; }
public IMongoCollection<SeatReadModel> Seat { get; }
}

View File

@ -0,0 +1,583 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<long>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<long>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("aircraft_id");
b.Property<Guid>("ArriveAirportId")
.HasColumnType("uuid")
.HasColumnName("arrive_airport_id");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<Guid>("DepartureAirportId")
.HasColumnType("uuid")
.HasColumnName("departure_airport_id");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("Unknown")
.HasColumnName("status");
b.Property<long>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<string>("Class")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("Unknown")
.HasColumnName("class");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("flight_id");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<string>("Type")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("Unknown")
.HasColumnName("type");
b.Property<long>("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<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<int>("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<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AirportId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AirportId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AirportId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<DateTime>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<DateTime>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<decimal>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<DateTime>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<decimal>("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<Guid>("SeatId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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
}
}
}

View File

@ -0,0 +1,163 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Flight.Data.Migrations
{
/// <inheritdoc />
public partial class initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "aircraft",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
model = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
manufacturingyear = table.Column<int>(name: "manufacturing_year", type: "integer", maxLength: 5, nullable: false),
createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: true),
createdby = table.Column<long>(name: "created_by", type: "bigint", nullable: true),
lastmodified = table.Column<DateTime>(name: "last_modified", type: "timestamp with time zone", nullable: true),
lastmodifiedby = table.Column<long>(name: "last_modified_by", type: "bigint", nullable: true),
isdeleted = table.Column<bool>(name: "is_deleted", type: "boolean", nullable: false),
version = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_aircraft", x => x.id);
});
migrationBuilder.CreateTable(
name: "airport",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
address = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: true),
createdby = table.Column<long>(name: "created_by", type: "bigint", nullable: true),
lastmodified = table.Column<DateTime>(name: "last_modified", type: "timestamp with time zone", nullable: true),
lastmodifiedby = table.Column<long>(name: "last_modified_by", type: "bigint", nullable: true),
isdeleted = table.Column<bool>(name: "is_deleted", type: "boolean", nullable: false),
version = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_airport", x => x.id);
});
migrationBuilder.CreateTable(
name: "flight",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
flightnumber = table.Column<string>(name: "flight_number", type: "character varying(50)", maxLength: 50, nullable: false),
aircraftid = table.Column<Guid>(name: "aircraft_id", type: "uuid", nullable: false),
departureairportid = table.Column<Guid>(name: "departure_airport_id", type: "uuid", nullable: false),
arriveairportid = table.Column<Guid>(name: "arrive_airport_id", type: "uuid", nullable: false),
durationminutes = table.Column<decimal>(name: "duration_minutes", type: "numeric", maxLength: 50, nullable: false),
status = table.Column<string>(type: "text", nullable: false, defaultValue: "Unknown"),
price = table.Column<decimal>(type: "numeric", maxLength: 10, nullable: false),
arrivedate = table.Column<DateTime>(name: "arrive_date", type: "timestamp with time zone", nullable: false),
departuredate = table.Column<DateTime>(name: "departure_date", type: "timestamp with time zone", nullable: false),
flightdate = table.Column<DateTime>(name: "flight_date", type: "timestamp with time zone", nullable: false),
createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: true),
createdby = table.Column<long>(name: "created_by", type: "bigint", nullable: true),
lastmodified = table.Column<DateTime>(name: "last_modified", type: "timestamp with time zone", nullable: true),
lastmodifiedby = table.Column<long>(name: "last_modified_by", type: "bigint", nullable: true),
isdeleted = table.Column<bool>(name: "is_deleted", type: "boolean", nullable: false),
version = table.Column<long>(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<Guid>(type: "uuid", nullable: false),
seatnumber = table.Column<string>(name: "seat_number", type: "character varying(50)", maxLength: 50, nullable: false),
type = table.Column<string>(type: "text", nullable: false, defaultValue: "Unknown"),
@class = table.Column<string>(name: "class", type: "text", nullable: false, defaultValue: "Unknown"),
flightid = table.Column<Guid>(name: "flight_id", type: "uuid", nullable: false),
createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: true),
createdby = table.Column<long>(name: "created_by", type: "bigint", nullable: true),
lastmodified = table.Column<DateTime>(name: "last_modified", type: "timestamp with time zone", nullable: true),
lastmodifiedby = table.Column<long>(name: "last_modified_by", type: "bigint", nullable: true),
isdeleted = table.Column<bool>(name: "is_deleted", type: "boolean", nullable: false),
version = table.Column<long>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "seat");
migrationBuilder.DropTable(
name: "flight");
migrationBuilder.DropTable(
name: "aircraft");
migrationBuilder.DropTable(
name: "airport");
}
}
}

View File

@ -0,0 +1,580 @@
// <auto-generated />
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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<long>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<long>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("aircraft_id");
b.Property<Guid>("ArriveAirportId")
.HasColumnType("uuid")
.HasColumnName("arrive_airport_id");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<Guid>("DepartureAirportId")
.HasColumnType("uuid")
.HasColumnName("departure_airport_id");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("Unknown")
.HasColumnName("status");
b.Property<long>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<string>("Class")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("Unknown")
.HasColumnName("class");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasColumnName("created_by");
b.Property<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("flight_id");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTime?>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<long?>("LastModifiedBy")
.HasColumnType("bigint")
.HasColumnName("last_modified_by");
b.Property<string>("Type")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("Unknown")
.HasColumnName("type");
b.Property<long>("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<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<int>("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<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AircraftId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AirportId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AirportId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("AirportId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<DateTime>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<DateTime>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<decimal>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<DateTime>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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<Guid>("FlightId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<decimal>("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<Guid>("SeatId")
.HasColumnType("uuid")
.HasColumnName("id");
b1.Property<string>("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
}
}
}

View File

@ -0,0 +1,88 @@
using BuildingBlocks.EFCore;
using Flight.Aircrafts.Models;
using Flight.Airports.Models;
using Flight.Flights.Models;
using Flight.Seats.Models;
using MapsterMapper;
using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Flight.Data.Seed;
public class FlightDataSeeder(
FlightDbContext flightDbContext,
FlightReadDbContext flightReadDbContext,
IMapper mapper
) : IDataSeeder
{
public async Task SeedAllAsync()
{
var pendingMigrations = await flightDbContext.Database.GetPendingMigrationsAsync();
if (!pendingMigrations.Any())
{
await SeedAirportAsync();
await SeedAircraftAsync();
await SeedFlightAsync();
await SeedSeatAsync();
}
}
private async Task SeedAirportAsync()
{
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Airports))
{
await flightDbContext.Airports.AddRangeAsync(InitialData.Airports);
await flightDbContext.SaveChangesAsync();
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Airport.AsQueryable()))
{
await flightReadDbContext.Airport.InsertManyAsync(mapper.Map<List<AirportReadModel>>(InitialData.Airports));
}
}
}
private async Task SeedAircraftAsync()
{
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Aircraft))
{
await flightDbContext.Aircraft.AddRangeAsync(InitialData.Aircrafts);
await flightDbContext.SaveChangesAsync();
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Aircraft.AsQueryable()))
{
await flightReadDbContext.Aircraft.InsertManyAsync(mapper.Map<List<AircraftReadModel>>(InitialData.Aircrafts));
}
}
}
private async Task SeedSeatAsync()
{
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Seats))
{
await flightDbContext.Seats.AddRangeAsync(InitialData.Seats);
await flightDbContext.SaveChangesAsync();
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Seat.AsQueryable()))
{
await flightReadDbContext.Seat.InsertManyAsync(mapper.Map<List<SeatReadModel>>(InitialData.Seats));
}
}
}
private async Task SeedFlightAsync()
{
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Flights))
{
await flightDbContext.Flights.AddRangeAsync(InitialData.Flights);
await flightDbContext.SaveChangesAsync();
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Flight.AsQueryable()))
{
await flightReadDbContext.Flight.InsertManyAsync(mapper.Map<List<FlightReadModel>>(InitialData.Flights));
}
}
}
}

View File

@ -0,0 +1,61 @@
namespace Flight.Data.Seed;
using System;
using System.Collections.Generic;
using System.Linq;
using Aircrafts.Models;
using Airports.Models;
using Airports.ValueObjects;
using Flight.Aircrafts.ValueObjects;
using Flights.Models;
using Flights.ValueObjects;
using MassTransit;
using Seats.Models;
using Seats.ValueObjects;
using AirportName = Airports.ValueObjects.Name;
using Name = Aircrafts.ValueObjects.Name;
public static class InitialData
{
public static List<Airport> Airports { get; }
public static List<Aircraft> Aircrafts { get; }
public static List<Seat> Seats { get; }
public static List<Flight> Flights { get; }
static InitialData()
{
Airports = new List<Airport>
{
Airport.Create(AirportId.Of(new Guid("3c5c0000-97c6-fc34-a0cb-08db322230c8")), AirportName.Of("Lisbon International Airport"), Address.Of("LIS"), Code.Of("12988")),
Airport.Create(AirportId.Of(new Guid("3c5c0000-97c6-fc34-fc3c-08db322230c8")), AirportName.Of("Sao Paulo International Airport"), Address.Of("BRZ"), Code.Of("11200"))
};
Aircrafts = new List<Aircraft>
{
Aircraft.Create(AircraftId.Of(new Guid("3c5c0000-97c6-fc34-fcd3-08db322230c8")), Name.Of("Boeing 737"), Model.Of("B737"), ManufacturingYear.Of(2005)),
Aircraft.Create(AircraftId.Of(new Guid("3c5c0000-97c6-fc34-2e04-08db322230c9")), Name.Of("Airbus 300"), Model.Of("A300"), ManufacturingYear.Of(2000)),
Aircraft.Create(AircraftId.Of(new Guid("3c5c0000-97c6-fc34-2e11-08db322230c9")), Name.Of("Airbus 320"), Model.Of("A320"), ManufacturingYear.Of(2003))
};
Flights = new List<Flight>
{
Flight.Create(FlightId.Of(new Guid("3c5c0000-97c6-fc34-2eb9-08db322230c9")), FlightNumber.Of("BD467"), AircraftId.Of(Aircrafts.First().Id.Value), AirportId.Of( Airports.First().Id), DepartureDate.Of(new DateTime(2022, 1, 31, 12, 0, 0)),
ArriveDate.Of(new DateTime(2022, 1, 31, 14, 0, 0)),
AirportId.Of(Airports.Last().Id), DurationMinutes.Of(120m),
FlightDate.Of(new DateTime(2022, 1, 31, 13, 0, 0)), global::Flight.Flights.Enums.FlightStatus.Completed,
Price.Of(8000))
};
Seats = new List<Seat>
{
Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of( "12A"), global::Flight.Seats.Enums.SeatType.Window, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid)Flights.First().Id)),
Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12B"), global::Flight.Seats.Enums.SeatType.Window, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid)Flights.First().Id)),
Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12C"), global::Flight.Seats.Enums.SeatType.Middle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)),
Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12D"), global::Flight.Seats.Enums.SeatType.Middle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)),
Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12E"), global::Flight.Seats.Enums.SeatType.Aisle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id)),
Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12F"), global::Flight.Seats.Enums.SeatType.Aisle, global::Flight.Seats.Enums.SeatClass.Economy, FlightId.Of((Guid) Flights.First().Id))
};
}
}

View File

@ -0,0 +1,2 @@
dotnet ef migrations add initial --context FlightDbContext -o "Data\Migrations"
dotnet ef database update --context FlightDbContext

View File

@ -0,0 +1,51 @@
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core;
using BuildingBlocks.Core.Event;
namespace Flight;
using Aircrafts.Features.CreatingAircraft.V1;
using Aircrafts.ValueObjects;
using Airports.Features.CreatingAirport.V1;
using Flights.Features.CreatingFlight.V1;
using Flights.Features.DeletingFlight.V1;
using Flights.Features.UpdatingFlight.V1;
using Seats.Features.CreatingSeat.V1;
using Seats.Features.ReservingSeat.V1;
// ref: https://www.ledjonbehluli.com/posts/domain_to_integration_event/
public sealed class EventMapper : IEventMapper
{
public IIntegrationEvent? MapToIntegrationEvent(IDomainEvent @event)
{
return @event switch
{
FlightCreatedDomainEvent e => new FlightCreated(e.Id),
FlightUpdatedDomainEvent e => new FlightUpdated(e.Id),
FlightDeletedDomainEvent e => new FlightDeleted(e.Id),
AirportCreatedDomainEvent e => new AirportCreated(e.Id),
AircraftCreatedDomainEvent e => new AircraftCreated(e.Id),
SeatCreatedDomainEvent e => new SeatCreated(e.Id),
SeatReservedDomainEvent e => new SeatReserved(e.Id),
_ => null
};
}
public IInternalCommand? MapToInternalCommand(IDomainEvent @event)
{
return @event switch
{
FlightCreatedDomainEvent e => new CreateFlightMongo(e.Id, e.FlightNumber, e.AircraftId, e.DepartureDate, e.DepartureAirportId,
e.ArriveDate, e.ArriveAirportId, e.DurationMinutes, e.FlightDate, e.Status, e.Price, e.IsDeleted),
FlightUpdatedDomainEvent e => new UpdateFlightMongo(e.Id, e.FlightNumber, e.AircraftId, e.DepartureDate, e.DepartureAirportId,
e.ArriveDate, e.ArriveAirportId, e.DurationMinutes, e.FlightDate, e.Status, e.Price, e.IsDeleted),
FlightDeletedDomainEvent e => new DeleteFlightMongo(e.Id, e.FlightNumber, e.AircraftId, e.DepartureDate, e.DepartureAirportId,
e.ArriveDate, e.ArriveAirportId, e.DurationMinutes, e.FlightDate, e.Status, e.Price, e.IsDeleted),
AircraftCreatedDomainEvent e => new CreateAircraftMongo(e.Id, e.Name, e.Model, e.ManufacturingYear, e.IsDeleted),
AirportCreatedDomainEvent e => new CreateAirportMongo(e.Id, e.Name, e.Address, e.Code, e.IsDeleted),
SeatCreatedDomainEvent e => new CreateSeatMongo(e.Id, e.SeatNumber, e.Type, e.Class, e.FlightId, e.IsDeleted),
SeatReservedDomainEvent e => new ReserveSeatMongo(e.Id, e.SeatNumber, e.Type, e.Class, e.FlightId, e.IsDeleted),
_ => null
};
}
}

View File

@ -0,0 +1,35 @@
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
using BuildingBlocks.Mongo;
using Flight.Data;
using Flight.Data.Seed;
using Flight.GrpcServer.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Flight.Extensions.Infrastructure;
public static class InfrastructureExtensions
{
public static WebApplicationBuilder AddFlightModules(this WebApplicationBuilder builder)
{
builder.Services.AddScoped<IEventMapper, EventMapper>();
builder.AddCustomDbContext<FlightDbContext>(nameof(Flight));
builder.Services.AddScoped<IDataSeeder, FlightDataSeeder>();
builder.AddMongoDbContext<FlightReadDbContext>();
builder.Services.AddCustomMediatR();
return builder;
}
public static WebApplication UseFlightModules(this WebApplication app)
{
app.UseMigration<FlightDbContext>();
app.MapGrpcService<FlightGrpcServices>();
return app;
}
}

View File

@ -0,0 +1,23 @@
using BuildingBlocks.Caching;
using BuildingBlocks.Logging;
using BuildingBlocks.Validation;
using Flight.Data;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace Flight.Extensions.Infrastructure;
public static class MediatRExtensions
{
public static IServiceCollection AddCustomMediatR(this IServiceCollection services)
{
services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(FlightRoot).Assembly));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
// services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxFlightBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(InvalidateCachingBehavior<,>));
return services;
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.67.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
<PackageReference Include="Grpc.Tools" Version="2.68.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="GrpcServer\Protos\flight.proto" GrpcServices="Both" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\building-blocks\BuildingBlocks.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
namespace Flight;
public class FlightRoot
{
}

View File

@ -0,0 +1,6 @@
using System;
namespace Flight.Flights.Dtos;
public record FlightDto(Guid Id, string FlightNumber, Guid AircraftId, Guid DepartureAirportId,
DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate,
Enums.FlightStatus Status, decimal Price);

View File

@ -0,0 +1,10 @@
namespace Flight.Flights.Enums;
public enum FlightStatus
{
Unknown = 0,
Flying = 1,
Delay = 2,
Canceled = 3,
Completed = 4
}

View File

@ -0,0 +1,10 @@
namespace Flight.Flights.Exceptions;
using BuildingBlocks.Exception;
public class FlightAlreadyExistException : ConflictException
{
public FlightAlreadyExistException(int? code = default) : base("Flight already exist!", code)
{
}
}

View File

@ -0,0 +1,14 @@
namespace Flight.Flights.Exceptions;
using System;
using BuildingBlocks.Exception;
public class FlightExceptions : BadRequestException
{
public FlightExceptions(DateTime departureDate, DateTime arriveDate) :
base($"Departure date: '{departureDate}' must be before arrive date: '{arriveDate}'.")
{ }
public FlightExceptions(DateTime flightDate) :
base($"Flight date: '{flightDate}' must be between departure and arrive dates.")
{ }
}

View File

@ -0,0 +1,10 @@
using BuildingBlocks.Exception;
namespace Flight.Flights.Exceptions;
public class FlightNotFountException : NotFoundException
{
public FlightNotFountException() : base("Flight not found!")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Flight.Flights.Exceptions;
using System;
using BuildingBlocks.Exception;
public class InvalidArriveDateException : BadRequestException
{
public InvalidArriveDateException(DateTime arriveDate)
: base($"Arrive Date: '{arriveDate}' is invalid.")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Flight.Flights.Exceptions;
using System;
using BuildingBlocks.Exception;
public class InvalidDepartureDateException : BadRequestException
{
public InvalidDepartureDateException(DateTime departureDate)
: base($"Departure Date: '{departureDate}' is invalid.")
{
}
}

View File

@ -0,0 +1,10 @@
namespace Flight.Flights.Exceptions;
using BuildingBlocks.Exception;
public class InvalidDurationException : BadRequestException
{
public InvalidDurationException()
: base("Duration cannot be negative.")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Flight.Flights.Exceptions;
using System;
using BuildingBlocks.Exception;
public class InvalidFlightDateException : BadRequestException
{
public InvalidFlightDateException(DateTime flightDate)
: base($"Flight Date: '{flightDate}' is invalid.")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Flight.Flights.Exceptions;
using System;
using BuildingBlocks.Exception;
public class InvalidFlightIdException : BadRequestException
{
public InvalidFlightIdException(Guid flightId)
: base($"flightId: '{flightId}' is invalid.")
{
}
}

View File

@ -0,0 +1,10 @@
namespace Flight.Flights.Exceptions;
using BuildingBlocks.Exception;
public class InvalidFlightNumberException : BadRequestException
{
public InvalidFlightNumberException(string flightNumber)
: base($"Flight Number: '{flightNumber}' is invalid.")
{
}
}

View File

@ -0,0 +1,11 @@
namespace Flight.Flights.Exceptions;
using BuildingBlocks.Exception;
public class InvalidPriceException : BadRequestException
{
public InvalidPriceException()
: base($"Price Cannot be negative.")
{
}
}

View File

@ -0,0 +1,127 @@
namespace Flight.Flights.Features.CreatingFlight.V1;
using System;
using System.Threading;
using System.Threading.Tasks;
using Aircrafts.ValueObjects;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.Web;
using Data;
using Duende.IdentityServer.EntityFramework.Entities;
using Exceptions;
using Flight.Airports.ValueObjects;
using FluentValidation;
using Mapster;
using MapsterMapper;
using MassTransit;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using ValueObjects;
public record CreateFlight(string FlightNumber, Guid AircraftId, Guid DepartureAirportId,
DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId,
decimal DurationMinutes, DateTime FlightDate, Enums.FlightStatus Status,
decimal Price) : ICommand<CreateFlightResult>, IInternalCommand
{
public Guid Id { get; init; } = NewId.NextGuid();
}
public record CreateFlightResult(Guid Id);
public record FlightCreatedDomainEvent(Guid Id, string FlightNumber, Guid AircraftId, DateTime DepartureDate,
Guid DepartureAirportId, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes,
DateTime FlightDate, Enums.FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent;
public record CreateFlightRequestDto(string FlightNumber, Guid AircraftId, Guid DepartureAirportId,
DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId,
decimal DurationMinutes, DateTime FlightDate, Enums.FlightStatus Status, decimal Price);
public record CreateFlightResponseDto(Guid Id);
public class CreateFlightEndpoint : IMinimalEndpoint
{
public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder)
{
builder.MapPost($"{EndpointConfig.BaseApiPath}/flight", async (CreateFlightRequestDto request,
IMediator mediator, IMapper mapper,
CancellationToken cancellationToken) =>
{
var command = mapper.Map<CreateFlight>(request);
var result = await mediator.Send(command, cancellationToken);
var response = result.Adapt<CreateFlightResponseDto>();
return Results.CreatedAtRoute("GetFlightById", new { id = result.Id }, response);
})
.RequireAuthorization(nameof(ApiScope))
.WithName("CreateFlight")
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.Produces<CreateFlightResponseDto>(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Create Flight")
.WithDescription("Create Flight")
.WithOpenApi()
.HasApiVersion(1.0);
return builder;
}
}
public class CreateFlightValidator : AbstractValidator<CreateFlight>
{
public CreateFlightValidator()
{
RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0");
RuleFor(x => x.Status).Must(p => (p.GetType().IsEnum &&
p == Enums.FlightStatus.Flying) ||
p == Enums.FlightStatus.Canceled ||
p == Enums.FlightStatus.Delay ||
p == Enums.FlightStatus.Completed)
.WithMessage("Status must be Flying, Delay, Canceled or Completed");
RuleFor(x => x.AircraftId).NotEmpty().WithMessage("AircraftId must be not empty");
RuleFor(x => x.DepartureAirportId).NotEmpty().WithMessage("DepartureAirportId must be not empty");
RuleFor(x => x.ArriveAirportId).NotEmpty().WithMessage("ArriveAirportId must be not empty");
RuleFor(x => x.DurationMinutes).GreaterThan(0).WithMessage("DurationMinutes must be greater than 0");
RuleFor(x => x.FlightDate).NotEmpty().WithMessage("FlightDate must be not empty");
}
}
internal class CreateFlightHandler : ICommandHandler<CreateFlight, CreateFlightResult>
{
private readonly FlightDbContext _flightDbContext;
public CreateFlightHandler(FlightDbContext flightDbContext)
{
_flightDbContext = flightDbContext;
}
public async Task<CreateFlightResult> Handle(CreateFlight request, CancellationToken cancellationToken)
{
Guard.Against.Null(request, nameof(request));
var flight = await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.Id == request.Id,
cancellationToken);
if (flight is not null)
{
throw new FlightAlreadyExistException();
}
var flightEntity = Models.Flight.Create(FlightId.Of(request.Id), FlightNumber.Of(request.FlightNumber), AircraftId.Of(request.AircraftId),
AirportId.Of(request.DepartureAirportId), DepartureDate.Of(request.DepartureDate),
ArriveDate.Of(request.ArriveDate), AirportId.Of(request.ArriveAirportId), DurationMinutes.Of(request.DurationMinutes), FlightDate.Of(request.FlightDate), request.Status,
Price.Of(request.Price));
var newFlight = (await _flightDbContext.Flights.AddAsync(flightEntity, cancellationToken)).Entity;
return new CreateFlightResult(newFlight.Id);
}
}

View File

@ -0,0 +1,52 @@
namespace Flight.Flights.Features.CreatingFlight.V1;
using System;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using Data;
using Exceptions;
using MapsterMapper;
using MediatR;
using Models;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
public record CreateFlightMongo(Guid Id, string FlightNumber, Guid AircraftId, DateTime DepartureDate,
Guid DepartureAirportId, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate,
Enums.FlightStatus Status, decimal Price, bool IsDeleted = false) : InternalCommand;
internal class CreateFlightMongoHandler : ICommandHandler<CreateFlightMongo>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public CreateFlightMongoHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(CreateFlightMongo request, CancellationToken cancellationToken)
{
Guard.Against.Null(request, nameof(request));
var flightReadModel = _mapper.Map<FlightReadModel>(request);
var flight = await _flightReadDbContext.Flight.AsQueryable()
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken);
if (flight is not null)
{
throw new FlightAlreadyExistException();
}
await _flightReadDbContext.Flight.InsertOneAsync(flightReadModel, cancellationToken: cancellationToken);
return Unit.Value;
}
}

Some files were not shown because too many files have changed in this diff Show More