From aab226ae7a383e567b6e39438e1d594d264b8a6c Mon Sep 17 00:00:00 2001 From: meysamhadeli Date: Fri, 17 Jun 2022 01:44:05 +0430 Subject: [PATCH] Add Outbox and Internal Command --- .editorconfig | 2 +- .../EventBus.Messages/FlighContracts.cs | 2 +- .../EventBus.Messages/IdentityContracts.cs | 2 +- .../EventBus.Messages/ReservationContracts.cs | 2 +- src/BuildingBlocks/Core/CQRS/ICommand.cs | 12 + .../Core/CQRS/ICommandHandler.cs | 14 + src/BuildingBlocks/Core/CQRS/IQuery.cs | 8 + src/BuildingBlocks/Core/CQRS/IQueryHandler.cs | 9 + .../{Domain => Core}/Event/EventType.cs | 2 +- src/BuildingBlocks/Core/Event/IDomainEvent.cs | 6 + .../{Domain => Core}/Event/IEvent.cs | 3 +- .../Event/IHaveIntegrationEvent.cs | 2 +- .../Event/IIntegrationEvent.cs | 3 +- .../Core/Event/IInternalCommand.cs | 10 + .../Core/Event/InternalCommand.cs | 13 + .../Core/Event/MessageEnvelope.cs | 26 ++ .../EventDispatcher.cs} | 79 ++--- .../IEventDispatcher.cs} | 6 +- .../{Domain => Core}/IEventMapper.cs | 4 +- .../IntegrationEventWrapper.cs | 4 +- .../{Domain => Core}/Model/Aggregate.cs | 4 +- .../{Domain => Core}/Model/Entity.cs | 2 +- .../{Domain => Core}/Model/IAggregate.cs | 4 +- .../{Domain => Core}/Model/IEntity.cs | 2 +- .../Domain/Event/IDomainEvent.cs | 8 - src/BuildingBlocks/EFCore/AppDbContextBase.cs | 19 +- src/BuildingBlocks/EFCore/EfTxBehavior.cs | 10 +- src/BuildingBlocks/EFCore/Extensions.cs | 2 +- src/BuildingBlocks/EFCore/IDbContext.cs | 4 +- .../Events/AggregateEventSourcing.cs | 4 +- .../Events/AggregateStreamExtensions.cs | 3 +- .../Events/IAggregateEventSourcing.cs | 4 +- .../EventStoreDB/Events/IEventHandler.cs | 2 +- .../EventStoreDB/Events/IExternalEvent.cs | 2 +- .../EventStoreDB/Events/StreamEvent.cs | 2 +- .../Repository/RepositoryExtensions.cs | 1 - ...StoreDBSubscriptionCheckpointRepository.cs | 2 +- src/BuildingBlocks/MassTransit/Extensions.cs | 2 +- .../MessageProcessor/Extensions.cs | 21 ++ .../IPersistMessageProcessor.cs | 31 ++ .../MessageProcessor/MessageDeliveryType.cs | 9 + .../MessageProcessor/MessageStatus.cs | 7 + .../MessageProcessor/PersistMessage.cs | 33 ++ .../PersistMessageBackgroundService.cs | 59 ++++ .../MessageProcessor/PersistMessageOptions.cs | 7 + .../PersistMessageProcessor.cs | 181 ++++++++++ src/BuildingBlocks/Mongo/Extensions.cs | 2 - src/BuildingBlocks/Mongo/IMongoRepository.cs | 2 +- src/BuildingBlocks/Mongo/IRepository.cs | 2 +- src/BuildingBlocks/Mongo/MongoRepository.cs | 2 +- src/BuildingBlocks/Utils/TypeProvider.cs | 72 ++++ .../src/Booking.Api/Booking.Api.csproj | 2 +- .../Booking/src/Booking.Api/Program.cs | 6 +- .../Booking/src/Booking.Api/appsettings.json | 4 + .../src/Booking.Api/appsettings.test.json | 23 ++ .../Domain/BookingCreatedDomainEvent.cs | 2 +- .../CreateBooking/CreateBookingCommand.cs | 5 +- .../CreateBookingCommandHandler.cs | 3 +- .../src/Booking/Booking/Models/Booking.cs | 1 - .../src/Booking/Data/BookingDbContext.cs | 2 + .../Configurations/BookingConfiguration.cs | 2 +- .../PersistMessageConfiguration.cs | 42 +++ ...0616121920_Add-PersistMessages.Designer.cs | 152 ++++++++ .../20220616121920_Add-PersistMessages.cs | 38 ++ .../BookingDbContextModelSnapshot.cs | 35 ++ .../Booking/src/Booking/EventMapper.cs | 4 +- .../IntegrationTest/IntegrationTestFixture.cs | 3 +- src/Services/Flight/src/Flight.Api/Program.cs | 8 +- .../Flight/src/Flight.Api/appsettings.json | 8 + .../src/Flight.Api/appsettings.test.json | 4 + .../Events/AircraftCreatedDomainEvent.cs | 2 +- .../src/Flight/Aircrafts/Models/Aircraft.cs | 2 +- .../Events/AirportCreatedDomainEvent.cs | 2 +- .../src/Flight/Airports/Models/Airport.cs | 2 +- .../Configurations/AircraftConfiguration.cs | 2 +- .../Configurations/AirportConfiguration.cs | 2 +- .../Configurations/FlightConfiguration.cs | 2 +- .../PersistMessageConfiguration.cs | 43 +++ .../Data/Configurations/SeatConfiguration.cs | 2 +- .../Flight/src/Flight/Data/FlightDbContext.cs | 1 + ...0616121204_Add-PersistMessages.Designer.cs | 266 ++++++++++++++ .../20220616121204_Add-PersistMessages.cs | 38 ++ .../FlightDbContextModelSnapshot.cs | 35 ++ src/Services/Flight/src/Flight/EventMapper.cs | 4 +- .../Events/Domain/FlightCreatedDomainEvent.cs | 2 +- .../Events/Domain/FlightDeletedDomainEvent.cs | 2 +- .../Events/Domain/FlightUpdatedDomainEvent.cs | 2 +- .../CreateFlight/CreateFlightCommand.cs | 4 +- .../CreateFlightCommandHandler.cs | 23 +- .../Reads/CreateFlightMongoCommand.cs | 43 +++ .../Reads/CreateFlightMongoCommandHandler.cs | 35 ++ .../DeleteFlight/DeleteFlightCommand.cs | 6 +- .../DeleteFlightCommandHandler.cs | 3 +- .../Flight/Flights/Features/FlightMappings.cs | 6 +- .../GetAvailableFlightsQuery.cs | 3 +- .../GetAvailableFlightsQueryHandler.cs | 3 +- .../GetFlightById/GetFlightByIdQuery.cs | 4 +- .../GetFlightByIdQueryHandler.cs | 3 +- .../UpdateFlight/UpdateFlightCommand.cs | 3 +- .../UpdateFlightCommandHandler.cs | 3 +- .../src/Flight/Flights/Models/Flight.cs | 2 +- .../Seats/Events/SeatCreatedDomainEvent.cs | 2 +- .../Flight/src/Flight/Seats/Models/Seat.cs | 2 +- .../IntegrationTest/IntegrationTestFixture.cs | 5 +- .../CreateFlightCommandHandlerTests.cs | 5 +- .../Identity/src/Identity.Api/Program.cs | 7 +- .../src/Identity.Api/appsettings.json | 4 + .../src/Identity.Api/appsettings.test.json | 4 + .../PersistMessageConfiguration.cs | 43 +++ .../src/Identity/Data/IdentityContext.cs | 11 +- ...0616123336_Add-PersistMessages.Designer.cs | 325 ++++++++++++++++++ .../20220616123336_Add-PersistMessages.cs | 82 +++++ .../IdentityContextModelSnapshot.cs | 65 +--- .../Identity/src/Identity/EventMapper.cs | 4 +- .../Identity/src/Identity/Identity.csproj | 2 +- .../RegisterNewUser/RegisterNewUserCommand.cs | 5 +- .../RegisterNewUserCommandHandler.cs | 13 +- .../IntegrationTest/IntegrationTestFixture.cs | 3 +- .../Passenger/src/Passenger.Api/Program.cs | 6 +- .../src/Passenger.Api/appsettings.json | 4 + .../src/Passenger.Api/appsettings.test.json | 4 + .../PersistMessageConfiguration.cs | 42 +++ ...0616122705_Add-PersistMessages.Designer.cs | 104 ++++++ .../20220616122705_Add-PersistMessages.cs | 38 ++ .../PassengerDbContextModelSnapshot.cs | 35 ++ .../src/Passenger/Data/PassengerDbContext.cs | 2 + .../Passenger/src/Passenger/EventMapper.cs | 4 +- .../Domain/PassengerCreatedDomainEvent.cs | 2 +- .../CompleteRegisterPassengerCommand.cs | 4 +- ...CompleteRegisterPassengerCommandHandler.cs | 3 +- .../GetPassengerById/GetPassengerQueryById.cs | 4 +- .../GetPassengerQueryByIdHandler.cs | 3 +- .../Passenger/Passengers/Models/Passenger.cs | 2 +- .../IntegrationTest/IntegrationTestFixture.cs | 3 +- 134 files changed, 2202 insertions(+), 248 deletions(-) create mode 100644 src/BuildingBlocks/Core/CQRS/ICommand.cs create mode 100644 src/BuildingBlocks/Core/CQRS/ICommandHandler.cs create mode 100644 src/BuildingBlocks/Core/CQRS/IQuery.cs create mode 100644 src/BuildingBlocks/Core/CQRS/IQueryHandler.cs rename src/BuildingBlocks/{Domain => Core}/Event/EventType.cs (67%) create mode 100644 src/BuildingBlocks/Core/Event/IDomainEvent.cs rename src/BuildingBlocks/{Domain => Core}/Event/IEvent.cs (78%) rename src/BuildingBlocks/{Domain => Core}/Event/IHaveIntegrationEvent.cs (52%) rename src/BuildingBlocks/{Domain => Core}/Event/IIntegrationEvent.cs (57%) create mode 100644 src/BuildingBlocks/Core/Event/IInternalCommand.cs create mode 100644 src/BuildingBlocks/Core/Event/InternalCommand.cs create mode 100644 src/BuildingBlocks/Core/Event/MessageEnvelope.cs rename src/BuildingBlocks/{Domain/BusPublisher.cs => Core/EventDispatcher.cs} (64%) rename src/BuildingBlocks/{Domain/IBusPublisher.cs => Core/IEventDispatcher.cs} (82%) rename src/BuildingBlocks/{Domain => Core}/IEventMapper.cs (70%) rename src/BuildingBlocks/{Domain => Core}/IntegrationEventWrapper.cs (68%) rename src/BuildingBlocks/{Domain => Core}/Model/Aggregate.cs (90%) rename src/BuildingBlocks/{Domain => Core}/Model/Entity.cs (87%) rename src/BuildingBlocks/{Domain => Core}/Model/IAggregate.cs (75%) rename src/BuildingBlocks/{Domain => Core}/Model/IEntity.cs (86%) delete mode 100644 src/BuildingBlocks/Domain/Event/IDomainEvent.cs create mode 100644 src/BuildingBlocks/MessageProcessor/Extensions.cs create mode 100644 src/BuildingBlocks/MessageProcessor/IPersistMessageProcessor.cs create mode 100644 src/BuildingBlocks/MessageProcessor/MessageDeliveryType.cs create mode 100644 src/BuildingBlocks/MessageProcessor/MessageStatus.cs create mode 100644 src/BuildingBlocks/MessageProcessor/PersistMessage.cs create mode 100644 src/BuildingBlocks/MessageProcessor/PersistMessageBackgroundService.cs create mode 100644 src/BuildingBlocks/MessageProcessor/PersistMessageOptions.cs create mode 100644 src/BuildingBlocks/MessageProcessor/PersistMessageProcessor.cs create mode 100644 src/Services/Booking/src/Booking.Api/appsettings.test.json create mode 100644 src/Services/Booking/src/Booking/Data/Configurations/PersistMessageConfiguration.cs create mode 100644 src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.Designer.cs create mode 100644 src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.cs create mode 100644 src/Services/Flight/src/Flight/Data/Configurations/PersistMessageConfiguration.cs create mode 100644 src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.Designer.cs create mode 100644 src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.cs create mode 100644 src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommand.cs create mode 100644 src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommandHandler.cs create mode 100644 src/Services/Identity/src/Identity/Data/Configurations/PersistMessageConfiguration.cs create mode 100644 src/Services/Identity/src/Identity/Data/Migrations/20220616123336_Add-PersistMessages.Designer.cs create mode 100644 src/Services/Identity/src/Identity/Data/Migrations/20220616123336_Add-PersistMessages.cs create mode 100644 src/Services/Passenger/src/Passenger/Data/Configurations/PersistMessageConfiguration.cs create mode 100644 src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.Designer.cs create mode 100644 src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.cs diff --git a/.editorconfig b/.editorconfig index e2d4cfd..58dba47 100644 --- a/.editorconfig +++ b/.editorconfig @@ -269,7 +269,7 @@ dotnet_diagnostic.S3903.severity = None # # MA0004: Use Task.ConfigureAwait(false) -dotnet_diagnostic.MA0004.severity = Suggestion +dotnet_diagnostic.MA0004.severity = None # MA0049: Type name should not match containing namespace dotnet_diagnostic.MA0049.severity = Suggestion diff --git a/src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs b/src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs index 7ba0425..c6d9bba 100644 --- a/src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs +++ b/src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace BuildingBlocks.Contracts.EventBus.Messages; diff --git a/src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs b/src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs index 20e45f8..c39cba5 100644 --- a/src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs +++ b/src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace BuildingBlocks.Contracts.EventBus.Messages; diff --git a/src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs b/src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs index 81cd928..ca7bdfb 100644 --- a/src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs +++ b/src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace BuildingBlocks.Contracts.EventBus.Messages; diff --git a/src/BuildingBlocks/Core/CQRS/ICommand.cs b/src/BuildingBlocks/Core/CQRS/ICommand.cs new file mode 100644 index 0000000..a70656d --- /dev/null +++ b/src/BuildingBlocks/Core/CQRS/ICommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace BuildingBlocks.Core.CQRS; + +public interface ICommand : ICommand +{ +} + +public interface ICommand : IRequest + where T : notnull +{ +} diff --git a/src/BuildingBlocks/Core/CQRS/ICommandHandler.cs b/src/BuildingBlocks/Core/CQRS/ICommandHandler.cs new file mode 100644 index 0000000..0dd760a --- /dev/null +++ b/src/BuildingBlocks/Core/CQRS/ICommandHandler.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace BuildingBlocks.Core.CQRS; + +public interface ICommandHandler : ICommandHandler + where TCommand : ICommand +{ +} + +public interface ICommandHandler : IRequestHandler + where TCommand : ICommand + where TResponse : notnull +{ +} diff --git a/src/BuildingBlocks/Core/CQRS/IQuery.cs b/src/BuildingBlocks/Core/CQRS/IQuery.cs new file mode 100644 index 0000000..b967f92 --- /dev/null +++ b/src/BuildingBlocks/Core/CQRS/IQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace BuildingBlocks.Core.CQRS; + +public interface IQuery : IRequest + where T : notnull +{ +} diff --git a/src/BuildingBlocks/Core/CQRS/IQueryHandler.cs b/src/BuildingBlocks/Core/CQRS/IQueryHandler.cs new file mode 100644 index 0000000..cd1a7e9 --- /dev/null +++ b/src/BuildingBlocks/Core/CQRS/IQueryHandler.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace BuildingBlocks.Core.CQRS; + +public interface IQueryHandler : IRequestHandler + where TQuery : IQuery + where TResponse : notnull +{ +} diff --git a/src/BuildingBlocks/Domain/Event/EventType.cs b/src/BuildingBlocks/Core/Event/EventType.cs similarity index 67% rename from src/BuildingBlocks/Domain/Event/EventType.cs rename to src/BuildingBlocks/Core/Event/EventType.cs index c18ccd0..1ec808c 100644 --- a/src/BuildingBlocks/Domain/Event/EventType.cs +++ b/src/BuildingBlocks/Core/Event/EventType.cs @@ -1,4 +1,4 @@ -namespace BuildingBlocks.Domain.Event; +namespace BuildingBlocks.Core.Event; [Flags] public enum EventType diff --git a/src/BuildingBlocks/Core/Event/IDomainEvent.cs b/src/BuildingBlocks/Core/Event/IDomainEvent.cs new file mode 100644 index 0000000..3f3afed --- /dev/null +++ b/src/BuildingBlocks/Core/Event/IDomainEvent.cs @@ -0,0 +1,6 @@ +namespace BuildingBlocks.Core.Event; + +public interface IDomainEvent : IEvent +{ + +} diff --git a/src/BuildingBlocks/Domain/Event/IEvent.cs b/src/BuildingBlocks/Core/Event/IEvent.cs similarity index 78% rename from src/BuildingBlocks/Domain/Event/IEvent.cs rename to src/BuildingBlocks/Core/Event/IEvent.cs index a7d67c3..5bb222a 100644 --- a/src/BuildingBlocks/Domain/Event/IEvent.cs +++ b/src/BuildingBlocks/Core/Event/IEvent.cs @@ -1,7 +1,6 @@ -using MassTransit; using MediatR; -namespace BuildingBlocks.Domain.Event; +namespace BuildingBlocks.Core.Event; public interface IEvent : INotification { diff --git a/src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs b/src/BuildingBlocks/Core/Event/IHaveIntegrationEvent.cs similarity index 52% rename from src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs rename to src/BuildingBlocks/Core/Event/IHaveIntegrationEvent.cs index a1f1ff8..ecaf3d4 100644 --- a/src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs +++ b/src/BuildingBlocks/Core/Event/IHaveIntegrationEvent.cs @@ -1,4 +1,4 @@ -namespace BuildingBlocks.Domain.Event; +namespace BuildingBlocks.Core.Event; public interface IHaveIntegrationEvent { diff --git a/src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs b/src/BuildingBlocks/Core/Event/IIntegrationEvent.cs similarity index 57% rename from src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs rename to src/BuildingBlocks/Core/Event/IIntegrationEvent.cs index 0ab1a76..d546d5d 100644 --- a/src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs +++ b/src/BuildingBlocks/Core/Event/IIntegrationEvent.cs @@ -1,7 +1,6 @@ using MassTransit; -using MassTransit.Topology; -namespace BuildingBlocks.Domain.Event; +namespace BuildingBlocks.Core.Event; [ExcludeFromTopology] public interface IIntegrationEvent : IEvent diff --git a/src/BuildingBlocks/Core/Event/IInternalCommand.cs b/src/BuildingBlocks/Core/Event/IInternalCommand.cs new file mode 100644 index 0000000..683db88 --- /dev/null +++ b/src/BuildingBlocks/Core/Event/IInternalCommand.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Core.CQRS; + +namespace BuildingBlocks.Core.Event; + +public interface IInternalCommand : ICommand +{ + long Id { get; } + DateTime OccurredOn { get; } + string Type { get; } +} diff --git a/src/BuildingBlocks/Core/Event/InternalCommand.cs b/src/BuildingBlocks/Core/Event/InternalCommand.cs new file mode 100644 index 0000000..0ddcfc0 --- /dev/null +++ b/src/BuildingBlocks/Core/Event/InternalCommand.cs @@ -0,0 +1,13 @@ +using BuildingBlocks.IdsGenerator; +using BuildingBlocks.Utils; + +namespace BuildingBlocks.Core.Event; + +public class InternalCommand : IInternalCommand +{ + public long Id { get; set; } = SnowFlakIdGenerator.NewId(); + + public DateTime OccurredOn => DateTime.Now; + + public string Type { get => TypeProvider.GetTypeName(GetType()); } +} diff --git a/src/BuildingBlocks/Core/Event/MessageEnvelope.cs b/src/BuildingBlocks/Core/Event/MessageEnvelope.cs new file mode 100644 index 0000000..b94d683 --- /dev/null +++ b/src/BuildingBlocks/Core/Event/MessageEnvelope.cs @@ -0,0 +1,26 @@ +using Google.Protobuf; + +namespace BuildingBlocks.Core.Event; + +public class MessageEnvelope +{ + public MessageEnvelope(object? message, IDictionary? headers = null) + { + Message = message; + Headers = headers ?? new Dictionary(); + } + + public object? Message { get; init; } + public IDictionary Headers { get; init; } +} + +public class MessageEnvelope : MessageEnvelope + where TMessage : class, IMessage +{ + public MessageEnvelope(TMessage message, IDictionary header) : base(message, header) + { + Message = message; + } + + public new TMessage? Message { get; } +} diff --git a/src/BuildingBlocks/Domain/BusPublisher.cs b/src/BuildingBlocks/Core/EventDispatcher.cs similarity index 64% rename from src/BuildingBlocks/Domain/BusPublisher.cs rename to src/BuildingBlocks/Core/EventDispatcher.cs index 45f4835..fb23629 100644 --- a/src/BuildingBlocks/Domain/BusPublisher.cs +++ b/src/BuildingBlocks/Core/EventDispatcher.cs @@ -1,88 +1,71 @@ using System.Security.Claims; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; +using BuildingBlocks.MessageProcessor; using BuildingBlocks.Web; -using MassTransit; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using MessageEnvelope = BuildingBlocks.Core.Event.MessageEnvelope; -namespace BuildingBlocks.Domain; +namespace BuildingBlocks.Core; -public sealed class BusPublisher : IBusPublisher +public sealed class EventDispatcher : IEventDispatcher { private readonly IEventMapper _eventMapper; - private readonly ILogger _logger; - private readonly IPublishEndpoint _publishEndpoint; + private readonly ILogger _logger; + private readonly IPersistMessageProcessor _persistMessageProcessor; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IServiceScopeFactory _serviceScopeFactory; - public BusPublisher(IServiceScopeFactory serviceScopeFactory, + public EventDispatcher(IServiceScopeFactory serviceScopeFactory, IEventMapper eventMapper, - ILogger logger, - IPublishEndpoint publishEndpoint, + ILogger logger, + IPersistMessageProcessor persistMessageProcessor, IHttpContextAccessor httpContextAccessor) { _serviceScopeFactory = serviceScopeFactory; _eventMapper = eventMapper; _logger = logger; - _publishEndpoint = publishEndpoint; + _persistMessageProcessor = persistMessageProcessor; _httpContextAccessor = httpContextAccessor; } public async Task SendAsync(IDomainEvent domainEvent, - CancellationToken cancellationToken = default) => await SendAsync(new[] { domainEvent }, cancellationToken); + CancellationToken cancellationToken = default) => await SendAsync(new[] {domainEvent}, cancellationToken); public async Task SendAsync(IReadOnlyList domainEvents, CancellationToken cancellationToken = default) { if (domainEvents is null) return; - _logger.LogTrace("Processing integration events start..."); - var integrationEvents = await MapDomainEventToIntegrationEventAsync(domainEvents).ConfigureAwait(false); - if (!integrationEvents.Any()) return; + if (integrationEvents.Count == 0) return; - await PublishMessageToBroker(integrationEvents, cancellationToken); - - _logger.LogTrace("Processing integration events done..."); + foreach (var integrationEvent in integrationEvents) + { + await _persistMessageProcessor.PublishMessageAsync(new MessageEnvelope(integrationEvent, SetHeaders()), + cancellationToken); + } } - public async Task SendAsync(IIntegrationEvent integrationEvent, - CancellationToken cancellationToken = default) => await SendAsync(new[] { integrationEvent }, cancellationToken); + CancellationToken cancellationToken = default) => await SendAsync(new[] {integrationEvent}, cancellationToken); - public async Task SendAsync(IReadOnlyList integrationEvents, CancellationToken cancellationToken = default) + public async Task SendAsync(IReadOnlyList integrationEvents, + CancellationToken cancellationToken = default) { if (integrationEvents is null) return; - _logger.LogTrace("Processing integration events start..."); - - await PublishMessageToBroker(integrationEvents, cancellationToken); - - _logger.LogTrace("Processing integration events done..."); - } - - private async Task PublishMessageToBroker(IReadOnlyList integrationEvents, CancellationToken cancellationToken) - { - foreach (var integrationEvent in integrationEvents) - { - await _publishEndpoint.Publish((object) integrationEvent, context => - { - context.CorrelationId = _httpContextAccessor?.HttpContext?.GetCorrelationId(); - context.Headers.Set("UserId", - _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier)); - context.Headers.Set("UserName", - _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.Name)); - }, cancellationToken); - - _logger.LogTrace("Publish a message with ID: {Id}", integrationEvent?.EventId); - } + await _persistMessageProcessor.PublishMessageAsync(new MessageEnvelope(integrationEvents, SetHeaders()), + cancellationToken); } private Task> MapDomainEventToIntegrationEventAsync( IReadOnlyList events) { + _logger.LogTrace("Processing integration events start..."); + var wrappedIntegrationEvents = GetWrappedIntegrationEvents(events.ToList())?.ToList(); if (wrappedIntegrationEvents?.Count > 0) return Task.FromResult>(wrappedIntegrationEvents); @@ -101,6 +84,8 @@ public sealed class BusPublisher : IBusPublisher integrationEvents.Add(integrationEvent); } + _logger.LogTrace("Processing integration events done..."); + return Task.FromResult>(integrationEvents); } @@ -118,4 +103,14 @@ public sealed class BusPublisher : IBusPublisher yield return domainNotificationEvent; } } + + private IDictionary SetHeaders() + { + var headers = new Dictionary(); + headers.Add("CorrelationId", _httpContextAccessor?.HttpContext?.GetCorrelationId()); + headers.Add("UserId", _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier)); + headers.Add("UserName", _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.Name)); + + return headers; + } } diff --git a/src/BuildingBlocks/Domain/IBusPublisher.cs b/src/BuildingBlocks/Core/IEventDispatcher.cs similarity index 82% rename from src/BuildingBlocks/Domain/IBusPublisher.cs rename to src/BuildingBlocks/Core/IEventDispatcher.cs index 8d57005..348fd10 100644 --- a/src/BuildingBlocks/Domain/IBusPublisher.cs +++ b/src/BuildingBlocks/Core/IEventDispatcher.cs @@ -1,8 +1,8 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; -namespace BuildingBlocks.Domain; +namespace BuildingBlocks.Core; -public interface IBusPublisher +public interface IEventDispatcher { public Task SendAsync(IReadOnlyList domainEvents, CancellationToken cancellationToken = default); public Task SendAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default); diff --git a/src/BuildingBlocks/Domain/IEventMapper.cs b/src/BuildingBlocks/Core/IEventMapper.cs similarity index 70% rename from src/BuildingBlocks/Domain/IEventMapper.cs rename to src/BuildingBlocks/Core/IEventMapper.cs index 970b088..aa5d359 100644 --- a/src/BuildingBlocks/Domain/IEventMapper.cs +++ b/src/BuildingBlocks/Core/IEventMapper.cs @@ -1,6 +1,6 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; -namespace BuildingBlocks.Domain; +namespace BuildingBlocks.Core; public interface IEventMapper { diff --git a/src/BuildingBlocks/Domain/IntegrationEventWrapper.cs b/src/BuildingBlocks/Core/IntegrationEventWrapper.cs similarity index 68% rename from src/BuildingBlocks/Domain/IntegrationEventWrapper.cs rename to src/BuildingBlocks/Core/IntegrationEventWrapper.cs index abcf1cc..562727c 100644 --- a/src/BuildingBlocks/Domain/IntegrationEventWrapper.cs +++ b/src/BuildingBlocks/Core/IntegrationEventWrapper.cs @@ -1,6 +1,6 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; -namespace BuildingBlocks.Domain; +namespace BuildingBlocks.Core; public record IntegrationEventWrapper(TDomainEventType DomainEvent) : IIntegrationEvent where TDomainEventType : IDomainEvent; diff --git a/src/BuildingBlocks/Domain/Model/Aggregate.cs b/src/BuildingBlocks/Core/Model/Aggregate.cs similarity index 90% rename from src/BuildingBlocks/Domain/Model/Aggregate.cs rename to src/BuildingBlocks/Core/Model/Aggregate.cs index f2e3b17..b90d19b 100644 --- a/src/BuildingBlocks/Domain/Model/Aggregate.cs +++ b/src/BuildingBlocks/Core/Model/Aggregate.cs @@ -1,6 +1,6 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; -namespace BuildingBlocks.Domain.Model +namespace BuildingBlocks.Core.Model { public abstract class Aggregate : Aggregate { diff --git a/src/BuildingBlocks/Domain/Model/Entity.cs b/src/BuildingBlocks/Core/Model/Entity.cs similarity index 87% rename from src/BuildingBlocks/Domain/Model/Entity.cs rename to src/BuildingBlocks/Core/Model/Entity.cs index da20d10..a8eaaaa 100644 --- a/src/BuildingBlocks/Domain/Model/Entity.cs +++ b/src/BuildingBlocks/Core/Model/Entity.cs @@ -1,4 +1,4 @@ -namespace BuildingBlocks.Domain.Model; +namespace BuildingBlocks.Core.Model; public abstract class Entity : IEntity { diff --git a/src/BuildingBlocks/Domain/Model/IAggregate.cs b/src/BuildingBlocks/Core/Model/IAggregate.cs similarity index 75% rename from src/BuildingBlocks/Domain/Model/IAggregate.cs rename to src/BuildingBlocks/Core/Model/IAggregate.cs index 9725ed2..b451bd9 100644 --- a/src/BuildingBlocks/Domain/Model/IAggregate.cs +++ b/src/BuildingBlocks/Core/Model/IAggregate.cs @@ -1,6 +1,6 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; -namespace BuildingBlocks.Domain.Model; +namespace BuildingBlocks.Core.Model; public interface IAggregate : IEntity { diff --git a/src/BuildingBlocks/Domain/Model/IEntity.cs b/src/BuildingBlocks/Core/Model/IEntity.cs similarity index 86% rename from src/BuildingBlocks/Domain/Model/IEntity.cs rename to src/BuildingBlocks/Core/Model/IEntity.cs index c48524c..fcffe38 100644 --- a/src/BuildingBlocks/Domain/Model/IEntity.cs +++ b/src/BuildingBlocks/Core/Model/IEntity.cs @@ -1,4 +1,4 @@ -namespace BuildingBlocks.Domain.Model; +namespace BuildingBlocks.Core.Model; public interface IEntity { diff --git a/src/BuildingBlocks/Domain/Event/IDomainEvent.cs b/src/BuildingBlocks/Domain/Event/IDomainEvent.cs deleted file mode 100644 index 42f0c01..0000000 --- a/src/BuildingBlocks/Domain/Event/IDomainEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace BuildingBlocks.Domain.Event; - -public interface IDomainEvent : IEvent -{ - -} diff --git a/src/BuildingBlocks/EFCore/AppDbContextBase.cs b/src/BuildingBlocks/EFCore/AppDbContextBase.cs index dbe629e..db1a1cd 100644 --- a/src/BuildingBlocks/EFCore/AppDbContextBase.cs +++ b/src/BuildingBlocks/EFCore/AppDbContextBase.cs @@ -2,8 +2,8 @@ using System.Collections.Immutable; using System.Data; using System.Reflection; using System.Security.Claims; -using BuildingBlocks.Domain.Event; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Core.Model; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -21,6 +21,12 @@ public abstract class AppDbContextBase : DbContext, IDbContext _httpContextAccessor = httpContextAccessor; } + protected override void OnModelCreating(ModelBuilder builder) + { + // ref: https://github.com/pdevito3/MessageBusTestingInMemHarness/blob/main/RecipeManagement/src/RecipeManagement/Databases/RecipesDbContext.cs + builder.FilterSoftDeletedProperties(); + } + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { if (_currentTransaction != null) return; @@ -83,15 +89,6 @@ public abstract class AppDbContextBase : DbContext, IDbContext return domainEvents.ToImmutableList(); } - protected override void OnModelCreating(ModelBuilder builder) - { - builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); - base.OnModelCreating(builder); - - // ref: https://github.com/pdevito3/MessageBusTestingInMemHarness/blob/main/RecipeManagement/src/RecipeManagement/Databases/RecipesDbContext.cs - builder.FilterSoftDeletedProperties(); - } - // ref: https://www.meziantou.net/entity-framework-core-generate-tracking-columns.htm // ref: https://www.meziantou.net/entity-framework-core-soft-delete-using-query-filters.htm private void OnBeforeSaving() diff --git a/src/BuildingBlocks/EFCore/EfTxBehavior.cs b/src/BuildingBlocks/EFCore/EfTxBehavior.cs index 9d56f9e..4a84a39 100644 --- a/src/BuildingBlocks/EFCore/EfTxBehavior.cs +++ b/src/BuildingBlocks/EFCore/EfTxBehavior.cs @@ -1,6 +1,6 @@ using System.Data; using System.Text.Json; -using BuildingBlocks.Domain; +using BuildingBlocks.Core; using MediatR; using Microsoft.Extensions.Logging; @@ -12,16 +12,16 @@ public class EfTxBehavior : IPipelineBehavior> _logger; private readonly IDbContext _dbContextBase; - private readonly IBusPublisher _busPublisher; + private readonly IEventDispatcher _eventDispatcher; public EfTxBehavior( ILogger> logger, IDbContext dbContextBase, - IBusPublisher busPublisher) + IEventDispatcher eventDispatcher) { _logger = logger; _dbContextBase = dbContextBase; - _busPublisher = busPublisher; + _eventDispatcher = eventDispatcher; } public async Task Handle( @@ -60,7 +60,7 @@ public class EfTxBehavior : IPipelineBehavior Set() where TEntity : class; + DbSet PersistMessages => Set(); IReadOnlyList GetDomainEvents(); Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task CommitTransactionAsync(CancellationToken cancellationToken = default); diff --git a/src/BuildingBlocks/EventStoreDB/Events/AggregateEventSourcing.cs b/src/BuildingBlocks/EventStoreDB/Events/AggregateEventSourcing.cs index c55c4eb..853d631 100644 --- a/src/BuildingBlocks/EventStoreDB/Events/AggregateEventSourcing.cs +++ b/src/BuildingBlocks/EventStoreDB/Events/AggregateEventSourcing.cs @@ -1,5 +1,5 @@ -using BuildingBlocks.Domain.Event; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Core.Model; namespace BuildingBlocks.EventStoreDB.Events { diff --git a/src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs b/src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs index f04385c..03d93ec 100644 --- a/src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs +++ b/src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs @@ -1,5 +1,4 @@ -using BuildingBlocks.Domain.Model; -using BuildingBlocks.EventStoreDB.Serialization; +using BuildingBlocks.EventStoreDB.Serialization; using EventStore.Client; namespace BuildingBlocks.EventStoreDB.Events; diff --git a/src/BuildingBlocks/EventStoreDB/Events/IAggregateEventSourcing.cs b/src/BuildingBlocks/EventStoreDB/Events/IAggregateEventSourcing.cs index 42ceccd..c13a627 100644 --- a/src/BuildingBlocks/EventStoreDB/Events/IAggregateEventSourcing.cs +++ b/src/BuildingBlocks/EventStoreDB/Events/IAggregateEventSourcing.cs @@ -1,5 +1,5 @@ -using BuildingBlocks.Domain.Event; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Core.Model; namespace BuildingBlocks.EventStoreDB.Events { diff --git a/src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs b/src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs index bdb650e..90c0a09 100644 --- a/src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs +++ b/src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; using MediatR; namespace BuildingBlocks.EventStoreDB.Events; diff --git a/src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs b/src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs index 23cacfd..bfc42fc 100644 --- a/src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs +++ b/src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace BuildingBlocks.EventStoreDB.Events; diff --git a/src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs b/src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs index 763258a..2836b3e 100644 --- a/src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs +++ b/src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace BuildingBlocks.EventStoreDB.Events; diff --git a/src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs b/src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs index 08466ae..be1c408 100644 --- a/src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs +++ b/src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs @@ -1,4 +1,3 @@ -using BuildingBlocks.Domain.Model; using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.Exception; diff --git a/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs b/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs index 0a0b727..ea58df7 100644 --- a/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs +++ b/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.EventStoreDB.Serialization; using EventStore.Client; diff --git a/src/BuildingBlocks/MassTransit/Extensions.cs b/src/BuildingBlocks/MassTransit/Extensions.cs index bce3f2e..4a7141d 100644 --- a/src/BuildingBlocks/MassTransit/Extensions.cs +++ b/src/BuildingBlocks/MassTransit/Extensions.cs @@ -1,5 +1,5 @@ using System.Reflection; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; using BuildingBlocks.Utils; using BuildingBlocks.Web; using Humanizer; diff --git a/src/BuildingBlocks/MessageProcessor/Extensions.cs b/src/BuildingBlocks/MessageProcessor/Extensions.cs new file mode 100644 index 0000000..d7329ae --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/Extensions.cs @@ -0,0 +1,21 @@ +using BuildingBlocks.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.MessageProcessor; + +public static class Extensions +{ + public static IServiceCollection AddPersistMessage(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection(nameof(PersistMessageOptions))) + .ValidateDataAnnotations(); + + services.AddScoped(); + services.AddScoped(); + services.AddHostedService(); + + return services; + } +} diff --git a/src/BuildingBlocks/MessageProcessor/IPersistMessageProcessor.cs b/src/BuildingBlocks/MessageProcessor/IPersistMessageProcessor.cs new file mode 100644 index 0000000..54968c9 --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/IPersistMessageProcessor.cs @@ -0,0 +1,31 @@ +using BuildingBlocks.Core.Event; + +namespace BuildingBlocks.MessageProcessor; + +// Ref: http://www.kamilgrzybek.com/design/the-outbox-pattern/ +// Ref: https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/ +// Ref: https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/ +// Ref: https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/subscribe-events#designing-atomicity-and-resiliency-when-publishing-to-the-event-bus +// Ref: https://github.com/kgrzybek/modular-monolith-with-ddd#38-internal-processing +public interface IPersistMessageProcessor +{ + Task PublishMessageAsync( + TMessageEnvelope messageEnvelope, + CancellationToken cancellationToken = default) + where TMessageEnvelope : MessageEnvelope; + + Task AddReceivedMessageAsync( + TMessageEnvelope messageEnvelope, + CancellationToken cancellationToken = default) + where TMessageEnvelope : MessageEnvelope; + + Task AddInternalMessageAsync( + TCommand internalCommand, + CancellationToken cancellationToken = default) + where TCommand : class, IInternalCommand; + + + Task ProcessAsync(Guid messageId, MessageDeliveryType deliveryType, CancellationToken cancellationToken = default); + + Task ProcessAllAsync(CancellationToken cancellationToken = default); +} diff --git a/src/BuildingBlocks/MessageProcessor/MessageDeliveryType.cs b/src/BuildingBlocks/MessageProcessor/MessageDeliveryType.cs new file mode 100644 index 0000000..e9eff7f --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/MessageDeliveryType.cs @@ -0,0 +1,9 @@ +namespace BuildingBlocks.MessageProcessor; + +[Flags] +public enum MessageDeliveryType +{ + Outbox = 1, + Inbox = 2, + Internal = 4 +} diff --git a/src/BuildingBlocks/MessageProcessor/MessageStatus.cs b/src/BuildingBlocks/MessageProcessor/MessageStatus.cs new file mode 100644 index 0000000..c7f0cb8 --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/MessageStatus.cs @@ -0,0 +1,7 @@ +namespace BuildingBlocks.MessageProcessor; + +public enum MessageStatus +{ + InProgress = 1, + Processed = 2 +} diff --git a/src/BuildingBlocks/MessageProcessor/PersistMessage.cs b/src/BuildingBlocks/MessageProcessor/PersistMessage.cs new file mode 100644 index 0000000..c884f09 --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/PersistMessage.cs @@ -0,0 +1,33 @@ +namespace BuildingBlocks.MessageProcessor; + +public class PersistMessage +{ + public PersistMessage(Guid id, string dataType, string data, MessageDeliveryType deliveryType) + { + Id = id; + DataType = dataType; + Data = data; + DeliveryType = deliveryType; + Created = DateTime.Now; + MessageStatus = MessageStatus.InProgress; + RetryCount = 0; + } + + public Guid Id { get; private set; } + public string DataType { get; private set; } + public string Data { get; private set; } + public DateTime Created { get; private set; } + public int RetryCount { get; private set; } + public MessageStatus MessageStatus { get; private set; } + public MessageDeliveryType DeliveryType { get; private set; } + + public void ChangeState(MessageStatus messageStatus) + { + MessageStatus = messageStatus; + } + + public void IncreaseRetry() + { + RetryCount++; + } +} diff --git a/src/BuildingBlocks/MessageProcessor/PersistMessageBackgroundService.cs b/src/BuildingBlocks/MessageProcessor/PersistMessageBackgroundService.cs new file mode 100644 index 0000000..76f50b0 --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/PersistMessageBackgroundService.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace BuildingBlocks.MessageProcessor; + +public class PersistMessageBackgroundService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private PersistMessageOptions _options; + + private Task? _executingTask; + + public PersistMessageBackgroundService( + ILogger logger, + IServiceProvider serviceProvider, + IOptions options) + { + _logger = logger; + _serviceProvider = serviceProvider; + _options = options.Value; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("PersistMessage Background Service Start"); + + _executingTask = ProcessAsync(stoppingToken); + + return _executingTask; + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("PersistMessage Background Service Stop"); + + return base.StopAsync(cancellationToken); + } + + private async Task ProcessAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await using (var scope = _serviceProvider.CreateAsyncScope()) + { + var service = scope.ServiceProvider.GetRequiredService(); + await service.ProcessAllAsync(stoppingToken); + } + + var delay = _options.Interval is { } + ? TimeSpan.FromSeconds((int)_options.Interval) + : TimeSpan.FromSeconds(30); + + await Task.Delay(delay, stoppingToken); + } + } +} diff --git a/src/BuildingBlocks/MessageProcessor/PersistMessageOptions.cs b/src/BuildingBlocks/MessageProcessor/PersistMessageOptions.cs new file mode 100644 index 0000000..d2baa1c --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/PersistMessageOptions.cs @@ -0,0 +1,7 @@ +namespace BuildingBlocks.MessageProcessor; + +public class PersistMessageOptions +{ + public int? Interval { get; set; } = 30; + public bool Enabled { get; set; } = true; +} diff --git a/src/BuildingBlocks/MessageProcessor/PersistMessageProcessor.cs b/src/BuildingBlocks/MessageProcessor/PersistMessageProcessor.cs new file mode 100644 index 0000000..cc42392 --- /dev/null +++ b/src/BuildingBlocks/MessageProcessor/PersistMessageProcessor.cs @@ -0,0 +1,181 @@ +using System.Text.Json; +using Ardalis.GuardClauses; +using BuildingBlocks.Core.Event; +using BuildingBlocks.EFCore; +using BuildingBlocks.Utils; +using MassTransit; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace BuildingBlocks.MessageProcessor; + +public class PersistMessageProcessor : IPersistMessageProcessor +{ + private readonly ILogger _logger; + private readonly IMediator _mediator; + private readonly IDbContext _dbContext; + private readonly IPublishEndpoint _publishEndpoint; + + public PersistMessageProcessor( + ILogger logger, + IMediator mediator, + IDbContext dbContext, + IPublishEndpoint publishEndpoint) + { + _logger = logger; + _mediator = mediator; + _dbContext = dbContext; + _publishEndpoint = publishEndpoint; + } + + public async Task PublishMessageAsync( + TMessageEnvelope messageEnvelope, + CancellationToken cancellationToken = default) + where TMessageEnvelope : MessageEnvelope + { + await SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Outbox, cancellationToken); + } + + public async Task AddReceivedMessageAsync(TMessageEnvelope messageEnvelope, + CancellationToken cancellationToken = default) where TMessageEnvelope : MessageEnvelope + { + await SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Inbox, cancellationToken); + } + + public async Task AddInternalMessageAsync(TCommand internalCommand, + CancellationToken cancellationToken = default) where TCommand : class, IInternalCommand + { + await SavePersistMessageAsync(new MessageEnvelope(internalCommand), MessageDeliveryType.Internal, + cancellationToken); + } + + public async Task ProcessAsync( + Guid messageId, + MessageDeliveryType deliveryType, + CancellationToken cancellationToken = default) + { + var message = + await _dbContext.PersistMessages.FirstOrDefaultAsync( + x => x.Id == messageId && x.DeliveryType == deliveryType, cancellationToken); + + if (message is null) + return; + + switch (deliveryType) + { + case MessageDeliveryType.Inbox: + await ProcessInbox(message, cancellationToken); + break; + case MessageDeliveryType.Internal: + await ProcessInternal(message, cancellationToken); + break; + case MessageDeliveryType.Outbox: + await ProcessOutbox(message, cancellationToken); + break; + } + + message.ChangeState(MessageStatus.Processed); + + _dbContext.PersistMessages.Update(message); + + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task ProcessAllAsync(CancellationToken cancellationToken = default) + { + var messages = await _dbContext.PersistMessages.Where(x => x.MessageStatus != MessageStatus.Processed) + .ToListAsync(cancellationToken); + + foreach (var message in messages) + { + await ProcessAsync(message.Id, message.DeliveryType, cancellationToken); + } + } + + private async Task SavePersistMessageAsync( + MessageEnvelope messageEnvelope, + MessageDeliveryType deliveryType, + CancellationToken cancellationToken = default) + { + Guard.Against.Null(messageEnvelope.Message, nameof(messageEnvelope.Message)); + + Guid id; + if (messageEnvelope.Message is IEvent message) + { + id = message.EventId; + } + else + { + id = Guid.NewGuid(); + } + + await _dbContext.PersistMessages.AddAsync( + new PersistMessage( + id, + TypeProvider.GetTypeName(messageEnvelope.Message.GetType()), + JsonSerializer.Serialize(messageEnvelope), + deliveryType), + cancellationToken); + + await _dbContext.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Message with id: {MessageID} and delivery type: {DeliveryType} saved in persistence message store.", + id, + deliveryType.ToString()); + } + + private async Task ProcessOutbox(PersistMessage message, CancellationToken cancellationToken) + { + MessageEnvelope? messageEnvelope = JsonSerializer.Deserialize(message.Data); + + if (messageEnvelope is null || messageEnvelope.Message is null) + return; + + var data = JsonSerializer.Deserialize(messageEnvelope.Message.ToString()!, + TypeProvider.GetType(message.DataType)); + + if (data is IEvent) + { + await _publishEndpoint.Publish((object)data, context => + { + foreach (var header in messageEnvelope.Headers) + { + context.Headers.Set(header.Key, header.Value); + } + }, cancellationToken); + + _logger.LogInformation( + "Message with id: {MessageId} and delivery type: {DeliveryType} processed from the persistence message store.", + message.Id, + message.DeliveryType); + } + } + + private async Task ProcessInternal(PersistMessage message, CancellationToken cancellationToken) + { + MessageEnvelope? messageEnvelope = JsonSerializer.Deserialize(message.Data); + + if (messageEnvelope is null || messageEnvelope.Message is null) + return; + + var data = JsonSerializer.Deserialize(messageEnvelope.Message.ToString()!, + TypeProvider.GetType(message.DataType)); + + if (data is IInternalCommand internalCommand) + { + await _mediator.Send(internalCommand, cancellationToken); + + _logger.LogInformation( + "InternalCommand with id: {EventID} and delivery type: {DeliveryType} processed from the persistence message store.", + message.Id, + message.DeliveryType); + } + } + + private Task ProcessInbox(PersistMessage message, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/src/BuildingBlocks/Mongo/Extensions.cs b/src/BuildingBlocks/Mongo/Extensions.cs index 1de42c7..a59b110 100644 --- a/src/BuildingBlocks/Mongo/Extensions.cs +++ b/src/BuildingBlocks/Mongo/Extensions.cs @@ -17,8 +17,6 @@ namespace BuildingBlocks.Mongo where TContextService : IMongoDbContext where TContextImplementation : MongoDbContext, TContextService { - var mongoOptions = configuration.GetSection(nameof(MongoOptions)).Get() ?? new MongoOptions(); - services.Configure(configuration.GetSection(nameof(MongoOptions))); if (configurator is { }) { diff --git a/src/BuildingBlocks/Mongo/IMongoRepository.cs b/src/BuildingBlocks/Mongo/IMongoRepository.cs index 5b52f04..9e65e85 100644 --- a/src/BuildingBlocks/Mongo/IMongoRepository.cs +++ b/src/BuildingBlocks/Mongo/IMongoRepository.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; namespace BuildingBlocks.Mongo; diff --git a/src/BuildingBlocks/Mongo/IRepository.cs b/src/BuildingBlocks/Mongo/IRepository.cs index da6e390..cc2e7a7 100644 --- a/src/BuildingBlocks/Mongo/IRepository.cs +++ b/src/BuildingBlocks/Mongo/IRepository.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; namespace BuildingBlocks.Mongo; diff --git a/src/BuildingBlocks/Mongo/MongoRepository.cs b/src/BuildingBlocks/Mongo/MongoRepository.cs index e09cbe6..458cdea 100644 --- a/src/BuildingBlocks/Mongo/MongoRepository.cs +++ b/src/BuildingBlocks/Mongo/MongoRepository.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using MongoDB.Driver; namespace BuildingBlocks.Mongo; diff --git a/src/BuildingBlocks/Utils/TypeProvider.cs b/src/BuildingBlocks/Utils/TypeProvider.cs index a31f57a..78adeab 100644 --- a/src/BuildingBlocks/Utils/TypeProvider.cs +++ b/src/BuildingBlocks/Utils/TypeProvider.cs @@ -1,10 +1,14 @@ +using System.Collections.Concurrent; using System.Reflection; using System.Runtime.CompilerServices; +using Ardalis.GuardClauses; namespace BuildingBlocks.Utils; public static class TypeProvider { + private static readonly ConcurrentDictionary TypeNameMap = new(); + private static readonly ConcurrentDictionary TypeMap = new(); private static bool IsRecord(this Type objectType) { return objectType.GetMethod("$") != null || @@ -34,4 +38,72 @@ public static class TypeProvider .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName)) .FirstOrDefault(); } + + /// + /// Gets the type name from a generic Type class. + /// + /// + /// GetTypeName + public static string GetTypeName() => ToName(typeof(T)); + + /// + /// Gets the type name from a Type class. + /// + /// + /// TypeName + public static string GetTypeName(Type type) => ToName(type); + + /// + /// Gets the type name from a instance object. + /// + /// + /// TypeName + public static string GetTypeNameByObject(object o) => ToName(o.GetType()); + + /// + /// Gets the type class from a type name. + /// + /// + /// Type + public static Type GetType(string typeName) => ToType(typeName); + + public static void AddType(string name) => AddType(typeof(T), name); + + private static void AddType(Type type, string name) + { + ToName(type); + ToType(name); + } + + public static bool IsTypeRegistered() => TypeNameMap.ContainsKey(typeof(T)); + + private static string ToName(Type type) + { + Guard.Against.Null(type, nameof(type)); + + return TypeNameMap.GetOrAdd(type, _ => + { + var eventTypeName = type.FullName!.Replace(".", "_", StringComparison.Ordinal); + + TypeMap.GetOrAdd(eventTypeName, type); + + return eventTypeName; + }); + } + + private static Type ToType(string typeName) => TypeMap.GetOrAdd(typeName, _ => + { + Guard.Against.NullOrEmpty(typeName, nameof(typeName)); + + return TypeMap.GetOrAdd(typeName, _ => + { + var type = GetFirstMatchingTypeFromCurrentDomainAssembly( + typeName.Replace("_", ".", StringComparison.Ordinal))!; + + if (type == null) + throw new System.Exception($"Type map for '{typeName}' wasn't found!"); + + return type; + }); + }); } diff --git a/src/Services/Booking/src/Booking.Api/Booking.Api.csproj b/src/Services/Booking/src/Booking.Api/Booking.Api.csproj index b78bb73..278265d 100644 --- a/src/Services/Booking/src/Booking.Api/Booking.Api.csproj +++ b/src/Services/Booking/src/Booking.Api/Booking.Api.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Services/Booking/src/Booking.Api/Program.cs b/src/Services/Booking/src/Booking.Api/Program.cs index 5b5c283..0da21fe 100644 --- a/src/Services/Booking/src/Booking.Api/Program.cs +++ b/src/Services/Booking/src/Booking.Api/Program.cs @@ -2,7 +2,7 @@ using Booking; using Booking.Configuration; using Booking.Data; using Booking.Extensions; -using BuildingBlocks.Domain; +using BuildingBlocks.Core; using BuildingBlocks.EFCore; using BuildingBlocks.EventStoreDB; using BuildingBlocks.HealthCheck; @@ -11,6 +11,7 @@ using BuildingBlocks.Jwt; using BuildingBlocks.Logging; using BuildingBlocks.Mapster; using BuildingBlocks.MassTransit; +using BuildingBlocks.MessageProcessor; using BuildingBlocks.OpenTelemetry; using BuildingBlocks.Swagger; using BuildingBlocks.Web; @@ -30,8 +31,8 @@ builder.Services.Configure(options => configuration.GetSection("Grp Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); -builder.Services.AddTransient(); builder.Services.AddCustomDbContext(configuration); +builder.Services.AddPersistMessage(configuration); builder.AddCustomSerilog(); builder.Services.AddJwt(); @@ -46,7 +47,6 @@ builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly); builder.Services.AddHttpContextAccessor(); builder.Services.AddTransient(); -builder.Services.AddTransient(); builder.Services.AddCustomHealthCheck(); builder.Services.AddCustomMassTransit(typeof(BookingRoot).Assembly, env); builder.Services.AddCustomOpenTelemetry(); diff --git a/src/Services/Booking/src/Booking.Api/appsettings.json b/src/Services/Booking/src/Booking.Api/appsettings.json index 7d9ab6e..e7fd2f7 100644 --- a/src/Services/Booking/src/Booking.Api/appsettings.json +++ b/src/Services/Booking/src/Booking.Api/appsettings.json @@ -27,5 +27,9 @@ "EventStore": { "ConnectionString": "esdb://localhost:2113?tls=false" }, + "PersistMessageOptions": { + "Interval": 30, + "Enabled": true + }, "AllowedHosts": "*" } diff --git a/src/Services/Booking/src/Booking.Api/appsettings.test.json b/src/Services/Booking/src/Booking.Api/appsettings.test.json new file mode 100644 index 0000000..c61ce0a --- /dev/null +++ b/src/Services/Booking/src/Booking.Api/appsettings.test.json @@ -0,0 +1,23 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=.\\sqlexpress;Database=BookingDB_Test;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "RabbitMq": { + "HostName": "localhost", + "ExchangeName": "booking", + "UserName": "guest", + "Password": "guest" + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Debug", + "Microsoft.EntityFrameworkCore.Database.Command": "Debug" + } + }, + "PersistMessageOptions": { + "Interval": 1, + "Enabled": true + } +} diff --git a/src/Services/Booking/src/Booking/Booking/Events/Domain/BookingCreatedDomainEvent.cs b/src/Services/Booking/src/Booking/Booking/Events/Domain/BookingCreatedDomainEvent.cs index 12561bb..c15112c 100644 --- a/src/Services/Booking/src/Booking/Booking/Events/Domain/BookingCreatedDomainEvent.cs +++ b/src/Services/Booking/src/Booking/Booking/Events/Domain/BookingCreatedDomainEvent.cs @@ -1,5 +1,5 @@ using Booking.Booking.Models.ValueObjects; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace Booking.Booking.Events.Domain; diff --git a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommand.cs b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommand.cs index 06c39f4..d4f250c 100644 --- a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommand.cs +++ b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommand.cs @@ -1,10 +1,9 @@ +using BuildingBlocks.Core.CQRS; using BuildingBlocks.IdsGenerator; -using MediatR; namespace Booking.Booking.Features.CreateBooking; -public record CreateBookingCommand - (long PassengerId, long FlightId, string Description) : IRequest +public record CreateBookingCommand(long PassengerId, long FlightId, string Description) : ICommand { public long Id { get; set; } = SnowFlakIdGenerator.NewId(); } diff --git a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandHandler.cs b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandHandler.cs index 35085ff..2c60f0f 100644 --- a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandHandler.cs +++ b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandHandler.cs @@ -2,12 +2,13 @@ using Ardalis.GuardClauses; using Booking.Booking.Exceptions; using Booking.Booking.Models.ValueObjects; using BuildingBlocks.Contracts.Grpc; +using BuildingBlocks.Core.CQRS; using BuildingBlocks.EventStoreDB.Repository; using MediatR; namespace Booking.Booking.Features.CreateBooking; -public class CreateBookingCommandHandler : IRequestHandler +public class CreateBookingCommandHandler : ICommandHandler { private readonly IEventStoreDBRepository _eventStoreDbRepository; private readonly IFlightGrpcService _flightGrpcService; diff --git a/src/Services/Booking/src/Booking/Booking/Models/Booking.cs b/src/Services/Booking/src/Booking/Booking/Models/Booking.cs index a6c02a4..c5e6dae 100644 --- a/src/Services/Booking/src/Booking/Booking/Models/Booking.cs +++ b/src/Services/Booking/src/Booking/Booking/Models/Booking.cs @@ -1,6 +1,5 @@ using Booking.Booking.Events.Domain; using Booking.Booking.Models.ValueObjects; -using BuildingBlocks.Domain.Model; using BuildingBlocks.EventStoreDB.Events; namespace Booking.Booking.Models; diff --git a/src/Services/Booking/src/Booking/Data/BookingDbContext.cs b/src/Services/Booking/src/Booking/Data/BookingDbContext.cs index 7fa98fb..9a1c9b9 100644 --- a/src/Services/Booking/src/Booking/Data/BookingDbContext.cs +++ b/src/Services/Booking/src/Booking/Data/BookingDbContext.cs @@ -7,6 +7,8 @@ namespace Booking.Data; public class BookingDbContext : AppDbContextBase { + public const string DefaultSchema = "dbo"; + public BookingDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options, httpContextAccessor) { } diff --git a/src/Services/Booking/src/Booking/Data/Configurations/BookingConfiguration.cs b/src/Services/Booking/src/Booking/Data/Configurations/BookingConfiguration.cs index 01679d6..cedaf72 100644 --- a/src/Services/Booking/src/Booking/Data/Configurations/BookingConfiguration.cs +++ b/src/Services/Booking/src/Booking/Data/Configurations/BookingConfiguration.cs @@ -7,7 +7,7 @@ public class BookingConfiguration : IEntityTypeConfiguration builder) { - builder.ToTable("Booking", "dbo"); + builder.ToTable("Booking", BookingDbContext.DefaultSchema); builder.HasKey(r => r.Id); builder.Property(r => r.Id).ValueGeneratedNever(); diff --git a/src/Services/Booking/src/Booking/Data/Configurations/PersistMessageConfiguration.cs b/src/Services/Booking/src/Booking/Data/Configurations/PersistMessageConfiguration.cs new file mode 100644 index 0000000..26c0387 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/Configurations/PersistMessageConfiguration.cs @@ -0,0 +1,42 @@ +using BuildingBlocks.MessageProcessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Booking.Data.Configurations; + +public class PersistMessageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("PersistMessages", BookingDbContext.DefaultSchema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.MessageStatus) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageStatus)Enum.Parse(typeof(MessageStatus), v)) + .IsRequired() + .IsUnicode(false); + } +} diff --git a/src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.Designer.cs b/src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.Designer.cs new file mode 100644 index 0000000..1ed4593 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.Designer.cs @@ -0,0 +1,152 @@ +// +using System; +using Booking.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Booking.Data.Migrations +{ + [DbContext(typeof(BookingDbContext))] + [Migration("20220616121920_Add-PersistMessages")] + partial class AddPersistMessages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Booking.Booking.Models.Booking", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Booking", "dbo"); + }); + + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MessageStatus") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PersistMessages", "dbo"); + }); + + modelBuilder.Entity("Booking.Booking.Models.Booking", b => + { + b.OwnsOne("Booking.Booking.Models.ValueObjects.PassengerInfo", "PassengerInfo", b1 => + { + b1.Property("BookingId") + .HasColumnType("bigint"); + + b1.Property("Name") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("BookingId"); + + b1.ToTable("Booking", "dbo"); + + b1.WithOwner() + .HasForeignKey("BookingId"); + }); + + b.OwnsOne("Booking.Booking.Models.ValueObjects.Trip", "Trip", b1 => + { + b1.Property("BookingId") + .HasColumnType("bigint"); + + b1.Property("AircraftId") + .HasColumnType("bigint"); + + b1.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b1.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b1.Property("Description") + .HasColumnType("nvarchar(max)"); + + b1.Property("FlightDate") + .HasColumnType("datetime2"); + + b1.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b1.Property("Price") + .HasColumnType("decimal(18,2)"); + + b1.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("BookingId"); + + b1.ToTable("Booking", "dbo"); + + b1.WithOwner() + .HasForeignKey("BookingId"); + }); + + b.Navigation("PassengerInfo"); + + b.Navigation("Trip"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.cs b/src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.cs new file mode 100644 index 0000000..984a0e7 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/Migrations/20220616121920_Add-PersistMessages.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Booking.Data.Migrations +{ + public partial class AddPersistMessages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PersistMessages", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + DataType = table.Column(type: "nvarchar(max)", nullable: true), + Data = table.Column(type: "nvarchar(max)", nullable: true), + Created = table.Column(type: "datetime2", nullable: false), + RetryCount = table.Column(type: "int", nullable: false), + MessageStatus = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + DeliveryType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PersistMessages", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PersistMessages", + schema: "dbo"); + } + } +} diff --git a/src/Services/Booking/src/Booking/Data/Migrations/BookingDbContextModelSnapshot.cs b/src/Services/Booking/src/Booking/Data/Migrations/BookingDbContextModelSnapshot.cs index e8fd0f9..bc24711 100644 --- a/src/Services/Booking/src/Booking/Data/Migrations/BookingDbContextModelSnapshot.cs +++ b/src/Services/Booking/src/Booking/Data/Migrations/BookingDbContextModelSnapshot.cs @@ -50,6 +50,41 @@ namespace Booking.Data.Migrations b.ToTable("Booking", "dbo"); }); + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MessageStatus") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PersistMessages", "dbo"); + }); + modelBuilder.Entity("Booking.Booking.Models.Booking", b => { b.OwnsOne("Booking.Booking.Models.ValueObjects.PassengerInfo", "PassengerInfo", b1 => diff --git a/src/Services/Booking/src/Booking/EventMapper.cs b/src/Services/Booking/src/Booking/EventMapper.cs index 7aebd40..2de0fee 100644 --- a/src/Services/Booking/src/Booking/EventMapper.cs +++ b/src/Services/Booking/src/Booking/EventMapper.cs @@ -1,7 +1,7 @@ using Booking.Booking.Events.Domain; using BuildingBlocks.Contracts.EventBus.Messages; -using BuildingBlocks.Domain; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; namespace Booking; diff --git a/src/Services/Booking/tests/IntegrationTest/IntegrationTestFixture.cs b/src/Services/Booking/tests/IntegrationTest/IntegrationTestFixture.cs index 30213ce..b1e8679 100644 --- a/src/Services/Booking/tests/IntegrationTest/IntegrationTestFixture.cs +++ b/src/Services/Booking/tests/IntegrationTest/IntegrationTestFixture.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using Booking.Data; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using BuildingBlocks.MassTransit; using BuildingBlocks.Web; using Grpc.Net.Client; @@ -48,7 +48,6 @@ public class IntegrationTestFixture : IAsyncLifetime builder.UseEnvironment("test"); builder.ConfigureServices(services => { - services.RemoveAll(typeof(IHostedService)); services.ReplaceSingleton(AddHttpContextAccessorMock); TestRegistrationServices?.Invoke(services); services.AddMassTransitTestHarness(x => diff --git a/src/Services/Flight/src/Flight.Api/Program.cs b/src/Services/Flight/src/Flight.Api/Program.cs index 8c2cfeb..f76cfdf 100644 --- a/src/Services/Flight/src/Flight.Api/Program.cs +++ b/src/Services/Flight/src/Flight.Api/Program.cs @@ -1,6 +1,6 @@ using System.Reflection; using BuildingBlocks.Caching; -using BuildingBlocks.Domain; +using BuildingBlocks.Core; using BuildingBlocks.EFCore; using BuildingBlocks.Exception; using BuildingBlocks.HealthCheck; @@ -9,6 +9,8 @@ using BuildingBlocks.Jwt; using BuildingBlocks.Logging; using BuildingBlocks.Mapster; using BuildingBlocks.MassTransit; +using BuildingBlocks.MessageProcessor; +using BuildingBlocks.Mongo; using BuildingBlocks.OpenTelemetry; using BuildingBlocks.Swagger; using BuildingBlocks.Web; @@ -33,9 +35,11 @@ var env = builder.Environment; var appOptions = builder.Services.GetOptions("AppOptions"); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); -builder.Services.AddTransient(); builder.Services.AddCustomDbContext(configuration); builder.Services.AddScoped(); +builder.Services.AddMongoDbContext(configuration); + +builder.Services.AddPersistMessage(configuration); builder.AddCustomSerilog(); builder.Services.AddJwt(); diff --git a/src/Services/Flight/src/Flight.Api/appsettings.json b/src/Services/Flight/src/Flight.Api/appsettings.json index 47f91d8..5f5443c 100644 --- a/src/Services/Flight/src/Flight.Api/appsettings.json +++ b/src/Services/Flight/src/Flight.Api/appsettings.json @@ -10,6 +10,10 @@ "ConnectionStrings": { "DefaultConnection": "Server=.\\sqlexpress;Database=FlightDB;Trusted_Connection=True;MultipleActiveResultSets=true" }, + "MongoOptions": { + "ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "flight-db" + }, "Jwt": { "Authority": "https://localhost:5005", "Audience": "flight-api" @@ -20,5 +24,9 @@ "UserName": "guest", "Password": "guest" }, + "PersistMessageOptions": { + "Interval": 30, + "Enabled": true + }, "AllowedHosts": "*" } diff --git a/src/Services/Flight/src/Flight.Api/appsettings.test.json b/src/Services/Flight/src/Flight.Api/appsettings.test.json index 09c3487..4e0aab5 100644 --- a/src/Services/Flight/src/Flight.Api/appsettings.test.json +++ b/src/Services/Flight/src/Flight.Api/appsettings.test.json @@ -15,5 +15,9 @@ "Microsoft.Hosting.Lifetime": "Debug", "Microsoft.EntityFrameworkCore.Database.Command": "Debug" } + }, + "PersistMessageOptions": { + "Interval": 1, + "Enabled": true } } diff --git a/src/Services/Flight/src/Flight/Aircrafts/Events/AircraftCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Aircrafts/Events/AircraftCreatedDomainEvent.cs index 4d23d8d..e3dbdb7 100644 --- a/src/Services/Flight/src/Flight/Aircrafts/Events/AircraftCreatedDomainEvent.cs +++ b/src/Services/Flight/src/Flight/Aircrafts/Events/AircraftCreatedDomainEvent.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace Flight.Aircrafts.Events; diff --git a/src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs b/src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs index 9f7d7ba..2d04d75 100644 --- a/src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs +++ b/src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using BuildingBlocks.IdsGenerator; using Flight.Aircrafts.Events; diff --git a/src/Services/Flight/src/Flight/Airports/Events/AirportCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Airports/Events/AirportCreatedDomainEvent.cs index 2fcdad9..e04c281 100644 --- a/src/Services/Flight/src/Flight/Airports/Events/AirportCreatedDomainEvent.cs +++ b/src/Services/Flight/src/Flight/Airports/Events/AirportCreatedDomainEvent.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace Flight.Airports.Events; diff --git a/src/Services/Flight/src/Flight/Airports/Models/Airport.cs b/src/Services/Flight/src/Flight/Airports/Models/Airport.cs index 7837cff..ca33b8f 100644 --- a/src/Services/Flight/src/Flight/Airports/Models/Airport.cs +++ b/src/Services/Flight/src/Flight/Airports/Models/Airport.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using BuildingBlocks.IdsGenerator; using Flight.Airports.Events; diff --git a/src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs index 911fc8d..80705fd 100644 --- a/src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs +++ b/src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs @@ -8,7 +8,7 @@ public class AircraftConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("Aircraft", "dbo"); + builder.ToTable("Aircraft", FlightDbContext.DefaultSchema); builder.HasKey(r => r.Id); builder.Property(r => r.Id).ValueGeneratedNever(); } diff --git a/src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs index 0513096..d7c51e4 100644 --- a/src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs +++ b/src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs @@ -8,7 +8,7 @@ public class AirportConfiguration: IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("Airport", "dbo"); + builder.ToTable("Airport", FlightDbContext.DefaultSchema); builder.HasKey(r => r.Id); builder.Property(r => r.Id).ValueGeneratedNever(); diff --git a/src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs index 90e152a..d20c139 100644 --- a/src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs +++ b/src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs @@ -9,7 +9,7 @@ public class FlightConfiguration : IEntityTypeConfiguration builder) { - builder.ToTable("Flight", "dbo"); + builder.ToTable("Flight", FlightDbContext.DefaultSchema); builder.HasKey(r => r.Id); builder.Property(r => r.Id).ValueGeneratedNever(); diff --git a/src/Services/Flight/src/Flight/Data/Configurations/PersistMessageConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/PersistMessageConfiguration.cs new file mode 100644 index 0000000..e9455c9 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Configurations/PersistMessageConfiguration.cs @@ -0,0 +1,43 @@ +using System; +using BuildingBlocks.MessageProcessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Flight.Data.Configurations; + +public class PersistMessageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("PersistMessages", FlightDbContext.DefaultSchema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.MessageStatus) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageStatus)Enum.Parse(typeof(MessageStatus), v)) + .IsRequired() + .IsUnicode(false); + } +} diff --git a/src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs index 493ae90..ef85463 100644 --- a/src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs +++ b/src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs @@ -8,7 +8,7 @@ public class SeatConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("Seat", "dbo"); + builder.ToTable("Seat", FlightDbContext.DefaultSchema); builder.HasKey(r => r.Id); builder.Property(r => r.Id).ValueGeneratedNever(); diff --git a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs index 8793eba..dba23ab 100644 --- a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs +++ b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs @@ -10,6 +10,7 @@ namespace Flight.Data; public sealed class FlightDbContext : AppDbContextBase { + public const string DefaultSchema = "dbo"; public FlightDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base( options, httpContextAccessor) { diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.Designer.cs new file mode 100644 index 0000000..b5c33cf --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.Designer.cs @@ -0,0 +1,266 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220616121204_Add-PersistMessages")] + partial class AddPersistMessages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MessageStatus") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PersistMessages", "dbo"); + }); + + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.HasOne("Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.HasOne("Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.cs new file mode 100644 index 0000000..64a5739 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220616121204_Add-PersistMessages.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class AddPersistMessages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PersistMessages", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + DataType = table.Column(type: "nvarchar(max)", nullable: true), + Data = table.Column(type: "nvarchar(max)", nullable: true), + Created = table.Column(type: "datetime2", nullable: false), + RetryCount = table.Column(type: "int", nullable: false), + MessageStatus = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + DeliveryType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PersistMessages", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PersistMessages", + schema: "dbo"); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs b/src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs index de7d19c..730306d 100644 --- a/src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs +++ b/src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs @@ -22,6 +22,41 @@ namespace Flight.Data.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MessageStatus") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PersistMessages", "dbo"); + }); + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => { b.Property("Id") diff --git a/src/Services/Flight/src/Flight/EventMapper.cs b/src/Services/Flight/src/Flight/EventMapper.cs index a0812ef..ad0229b 100644 --- a/src/Services/Flight/src/Flight/EventMapper.cs +++ b/src/Services/Flight/src/Flight/EventMapper.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; using BuildingBlocks.Contracts.EventBus.Messages; -using BuildingBlocks.Domain; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; using Flight.Aircrafts.Events; using Flight.Airports.Events; using Flight.Flights.Events.Domain; diff --git a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightCreatedDomainEvent.cs index e37feef..f1900ef 100644 --- a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightCreatedDomainEvent.cs +++ b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightCreatedDomainEvent.cs @@ -1,5 +1,5 @@ using System; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; using Flight.Flights.Models; namespace Flight.Flights.Events.Domain; diff --git a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightDeletedDomainEvent.cs b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightDeletedDomainEvent.cs index 66040a4..0bd8ba6 100644 --- a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightDeletedDomainEvent.cs +++ b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightDeletedDomainEvent.cs @@ -1,5 +1,5 @@ using System; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; using Flight.Flights.Models; namespace Flight.Flights.Events.Domain; diff --git a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightUpdatedDomainEvent.cs b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightUpdatedDomainEvent.cs index c00b867..d3bdc27 100644 --- a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightUpdatedDomainEvent.cs +++ b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightUpdatedDomainEvent.cs @@ -1,5 +1,5 @@ using System; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; using Flight.Flights.Models; namespace Flight.Flights.Events.Domain; diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommand.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommand.cs index dd160de..7698298 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommand.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommand.cs @@ -1,14 +1,14 @@ using System; +using BuildingBlocks.Core.CQRS; using BuildingBlocks.IdsGenerator; using Flight.Flights.Dtos; using Flight.Flights.Models; -using MediatR; namespace Flight.Flights.Features.CreateFlight; public record CreateFlightCommand(string FlightNumber, long AircraftId, long DepartureAirportId, DateTime DepartureDate, DateTime ArriveDate, long ArriveAirportId, - decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, decimal Price) : IRequest + decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, decimal Price) : ICommand { public long Id { get; set; } = SnowFlakIdGenerator.NewId(); } diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandHandler.cs index ee42d97..859764a 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandHandler.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandHandler.cs @@ -1,25 +1,30 @@ using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; +using BuildingBlocks.MessageProcessor; using Flight.Data; using Flight.Flights.Dtos; using Flight.Flights.Exceptions; -using Flight.Flights.Models; +using Flight.Flights.Features.CreateFlight.Reads; using MapsterMapper; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Flight.Flights.Features.CreateFlight; -public class CreateFlightCommandHandler : IRequestHandler +public class CreateFlightCommandHandler : ICommandHandler { private readonly FlightDbContext _flightDbContext; + private readonly IPersistMessageProcessor _persistMessageProcessor; private readonly IMapper _mapper; - public CreateFlightCommandHandler(IMapper mapper, FlightDbContext flightDbContext) + public CreateFlightCommandHandler(IMapper mapper, + FlightDbContext flightDbContext, + IPersistMessageProcessor persistMessageProcessor) { _mapper = mapper; _flightDbContext = flightDbContext; + _persistMessageProcessor = persistMessageProcessor; } public async Task Handle(CreateFlightCommand command, CancellationToken cancellationToken) @@ -32,11 +37,17 @@ public class CreateFlightCommandHandler : IRequestHandler(newFlight.Entity); + + await _persistMessageProcessor.AddInternalMessageAsync(createFlightMongoCommand, cancellationToken); + return _mapper.Map(newFlight.Entity); } } diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommand.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommand.cs new file mode 100644 index 0000000..5108853 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommand.cs @@ -0,0 +1,43 @@ +using System; +using BuildingBlocks.Core.Event; +using Flight.Flights.Models; + +namespace Flight.Flights.Features.CreateFlight.Reads; + +public class CreateFlightMongoCommand : InternalCommand +{ + public CreateFlightMongoCommand() + { + + } + + public CreateFlightMongoCommand(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; init; } + public long AircraftId { get; init; } + public DateTime DepartureDate { get; init; } + public long DepartureAirportId { get; init; } + public DateTime ArriveDate { get; init; } + public long ArriveAirportId { get; init; } + public decimal DurationMinutes { get; init; } + public DateTime FlightDate { get; init; } + public FlightStatus Status { get; init; } + public decimal Price { get; init; } + public bool IsDeleted { get; init; } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommandHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommandHandler.cs new file mode 100644 index 0000000..aa92e58 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/Reads/CreateFlightMongoCommandHandler.cs @@ -0,0 +1,35 @@ +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; +using Flight.Data; +using Flight.Flights.Models.Reads; +using MapsterMapper; +using MediatR; + +namespace Flight.Flights.Features.CreateFlight.Reads; + +public class CreateFlightMongoCommandHandler : ICommandHandler +{ + private readonly FlightReadDbContext _flightReadDbContext; + private readonly IMapper _mapper; + + public CreateFlightMongoCommandHandler( + FlightReadDbContext flightReadDbContext, + IMapper mapper) + { + _flightReadDbContext = flightReadDbContext; + _mapper = mapper; + } + + public async Task Handle(CreateFlightMongoCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var flightReadModel = _mapper.Map(command); + + await _flightReadDbContext.Flight.InsertOneAsync(flightReadModel, cancellationToken: cancellationToken); + + return Unit.Value; + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommand.cs b/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommand.cs index ea1094e..d5fc3b1 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommand.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommand.cs @@ -1,6 +1,6 @@ -using Flight.Flights.Dtos; -using MediatR; +using BuildingBlocks.Core.CQRS; +using Flight.Flights.Dtos; namespace Flight.Flights.Features.DeleteFlight; -public record DeleteFlightCommand(long Id) : IRequest; +public record DeleteFlightCommand(long Id) : ICommand; diff --git a/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommandHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommandHandler.cs index 2a4c6ab..ec7054e 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommandHandler.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/DeleteFlight/DeleteFlightCommandHandler.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; using Flight.Data; using Flight.Flights.Dtos; using Flight.Flights.Exceptions; @@ -11,7 +12,7 @@ using Microsoft.EntityFrameworkCore; namespace Flight.Flights.Features.DeleteFlight; -public class DeleteFlightCommandHandler : IRequestHandler +public class DeleteFlightCommandHandler : ICommandHandler { private readonly FlightDbContext _flightDbContext; private readonly IMapper _mapper; diff --git a/src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs b/src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs index 306e555..adc1005 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs @@ -1,5 +1,7 @@ using AutoMapper; using Flight.Flights.Dtos; +using Flight.Flights.Features.CreateFlight.Reads; +using Flight.Flights.Models.Reads; using Mapster; namespace Flight.Flights.Features; @@ -9,5 +11,7 @@ public class FlightMappings : Profile public void Register(TypeAdapterConfig config) { config.NewConfig(); - } + config.NewConfig(); + config.NewConfig(); + } } diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQuery.cs b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQuery.cs index a4f67f7..f5c8f12 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQuery.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQuery.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using BuildingBlocks.Caching; +using BuildingBlocks.Core.CQRS; using Flight.Flights.Dtos; using MediatR; namespace Flight.Flights.Features.GetAvailableFlights; -public record GetAvailableFlightsQuery : IRequest>, ICacheRequest +public record GetAvailableFlightsQuery : IQuery>, ICacheRequest { public string CacheKey => "GetAvailableFlightsQuery"; public DateTime? AbsoluteExpirationRelativeToNow => DateTime.Now.AddHours(1); diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryHandler.cs index 2b8a8e5..c5c403d 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryHandler.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryHandler.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; using Flight.Data; using Flight.Flights.Dtos; using Flight.Flights.Exceptions; @@ -12,7 +13,7 @@ using Microsoft.EntityFrameworkCore; namespace Flight.Flights.Features.GetAvailableFlights; -public class GetAvailableFlightsQueryHandler : IRequestHandler> +public class GetAvailableFlightsQueryHandler : IQueryHandler> { private readonly FlightDbContext _flightDbContext; private readonly IMapper _mapper; diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQuery.cs b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQuery.cs index f6bc974..c093b98 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQuery.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQuery.cs @@ -1,6 +1,6 @@ +using BuildingBlocks.Core.CQRS; using Flight.Flights.Dtos; -using MediatR; namespace Flight.Flights.Features.GetFlightById; -public record GetFlightByIdQuery(long Id) : IRequest; +public record GetFlightByIdQuery(long Id) : IQuery; diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryHandler.cs index c1efa05..968ffa4 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryHandler.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryHandler.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; using Flight.Data; using Flight.Flights.Dtos; using Flight.Flights.Exceptions; @@ -10,7 +11,7 @@ using Microsoft.EntityFrameworkCore; namespace Flight.Flights.Features.GetFlightById; -public class GetFlightByIdQueryHandler : IRequestHandler +public class GetFlightByIdQueryHandler : IQueryHandler { private readonly FlightDbContext _flightDbContext; private readonly IMapper _mapper; diff --git a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommand.cs b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommand.cs index b85bdd7..c51c0cb 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommand.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommand.cs @@ -1,12 +1,13 @@ using System; using BuildingBlocks.Caching; +using BuildingBlocks.Core.CQRS; using Flight.Flights.Dtos; using Flight.Flights.Models; using MediatR; namespace Flight.Flights.Features.UpdateFlight; -public record UpdateFlightCommand : IRequest, IInvalidateCacheRequest +public record UpdateFlightCommand : ICommand, IInvalidateCacheRequest { public long Id { get; init; } public string FlightNumber { get; init; } diff --git a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandHandler.cs index 3ba88ea..a2cbe6a 100644 --- a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandHandler.cs +++ b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandHandler.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; using BuildingBlocks.EventStoreDB.Repository; using Flight.Data; using Flight.Flights.Dtos; @@ -12,7 +13,7 @@ using Microsoft.EntityFrameworkCore; namespace Flight.Flights.Features.UpdateFlight; -public class UpdateFlightCommandHandler : IRequestHandler +public class UpdateFlightCommandHandler : ICommandHandler { private readonly FlightDbContext _flightDbContext; private readonly IMapper _mapper; diff --git a/src/Services/Flight/src/Flight/Flights/Models/Flight.cs b/src/Services/Flight/src/Flight/Flights/Models/Flight.cs index fcf3e4b..d127a90 100644 --- a/src/Services/Flight/src/Flight/Flights/Models/Flight.cs +++ b/src/Services/Flight/src/Flight/Flights/Models/Flight.cs @@ -1,5 +1,5 @@ using System; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using Flight.Flights.Events.Domain; namespace Flight.Flights.Models; diff --git a/src/Services/Flight/src/Flight/Seats/Events/SeatCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Seats/Events/SeatCreatedDomainEvent.cs index 3c7afed..ecd610e 100644 --- a/src/Services/Flight/src/Flight/Seats/Events/SeatCreatedDomainEvent.cs +++ b/src/Services/Flight/src/Flight/Seats/Events/SeatCreatedDomainEvent.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; using Flight.Seats.Models; namespace Flight.Seats.Events; diff --git a/src/Services/Flight/src/Flight/Seats/Models/Seat.cs b/src/Services/Flight/src/Flight/Seats/Models/Seat.cs index eb254c2..098177f 100644 --- a/src/Services/Flight/src/Flight/Seats/Models/Seat.cs +++ b/src/Services/Flight/src/Flight/Seats/Models/Seat.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; namespace Flight.Seats.Models; diff --git a/src/Services/Flight/tests/IntegrationTest/IntegrationTestFixture.cs b/src/Services/Flight/tests/IntegrationTest/IntegrationTestFixture.cs index 8a57d30..5010761 100644 --- a/src/Services/Flight/tests/IntegrationTest/IntegrationTestFixture.cs +++ b/src/Services/Flight/tests/IntegrationTest/IntegrationTestFixture.cs @@ -1,9 +1,10 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using BuildingBlocks.EFCore; using BuildingBlocks.MassTransit; +using BuildingBlocks.MessageProcessor; using BuildingBlocks.Web; using Flight.Data; using FluentAssertions.Common; @@ -66,8 +67,6 @@ public class IntegrationTestFixture : IAsyncLifetime builder.UseEnvironment("test"); builder.ConfigureServices(services => { - services.RemoveAll(typeof(IHostedService)); - services.ReplaceSingleton(AddHttpContextAccessorMock); TestRegistrationServices?.Invoke(services); services.AddMassTransitTestHarness(x => { diff --git a/src/Services/Flight/tests/UnitTest/Flight/Features/CreateFlight/CreateFlightCommandHandlerTests.cs b/src/Services/Flight/tests/UnitTest/Flight/Features/CreateFlight/CreateFlightCommandHandlerTests.cs index 318ce60..a59f5dd 100644 --- a/src/Services/Flight/tests/UnitTest/Flight/Features/CreateFlight/CreateFlightCommandHandlerTests.cs +++ b/src/Services/Flight/tests/UnitTest/Flight/Features/CreateFlight/CreateFlightCommandHandlerTests.cs @@ -1,9 +1,12 @@ using System; using System.Threading; using System.Threading.Tasks; +using BuildingBlocks.MessageProcessor; using Flight.Flights.Dtos; using Flight.Flights.Features.CreateFlight; using FluentAssertions; +using Microsoft.AspNetCore.Http; +using NSubstitute; using Unit.Test.Common; using Unit.Test.Fakes; using Xunit; @@ -22,7 +25,7 @@ public class CreateFlightCommandHandlerTests public CreateFlightCommandHandlerTests(UnitTestFixture fixture) { _fixture = fixture; - _handler = new CreateFlightCommandHandler(fixture.Mapper, fixture.DbContext); + _handler = new CreateFlightCommandHandler(fixture.Mapper, fixture.DbContext, Substitute.For()); } [Fact] diff --git a/src/Services/Identity/src/Identity.Api/Program.cs b/src/Services/Identity/src/Identity.Api/Program.cs index 6011e81..aebc6ab 100644 --- a/src/Services/Identity/src/Identity.Api/Program.cs +++ b/src/Services/Identity/src/Identity.Api/Program.cs @@ -1,9 +1,10 @@ -using BuildingBlocks.Domain; +using BuildingBlocks.Core; using BuildingBlocks.EFCore; using BuildingBlocks.HealthCheck; using BuildingBlocks.Logging; using BuildingBlocks.Mapster; using BuildingBlocks.MassTransit; +using BuildingBlocks.MessageProcessor; using BuildingBlocks.OpenTelemetry; using BuildingBlocks.Swagger; using BuildingBlocks.Utils; @@ -26,7 +27,6 @@ var env = builder.Environment; var appOptions = builder.Services.GetOptions("AppOptions"); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); -builder.Services.AddTransient(); builder.Services.AddScoped(provider => provider.GetService()!); builder.Services.AddDbContext(options => @@ -34,6 +34,8 @@ builder.Services.AddDbContext(options => configuration.GetConnectionString("DefaultConnection"), x => x.MigrationsAssembly(typeof(IdentityRoot).Assembly.GetName().Name))); +builder.Services.AddPersistMessage(configuration); + builder.AddCustomSerilog(); builder.Services.AddControllers(); builder.Services.AddCustomSwagger(builder.Configuration, typeof(IdentityRoot).Assembly); @@ -45,7 +47,6 @@ builder.Services.AddCustomMapster(typeof(IdentityRoot).Assembly); builder.Services.AddScoped(); builder.Services.AddCustomHealthCheck(); builder.Services.AddTransient(); -builder.Services.AddTransient(); builder.Services.AddCustomMassTransit(typeof(IdentityRoot).Assembly, env); builder.Services.AddCustomOpenTelemetry(); diff --git a/src/Services/Identity/src/Identity.Api/appsettings.json b/src/Services/Identity/src/Identity.Api/appsettings.json index 126be44..645577e 100644 --- a/src/Services/Identity/src/Identity.Api/appsettings.json +++ b/src/Services/Identity/src/Identity.Api/appsettings.json @@ -16,5 +16,9 @@ "LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}", "ElasticUri": "http://localhost:9200" }, + "PersistMessageOptions": { + "Interval": 30, + "Enabled": true + }, "AllowedHosts": "*" } diff --git a/src/Services/Identity/src/Identity.Api/appsettings.test.json b/src/Services/Identity/src/Identity.Api/appsettings.test.json index 1243108..738f8a4 100644 --- a/src/Services/Identity/src/Identity.Api/appsettings.test.json +++ b/src/Services/Identity/src/Identity.Api/appsettings.test.json @@ -15,5 +15,9 @@ "Microsoft.Hosting.Lifetime": "Debug", "Microsoft.EntityFrameworkCore.Database.Command": "Debug" } + }, + "PersistMessageOptions": { + "Interval": 1, + "Enabled": true } } diff --git a/src/Services/Identity/src/Identity/Data/Configurations/PersistMessageConfiguration.cs b/src/Services/Identity/src/Identity/Data/Configurations/PersistMessageConfiguration.cs new file mode 100644 index 0000000..843d725 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Configurations/PersistMessageConfiguration.cs @@ -0,0 +1,43 @@ +using System; +using BuildingBlocks.MessageProcessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Identity.Data.Configurations; + +public class PersistMessageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("PersistMessages", IdentityContext.DefaultSchema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.MessageStatus) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageStatus)Enum.Parse(typeof(MessageStatus), v)) + .IsRequired() + .IsUnicode(false); + } +} diff --git a/src/Services/Identity/src/Identity/Data/IdentityContext.cs b/src/Services/Identity/src/Identity/Data/IdentityContext.cs index 396fd0d..8dda0f3 100644 --- a/src/Services/Identity/src/Identity/Data/IdentityContext.cs +++ b/src/Services/Identity/src/Identity/Data/IdentityContext.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using BuildingBlocks.Domain.Event; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Event; +using BuildingBlocks.Core.Model; using BuildingBlocks.EFCore; using Identity.Identity.Models; using Microsoft.AspNetCore.Http; @@ -21,9 +21,12 @@ public sealed class IdentityContext : IdentityDbContext, IdentityUserRole, IdentityUserLogin, IdentityRoleClaim, IdentityUserToken>, IDbContext { + public const string DefaultSchema = "dbo"; private IDbContextTransaction _currentTransaction; - public IdentityContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options) + + public IdentityContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : + base(options) { } @@ -31,6 +34,8 @@ public sealed class IdentityContext : IdentityDbContext +using System; +using Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20220616123336_Add-PersistMessages")] + partial class AddPersistMessages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MessageStatus") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PersistMessages", "dbo"); + }); + + modelBuilder.Entity("Identity.Identity.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PassPortNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220616123336_Add-PersistMessages.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220616123336_Add-PersistMessages.cs new file mode 100644 index 0000000..576e0ff --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220616123336_Add-PersistMessages.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Identity.Data.Migrations +{ + public partial class AddPersistMessages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InternalMessages"); + + migrationBuilder.DropTable( + name: "OutboxMessages"); + + migrationBuilder.EnsureSchema( + name: "dbo"); + + migrationBuilder.CreateTable( + name: "PersistMessages", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + DataType = table.Column(type: "nvarchar(max)", nullable: true), + Data = table.Column(type: "nvarchar(max)", nullable: true), + Created = table.Column(type: "datetime2", nullable: false), + RetryCount = table.Column(type: "int", nullable: false), + MessageStatus = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + DeliveryType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PersistMessages", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PersistMessages", + schema: "dbo"); + + migrationBuilder.CreateTable( + name: "InternalMessages", + columns: table => new + { + EventId = table.Column(type: "uniqueidentifier", nullable: false), + CommandType = table.Column(type: "nvarchar(max)", nullable: false), + CorrelationId = table.Column(type: "uniqueidentifier", nullable: true), + Data = table.Column(type: "nvarchar(max)", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InternalMessages", x => x.EventId); + }); + + migrationBuilder.CreateTable( + name: "OutboxMessages", + columns: table => new + { + EventId = table.Column(type: "uniqueidentifier", nullable: false), + CorrelationId = table.Column(type: "uniqueidentifier", nullable: true), + Data = table.Column(type: "nvarchar(max)", nullable: false), + EventType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true), + Type = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.EventId); + }); + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs b/src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs index fdb99f4..862f04d 100644 --- a/src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs +++ b/src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs @@ -22,74 +22,39 @@ namespace Identity.Data.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); - modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => { - b.Property("EventId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CommandType") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("CorrelationId") - .HasColumnType("uniqueidentifier"); - - b.Property("Data") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("OccurredOn") + b.Property("Created") .HasColumnType("datetime2"); - b.Property("ProcessedOn") - .HasColumnType("datetime2"); - - b.HasKey("EventId"); - - b.ToTable("InternalMessages", (string)null); - }); - - modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => - { - b.Property("EventId") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CorrelationId") - .HasColumnType("uniqueidentifier"); - b.Property("Data") - .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("EventType") + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") .IsRequired() .HasMaxLength(50) .IsUnicode(false) .HasColumnType("varchar(50)"); - b.Property("Name") + b.Property("MessageStatus") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); - b.Property("OccurredOn") - .HasColumnType("datetime2"); + b.Property("RetryCount") + .HasColumnType("int"); - b.Property("ProcessedOn") - .HasColumnType("datetime2"); + b.HasKey("Id"); - b.Property("Type") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("EventId"); - - b.ToTable("OutboxMessages", (string)null); + b.ToTable("PersistMessages", "dbo"); }); modelBuilder.Entity("Identity.Identity.Models.ApplicationUser", b => diff --git a/src/Services/Identity/src/Identity/EventMapper.cs b/src/Services/Identity/src/Identity/EventMapper.cs index 2a5fa56..f8295c8 100644 --- a/src/Services/Identity/src/Identity/EventMapper.cs +++ b/src/Services/Identity/src/Identity/EventMapper.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -using BuildingBlocks.Domain; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; namespace Identity; diff --git a/src/Services/Identity/src/Identity/Identity.csproj b/src/Services/Identity/src/Identity/Identity.csproj index 27a3c70..74cbbc4 100644 --- a/src/Services/Identity/src/Identity/Identity.csproj +++ b/src/Services/Identity/src/Identity/Identity.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommand.cs b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommand.cs index 9b159d5..77763da 100644 --- a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommand.cs +++ b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommand.cs @@ -1,6 +1,7 @@ +using BuildingBlocks.Core.CQRS; using Identity.Identity.Dtos; -using MediatR; namespace Identity.Identity.Features.RegisterNewUser; -public record RegisterNewUserCommand(string FirstName, string LastName, string Username, string Email, string Password, string ConfirmPassword, string PassportNumber) : IRequest; +public record RegisterNewUserCommand(string FirstName, string LastName, string Username, string Email, + string Password, string ConfirmPassword, string PassportNumber) : ICommand; diff --git a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommandHandler.cs b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommandHandler.cs index f394bc2..5088d5c 100644 --- a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommandHandler.cs +++ b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommandHandler.cs @@ -3,7 +3,8 @@ using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; using BuildingBlocks.Contracts.EventBus.Messages; -using BuildingBlocks.Domain; +using BuildingBlocks.Core; +using BuildingBlocks.Core.CQRS; using Identity.Identity.Dtos; using Identity.Identity.Exceptions; using Identity.Identity.Models; @@ -12,16 +13,16 @@ using Microsoft.AspNetCore.Identity; namespace Identity.Identity.Features.RegisterNewUser; -public class RegisterNewUserCommandHandler : IRequestHandler +public class RegisterNewUserCommandHandler : ICommandHandler { - private readonly IBusPublisher _busPublisher; + private readonly IEventDispatcher _eventDispatcher; private readonly UserManager _userManager; public RegisterNewUserCommandHandler(UserManager userManager, - IBusPublisher busPublisher) + IEventDispatcher eventDispatcher) { _userManager = userManager; - _busPublisher = busPublisher; + _eventDispatcher = eventDispatcher; } public async Task Handle(RegisterNewUserCommand command, @@ -48,7 +49,7 @@ public class RegisterNewUserCommandHandler : IRequestHandler e.Description))); - await _busPublisher.SendAsync(new UserCreated(applicationUser.Id, applicationUser.FirstName + " " + applicationUser.LastName, + await _eventDispatcher.SendAsync(new UserCreated(applicationUser.Id, applicationUser.FirstName + " " + applicationUser.LastName, applicationUser.PassPortNumber), cancellationToken); return new RegisterNewUserResponseDto diff --git a/src/Services/Identity/tests/IntegrationTest/IntegrationTestFixture.cs b/src/Services/Identity/tests/IntegrationTest/IntegrationTestFixture.cs index 271ad1d..da891f6 100644 --- a/src/Services/Identity/tests/IntegrationTest/IntegrationTestFixture.cs +++ b/src/Services/Identity/tests/IntegrationTest/IntegrationTestFixture.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using BuildingBlocks.MassTransit; using BuildingBlocks.Web; using Grpc.Net.Client; @@ -48,7 +48,6 @@ public class IntegrationTestFixture : IAsyncLifetime builder.UseEnvironment("test"); builder.ConfigureServices(services => { - services.RemoveAll(typeof(IHostedService)); services.ReplaceSingleton(AddHttpContextAccessorMock); TestRegistrationServices?.Invoke(services); services.AddMassTransitTestHarness(x => diff --git a/src/Services/Passenger/src/Passenger.Api/Program.cs b/src/Services/Passenger/src/Passenger.Api/Program.cs index 5046442..bcb5804 100644 --- a/src/Services/Passenger/src/Passenger.Api/Program.cs +++ b/src/Services/Passenger/src/Passenger.Api/Program.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain; +using BuildingBlocks.Core; using BuildingBlocks.EFCore; using BuildingBlocks.Exception; using BuildingBlocks.HealthCheck; @@ -7,6 +7,7 @@ using BuildingBlocks.Jwt; using BuildingBlocks.Logging; using BuildingBlocks.Mapster; using BuildingBlocks.MassTransit; +using BuildingBlocks.MessageProcessor; using BuildingBlocks.OpenTelemetry; using BuildingBlocks.Swagger; using BuildingBlocks.Web; @@ -27,8 +28,9 @@ var env = builder.Environment; var appOptions = builder.Services.GetOptions("AppOptions"); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); -builder.Services.AddTransient(); builder.Services.AddCustomDbContext(configuration); +builder.Services.AddPersistMessage(configuration); + builder.AddCustomSerilog(); builder.Services.AddJwt(); builder.Services.AddControllers(); diff --git a/src/Services/Passenger/src/Passenger.Api/appsettings.json b/src/Services/Passenger/src/Passenger.Api/appsettings.json index f178d51..ff5f1f2 100644 --- a/src/Services/Passenger/src/Passenger.Api/appsettings.json +++ b/src/Services/Passenger/src/Passenger.Api/appsettings.json @@ -20,5 +20,9 @@ "LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}", "ElasticUri": "http://localhost:9200" }, + "PersistMessageOptions": { + "Interval": 30, + "Enabled": true + }, "AllowedHosts": "*" } diff --git a/src/Services/Passenger/src/Passenger.Api/appsettings.test.json b/src/Services/Passenger/src/Passenger.Api/appsettings.test.json index 0d920ea..69307e9 100644 --- a/src/Services/Passenger/src/Passenger.Api/appsettings.test.json +++ b/src/Services/Passenger/src/Passenger.Api/appsettings.test.json @@ -15,5 +15,9 @@ "Microsoft.Hosting.Lifetime": "Debug", "Microsoft.EntityFrameworkCore.Database.Command": "Debug" } + }, + "PersistMessageOptions": { + "Interval": 1, + "Enabled": true } } diff --git a/src/Services/Passenger/src/Passenger/Data/Configurations/PersistMessageConfiguration.cs b/src/Services/Passenger/src/Passenger/Data/Configurations/PersistMessageConfiguration.cs new file mode 100644 index 0000000..a711b6a --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Configurations/PersistMessageConfiguration.cs @@ -0,0 +1,42 @@ +using BuildingBlocks.MessageProcessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Passenger.Data.Configurations; + +public class PersistMessageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("PersistMessages", PassengerDbContext.DefaultSchema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.DeliveryType) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageDeliveryType)Enum.Parse(typeof(MessageDeliveryType), v)) + .IsRequired() + .IsUnicode(false); + + builder.Property(x => x.MessageStatus) + .HasMaxLength(50) + .HasConversion( + v => v.ToString(), + v => (MessageStatus)Enum.Parse(typeof(MessageStatus), v)) + .IsRequired() + .IsUnicode(false); + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.Designer.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.Designer.cs new file mode 100644 index 0000000..eedd065 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.Designer.cs @@ -0,0 +1,104 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Passenger.Data; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + [DbContext(typeof(PassengerDbContext))] + [Migration("20220616122705_Add-PersistMessages")] + partial class AddPersistMessages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MessageStatus") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PersistMessages", "dbo"); + }); + + modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PassengerType") + .HasColumnType("int"); + + b.Property("PassportNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Passenger", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.cs new file mode 100644 index 0000000..faae54a --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220616122705_Add-PersistMessages.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + public partial class AddPersistMessages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PersistMessages", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + DataType = table.Column(type: "nvarchar(max)", nullable: true), + Data = table.Column(type: "nvarchar(max)", nullable: true), + Created = table.Column(type: "datetime2", nullable: false), + RetryCount = table.Column(type: "int", nullable: false), + MessageStatus = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + DeliveryType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PersistMessages", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PersistMessages", + schema: "dbo"); + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs index df21933..3687087 100644 --- a/src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs @@ -22,6 +22,41 @@ namespace Passenger.Data.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DataType") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MessageStatus") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PersistMessages", "dbo"); + }); + modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b => { b.Property("Id") diff --git a/src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs b/src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs index fc202b0..007f2c9 100644 --- a/src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs +++ b/src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs @@ -7,6 +7,8 @@ namespace Passenger.Data; public sealed class PassengerDbContext : AppDbContextBase { + public const string DefaultSchema = "dbo"; + public PassengerDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options, httpContextAccessor) { } diff --git a/src/Services/Passenger/src/Passenger/EventMapper.cs b/src/Services/Passenger/src/Passenger/EventMapper.cs index 11bb213..a11f997 100644 --- a/src/Services/Passenger/src/Passenger/EventMapper.cs +++ b/src/Services/Passenger/src/Passenger/EventMapper.cs @@ -1,5 +1,5 @@ -using BuildingBlocks.Domain; -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core; +using BuildingBlocks.Core.Event; namespace Passenger; diff --git a/src/Services/Passenger/src/Passenger/Passengers/Events/Domain/PassengerCreatedDomainEvent.cs b/src/Services/Passenger/src/Passenger/Passengers/Events/Domain/PassengerCreatedDomainEvent.cs index cddd5af..65eddc1 100644 --- a/src/Services/Passenger/src/Passenger/Passengers/Events/Domain/PassengerCreatedDomainEvent.cs +++ b/src/Services/Passenger/src/Passenger/Passengers/Events/Domain/PassengerCreatedDomainEvent.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Event; +using BuildingBlocks.Core.Event; namespace Passenger.Passengers.Events.Domain; diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommand.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommand.cs index 555dc3d..6d265f4 100644 --- a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommand.cs +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommand.cs @@ -1,11 +1,11 @@ +using BuildingBlocks.Core.CQRS; using BuildingBlocks.IdsGenerator; -using MediatR; using Passenger.Passengers.Dtos; using Passenger.Passengers.Models; namespace Passenger.Passengers.Features.CompleteRegisterPassenger; -public record CompleteRegisterPassengerCommand(string PassportNumber, PassengerType PassengerType, int Age) : IRequest +public record CompleteRegisterPassengerCommand(string PassportNumber, PassengerType PassengerType, int Age) : ICommand { public long Id { get; set; } = SnowFlakIdGenerator.NewId(); } diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandHandler.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandHandler.cs index 55eaabd..6b5c558 100644 --- a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandHandler.cs +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandHandler.cs @@ -1,4 +1,5 @@ using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; using MapsterMapper; using MediatR; using Microsoft.EntityFrameworkCore; @@ -8,7 +9,7 @@ using Passenger.Passengers.Exceptions; namespace Passenger.Passengers.Features.CompleteRegisterPassenger; -public class CompleteRegisterPassengerCommandHandler : IRequestHandler +public class CompleteRegisterPassengerCommandHandler : ICommandHandler { private readonly IMapper _mapper; private readonly PassengerDbContext _passengerDbContext; diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryById.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryById.cs index ac5611a..21b8f5e 100644 --- a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryById.cs +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryById.cs @@ -1,6 +1,6 @@ -using MediatR; +using BuildingBlocks.Core.CQRS; using Passenger.Passengers.Dtos; namespace Passenger.Passengers.Features.GetPassengerById; -public record GetPassengerQueryById(long Id) : IRequest; \ No newline at end of file +public record GetPassengerQueryById(long Id) : IQuery; diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdHandler.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdHandler.cs index 7432a12..e42d9ef 100644 --- a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdHandler.cs +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdHandler.cs @@ -1,4 +1,5 @@ using Ardalis.GuardClauses; +using BuildingBlocks.Core.CQRS; using MapsterMapper; using MediatR; using Microsoft.EntityFrameworkCore; @@ -8,7 +9,7 @@ using Passenger.Passengers.Exceptions; namespace Passenger.Passengers.Features.GetPassengerById; -public class GetPassengerQueryByIdHandler : IRequestHandler +public class GetPassengerQueryByIdHandler : IQueryHandler { private readonly PassengerDbContext _passengerDbContext; private readonly IMapper _mapper; diff --git a/src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs b/src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs index 879b606..02186a7 100644 --- a/src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs +++ b/src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs @@ -1,4 +1,4 @@ -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; namespace Passenger.Passengers.Models; diff --git a/src/Services/Passenger/tests/IntegrationTest/IntegrationTestFixture.cs b/src/Services/Passenger/tests/IntegrationTest/IntegrationTestFixture.cs index 709133e..11ef517 100644 --- a/src/Services/Passenger/tests/IntegrationTest/IntegrationTestFixture.cs +++ b/src/Services/Passenger/tests/IntegrationTest/IntegrationTestFixture.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using BuildingBlocks.Domain.Model; +using BuildingBlocks.Core.Model; using BuildingBlocks.MassTransit; using BuildingBlocks.Web; using Grpc.Net.Client; @@ -48,7 +48,6 @@ public class IntegrationTestFixture : IAsyncLifetime builder.UseEnvironment("test"); builder.ConfigureServices(services => { - services.RemoveAll(typeof(IHostedService)); services.ReplaceSingleton(AddHttpContextAccessorMock); TestRegistrationServices?.Invoke(services); services.AddMassTransitTestHarness(x =>