Merge pull request #32 from meysamhadeli/develop

Develop
This commit is contained in:
Meysam Hadeli 2022-07-16 01:31:41 +04:30 committed by GitHub
commit fb6c7f3a10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 813 additions and 141 deletions

View File

@ -56,6 +56,21 @@ authorization: bearer {{Authenticate.response.body.access_token}}
GET {{flight-api}} GET {{flight-api}}
### ###
###
# @name Create_Seat
Post {{api-gateway}}/api/v1/flight/seat
accept: application/json
Content-Type: application/json
authorization: bearer {{Authenticate.response.body.access_token}}
{
"seatNumber": "12H9",
"type": 1,
"class": 1,
"flightId": 1
}
###
### ###
# @name Reserve_Seat # @name Reserve_Seat
@ -66,7 +81,7 @@ authorization: bearer {{Authenticate.response.body.access_token}}
{ {
"flightId": 1, "flightId": 1,
"seatNumber": "12C" "seatNumber": "12H9"
} }
### ###
@ -143,6 +158,20 @@ authorization: bearer {{Authenticate.response.body.access_token}}
} }
### ###
###
# @name Delete_Flights
DELETE {{api-gateway}}/api/v1/flight
accept: application/json
Content-Type: application/json
authorization: bearer {{Authenticate.response.body.access_token}}
{
"id": 1
}
###
### ###
# @name Create_Airport # @name Create_Airport
POST {{api-gateway}}/api/v1/flight/airport POST {{api-gateway}}/api/v1/flight/airport
@ -167,9 +196,9 @@ Content-Type: application/json
authorization: bearer {{Authenticate.response.body.access_token}} authorization: bearer {{Authenticate.response.body.access_token}}
{ {
"name": "airbus", "name": "airbus2",
"model": "320", "model": "322",
"manufacturingYear": 2010 "manufacturingYear": 2012
} }
### ###

View File

@ -7,3 +7,5 @@ public record FlightUpdated(long Id) : IIntegrationEvent;
public record FlightDeleted(long Id) : IIntegrationEvent; public record FlightDeleted(long Id) : IIntegrationEvent;
public record AircraftCreated(long Id) : IIntegrationEvent; public record AircraftCreated(long Id) : IIntegrationEvent;
public record AirportCreated(long Id) : IIntegrationEvent; public record AirportCreated(long Id) : IIntegrationEvent;
public record SeatCreated(long Id) : IIntegrationEvent;
public record SeatReserved(long Id) : IIntegrationEvent;

View File

@ -3,6 +3,7 @@ namespace BuildingBlocks.Core.Event;
[Flags] [Flags]
public enum EventType public enum EventType
{ {
IntegrationEvent = 1, DomainEvent = 1,
DomainEvent = 2 IntegrationEvent = 2,
InternalCommand = 4
} }

View File

@ -1,10 +0,0 @@
using BuildingBlocks.Core.CQRS;
namespace BuildingBlocks.Core.Event;
public interface IInternalCommand : ICommand
{
long Id { get; }
DateTime OccurredOn { get; }
string Type { get; }
}

View File

@ -0,0 +1,11 @@
using BuildingBlocks.IdsGenerator;
using BuildingBlocks.Utils;
public interface IInternalCommand
{
public long Id => SnowFlakIdGenerator.NewId();
public DateTime OccurredOn => DateTime.Now;
public string Type => TypeProvider.GetTypeName(GetType());
}

View File

@ -1,9 +1,10 @@
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using BuildingBlocks.Utils; using BuildingBlocks.Utils;
using ICommand = BuildingBlocks.Core.CQRS.ICommand;
namespace BuildingBlocks.Core.Event; namespace BuildingBlocks.Core.Event;
public class InternalCommand : IInternalCommand public class InternalCommand : IInternalCommand, ICommand
{ {
public long Id { get; set; } = SnowFlakIdGenerator.NewId(); public long Id { get; set; } = SnowFlakIdGenerator.NewId();

View File

@ -31,20 +31,22 @@ public sealed class EventDispatcher : IEventDispatcher
} }
public async Task SendAsync<T>(IReadOnlyList<T> events, CancellationToken cancellationToken = default) public async Task SendAsync<T>(IReadOnlyList<T> events, EventType eventType = default,
CancellationToken cancellationToken = default)
where T : IEvent where T : IEvent
{ {
async Task PublishIntegrationEvent(IReadOnlyList<IIntegrationEvent> integrationEvents)
{
foreach (var integrationEvent in integrationEvents)
{
await _persistMessageProcessor.PublishMessageAsync(new MessageEnvelope(integrationEvent, SetHeaders()),
cancellationToken);
}
}
if (events.Count > 0) if (events.Count > 0)
{ {
async Task PublishIntegrationEvent(IReadOnlyList<IIntegrationEvent> integrationEvents)
{
foreach (var integrationEvent in integrationEvents)
{
await _persistMessageProcessor.PublishMessageAsync(
new MessageEnvelope(integrationEvent, SetHeaders()),
cancellationToken);
}
}
switch (events) switch (events)
{ {
case IReadOnlyList<IDomainEvent> domainEvents: case IReadOnlyList<IDomainEvent> domainEvents:
@ -60,12 +62,24 @@ public sealed class EventDispatcher : IEventDispatcher
await PublishIntegrationEvent(integrationEvents); await PublishIntegrationEvent(integrationEvents);
break; break;
} }
if (eventType == EventType.InternalCommand)
{
var internalMessages = await MapDomainEventToInternalCommandAsync(events as IReadOnlyList<IDomainEvent>)
.ConfigureAwait(false);
foreach (var internalMessage in internalMessages)
{
await _persistMessageProcessor.AddInternalMessageAsync(internalMessage, cancellationToken);
}
}
} }
} }
public async Task SendAsync<T>(T @event, CancellationToken cancellationToken = default) public async Task SendAsync<T>(T @event, EventType eventType = default,
CancellationToken cancellationToken = default)
where T : IEvent => where T : IEvent =>
await SendAsync(new[] {@event}, cancellationToken); await SendAsync(new[] {@event}, eventType, cancellationToken);
private Task<IReadOnlyList<IIntegrationEvent>> MapDomainEventToIntegrationEventAsync( private Task<IReadOnlyList<IIntegrationEvent>> MapDomainEventToIntegrationEventAsync(
@ -84,7 +98,7 @@ public sealed class EventDispatcher : IEventDispatcher
var eventType = @event.GetType(); var eventType = @event.GetType();
_logger.LogTrace($"Handling domain event: {eventType.Name}"); _logger.LogTrace($"Handling domain event: {eventType.Name}");
var integrationEvent = _eventMapper.Map(@event); var integrationEvent = _eventMapper.MapToIntegrationEvent(@event);
if (integrationEvent is null) continue; if (integrationEvent is null) continue;
@ -96,6 +110,31 @@ public sealed class EventDispatcher : IEventDispatcher
return Task.FromResult<IReadOnlyList<IIntegrationEvent>>(integrationEvents); return Task.FromResult<IReadOnlyList<IIntegrationEvent>>(integrationEvents);
} }
private Task<IReadOnlyList<InternalCommand>> MapDomainEventToInternalCommandAsync(
IReadOnlyList<IDomainEvent> events)
{
_logger.LogTrace("Processing internal message start...");
var internalCommands = new List<InternalCommand>();
using var scope = _serviceScopeFactory.CreateScope();
foreach (var @event in events)
{
var eventType = @event.GetType();
_logger.LogTrace($"Handling domain event: {eventType.Name}");
var integrationEvent = _eventMapper.MapToInternalCommand(@event);
if (integrationEvent is null) continue;
internalCommands.Add(integrationEvent);
}
_logger.LogTrace("Processing internal message done...");
return Task.FromResult<IReadOnlyList<InternalCommand>>(internalCommands);
}
private IEnumerable<IIntegrationEvent> GetWrappedIntegrationEvents(IReadOnlyList<IDomainEvent> domainEvents) private IEnumerable<IIntegrationEvent> GetWrappedIntegrationEvents(IReadOnlyList<IDomainEvent> domainEvents)
{ {
foreach (var domainEvent in domainEvents.Where(x => foreach (var domainEvent in domainEvents.Where(x =>

View File

@ -4,8 +4,8 @@ namespace BuildingBlocks.Core;
public interface IEventDispatcher public interface IEventDispatcher
{ {
public Task SendAsync<T>(IReadOnlyList<T> events, CancellationToken cancellationToken = default) public Task SendAsync<T>(IReadOnlyList<T> events, EventType eventType = default, CancellationToken cancellationToken = default)
where T : IEvent; where T : IEvent;
public Task SendAsync<T>(T @event, CancellationToken cancellationToken = default) public Task SendAsync<T>(T @event, EventType eventType = default, CancellationToken cancellationToken = default)
where T : IEvent; where T : IEvent;
} }

View File

@ -4,6 +4,6 @@ namespace BuildingBlocks.Core;
public interface IEventMapper public interface IEventMapper
{ {
IIntegrationEvent Map(IDomainEvent @event); IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event);
IEnumerable<IIntegrationEvent> MapAll(IEnumerable<IDomainEvent> events); InternalCommand MapToInternalCommand(IDomainEvent @event);
} }

View File

@ -61,7 +61,9 @@ public class EfTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TRe
var domainEvents = _dbContextBase.GetDomainEvents(); var domainEvents = _dbContextBase.GetDomainEvents();
await _eventDispatcher.SendAsync(domainEvents.ToArray(), cancellationToken); var eventType = typeof(TRequest).IsAssignableTo(typeof(IInternalCommand)) ? EventType.InternalCommand : EventType.DomainEvent;
await _eventDispatcher.SendAsync(domainEvents.ToArray(), eventType, cancellationToken);
return response; return response;
} }

View File

@ -23,12 +23,15 @@ public static class Extensions
var mongoOptions = services.GetOptions<MongoOptions>("MongoOptions"); var mongoOptions = services.GetOptions<MongoOptions>("MongoOptions");
var logOptions = services.GetOptions<LogOptions>("LogOptions"); var logOptions = services.GetOptions<LogOptions>("LogOptions");
services.AddHealthChecks() var healthChecksBuilder = services.AddHealthChecks()
.AddSqlServer(sqlOptions.DefaultConnection) .AddSqlServer(sqlOptions.DefaultConnection)
.AddMongoDb(mongoOptions.ConnectionString)
.AddRabbitMQ(rabbitConnectionString: $"amqp://{rabbitMqOptions.UserName}:{rabbitMqOptions.Password}@{rabbitMqOptions.HostName}") .AddRabbitMQ(rabbitConnectionString: $"amqp://{rabbitMqOptions.UserName}:{rabbitMqOptions.Password}@{rabbitMqOptions.HostName}")
.AddElasticsearch(logOptions.ElasticUri); .AddElasticsearch(logOptions.ElasticUri);
if (mongoOptions.ConnectionString is not null)
healthChecksBuilder.AddMongoDb(mongoOptions.ConnectionString);
services.AddHealthChecksUI(setup => services.AddHealthChecksUI(setup =>
{ {
setup.SetEvaluationTimeInSeconds(60); // time in seconds between check setup.SetEvaluationTimeInSeconds(60); // time in seconds between check

View File

@ -7,12 +7,7 @@ namespace Booking;
public sealed class EventMapper : IEventMapper public sealed class EventMapper : IEventMapper
{ {
public IEnumerable<IIntegrationEvent> MapAll(IEnumerable<IDomainEvent> events) public IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event)
{
return events.Select(Map);
}
public IIntegrationEvent Map(IDomainEvent @event)
{ {
return @event switch return @event switch
{ {
@ -20,4 +15,12 @@ public sealed class EventMapper : IEventMapper
_ => null _ => null
}; };
} }
public InternalCommand MapToInternalCommand(IDomainEvent @event)
{
return @event switch
{
_ => null
};
}
} }

View File

@ -20,9 +20,7 @@ using Flight.Data;
using Flight.Data.Seed; using Flight.Data.Seed;
using Flight.Extensions; using Flight.Extensions;
using FluentValidation; using FluentValidation;
using HealthChecks.UI.Client;
using Hellang.Middleware.ProblemDetails; using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Prometheus; using Prometheus;
using Serilog; using Serilog;

View File

@ -2,4 +2,4 @@ using BuildingBlocks.Core.Event;
namespace Flight.Aircrafts.Events; namespace Flight.Aircrafts.Events;
public record AircraftCreatedDomainEvent(long Id, string Name, string Model, int ManufacturingYear) : IDomainEvent; public record AircraftCreatedDomainEvent(long Id, string Name, string Model, int ManufacturingYear, bool IsDeleted) : IDomainEvent;

View File

@ -0,0 +1,16 @@
using BuildingBlocks.IdsGenerator;
using Flight.Aircrafts.Features.CreateAircraft.Reads;
using Flight.Aircrafts.Models.Reads;
using Mapster;
namespace Flight.Aircrafts.Features;
public class AircraftMappings : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<CreateAircraftMongoCommand, AircraftReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.AircraftId, s => s.Id);
}
}

View File

@ -1,10 +1,11 @@
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using Flight.Aircrafts.Dtos; using Flight.Aircrafts.Dtos;
using MediatR; using MediatR;
namespace Flight.Aircrafts.Features.CreateAircraft; namespace Flight.Aircrafts.Features.CreateAircraft;
public record CreateAircraftCommand(string Name, string Model, int ManufacturingYear) : IRequest<AircraftResponseDto> public record CreateAircraftCommand(string Name, string Model, int ManufacturingYear) : ICommand<AircraftResponseDto>, IInternalCommand
{ {
public long Id { get; set; } = SnowFlakIdGenerator.NewId(); public long Id { get; set; } = SnowFlakIdGenerator.NewId();
} }

View File

@ -0,0 +1,22 @@
using BuildingBlocks.Core.Event;
namespace Flight.Aircrafts.Features.CreateAircraft.Reads;
public class CreateAircraftMongoCommand : InternalCommand
{
public CreateAircraftMongoCommand(long id, string name, string model, int manufacturingYear, bool isDeleted)
{
Id = id;
Name = name;
Model = model;
ManufacturingYear = manufacturingYear;
IsDeleted = isDeleted;
}
public long Id { get; }
public string Name { get; }
public string Model { get; }
public int ManufacturingYear { get; }
public bool IsDeleted { get; }
}

View File

@ -0,0 +1,44 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using Flight.Aircrafts.Exceptions;
using Flight.Aircrafts.Models.Reads;
using Flight.Data;
using MapsterMapper;
using MediatR;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Flight.Aircrafts.Features.CreateAircraft.Reads;
public class CreateAircraftMongoCommandHandler : ICommandHandler<CreateAircraftMongoCommand>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public CreateAircraftMongoCommandHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(CreateAircraftMongoCommand command, CancellationToken cancellationToken)
{
Guard.Against.Null(command, nameof(command));
var aircraftReadModel = _mapper.Map<AircraftReadModel>(command);
var aircraft = await _flightReadDbContext.Aircraft.AsQueryable()
.FirstOrDefaultAsync(x => x.Id == aircraftReadModel.Id, cancellationToken);
if (aircraft is not null)
throw new AircraftAlreadyExistException();
await _flightReadDbContext.Aircraft.InsertOneAsync(aircraftReadModel, cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@ -14,7 +14,7 @@ public class Aircraft : Aggregate<long>
public string Model { get; private set; } public string Model { get; private set; }
public int ManufacturingYear { get; private set; } public int ManufacturingYear { get; private set; }
public static Aircraft Create(long id, string name, string model, int manufacturingYear) public static Aircraft Create(long id, string name, string model, int manufacturingYear, bool isDeleted = false)
{ {
var aircraft = new Aircraft var aircraft = new Aircraft
{ {
@ -28,7 +28,8 @@ public class Aircraft : Aggregate<long>
aircraft.Id, aircraft.Id,
aircraft.Name, aircraft.Name,
aircraft.Model, aircraft.Model,
aircraft.ManufacturingYear); aircraft.ManufacturingYear,
isDeleted);
aircraft.AddDomainEvent(@event); aircraft.AddDomainEvent(@event);

View File

@ -0,0 +1,11 @@
namespace Flight.Aircrafts.Models.Reads;
public class AircraftReadModel
{
public long Id { get; init; }
public long AircraftId { get; init; }
public string Name { get; init; }
public string Model { get; init; }
public int ManufacturingYear { get; init; }
public bool IsDeleted { get; init; }
}

View File

@ -0,0 +1,16 @@
using BuildingBlocks.IdsGenerator;
using Flight.Airports.Features.CreateAirport.Reads;
using Flight.Airports.Models.Reads;
using Mapster;
namespace Flight.Airports;
public class AirportMappings : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<CreateAirportMongoCommand, AirportReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.AirportId, s => s.Id);
}
}

View File

@ -2,4 +2,4 @@ using BuildingBlocks.Core.Event;
namespace Flight.Airports.Events; namespace Flight.Airports.Events;
public record AirportCreatedDomainEvent(long Id, string Name, string Address, string Code) : IDomainEvent; public record AirportCreatedDomainEvent(long Id, string Name, string Address, string Code, bool IsDeleted) : IDomainEvent;

View File

@ -1,10 +1,11 @@
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using Flight.Airports.Dtos; using Flight.Airports.Dtos;
using MediatR; using MediatR;
namespace Flight.Airports.Features.CreateAirport; namespace Flight.Airports.Features.CreateAirport;
public record CreateAirportCommand(string Name, string Address, string Code) : IRequest<AirportResponseDto> public record CreateAirportCommand(string Name, string Address, string Code) : ICommand<AirportResponseDto>, IInternalCommand
{ {
public long Id { get; set; } = SnowFlakIdGenerator.NewId(); public long Id { get; set; } = SnowFlakIdGenerator.NewId();
} }

View File

@ -0,0 +1,21 @@
using BuildingBlocks.Core.Event;
namespace Flight.Airports.Features.CreateAirport.Reads;
public class CreateAirportMongoCommand : InternalCommand
{
public CreateAirportMongoCommand(long id, string name, string address, string code, bool isDeleted)
{
Id = id;
Name = name;
Address = address;
Code = code;
IsDeleted = isDeleted;
}
public long Id { get; }
public string Name { get; }
public string Address { get; }
public string Code { get; }
public bool IsDeleted { get; }
}

View File

@ -0,0 +1,44 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using Flight.Airports.Exceptions;
using Flight.Airports.Models.Reads;
using Flight.Data;
using MapsterMapper;
using MediatR;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Flight.Airports.Features.CreateAirport.Reads;
public class CreateAirportMongoCommandHandler : ICommandHandler<CreateAirportMongoCommand>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public CreateAirportMongoCommandHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(CreateAirportMongoCommand command, CancellationToken cancellationToken)
{
Guard.Against.Null(command, nameof(command));
var airportReadModel = _mapper.Map<AirportReadModel>(command);
var aircraft = await _flightReadDbContext.Airport.AsQueryable()
.FirstOrDefaultAsync(x => x.Id == airportReadModel.Id, cancellationToken);
if (aircraft is not null)
throw new AirportAlreadyExistException();
await _flightReadDbContext.Airport.InsertOneAsync(airportReadModel, cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@ -14,7 +14,7 @@ public class Airport : Aggregate<long>
public string Address { get; private set; } public string Address { get; private set; }
public string Code { get; private set; } public string Code { get; private set; }
public static Airport Create(long id, string name, string address, string code) public static Airport Create(long id, string name, string address, string code, bool isDeleted = false)
{ {
var airport = new Airport var airport = new Airport
{ {
@ -28,7 +28,8 @@ public class Airport : Aggregate<long>
airport.Id, airport.Id,
airport.Name, airport.Name,
airport.Address, airport.Address,
airport.Code); airport.Code,
isDeleted);
airport.AddDomainEvent(@event); airport.AddDomainEvent(@event);

View File

@ -0,0 +1,11 @@
namespace Flight.Airports.Models.Reads;
public class AirportReadModel
{
public long Id { get; init; }
public long AirportId { get; init; }
public string Name { get; init; }
public string Address { get; init; }
public string Code { get; init; }
public bool IsDeleted { get; set; }
}

View File

@ -1,5 +1,9 @@
using BuildingBlocks.Mongo; using BuildingBlocks.Mongo;
using Flight.Aircrafts.Models.Reads;
using Flight.Airports.Models;
using Flight.Airports.Models.Reads;
using Flight.Flights.Models.Reads; using Flight.Flights.Models.Reads;
using Flight.Seats.Models.Reads;
using Humanizer; using Humanizer;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Driver; using MongoDB.Driver;
@ -11,7 +15,13 @@ public class FlightReadDbContext : MongoDbContext
public FlightReadDbContext(IOptions<MongoOptions> options) : base(options) public FlightReadDbContext(IOptions<MongoOptions> options) : base(options)
{ {
Flight = GetCollection<FlightReadModel>(nameof(Flight).Underscore()); 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<FlightReadModel> Flight { get; }
public IMongoCollection<AircraftReadModel> Aircraft { get; }
public IMongoCollection<AirportReadModel> Airport { get; }
public IMongoCollection<SeatReadModel> Seat { get; }
} }

View File

@ -1,20 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
using Flight.Aircrafts.Events; using Flight.Aircrafts.Events;
using Flight.Aircrafts.Features.CreateAircraft.Reads;
using Flight.Airports.Events; using Flight.Airports.Events;
using Flight.Airports.Features.CreateAirport.Reads;
using Flight.Flights.Events.Domain; using Flight.Flights.Events.Domain;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Flights.Features.DeleteFlight.Reads;
using Flight.Flights.Features.UpdateFlight.Reads;
using Flight.Seats.Events;
using Flight.Seats.Features.CreateSeat.Reads;
using Flight.Seats.Features.ReserveSeat.Reads;
namespace Flight; namespace Flight;
// ref: https://www.ledjonbehluli.com/posts/domain_to_integration_event/ // ref: https://www.ledjonbehluli.com/posts/domain_to_integration_event/
public sealed class EventMapper : IEventMapper public sealed class EventMapper : IEventMapper
{ {
public IEnumerable<IIntegrationEvent> MapAll(IEnumerable<IDomainEvent> events) => events.Select(Map); public IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event)
public IIntegrationEvent Map(IDomainEvent @event)
{ {
return @event switch return @event switch
{ {
@ -23,6 +27,26 @@ public sealed class EventMapper : IEventMapper
FlightDeletedDomainEvent e => new FlightDeleted(e.Id), FlightDeletedDomainEvent e => new FlightDeleted(e.Id),
AirportCreatedDomainEvent e => new AirportCreated(e.Id), AirportCreatedDomainEvent e => new AirportCreated(e.Id),
AircraftCreatedDomainEvent e => new AircraftCreated(e.Id), AircraftCreatedDomainEvent e => new AircraftCreated(e.Id),
SeatCreatedDomainEvent e => new SeatCreated(e.Id),
SeatReservedDomainEvent e => new SeatReserved(e.Id),
_ => null
};
}
public InternalCommand MapToInternalCommand(IDomainEvent @event)
{
return @event switch
{
FlightCreatedDomainEvent e => new CreateFlightMongoCommand(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 UpdateFlightMongoCommand(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 DeleteFlightMongoCommand(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 CreateAircraftMongoCommand(e.Id, e.Name, e.Model, e.ManufacturingYear, e.IsDeleted),
AirportCreatedDomainEvent e => new CreateAirportMongoCommand(e.Id, e.Name, e.Address, e.Code, e.IsDeleted),
SeatCreatedDomainEvent e => new CreateSeatMongoCommand(e.Id, e.SeatNumber, e.Type, e.Class, e.FlightId, e.IsDeleted),
SeatReservedDomainEvent e => new ReserveSeatMongoCommand(e.Id, e.SeatNumber, e.Type, e.Class, e.FlightId, e.IsDeleted),
_ => null _ => null
}; };
} }

View File

@ -16,8 +16,6 @@
<Folder Include="Airports\Exceptions" /> <Folder Include="Airports\Exceptions" />
<Folder Include="Data\Migrations" /> <Folder Include="Data\Migrations" />
<Folder Include="Enum" /> <Folder Include="Enum" />
<Folder Include="Flights\Features\UpdateFlight" />
<Folder Include="Seats\Features\CreateSeat" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -8,7 +8,7 @@ namespace Flight.Flights.Features.CreateFlight;
public record CreateFlightCommand(string FlightNumber, long AircraftId, long DepartureAirportId, public record CreateFlightCommand(string FlightNumber, long AircraftId, long DepartureAirportId,
DateTime DepartureDate, DateTime ArriveDate, long ArriveAirportId, DateTime DepartureDate, DateTime ArriveDate, long ArriveAirportId,
decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, decimal Price) : ICommand<FlightResponseDto> decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, decimal Price) : ICommand<FlightResponseDto>, IInternalCommand
{ {
public long Id { get; set; } = SnowFlakIdGenerator.NewId(); public long Id { get; set; } = SnowFlakIdGenerator.NewId();
} }

View File

@ -44,10 +44,6 @@ public class CreateFlightCommandHandler : ICommandHandler<CreateFlightCommand, F
var newFlight = await _flightDbContext.Flights.AddAsync(flightEntity, cancellationToken); var newFlight = await _flightDbContext.Flights.AddAsync(flightEntity, cancellationToken);
var createFlightMongoCommand = _mapper.Map<CreateFlightMongoCommand>(newFlight.Entity);
await _persistMessageProcessor.AddInternalMessageAsync(createFlightMongoCommand, cancellationToken);
return _mapper.Map<FlightResponseDto>(newFlight.Entity); return _mapper.Map<FlightResponseDto>(newFlight.Entity);
} }
} }

View File

@ -6,12 +6,8 @@ namespace Flight.Flights.Features.CreateFlight.Reads;
public class CreateFlightMongoCommand : InternalCommand public class CreateFlightMongoCommand : InternalCommand
{ {
public CreateFlightMongoCommand() public CreateFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
{ long DepartureAirportId,
}
public CreateFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate, long DepartureAirportId,
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status,
decimal Price, bool IsDeleted) decimal Price, bool IsDeleted)
{ {
@ -29,15 +25,15 @@ public class CreateFlightMongoCommand : InternalCommand
this.IsDeleted = IsDeleted; this.IsDeleted = IsDeleted;
} }
public string FlightNumber { get; init; } public string FlightNumber { get; }
public long AircraftId { get; init; } public long AircraftId { get; }
public DateTime DepartureDate { get; init; } public DateTime DepartureDate { get; }
public long DepartureAirportId { get; init; } public long DepartureAirportId { get; }
public DateTime ArriveDate { get; init; } public DateTime ArriveDate { get; }
public long ArriveAirportId { get; init; } public long ArriveAirportId { get; }
public decimal DurationMinutes { get; init; } public decimal DurationMinutes { get; }
public DateTime FlightDate { get; init; } public DateTime FlightDate { get; }
public FlightStatus Status { get; init; } public FlightStatus Status { get; }
public decimal Price { get; init; } public decimal Price { get; }
public bool IsDeleted { get; init; } public bool IsDeleted { get; }
} }

View File

@ -1,5 +1,4 @@
using System.Linq; using System.Threading;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
@ -33,7 +32,7 @@ public class CreateFlightMongoCommandHandler : ICommandHandler<CreateFlightMongo
var flightReadModel = _mapper.Map<FlightReadModel>(command); var flightReadModel = _mapper.Map<FlightReadModel>(command);
var flight = await _flightReadDbContext.Flight.AsQueryable() var flight = await _flightReadDbContext.Flight.AsQueryable()
.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken); .FirstOrDefaultAsync(x => x.Id == flightReadModel.Id, cancellationToken);
if (flight is not null) if (flight is not null)
throw new FlightAlreadyExistException(); throw new FlightAlreadyExistException();

View File

@ -3,4 +3,4 @@ using Flight.Flights.Dtos;
namespace Flight.Flights.Features.DeleteFlight; namespace Flight.Flights.Features.DeleteFlight;
public record DeleteFlightCommand(long Id) : ICommand<FlightResponseDto>; public record DeleteFlightCommand(long Id) : ICommand<FlightResponseDto>, IInternalCommand;

View File

@ -0,0 +1,39 @@
using System;
using BuildingBlocks.Core.Event;
using Flight.Flights.Models;
namespace Flight.Flights.Features.DeleteFlight.Reads;
public class DeleteFlightMongoCommand : InternalCommand
{
public DeleteFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
long DepartureAirportId,
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status,
decimal Price, bool IsDeleted)
{
this.Id = Id;
this.FlightNumber = FlightNumber;
this.AircraftId = AircraftId;
this.DepartureDate = DepartureDate;
this.DepartureAirportId = DepartureAirportId;
this.ArriveDate = ArriveDate;
this.ArriveAirportId = ArriveAirportId;
this.DurationMinutes = DurationMinutes;
this.FlightDate = FlightDate;
this.Status = Status;
this.Price = Price;
this.IsDeleted = IsDeleted;
}
public string FlightNumber { get; }
public long AircraftId { get; }
public DateTime DepartureDate { get; }
public long DepartureAirportId { get; }
public DateTime ArriveDate { get; }
public long ArriveAirportId { get; }
public decimal DurationMinutes { get; }
public DateTime FlightDate { get; }
public FlightStatus Status { get; }
public decimal Price { get; }
public bool IsDeleted { get; }
}

View File

@ -0,0 +1,48 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using Flight.Data;
using Flight.Flights.Exceptions;
using Flight.Flights.Models.Reads;
using MapsterMapper;
using MediatR;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Flight.Flights.Features.DeleteFlight.Reads;
public class DeleteFlightMongoCommandHandler : ICommandHandler<DeleteFlightMongoCommand>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public DeleteFlightMongoCommandHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(DeleteFlightMongoCommand command, CancellationToken cancellationToken)
{
Guard.Against.Null(command, nameof(command));
var flightReadModel = _mapper.Map<FlightReadModel>(command);
var flight = await _flightReadDbContext.Flight.AsQueryable()
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId, cancellationToken);
if (flight is null)
throw new FlightNotFountException();
await _flightReadDbContext.Flight.UpdateOneAsync(
x => x.FlightId == flightReadModel.FlightId,
Builders<FlightReadModel>.Update
.Set(x => x.IsDeleted, flightReadModel.IsDeleted),
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@ -1,17 +1,25 @@
using AutoMapper; using AutoMapper;
using BuildingBlocks.IdsGenerator;
using Flight.Flights.Dtos; using Flight.Flights.Dtos;
using Flight.Flights.Features.CreateFlight.Reads; using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Flights.Features.DeleteFlight.Reads;
using Flight.Flights.Features.UpdateFlight.Reads;
using Flight.Flights.Models.Reads; using Flight.Flights.Models.Reads;
using Mapster; using Mapster;
namespace Flight.Flights.Features; namespace Flight.Flights.Features;
public class FlightMappings : Profile public class FlightMappings : IRegister
{ {
public void Register(TypeAdapterConfig config) public void Register(TypeAdapterConfig config)
{ {
config.NewConfig<Models.Flight, FlightResponseDto>(); config.NewConfig<Models.Flight, FlightResponseDto>();
config.NewConfig<Models.Flight, CreateFlightMongoCommand>(); config.NewConfig<CreateFlightMongoCommand, FlightReadModel>()
config.NewConfig<CreateFlightMongoCommand, FlightReadModel>(); .Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.FlightId, s => s.Id);
config.NewConfig<UpdateFlightMongoCommand, FlightReadModel>()
.Map(d => d.FlightId, s => s.Id);
config.NewConfig<DeleteFlightMongoCommand, FlightReadModel>()
.Map(d => d.FlightId, s => s.Id);
} }
} }

View File

@ -8,20 +8,19 @@ using Flight.Data;
using Flight.Flights.Dtos; using Flight.Flights.Dtos;
using Flight.Flights.Exceptions; using Flight.Flights.Exceptions;
using MapsterMapper; using MapsterMapper;
using MediatR; using MongoDB.Driver;
using Microsoft.EntityFrameworkCore;
namespace Flight.Flights.Features.GetAvailableFlights; namespace Flight.Flights.Features.GetAvailableFlights;
public class GetAvailableFlightsQueryHandler : IQueryHandler<GetAvailableFlightsQuery, IEnumerable<FlightResponseDto>> public class GetAvailableFlightsQueryHandler : IQueryHandler<GetAvailableFlightsQuery, IEnumerable<FlightResponseDto>>
{ {
private readonly FlightDbContext _flightDbContext;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly FlightReadDbContext _flightReadDbContext;
public GetAvailableFlightsQueryHandler(IMapper mapper, FlightDbContext flightDbContext) public GetAvailableFlightsQueryHandler(IMapper mapper, FlightReadDbContext flightReadDbContext)
{ {
_mapper = mapper; _mapper = mapper;
_flightDbContext = flightDbContext; _flightReadDbContext = flightReadDbContext;
} }
public async Task<IEnumerable<FlightResponseDto>> Handle(GetAvailableFlightsQuery query, public async Task<IEnumerable<FlightResponseDto>> Handle(GetAvailableFlightsQuery query,
@ -29,7 +28,8 @@ public class GetAvailableFlightsQueryHandler : IQueryHandler<GetAvailableFlights
{ {
Guard.Against.Null(query, nameof(query)); Guard.Against.Null(query, nameof(query));
var flight = await _flightDbContext.Flights.ToListAsync(cancellationToken); var flight = (await _flightReadDbContext.Flight.AsQueryable().ToListAsync(cancellationToken))
.Where(x => !x.IsDeleted);
if (!flight.Any()) if (!flight.Any())
throw new FlightNotFountException(); throw new FlightNotFountException();

View File

@ -10,18 +10,16 @@ using Swashbuckle.AspNetCore.Annotations;
namespace Flight.Flights.Features.GetFlightById; namespace Flight.Flights.Features.GetFlightById;
[Route(BaseApiPath + "/flight")] [Route(BaseApiPath + "/flight")]
public class GetFlightByIdEndpoint: BaseController public class GetFlightByIdEndpoint : BaseController
{ {
// [Authorize] [Authorize]
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerOperation(Summary = "Get flight by id", Description = "Get flight by id")] [SwaggerOperation(Summary = "Get flight by id", Description = "Get flight by id")]
public async Task<ActionResult> GetById([FromRoute] GetFlightByIdQuery query, CancellationToken cancellationToken) public async Task<ActionResult> GetById([FromRoute] GetFlightByIdQuery query, CancellationToken cancellationToken)
{ {
throw new Exception();
var result = await Mediator.Send(query, cancellationToken); var result = await Mediator.Send(query, cancellationToken);
return Ok(result); return Ok(result);
} }
} }

View File

@ -6,20 +6,20 @@ using Flight.Data;
using Flight.Flights.Dtos; using Flight.Flights.Dtos;
using Flight.Flights.Exceptions; using Flight.Flights.Exceptions;
using MapsterMapper; using MapsterMapper;
using MediatR; using MongoDB.Driver;
using Microsoft.EntityFrameworkCore; using MongoDB.Driver.Linq;
namespace Flight.Flights.Features.GetFlightById; namespace Flight.Flights.Features.GetFlightById;
public class GetFlightByIdQueryHandler : IQueryHandler<GetFlightByIdQuery, FlightResponseDto> public class GetFlightByIdQueryHandler : IQueryHandler<GetFlightByIdQuery, FlightResponseDto>
{ {
private readonly FlightDbContext _flightDbContext;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly FlightReadDbContext _flightReadDbContext;
public GetFlightByIdQueryHandler(IMapper mapper, FlightDbContext flightDbContext) public GetFlightByIdQueryHandler(IMapper mapper, FlightReadDbContext flightReadDbContext)
{ {
_mapper = mapper; _mapper = mapper;
_flightDbContext = flightDbContext; _flightReadDbContext = flightReadDbContext;
} }
public async Task<FlightResponseDto> Handle(GetFlightByIdQuery query, CancellationToken cancellationToken) public async Task<FlightResponseDto> Handle(GetFlightByIdQuery query, CancellationToken cancellationToken)
@ -27,7 +27,7 @@ public class GetFlightByIdQueryHandler : IQueryHandler<GetFlightByIdQuery, Fligh
Guard.Against.Null(query, nameof(query)); Guard.Against.Null(query, nameof(query));
var flight = var flight =
await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.Id == query.Id, cancellationToken); await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync(x => x.Id == query.Id, cancellationToken);
if (flight is null) if (flight is null)
throw new FlightNotFountException(); throw new FlightNotFountException();

View File

@ -0,0 +1,39 @@
using System;
using BuildingBlocks.Core.Event;
using Flight.Flights.Models;
namespace Flight.Flights.Features.UpdateFlight.Reads;
public class UpdateFlightMongoCommand : InternalCommand
{
public UpdateFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
long DepartureAirportId,
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status,
decimal Price, bool IsDeleted)
{
this.Id = Id;
this.FlightNumber = FlightNumber;
this.AircraftId = AircraftId;
this.DepartureDate = DepartureDate;
this.DepartureAirportId = DepartureAirportId;
this.ArriveDate = ArriveDate;
this.ArriveAirportId = ArriveAirportId;
this.DurationMinutes = DurationMinutes;
this.FlightDate = FlightDate;
this.Status = Status;
this.Price = Price;
this.IsDeleted = IsDeleted;
}
public string FlightNumber { get; }
public long AircraftId { get; }
public DateTime DepartureDate { get; }
public long DepartureAirportId { get; }
public DateTime ArriveDate { get; }
public long ArriveAirportId { get; }
public decimal DurationMinutes { get; }
public DateTime FlightDate { get; }
public FlightStatus Status { get; }
public decimal Price { get; }
public bool IsDeleted { get; }
}

View File

@ -0,0 +1,60 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using Flight.Data;
using Flight.Flights.Exceptions;
using Flight.Flights.Models.Reads;
using MapsterMapper;
using MediatR;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Flight.Flights.Features.UpdateFlight.Reads;
public class UpdateFlightMongoCommandHandler : ICommandHandler<UpdateFlightMongoCommand>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public UpdateFlightMongoCommandHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(UpdateFlightMongoCommand command, CancellationToken cancellationToken)
{
Guard.Against.Null(command, nameof(command));
var flightReadModel = _mapper.Map<FlightReadModel>(command);
var flight = await _flightReadDbContext.Flight.AsQueryable()
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId, cancellationToken);
if (flight is null)
throw new FlightNotFountException();
await _flightReadDbContext.Flight.UpdateOneAsync(
x => x.FlightId == flightReadModel.FlightId,
Builders<FlightReadModel>.Update
.Set(x => x.Id, flightReadModel.Id)
.Set(x => x.Price, flightReadModel.Price)
.Set(x => x.ArriveDate, flightReadModel.ArriveDate)
.Set(x => x.AircraftId, flightReadModel.AircraftId)
.Set(x => x.DurationMinutes, flightReadModel.DurationMinutes)
.Set(x => x.DepartureDate, flightReadModel.DepartureDate)
.Set(x => x.FlightDate, flightReadModel.FlightDate)
.Set(x => x.FlightId, flightReadModel.FlightId)
.Set(x => x.FlightNumber, flightReadModel.FlightNumber)
.Set(x => x.IsDeleted, flightReadModel.IsDeleted)
.Set(x => x.Status, flightReadModel.Status)
.Set(x => x.ArriveAirportId, flightReadModel.ArriveAirportId)
.Set(x => x.DepartureAirportId, flightReadModel.DepartureAirportId),
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@ -7,7 +7,7 @@ using MediatR;
namespace Flight.Flights.Features.UpdateFlight; namespace Flight.Flights.Features.UpdateFlight;
public record UpdateFlightCommand : ICommand<FlightResponseDto>, IInvalidateCacheRequest public record UpdateFlightCommand : ICommand<FlightResponseDto>, IInvalidateCacheRequest, IInternalCommand
{ {
public long Id { get; init; } public long Id { get; init; }
public string FlightNumber { get; init; } public string FlightNumber { get; init; }

View File

@ -7,6 +7,7 @@ namespace Flight.Flights.Models.Reads;
public class FlightReadModel public class FlightReadModel
{ {
public long Id { get; init; } public long Id { get; init; }
public long FlightId { get; set; }
public string FlightNumber { get; init; } public string FlightNumber { get; init; }
public long AircraftId { get; init; } public long AircraftId { get; init; }
public DateTime DepartureDate { get; init; } public DateTime DepartureDate { get; init; }

View File

@ -3,4 +3,4 @@ using Flight.Seats.Models;
namespace Flight.Seats.Events; namespace Flight.Seats.Events;
public record SeatCreatedDomainEvent(long Id, string SeatNumber, SeatType Type, SeatClass Class, long FlightId) : IDomainEvent; public record SeatCreatedDomainEvent(long Id, string SeatNumber, SeatType Type, SeatClass Class, long FlightId, bool IsDeleted) : IDomainEvent;

View File

@ -0,0 +1,6 @@
using BuildingBlocks.Core.Event;
using Flight.Seats.Models;
namespace Flight.Seats.Events;
public record SeatReservedDomainEvent(long Id, string SeatNumber, SeatType Type, SeatClass Class, long FlightId, bool IsDeleted) : IDomainEvent;

View File

@ -1,11 +1,11 @@
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using Flight.Seats.Dtos; using Flight.Seats.Dtos;
using Flight.Seats.Models; using Flight.Seats.Models;
using MediatR;
namespace Flight.Seats.Features.CreateSeat; namespace Flight.Seats.Features.CreateSeat;
public record CreateSeatCommand(string SeatNumber, SeatType Type, SeatClass Class, long FlightId) : IRequest<SeatResponseDto> public record CreateSeatCommand(string SeatNumber, SeatType Type, SeatClass Class, long FlightId) : ICommand<SeatResponseDto>, IInternalCommand
{ {
public long Id { get; set; } = SnowFlakIdGenerator.NewId(); public long Id { get; set; } = SnowFlakIdGenerator.NewId();
} }

View File

@ -40,8 +40,6 @@ public class CreateSeatCommandHandler : IRequestHandler<CreateSeatCommand, SeatR
var newSeat = await _flightDbContext.Seats.AddAsync(seatEntity, cancellationToken); var newSeat = await _flightDbContext.Seats.AddAsync(seatEntity, cancellationToken);
await _flightDbContext.SaveChangesAsync(cancellationToken);
return _mapper.Map<SeatResponseDto>(newSeat.Entity); return _mapper.Map<SeatResponseDto>(newSeat.Entity);
} }
} }

View File

@ -0,0 +1,25 @@
using BuildingBlocks.Core.Event;
using Flight.Seats.Models;
namespace Flight.Seats.Features.CreateSeat.Reads;
public class CreateSeatMongoCommand : InternalCommand
{
public CreateSeatMongoCommand(long id, string seatNumber, SeatType type, SeatClass @class,
long flightId, bool isDeleted)
{
Id = id;
SeatNumber = seatNumber;
Type = type;
Class = @class;
FlightId = flightId;
IsDeleted = isDeleted;
}
public long Id { get; }
public string SeatNumber { get; }
public SeatType Type { get; }
public SeatClass Class { get; }
public long FlightId { get; }
public bool IsDeleted { get; }
}

View File

@ -0,0 +1,44 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using Flight.Data;
using Flight.Seats.Exceptions;
using Flight.Seats.Models.Reads;
using MapsterMapper;
using MediatR;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Flight.Seats.Features.CreateSeat.Reads;
public class CreateSeatMongoCommandHandler : ICommandHandler<CreateSeatMongoCommand>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public CreateSeatMongoCommandHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(CreateSeatMongoCommand command, CancellationToken cancellationToken)
{
Guard.Against.Null(command, nameof(command));
var seatReadModel = _mapper.Map<SeatReadModel>(command);
var seat = await _flightReadDbContext.Seat.AsQueryable()
.FirstOrDefaultAsync(x => x.Id == seatReadModel.Id, cancellationToken);
if (seat is not null)
throw new SeatAlreadyExistException();
await _flightReadDbContext.Seat.InsertOneAsync(seatReadModel, cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using BuildingBlocks.Core.CQRS;
using Flight.Seats.Dtos; using Flight.Seats.Dtos;
using MediatR; using MediatR;
namespace Flight.Seats.Features.GetAvailableSeats; namespace Flight.Seats.Features.GetAvailableSeats;
public record GetAvailableSeatsQuery(long FlightId) : IRequest<IEnumerable<SeatResponseDto>>; public record GetAvailableSeatsQuery(long FlightId) : IQuery<IEnumerable<SeatResponseDto>>;

View File

@ -8,19 +8,19 @@ using Flight.Seats.Dtos;
using Flight.Seats.Exceptions; using Flight.Seats.Exceptions;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore; using MongoDB.Driver;
namespace Flight.Seats.Features.GetAvailableSeats; namespace Flight.Seats.Features.GetAvailableSeats;
public class GetAvailableSeatsQueryHandler : IRequestHandler<GetAvailableSeatsQuery, IEnumerable<SeatResponseDto>> public class GetAvailableSeatsQueryHandler : IRequestHandler<GetAvailableSeatsQuery, IEnumerable<SeatResponseDto>>
{ {
private readonly FlightDbContext _flightDbContext;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly FlightReadDbContext _flightReadDbContext;
public GetAvailableSeatsQueryHandler(IMapper mapper, FlightDbContext flightDbContext) public GetAvailableSeatsQueryHandler(IMapper mapper, FlightReadDbContext flightReadDbContext)
{ {
_mapper = mapper; _mapper = mapper;
_flightDbContext = flightDbContext; _flightReadDbContext = flightReadDbContext;
} }
@ -28,7 +28,8 @@ public class GetAvailableSeatsQueryHandler : IRequestHandler<GetAvailableSeatsQu
{ {
Guard.Against.Null(query, nameof(query)); Guard.Against.Null(query, nameof(query));
var seats = await _flightDbContext.Seats.Where(x => x.FlightId == query.FlightId).ToListAsync(cancellationToken); var seats = (await _flightReadDbContext.Seat.AsQueryable().ToListAsync(cancellationToken))
.Where(x => !x.IsDeleted);
if (!seats.Any()) if (!seats.Any())
throw new AllSeatsFullException(); throw new AllSeatsFullException();

View File

@ -0,0 +1,25 @@
using BuildingBlocks.Core.Event;
using Flight.Seats.Models;
namespace Flight.Seats.Features.ReserveSeat.Reads;
public class ReserveSeatMongoCommand : InternalCommand
{
public ReserveSeatMongoCommand(long id, string seatNumber, SeatType type, SeatClass @class, long flightId,
bool isDeleted)
{
Id = id;
SeatNumber = seatNumber;
Type = type;
Class = @class;
FlightId = flightId;
IsDeleted = isDeleted;
}
public long Id { get; }
public string SeatNumber { get; }
public SeatType Type { get; }
public SeatClass Class { get; }
public long FlightId { get; }
public bool IsDeleted { get; }
}

View File

@ -0,0 +1,40 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS;
using Flight.Data;
using Flight.Seats.Models.Reads;
using MapsterMapper;
using MediatR;
using MongoDB.Driver;
namespace Flight.Seats.Features.ReserveSeat.Reads;
public class ReserveSeatMongoCommandHandler : ICommandHandler<ReserveSeatMongoCommand>
{
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public ReserveSeatMongoCommandHandler(
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task<Unit> Handle(ReserveSeatMongoCommand command, CancellationToken cancellationToken)
{
Guard.Against.Null(command, nameof(command));
var seatReadModel = _mapper.Map<SeatReadModel>(command);
await _flightReadDbContext.Seat.UpdateOneAsync(
x => x.SeatId == seatReadModel.SeatId,
Builders<SeatReadModel>.Update
.Set(x => x.IsDeleted, seatReadModel.IsDeleted),
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@ -1,6 +1,6 @@
using BuildingBlocks.Core.CQRS;
using Flight.Seats.Dtos; using Flight.Seats.Dtos;
using MediatR;
namespace Flight.Seats.Features.ReserveSeat; namespace Flight.Seats.Features.ReserveSeat;
public record ReserveSeatCommand(long FlightId, string SeatNumber) : IRequest<SeatResponseDto>; public record ReserveSeatCommand(long FlightId, string SeatNumber) : ICommand<SeatResponseDto>, IInternalCommand;

View File

@ -1,5 +1,9 @@
using BuildingBlocks.IdsGenerator;
using Flight.Seats.Dtos; using Flight.Seats.Dtos;
using Flight.Seats.Features.CreateSeat.Reads;
using Flight.Seats.Features.ReserveSeat.Reads;
using Flight.Seats.Models; using Flight.Seats.Models;
using Flight.Seats.Models.Reads;
using Mapster; using Mapster;
namespace Flight.Seats.Features; namespace Flight.Seats.Features;
@ -9,6 +13,10 @@ public class SeatMappings : IRegister
public void Register(TypeAdapterConfig config) public void Register(TypeAdapterConfig config)
{ {
config.NewConfig<Seat, SeatResponseDto>(); config.NewConfig<Seat, SeatResponseDto>();
config.NewConfig<CreateSeatMongoCommand, SeatReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.SeatId, s => s.Id);
config.NewConfig<ReserveSeatMongoCommand, SeatReadModel>()
.Map(d => d.SeatId, s => s.Id);
} }
} }

View File

@ -0,0 +1,12 @@
namespace Flight.Seats.Models.Reads;
public class SeatReadModel
{
public long Id { get; init; }
public long SeatId { get; init; }
public string SeatNumber { get; init; }
public SeatType Type { get; init; }
public SeatClass Class { get; init; }
public long FlightId { get; init; }
public bool IsDeleted { get; init; }
}

View File

@ -1,12 +1,14 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Core.Model; using BuildingBlocks.Core.Model;
using Flight.Seats.Events;
namespace Flight.Seats.Models; namespace Flight.Seats.Models;
public class Seat : Aggregate<long> public class Seat : Aggregate<long>
{ {
public static Seat Create(long id, string seatNumber, SeatType type, SeatClass @class, long flightId) public static Seat Create(long id, string seatNumber, SeatType type, SeatClass @class, long flightId,
bool isDeleted = false)
{ {
var seat = new Seat() var seat = new Seat()
{ {
@ -14,9 +16,20 @@ public class Seat : Aggregate<long>
Class = @class, Class = @class,
Type = type, Type = type,
SeatNumber = seatNumber, SeatNumber = seatNumber,
FlightId = flightId FlightId = flightId,
IsDeleted = isDeleted
}; };
var @event = new SeatCreatedDomainEvent(
seat.Id,
seat.SeatNumber,
seat.Type,
seat.Class,
seat.FlightId,
isDeleted);
seat.AddDomainEvent(@event);
return seat; return seat;
} }
@ -24,6 +37,17 @@ public class Seat : Aggregate<long>
{ {
seat.IsDeleted = true; seat.IsDeleted = true;
seat.LastModified = DateTime.Now; seat.LastModified = DateTime.Now;
var @event = new SeatReservedDomainEvent(
seat.Id,
seat.SeatNumber,
seat.Type,
seat.Class,
seat.FlightId,
seat.IsDeleted);
seat.AddDomainEvent(@event);
return Task.FromResult(this); return Task.FromResult(this);
} }

View File

@ -63,10 +63,10 @@ app.UseRouting();
app.UseHttpMetrics(); app.UseHttpMetrics();
app.UseProblemDetails(); app.UseProblemDetails();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseCustomHealthCheck();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseIdentityServer(); app.UseIdentityServer();
app.UseCustomHealthCheck();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {

View File

@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
@ -7,12 +5,15 @@ namespace Identity;
public sealed class EventMapper : IEventMapper public sealed class EventMapper : IEventMapper
{ {
public IEnumerable<IIntegrationEvent> MapAll(IEnumerable<IDomainEvent> events) public IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event)
{ {
return events.Select(Map); return @event switch
{
_ => null
};
} }
public IIntegrationEvent Map(IDomainEvent @event) public InternalCommand MapToInternalCommand(IDomainEvent @event)
{ {
return @event switch return @event switch
{ {

View File

@ -50,7 +50,7 @@ public class RegisterNewUserCommandHandler : ICommandHandler<RegisterNewUserComm
throw new RegisterIdentityUserException(string.Join(',', roleResult.Errors.Select(e => e.Description))); throw new RegisterIdentityUserException(string.Join(',', roleResult.Errors.Select(e => e.Description)));
await _eventDispatcher.SendAsync(new UserCreated(applicationUser.Id, applicationUser.FirstName + " " + applicationUser.LastName, await _eventDispatcher.SendAsync(new UserCreated(applicationUser.Id, applicationUser.FirstName + " " + applicationUser.LastName,
applicationUser.PassPortNumber), cancellationToken); applicationUser.PassPortNumber), cancellationToken: cancellationToken);
return new RegisterNewUserResponseDto return new RegisterNewUserResponseDto
{ {

View File

@ -5,12 +5,15 @@ namespace Passenger;
public sealed class EventMapper : IEventMapper public sealed class EventMapper : IEventMapper
{ {
public IEnumerable<IIntegrationEvent> MapAll(IEnumerable<IDomainEvent> events) public IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event)
{ {
return events.Select(Map); return @event switch
{
_ => null
};
} }
public IIntegrationEvent Map(IDomainEvent @event) public InternalCommand MapToInternalCommand(IDomainEvent @event)
{ {
return @event switch return @event switch
{ {