mirror of
https://github.com/meysamhadeli/booking-microservices.git
synced 2026-04-11 02:20:20 +08:00
Merge pull request #36 from meysamhadeli/develop
refactor persist-message-processor
This commit is contained in:
commit
7fdf418c7c
16
booking.rest
16
booking.rest
@ -24,7 +24,7 @@ Content-Type: application/x-www-form-urlencoded
|
||||
grant_type=password
|
||||
&client_id=client
|
||||
&client_secret=secret
|
||||
&username=meysamh
|
||||
&username=samh
|
||||
&password=Admin@123456
|
||||
&scope=flight-api
|
||||
###
|
||||
@ -39,11 +39,11 @@ Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"firstName": "John6",
|
||||
"lastName": "Doe6",
|
||||
"username": "admin6",
|
||||
"passportNumber": "1234567896",
|
||||
"email": "admin6@admin.com",
|
||||
"firstName": "John",
|
||||
"lastName": "Do",
|
||||
"username": "admin",
|
||||
"passportNumber": "1290000000",
|
||||
"email": "admin@admin.com",
|
||||
"password": "Admin6@12345",
|
||||
"confirmPassword": "Admin6@12345"
|
||||
}
|
||||
@ -219,7 +219,7 @@ Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"passportNumber": "1234567896",
|
||||
"passportNumber": "123456789",
|
||||
"passengerType": 1,
|
||||
"age": 30
|
||||
}
|
||||
@ -251,7 +251,7 @@ Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"passengerId": 1,
|
||||
"passengerId": 4776722699124736,
|
||||
"flightId": 1,
|
||||
"description": "I want to fly to iran"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
using BuildingBlocks.Core.Event;
|
||||
|
||||
namespace BuildingBlocks.Contracts.EventBus.Messages;
|
||||
|
||||
public class PassengerContracts
|
||||
{
|
||||
|
||||
}
|
||||
public record PassengerRegistrationCompleted(long Id) : IIntegrationEvent;
|
||||
public record PassengerCreated(long Id) : IIntegrationEvent;
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.Core.Event;
|
||||
|
||||
public interface IEvent : INotification
|
||||
{
|
||||
Guid EventId => Guid.NewGuid();
|
||||
long EventId => SnowFlakIdGenerator.NewId();
|
||||
public DateTime OccurredOn => DateTime.Now;
|
||||
public string EventType => GetType().AssemblyQualifiedName;
|
||||
}
|
||||
|
||||
5
src/BuildingBlocks/Core/Event/IInternalCommand.cs
Normal file
5
src/BuildingBlocks/Core/Event/IInternalCommand.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace BuildingBlocks.Core.Event;
|
||||
|
||||
public interface IInternalCommand : IEvent
|
||||
{
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using BuildingBlocks.Utils;
|
||||
|
||||
public interface IInternalCommand
|
||||
{
|
||||
public long Id => SnowFlakIdGenerator.NewId();
|
||||
|
||||
public DateTime OccurredOn => DateTime.Now;
|
||||
|
||||
public string Type => TypeProvider.GetTypeName(GetType());
|
||||
}
|
||||
@ -1,14 +1,9 @@
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using BuildingBlocks.Utils;
|
||||
using ICommand = BuildingBlocks.Core.CQRS.ICommand;
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
|
||||
namespace BuildingBlocks.Core.Event;
|
||||
|
||||
public class InternalCommand : IInternalCommand, ICommand
|
||||
{
|
||||
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
|
||||
|
||||
public DateTime OccurredOn => DateTime.Now;
|
||||
|
||||
public string Type { get => TypeProvider.GetTypeName(GetType()); }
|
||||
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using BuildingBlocks.PersistMessageProcessor;
|
||||
using BuildingBlocks.Web;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -31,12 +31,16 @@ public sealed class EventDispatcher : IEventDispatcher
|
||||
}
|
||||
|
||||
|
||||
public async Task SendAsync<T>(IReadOnlyList<T> events, EventType eventType = default,
|
||||
public async Task SendAsync<T>(IReadOnlyList<T> events, Type type = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
where T : IEvent
|
||||
{
|
||||
if (events.Count > 0)
|
||||
{
|
||||
var eventType = type != null && type.IsAssignableTo(typeof(IInternalCommand))
|
||||
? EventType.InternalCommand
|
||||
: EventType.DomainEvent;
|
||||
|
||||
async Task PublishIntegrationEvent(IReadOnlyList<IIntegrationEvent> integrationEvents)
|
||||
{
|
||||
foreach (var integrationEvent in integrationEvents)
|
||||
@ -63,7 +67,7 @@ public sealed class EventDispatcher : IEventDispatcher
|
||||
break;
|
||||
}
|
||||
|
||||
if (eventType == EventType.InternalCommand)
|
||||
if (type != null && eventType == EventType.InternalCommand)
|
||||
{
|
||||
var internalMessages = await MapDomainEventToInternalCommandAsync(events as IReadOnlyList<IDomainEvent>)
|
||||
.ConfigureAwait(false);
|
||||
@ -76,10 +80,10 @@ public sealed class EventDispatcher : IEventDispatcher
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync<T>(T @event, EventType eventType = default,
|
||||
public async Task SendAsync<T>(T @event, Type type = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
where T : IEvent =>
|
||||
await SendAsync(new[] {@event}, eventType, cancellationToken);
|
||||
await SendAsync(new[] {@event}, type, cancellationToken);
|
||||
|
||||
|
||||
private Task<IReadOnlyList<IIntegrationEvent>> MapDomainEventToIntegrationEventAsync(
|
||||
@ -111,12 +115,12 @@ public sealed class EventDispatcher : IEventDispatcher
|
||||
}
|
||||
|
||||
|
||||
private Task<IReadOnlyList<InternalCommand>> MapDomainEventToInternalCommandAsync(
|
||||
private Task<IReadOnlyList<IInternalCommand>> MapDomainEventToInternalCommandAsync(
|
||||
IReadOnlyList<IDomainEvent> events)
|
||||
{
|
||||
_logger.LogTrace("Processing internal message start...");
|
||||
|
||||
var internalCommands = new List<InternalCommand>();
|
||||
var internalCommands = new List<IInternalCommand>();
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
foreach (var @event in events)
|
||||
{
|
||||
@ -132,7 +136,7 @@ public sealed class EventDispatcher : IEventDispatcher
|
||||
|
||||
_logger.LogTrace("Processing internal message done...");
|
||||
|
||||
return Task.FromResult<IReadOnlyList<InternalCommand>>(internalCommands);
|
||||
return Task.FromResult<IReadOnlyList<IInternalCommand>>(internalCommands);
|
||||
}
|
||||
|
||||
private IEnumerable<IIntegrationEvent> GetWrappedIntegrationEvents(IReadOnlyList<IDomainEvent> domainEvents)
|
||||
|
||||
@ -4,8 +4,8 @@ namespace BuildingBlocks.Core;
|
||||
|
||||
public interface IEventDispatcher
|
||||
{
|
||||
public Task SendAsync<T>(IReadOnlyList<T> events, EventType eventType = default, CancellationToken cancellationToken = default)
|
||||
public Task SendAsync<T>(IReadOnlyList<T> events, Type type = null, CancellationToken cancellationToken = default)
|
||||
where T : IEvent;
|
||||
public Task SendAsync<T>(T @event, EventType eventType = default, CancellationToken cancellationToken = default)
|
||||
public Task SendAsync<T>(T @event, Type type = null, CancellationToken cancellationToken = default)
|
||||
where T : IEvent;
|
||||
}
|
||||
|
||||
@ -5,5 +5,5 @@ namespace BuildingBlocks.Core;
|
||||
public interface IEventMapper
|
||||
{
|
||||
IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event);
|
||||
InternalCommand MapToInternalCommand(IDomainEvent @event);
|
||||
IInternalCommand MapToInternalCommand(IDomainEvent @event);
|
||||
}
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
|
||||
namespace BuildingBlocks.Core.Model
|
||||
{
|
||||
public abstract class Aggregate : Aggregate<long>
|
||||
public abstract record Aggregate : Aggregate<long>
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class Aggregate<TId> : Entity, IAggregate<TId>
|
||||
public abstract record Aggregate<TId> : Audit, IAggregate<TId>
|
||||
{
|
||||
private readonly List<IDomainEvent> _domainEvents = new();
|
||||
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
namespace BuildingBlocks.Core.Model;
|
||||
|
||||
public abstract class Entity : IEntity
|
||||
public abstract record Audit : IAudit
|
||||
{
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public long? CreatedBy { get; set; }
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
namespace BuildingBlocks.Core.Model;
|
||||
|
||||
public interface IAggregate : IEntity
|
||||
public interface IAggregate : IAudit
|
||||
{
|
||||
IReadOnlyList<IDomainEvent> DomainEvents { get; }
|
||||
IEvent[] ClearDomainEvents();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
namespace BuildingBlocks.Core.Model;
|
||||
|
||||
public interface IEntity
|
||||
public interface IAudit
|
||||
{
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public long? CreatedBy { get; set; }
|
||||
@ -1,10 +1,9 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.Core.Model;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using BuildingBlocks.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
@ -12,13 +11,13 @@ namespace BuildingBlocks.EFCore;
|
||||
|
||||
public abstract class AppDbContextBase : DbContext, IDbContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ICurrentUserProvider _currentUserProvider;
|
||||
|
||||
private IDbContextTransaction _currentTransaction;
|
||||
|
||||
protected AppDbContextBase(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options)
|
||||
protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider currentUserProvider = null) : base(options)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_currentUserProvider = currentUserProvider;
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
@ -92,13 +91,10 @@ public abstract class AppDbContextBase : DbContext, IDbContext
|
||||
// ref: https://www.meziantou.net/entity-framework-core-soft-delete-using-query-filters.htm
|
||||
private void OnBeforeSaving()
|
||||
{
|
||||
var nameIdentifier = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
long.TryParse(nameIdentifier, out var userId);
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<IAggregate>())
|
||||
{
|
||||
var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate));
|
||||
var userId = _currentUserProvider?.GetCurrentUserId();
|
||||
|
||||
if (isAuditable)
|
||||
{
|
||||
|
||||
@ -57,17 +57,15 @@ public class EfTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TRe
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName);
|
||||
|
||||
await _dbContextBase.CommitTransactionAsync(cancellationToken);
|
||||
|
||||
var domainEvents = _dbContextBase.GetDomainEvents();
|
||||
|
||||
var eventType = typeof(TRequest).IsAssignableTo(typeof(IInternalCommand)) ? EventType.InternalCommand : EventType.DomainEvent;
|
||||
await _eventDispatcher.SendAsync(domainEvents.ToArray(), typeof(TRequest), cancellationToken);
|
||||
|
||||
await _eventDispatcher.SendAsync(domainEvents.ToArray(), eventType, cancellationToken);
|
||||
await _dbContextBase.CommitTransactionAsync(cancellationToken);
|
||||
|
||||
return response;
|
||||
}
|
||||
catch(System.Exception ex)
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
await _dbContextBase.RollbackTransactionAsync(cancellationToken);
|
||||
throw;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
using System.Linq.Expressions;
|
||||
using BuildingBlocks.Core.Model;
|
||||
using BuildingBlocks.PersistMessageProcessor.Data;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -45,7 +45,7 @@ public static class Extensions
|
||||
{
|
||||
Expression<Func<IAggregate, bool>> filterExpr = e => !e.IsDeleted;
|
||||
foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes()
|
||||
.Where(m => m.ClrType.IsAssignableTo(typeof(IEntity))))
|
||||
.Where(m => m.ClrType.IsAssignableTo(typeof(IAudit))))
|
||||
{
|
||||
// modify expression to handle correct child type
|
||||
var parameter = Expression.Parameter(mutableEntityType.ClrType);
|
||||
@ -63,6 +63,9 @@ public static class Extensions
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
|
||||
var persistMessageContext = scope.ServiceProvider.GetRequiredService<PersistMessageDbContext>();
|
||||
await persistMessageContext.Database.MigrateAsync();
|
||||
|
||||
var context = scope.ServiceProvider.GetRequiredService<TContext>();
|
||||
await context.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System.Data;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BuildingBlocks.EFCore;
|
||||
@ -9,7 +7,7 @@ public interface IDbContext
|
||||
{
|
||||
DbSet<TEntity> Set<TEntity>()
|
||||
where TEntity : class;
|
||||
DbSet<PersistMessage> PersistMessages => Set<PersistMessage>();
|
||||
|
||||
IReadOnlyList<IDomainEvent> GetDomainEvents();
|
||||
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
|
||||
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
@ -68,7 +68,8 @@ public static class EventStoreDBConfigExtensions
|
||||
);
|
||||
}
|
||||
|
||||
public static IServiceCollection AddProjections(this IServiceCollection services, params Assembly[] assembliesToScan)
|
||||
public static IServiceCollection AddProjections(this IServiceCollection services,
|
||||
params Assembly[] assembliesToScan)
|
||||
{
|
||||
services.AddSingleton<IProjectionPublisher, ProjectionPublisher>();
|
||||
|
||||
@ -81,7 +82,7 @@ public static class EventStoreDBConfigExtensions
|
||||
{
|
||||
services.Scan(scan => scan
|
||||
.FromAssemblies(assembliesToScan)
|
||||
.AddClasses(classes => classes.AssignableTo<IProjection>()) // Filter classes
|
||||
.AddClasses(classes => classes.AssignableTo<IProjectionProcessor>()) // Filter classes
|
||||
.AsImplementedInterfaces()
|
||||
.WithTransientLifetime());
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ using BuildingBlocks.Core.Model;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events
|
||||
{
|
||||
public abstract class AggregateEventSourcing<TId> : Entity, IAggregateEventSourcing<TId>
|
||||
public abstract record AggregateEventSourcing<TId> : Audit, IAggregateEventSourcing<TId>
|
||||
{
|
||||
private readonly List<IDomainEvent> _domainEvents = new();
|
||||
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
@ -13,9 +13,9 @@ namespace BuildingBlocks.EventStoreDB.Events
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public IEvent[] ClearDomainEvents()
|
||||
public IDomainEvent[] ClearDomainEvents()
|
||||
{
|
||||
IEvent[] dequeuedEvents = _domainEvents.ToArray();
|
||||
var dequeuedEvents = _domainEvents.ToArray();
|
||||
|
||||
_domainEvents.Clear();
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@ using BuildingBlocks.Core.Model;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events
|
||||
{
|
||||
public interface IAggregateEventSourcing : IProjection, IEntity
|
||||
public interface IAggregateEventSourcing : IProjection, IAudit
|
||||
{
|
||||
IReadOnlyList<IDomainEvent> DomainEvents { get; }
|
||||
IEvent[] ClearDomainEvents();
|
||||
IDomainEvent[] ClearDomainEvents();
|
||||
long Version { get; }
|
||||
}
|
||||
|
||||
|
||||
@ -8,12 +8,16 @@ public interface IEventStoreDBRepository<T> where T : class, IAggregateEventSour
|
||||
{
|
||||
Task<T?> Find(long id, CancellationToken cancellationToken);
|
||||
Task<ulong> Add(T aggregate, CancellationToken cancellationToken);
|
||||
Task<ulong> Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ulong> Update(T aggregate, long? expectedRevision = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ulong> Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class EventStoreDBRepository<T>: IEventStoreDBRepository<T> where T : class, IAggregateEventSourcing<long>
|
||||
public class EventStoreDBRepository<T> : IEventStoreDBRepository<T> where T : class, IAggregateEventSourcing<long>
|
||||
{
|
||||
private static readonly long _currentUserId;
|
||||
private readonly EventStoreClient eventStore;
|
||||
|
||||
public EventStoreDBRepository(EventStoreClient eventStore)
|
||||
@ -21,11 +25,13 @@ public class EventStoreDBRepository<T>: IEventStoreDBRepository<T> where T : cla
|
||||
this.eventStore = eventStore ?? throw new ArgumentNullException(nameof(eventStore));
|
||||
}
|
||||
|
||||
public Task<T?> Find(long id, CancellationToken cancellationToken) =>
|
||||
eventStore.AggregateStream<T>(
|
||||
public Task<T?> Find(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
return eventStore.AggregateStream<T>(
|
||||
id,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<ulong> Add(T aggregate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@ -38,7 +44,8 @@ public class EventStoreDBRepository<T>: IEventStoreDBRepository<T> where T : cla
|
||||
return result.NextExpectedStreamRevision;
|
||||
}
|
||||
|
||||
public async Task<ulong> Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default)
|
||||
public async Task<ulong> Update(T aggregate, long? expectedRevision = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var nextVersion = expectedRevision ?? aggregate.Version;
|
||||
|
||||
@ -51,8 +58,11 @@ public class EventStoreDBRepository<T>: IEventStoreDBRepository<T> where T : cla
|
||||
return result.NextExpectedStreamRevision;
|
||||
}
|
||||
|
||||
public Task<ulong> Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default) =>
|
||||
Update(aggregate, expectedRevision, cancellationToken);
|
||||
public Task<ulong> Delete(T aggregate, long? expectedRevision = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Update(aggregate, expectedRevision, cancellationToken);
|
||||
}
|
||||
|
||||
private static IEnumerable<EventData> GetEventsToStore(T aggregate)
|
||||
{
|
||||
|
||||
@ -28,6 +28,6 @@ public static class RepositoryExtensions
|
||||
|
||||
action(entity);
|
||||
|
||||
return await repository.Update(entity, expectedVersion, cancellationToken);
|
||||
return await repository.Update(entity, expectedVersion,cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using BuildingBlocks.PersistMessageProcessor;
|
||||
using MassTransit;
|
||||
|
||||
namespace BuildingBlocks.MassTransit;
|
||||
|
||||
@ -1,208 +0,0 @@
|
||||
using System.Linq.Expressions;
|
||||
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<PersistMessageProcessor> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IDbContext _dbContext;
|
||||
private readonly IPublishEndpoint _publishEndpoint;
|
||||
|
||||
public PersistMessageProcessor(
|
||||
ILogger<PersistMessageProcessor> logger,
|
||||
IMediator mediator,
|
||||
IDbContext dbContext,
|
||||
IPublishEndpoint publishEndpoint)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_dbContext = dbContext;
|
||||
_publishEndpoint = publishEndpoint;
|
||||
}
|
||||
|
||||
public async Task PublishMessageAsync<TMessageEnvelope>(
|
||||
TMessageEnvelope messageEnvelope,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TMessageEnvelope : MessageEnvelope
|
||||
{
|
||||
await SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Outbox, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<Guid> AddReceivedMessageAsync<TMessageEnvelope>(TMessageEnvelope messageEnvelope,
|
||||
CancellationToken cancellationToken = default) where TMessageEnvelope : MessageEnvelope
|
||||
{
|
||||
return SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Inbox, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddInternalMessageAsync<TCommand>(TCommand internalCommand,
|
||||
CancellationToken cancellationToken = default) where TCommand : class, IInternalCommand
|
||||
{
|
||||
await SavePersistMessageAsync(new MessageEnvelope(internalCommand), MessageDeliveryType.Internal,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PersistMessage>> GetByFilterAsync(Expression<Func<PersistMessage, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var b = (await _dbContext.PersistMessages.Where(predicate).ToListAsync(cancellationToken)).AsReadOnly();
|
||||
return b;
|
||||
}
|
||||
|
||||
public Task<PersistMessage> ExistMessageAsync(Guid messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _dbContext.PersistMessages.FirstOrDefaultAsync(x =>
|
||||
x.Id == messageId &&
|
||||
x.DeliveryType == MessageDeliveryType.Inbox &&
|
||||
x.MessageStatus == MessageStatus.Processed,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(
|
||||
Guid messageId,
|
||||
MessageDeliveryType deliveryType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var message =
|
||||
await _dbContext.PersistMessages.FirstOrDefaultAsync(
|
||||
x => x.Id == messageId && x.DeliveryType == deliveryType, cancellationToken);
|
||||
|
||||
if (message is null)
|
||||
return;
|
||||
|
||||
switch (deliveryType)
|
||||
{
|
||||
case MessageDeliveryType.Internal:
|
||||
await ProcessInternalAsync(message, cancellationToken);
|
||||
break;
|
||||
case MessageDeliveryType.Outbox:
|
||||
await ProcessOutboxAsync(message, cancellationToken);
|
||||
break;
|
||||
}
|
||||
|
||||
await ChangeMessageStatusAsync(message, 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);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessInboxAsync(Guid messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var message = await _dbContext.PersistMessages.FirstOrDefaultAsync(
|
||||
x => x.Id == messageId &&
|
||||
x.DeliveryType == MessageDeliveryType.Inbox &&
|
||||
x.MessageStatus == MessageStatus.InProgress,
|
||||
cancellationToken);
|
||||
|
||||
await ChangeMessageStatusAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Guid> 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());
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private async Task ProcessOutboxAsync(PersistMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
MessageEnvelope? messageEnvelope = JsonSerializer.Deserialize<MessageEnvelope>(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 ProcessInternalAsync(PersistMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
MessageEnvelope? messageEnvelope = JsonSerializer.Deserialize<MessageEnvelope>(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 async Task ChangeMessageStatusAsync(PersistMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
message.ChangeState(MessageStatus.Processed);
|
||||
|
||||
_dbContext.PersistMessages.Update(message);
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,13 @@
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Booking.Data.Configurations;
|
||||
namespace BuildingBlocks.PersistMessageProcessor.Data.Configurations;
|
||||
|
||||
public class PersistMessageConfiguration : IEntityTypeConfiguration<PersistMessage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PersistMessage> builder)
|
||||
{
|
||||
builder.ToTable("PersistMessages", BookingDbContext.DefaultSchema);
|
||||
builder.ToTable("PersistMessage", PersistMessageDbContext.DefaultSchema);
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace BuildingBlocks.PersistMessageProcessor.Data;
|
||||
|
||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<PersistMessageDbContext>
|
||||
{
|
||||
public PersistMessageDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<PersistMessageDbContext>();
|
||||
|
||||
builder.UseSqlServer(
|
||||
"Data Source=.\\sqlexpress;Initial Catalog=PersistMessageDB;Persist Security Info=False;Integrated Security=SSPI");
|
||||
return new PersistMessageDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,20 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Booking.Data;
|
||||
using BuildingBlocks.PersistMessageProcessor.Data;
|
||||
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
|
||||
namespace Booking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(PassengerDbContext))]
|
||||
[Migration("20220616122705_Add-PersistMessages")]
|
||||
partial class AddPersistMessages
|
||||
[DbContext(typeof(PersistMessageDbContext))]
|
||||
[Migration("20220728155556_initial")]
|
||||
partial class initial
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
@ -24,12 +25,16 @@ namespace Passenger.Data.Migrations
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
|
||||
|
||||
modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b =>
|
||||
modelBuilder.Entity("BuildingBlocks.PersistMessageProcessor.PersistMessage", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ApplicationName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
@ -56,47 +61,7 @@ namespace Passenger.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PersistMessages", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Age")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("PassengerType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PassportNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Passenger", "dbo");
|
||||
b.ToTable("PersistMessage", "dbo");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
@ -5,16 +5,19 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Booking.Data.Migrations
|
||||
{
|
||||
public partial class AddPersistMessages : Migration
|
||||
public partial class initial : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "dbo");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PersistMessages",
|
||||
name: "PersistMessage",
|
||||
schema: "dbo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||
DataType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Data = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
@ -24,14 +27,14 @@ namespace Booking.Data.Migrations
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PersistMessages", x => x.Id);
|
||||
table.PrimaryKey("PK_PersistMessage", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PersistMessages",
|
||||
name: "PersistMessage",
|
||||
schema: "dbo");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Booking.Data;
|
||||
using BuildingBlocks.PersistMessageProcessor.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Booking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(PersistMessageDbContext))]
|
||||
partial class PersistMessageDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(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.PersistMessageProcessor.PersistMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DataType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DeliveryType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("MessageStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PersistMessage", "dbo");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
using BuildingBlocks.EFCore;
|
||||
using BuildingBlocks.PersistMessageProcessor.Data.Configurations;
|
||||
using BuildingBlocks.Utils;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BuildingBlocks.PersistMessageProcessor.Data;
|
||||
|
||||
public class PersistMessageDbContext : AppDbContextBase, IPersistMessageDbContext
|
||||
{
|
||||
public const string DefaultSchema = "dbo";
|
||||
|
||||
public PersistMessageDbContext(DbContextOptions<PersistMessageDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.ApplyConfiguration(new PersistMessageConfiguration());
|
||||
base.OnModelCreating(builder);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
dotnet ef migrations add initial --context PersistMessageDbContext -o "Data\Migrations"
|
||||
dotnet ef database update --context PersistMessageDbContext
|
||||
@ -1,9 +1,11 @@
|
||||
using BuildingBlocks.Core;
|
||||
using BuildingBlocks.Mongo;
|
||||
using BuildingBlocks.PersistMessageProcessor.Data;
|
||||
using BuildingBlocks.Web;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.MessageProcessor;
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
@ -13,8 +15,15 @@ public static class Extensions
|
||||
.Bind(configuration.GetSection(nameof(PersistMessageOptions)))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
var persistMessageOptions = services.GetOptions<PersistMessageOptions>("PersistMessageOptions");
|
||||
|
||||
services.AddDbContext<PersistMessageDbContext>(options =>
|
||||
options.UseSqlServer(persistMessageOptions.ConnectionString,
|
||||
x => x.MigrationsAssembly(typeof(PersistMessageDbContext).Assembly.GetName().Name)));
|
||||
|
||||
services.AddScoped<IPersistMessageDbContext>(provider => provider.GetService<PersistMessageDbContext>());
|
||||
|
||||
services.AddScoped<IPersistMessageProcessor, PersistMessageProcessor>();
|
||||
services.AddScoped<IEventDispatcher, EventDispatcher>();
|
||||
services.AddHostedService<PersistMessageBackgroundService>();
|
||||
|
||||
return services;
|
||||
@ -0,0 +1,9 @@
|
||||
using BuildingBlocks.EFCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
public interface IPersistMessageDbContext : IDbContext
|
||||
{
|
||||
DbSet<PersistMessage> PersistMessages => Set<PersistMessage>();
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Linq.Expressions;
|
||||
using BuildingBlocks.Core.Event;
|
||||
|
||||
namespace BuildingBlocks.MessageProcessor;
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
// Ref: http://www.kamilgrzybek.com/design/the-outbox-pattern/
|
||||
// Ref: https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/
|
||||
@ -15,7 +15,7 @@ public interface IPersistMessageProcessor
|
||||
CancellationToken cancellationToken = default)
|
||||
where TMessageEnvelope : MessageEnvelope;
|
||||
|
||||
Task<Guid> AddReceivedMessageAsync<TMessageEnvelope>(
|
||||
Task<long> AddReceivedMessageAsync<TMessageEnvelope>(
|
||||
TMessageEnvelope messageEnvelope,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TMessageEnvelope : MessageEnvelope;
|
||||
@ -30,14 +30,14 @@ public interface IPersistMessageProcessor
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<PersistMessage> ExistMessageAsync(
|
||||
Guid messageId,
|
||||
long messageId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task ProcessInboxAsync(
|
||||
Guid messageId,
|
||||
long messageId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task ProcessAsync(Guid messageId, MessageDeliveryType deliveryType, CancellationToken cancellationToken = default);
|
||||
Task ProcessAsync(long messageId, MessageDeliveryType deliveryType, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ProcessAllAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
namespace BuildingBlocks.MessageProcessor;
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
[Flags]
|
||||
public enum MessageDeliveryType
|
||||
@ -1,4 +1,4 @@
|
||||
namespace BuildingBlocks.MessageProcessor;
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
public enum MessageStatus
|
||||
{
|
||||
@ -1,8 +1,10 @@
|
||||
namespace BuildingBlocks.MessageProcessor;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
public class PersistMessage
|
||||
{
|
||||
public PersistMessage(Guid id, string dataType, string data, MessageDeliveryType deliveryType)
|
||||
public PersistMessage(long id, string dataType, string data, MessageDeliveryType deliveryType)
|
||||
{
|
||||
Id = id;
|
||||
DataType = dataType;
|
||||
@ -13,7 +15,7 @@ public class PersistMessage
|
||||
RetryCount = 0;
|
||||
}
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
public long Id { get; private set; }
|
||||
public string DataType { get; private set; }
|
||||
public string Data { get; private set; }
|
||||
public DateTime Created { get; private set; }
|
||||
@ -3,7 +3,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BuildingBlocks.MessageProcessor;
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
public class PersistMessageBackgroundService : BackgroundService
|
||||
{
|
||||
@ -43,17 +43,25 @@ public class PersistMessageBackgroundService : BackgroundService
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await using (var scope = _serviceProvider.CreateAsyncScope())
|
||||
try
|
||||
{
|
||||
var service = scope.ServiceProvider.GetRequiredService<IPersistMessageProcessor>();
|
||||
await service.ProcessAllAsync(stoppingToken);
|
||||
await using (var scope = _serviceProvider.CreateAsyncScope())
|
||||
{
|
||||
var service = scope.ServiceProvider.GetRequiredService<IPersistMessageProcessor>();
|
||||
await service.ProcessAllAsync(stoppingToken);
|
||||
}
|
||||
|
||||
var delay = _options.Interval is { }
|
||||
? TimeSpan.FromSeconds((int)_options.Interval)
|
||||
: TimeSpan.FromSeconds(30);
|
||||
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
|
||||
var delay = _options.Interval is { }
|
||||
? TimeSpan.FromSeconds((int)_options.Interval)
|
||||
: TimeSpan.FromSeconds(30);
|
||||
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
namespace BuildingBlocks.MessageProcessor;
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
public class PersistMessageOptions
|
||||
{
|
||||
public int? Interval { get; set; } = 30;
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
@ -0,0 +1,219 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.Json;
|
||||
using Ardalis.GuardClauses;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using BuildingBlocks.Utils;
|
||||
using MassTransit;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.PersistMessageProcessor;
|
||||
|
||||
public class PersistMessageProcessor : IPersistMessageProcessor
|
||||
{
|
||||
private readonly ILogger<PersistMessageProcessor> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IPersistMessageDbContext _persistMessageDbContext;
|
||||
private readonly IPublishEndpoint _publishEndpoint;
|
||||
|
||||
public PersistMessageProcessor(
|
||||
ILogger<PersistMessageProcessor> logger,
|
||||
IMediator mediator,
|
||||
IPersistMessageDbContext persistMessageDbContext,
|
||||
IPublishEndpoint publishEndpoint)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_persistMessageDbContext = persistMessageDbContext;
|
||||
_publishEndpoint = publishEndpoint;
|
||||
}
|
||||
|
||||
public async Task PublishMessageAsync<TMessageEnvelope>(
|
||||
TMessageEnvelope messageEnvelope,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TMessageEnvelope : MessageEnvelope
|
||||
{
|
||||
await SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Outbox, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<long> AddReceivedMessageAsync<TMessageEnvelope>(TMessageEnvelope messageEnvelope,
|
||||
CancellationToken cancellationToken = default) where TMessageEnvelope : MessageEnvelope
|
||||
{
|
||||
return SavePersistMessageAsync(messageEnvelope, MessageDeliveryType.Inbox, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddInternalMessageAsync<TCommand>(TCommand internalCommand,
|
||||
CancellationToken cancellationToken = default) where TCommand : class, IInternalCommand
|
||||
{
|
||||
await SavePersistMessageAsync(new MessageEnvelope(internalCommand), MessageDeliveryType.Internal,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PersistMessage>> GetByFilterAsync(Expression<Func<PersistMessage, bool>> predicate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return (await _persistMessageDbContext.PersistMessages.Where(predicate).ToListAsync(cancellationToken))
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
public Task<PersistMessage> ExistMessageAsync(long messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _persistMessageDbContext.PersistMessages.FirstOrDefaultAsync(x =>
|
||||
x.Id == messageId &&
|
||||
x.DeliveryType == MessageDeliveryType.Inbox &&
|
||||
x.MessageStatus == MessageStatus.Processed,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(
|
||||
long messageId,
|
||||
MessageDeliveryType deliveryType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var message =
|
||||
await _persistMessageDbContext.PersistMessages.FirstOrDefaultAsync(
|
||||
x => x.Id == messageId && x.DeliveryType == deliveryType, cancellationToken);
|
||||
|
||||
if (message is null)
|
||||
return;
|
||||
|
||||
switch (deliveryType)
|
||||
{
|
||||
case MessageDeliveryType.Internal:
|
||||
var sentInternalMessage = await ProcessInternalAsync(message, cancellationToken);
|
||||
if (sentInternalMessage)
|
||||
{
|
||||
await ChangeMessageStatusAsync(message, cancellationToken);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
case MessageDeliveryType.Outbox:
|
||||
var sentOutbox = await ProcessOutboxAsync(message, cancellationToken);
|
||||
if (sentOutbox)
|
||||
{
|
||||
await ChangeMessageStatusAsync(message, cancellationToken);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task ProcessAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var messages = await _persistMessageDbContext.PersistMessages
|
||||
.Where(x => x.MessageStatus != MessageStatus.Processed)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var message in messages) await ProcessAsync(message.Id, message.DeliveryType, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ProcessInboxAsync(long messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var message = await _persistMessageDbContext.PersistMessages.FirstOrDefaultAsync(
|
||||
x => x.Id == messageId &&
|
||||
x.DeliveryType == MessageDeliveryType.Inbox &&
|
||||
x.MessageStatus == MessageStatus.InProgress,
|
||||
cancellationToken);
|
||||
|
||||
await ChangeMessageStatusAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessOutboxAsync(PersistMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
var messageEnvelope = JsonSerializer.Deserialize<MessageEnvelope>(message.Data);
|
||||
|
||||
if (messageEnvelope is null || messageEnvelope.Message is null)
|
||||
return false;
|
||||
|
||||
var data = JsonSerializer.Deserialize(messageEnvelope.Message.ToString() ?? string.Empty,
|
||||
TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(message.DataType) ?? typeof(object));
|
||||
|
||||
if (data is not IEvent)
|
||||
return false;
|
||||
|
||||
await _publishEndpoint.Publish(data, context =>
|
||||
{
|
||||
foreach (var header in messageEnvelope.Headers) context.Headers.Set(header.Key, header.Value);
|
||||
}, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Message with id: {MessageId} and delivery type: {DeliveryType} processed from the persistence message store.",
|
||||
message.Id,
|
||||
message.DeliveryType);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessInternalAsync(PersistMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
var messageEnvelope = JsonSerializer.Deserialize<MessageEnvelope>(message.Data);
|
||||
|
||||
if (messageEnvelope is null || messageEnvelope.Message is null)
|
||||
return false;
|
||||
|
||||
var data = JsonSerializer.Deserialize(messageEnvelope.Message.ToString() ?? string.Empty,
|
||||
TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(message.DataType) ?? typeof(object));
|
||||
|
||||
if (data is not IInternalCommand internalCommand)
|
||||
return false;
|
||||
|
||||
await _mediator.Send(internalCommand, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"InternalCommand with id: {EventID} and delivery type: {DeliveryType} processed from the persistence message store.",
|
||||
message.Id,
|
||||
message.DeliveryType);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<long> SavePersistMessageAsync(
|
||||
MessageEnvelope messageEnvelope,
|
||||
MessageDeliveryType deliveryType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Guard.Against.Null(messageEnvelope.Message, nameof(messageEnvelope.Message));
|
||||
|
||||
long id;
|
||||
if (messageEnvelope.Message is IEvent message)
|
||||
id = message.EventId;
|
||||
else
|
||||
id = SnowFlakIdGenerator.NewId();
|
||||
|
||||
await _persistMessageDbContext.PersistMessages.AddAsync(
|
||||
new PersistMessage(
|
||||
id,
|
||||
messageEnvelope.Message.GetType().ToString(),
|
||||
JsonSerializer.Serialize(messageEnvelope),
|
||||
deliveryType),
|
||||
cancellationToken);
|
||||
|
||||
await _persistMessageDbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Message with id: {MessageID} and delivery type: {DeliveryType} saved in persistence message store.",
|
||||
id,
|
||||
deliveryType.ToString());
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private async Task ChangeMessageStatusAsync(PersistMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
message.ChangeState(MessageStatus.Processed);
|
||||
|
||||
_persistMessageDbContext.PersistMessages.Update(message);
|
||||
|
||||
await _persistMessageDbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
using Ardalis.GuardClauses;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.Core.Model;
|
||||
using BuildingBlocks.EFCore;
|
||||
using BuildingBlocks.EventStoreDB.Projections;
|
||||
using BuildingBlocks.MassTransit;
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using BuildingBlocks.Mongo;
|
||||
using BuildingBlocks.Utils;
|
||||
using BuildingBlocks.PersistMessageProcessor;
|
||||
using BuildingBlocks.Web;
|
||||
using Grpc.Net.Client;
|
||||
using MassTransit;
|
||||
@ -50,6 +51,7 @@ public class IntegrationTestFixture<TEntryPoint> : IAsyncLifetime
|
||||
{
|
||||
TestRegistrationServices?.Invoke(services);
|
||||
services.ReplaceSingleton(AddHttpContextAccessorMock);
|
||||
services.Unregister<IProjectionProcessor>();
|
||||
services.AddMassTransitTestHarness(x =>
|
||||
{
|
||||
x.UsingRabbitMq((context, cfg) =>
|
||||
@ -88,9 +90,11 @@ public class IntegrationTestFixture<TEntryPoint> : IAsyncLifetime
|
||||
public ILogger CreateLogger(ITestOutputHelper output)
|
||||
{
|
||||
if (output != null)
|
||||
{
|
||||
return new LoggerConfiguration()
|
||||
.WriteTo.TestOutput(output)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -160,7 +164,7 @@ public class IntegrationTestFixture<TEntryPoint> : IAsyncLifetime
|
||||
|
||||
var filter = await persistMessageProcessor.GetByFilterAsync(x =>
|
||||
x.DeliveryType == MessageDeliveryType.Internal &&
|
||||
TypeProvider.GetTypeName(typeof(TInternalCommand)) == x.DataType);
|
||||
typeof(TInternalCommand).ToString() == x.DataType);
|
||||
|
||||
var res = filter.Any(x => x.MessageStatus == MessageStatus.Processed);
|
||||
|
||||
@ -294,7 +298,7 @@ public class IntegrationTestFixture<TEntryPoint, TWContext> : IntegrationTestFix
|
||||
}
|
||||
|
||||
public Task<T> FindAsync<T>(long id)
|
||||
where T : class, IEntity
|
||||
where T : class, IAudit
|
||||
{
|
||||
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
|
||||
}
|
||||
@ -319,7 +323,8 @@ public class IntegrationTestFixture<TEntryPoint, TWContext, TRContext> : Integra
|
||||
public class IntegrationTestFixtureCore<TEntryPoint> : IAsyncLifetime
|
||||
where TEntryPoint : class
|
||||
{
|
||||
private Checkpoint _checkpoint;
|
||||
private Checkpoint _checkpointDefaultDB;
|
||||
private Checkpoint _checkpointPersistMessageDB;
|
||||
private MongoDbRunner _mongoRunner;
|
||||
|
||||
public IntegrationTestFixtureCore(IntegrationTestFixture<TEntryPoint> integrationTestFixture)
|
||||
@ -332,7 +337,8 @@ public class IntegrationTestFixtureCore<TEntryPoint> : IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
|
||||
_checkpointDefaultDB = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
|
||||
_checkpointPersistMessageDB = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
|
||||
|
||||
_mongoRunner = MongoDbRunner.Start();
|
||||
var mongoOptions = Fixture.ServiceProvider.GetRequiredService<IOptions<MongoOptions>>();
|
||||
@ -344,7 +350,8 @@ public class IntegrationTestFixtureCore<TEntryPoint> : IAsyncLifetime
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _checkpoint.Reset(Fixture.Configuration?.GetConnectionString("DefaultConnection"));
|
||||
await _checkpointDefaultDB.Reset(Fixture.Configuration?.GetConnectionString("DefaultConnection"));
|
||||
await _checkpointPersistMessageDB.Reset(Fixture.ServiceProvider.GetRequiredService<IOptions<PersistMessageOptions>>()?.Value?.ConnectionString);
|
||||
_mongoRunner.Dispose();
|
||||
}
|
||||
|
||||
|
||||
29
src/BuildingBlocks/Utils/CurrentUserProvider.cs
Normal file
29
src/BuildingBlocks/Utils/CurrentUserProvider.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BuildingBlocks.Utils;
|
||||
|
||||
public interface ICurrentUserProvider
|
||||
{
|
||||
long? GetCurrentUserId();
|
||||
}
|
||||
|
||||
public class CurrentUserProvider : ICurrentUserProvider
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public CurrentUserProvider(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
|
||||
public long? GetCurrentUserId()
|
||||
{
|
||||
var nameIdentifier = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
long.TryParse(nameIdentifier, out var userId);
|
||||
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,10 @@
|
||||
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<Type, string> TypeNameMap = new();
|
||||
private static readonly ConcurrentDictionary<string, Type> TypeMap = new();
|
||||
private static bool IsRecord(this Type objectType)
|
||||
{
|
||||
return objectType.GetMethod("<Clone>$") != null ||
|
||||
@ -34,76 +30,10 @@ public static class TypeProvider
|
||||
|
||||
public static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string typeName)
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
var result = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName))
|
||||
.FirstOrDefault();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type name from a generic Type class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns>GetTypeName</returns>
|
||||
public static string GetTypeName<T>() => ToName(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type name from a Type class.
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns>TypeName</returns>
|
||||
public static string GetTypeName(Type type) => ToName(type);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type name from a instance object.
|
||||
/// </summary>
|
||||
/// <param name="o"></param>
|
||||
/// <returns>TypeName</returns>
|
||||
public static string GetTypeNameByObject(object o) => ToName(o.GetType());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type class from a type name.
|
||||
/// </summary>
|
||||
/// <param name="typeName"></param>
|
||||
/// <returns>Type</returns>
|
||||
public static Type GetType(string typeName) => ToType(typeName);
|
||||
|
||||
public static void AddType<T>(string name) => AddType(typeof(T), name);
|
||||
|
||||
private static void AddType(Type type, string name)
|
||||
{
|
||||
ToName(type);
|
||||
ToType(name);
|
||||
}
|
||||
|
||||
public static bool IsTypeRegistered<T>() => 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -5,14 +5,17 @@ using Booking.Extensions;
|
||||
using BuildingBlocks.Core;
|
||||
using BuildingBlocks.EFCore;
|
||||
using BuildingBlocks.EventStoreDB;
|
||||
using BuildingBlocks.EventStoreDB.Projections;
|
||||
using BuildingBlocks.HealthCheck;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using BuildingBlocks.Jwt;
|
||||
using BuildingBlocks.Logging;
|
||||
using BuildingBlocks.Mapster;
|
||||
using BuildingBlocks.MassTransit;
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using BuildingBlocks.Mongo;
|
||||
using BuildingBlocks.OpenTelemetry;
|
||||
using BuildingBlocks.PersistMessageProcessor;
|
||||
using BuildingBlocks.PersistMessageProcessor.Data;
|
||||
using BuildingBlocks.Swagger;
|
||||
using BuildingBlocks.Web;
|
||||
using Figgle;
|
||||
@ -31,10 +34,11 @@ builder.Services.Configure<GrpcOptions>(options => configuration.GetSection("Grp
|
||||
|
||||
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
|
||||
|
||||
builder.Services.AddCustomDbContext<BookingDbContext>(configuration);
|
||||
builder.Services.AddPersistMessage(configuration);
|
||||
builder.Services.AddMongoDbContext<BookingReadDbContext>(configuration);
|
||||
|
||||
builder.AddCustomSerilog();
|
||||
builder.Services.AddCore();
|
||||
builder.Services.AddJwt();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
@ -44,9 +48,6 @@ builder.Services.AddCustomMediatR();
|
||||
builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly);
|
||||
builder.Services.AddCustomProblemDetails();
|
||||
builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly);
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddTransient<IEventMapper, EventMapper>();
|
||||
builder.Services.AddCustomHealthCheck();
|
||||
builder.Services.AddCustomMassTransit(typeof(BookingRoot).Assembly, env);
|
||||
builder.Services.AddCustomOpenTelemetry();
|
||||
@ -69,7 +70,6 @@ if (app.Environment.IsDevelopment())
|
||||
}
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseMigration<BookingDbContext>(env);
|
||||
app.UseCorrelationId();
|
||||
app.UseRouting();
|
||||
app.UseHttpMetrics();
|
||||
|
||||
@ -7,9 +7,6 @@
|
||||
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
|
||||
"ElasticUri": "http://localhost:9200"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=.\\sqlexpress;Database=BookingDB;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
},
|
||||
"Jwt": {
|
||||
"Authority": "https://localhost:5005",
|
||||
"Audience": "booking-api"
|
||||
@ -27,9 +24,14 @@
|
||||
"EventStore": {
|
||||
"ConnectionString": "esdb://localhost:2113?tls=false"
|
||||
},
|
||||
"MongoOptions": {
|
||||
"ConnectionString": "mongodb://localhost:27017",
|
||||
"DatabaseName": "booking-db"
|
||||
},
|
||||
"PersistMessageOptions": {
|
||||
"Interval": 30,
|
||||
"Enabled": true
|
||||
"Enabled": true,
|
||||
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageDB;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@ -18,6 +18,11 @@
|
||||
},
|
||||
"PersistMessageOptions": {
|
||||
"Interval": 1,
|
||||
"Enabled": true
|
||||
"Enabled": true,
|
||||
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageTestDB;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
},
|
||||
"MongoOptions": {
|
||||
"ConnectionString": "mongodb://localhost:27017",
|
||||
"DatabaseName": "booking-db-test"
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,6 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\Migrations" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Booking.Booking.Models.ValueObjects;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.Core.Model;
|
||||
|
||||
namespace Booking.Booking.Events.Domain;
|
||||
|
||||
public record BookingCreatedDomainEvent(long Id, PassengerInfo PassengerInfo, Trip Trip, bool IsDeleted) : IDomainEvent;
|
||||
public record BookingCreatedDomainEvent(long Id, PassengerInfo PassengerInfo, Trip Trip) : Audit, IDomainEvent;
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
using Booking.Booking.Dtos;
|
||||
using Booking.Booking.Events.Domain;
|
||||
using Booking.Booking.Models.Reads;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using Mapster;
|
||||
|
||||
namespace Booking.Booking.Features;
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
|
||||
namespace Booking.Booking.Features.CreateBooking;
|
||||
|
||||
public record CreateBookingCommand(long PassengerId, long FlightId, string Description) : ICommand<ulong>
|
||||
public record CreateBookingCommand(long PassengerId, long FlightId, string Description) : ICommand<ulong>, IInternalCommand
|
||||
{
|
||||
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
using Ardalis.GuardClauses;
|
||||
using Booking.Booking.Events.Domain;
|
||||
using Booking.Booking.Exceptions;
|
||||
using Booking.Booking.Models.ValueObjects;
|
||||
using BuildingBlocks.Contracts.Grpc;
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.EventStoreDB.Repository;
|
||||
using MediatR;
|
||||
using BuildingBlocks.Utils;
|
||||
|
||||
namespace Booking.Booking.Features.CreateBooking;
|
||||
|
||||
@ -12,15 +13,18 @@ public class CreateBookingCommandHandler : ICommandHandler<CreateBookingCommand,
|
||||
{
|
||||
private readonly IEventStoreDBRepository<Models.Booking> _eventStoreDbRepository;
|
||||
private readonly IFlightGrpcService _flightGrpcService;
|
||||
private readonly ICurrentUserProvider _currentUserProvider;
|
||||
private readonly IPassengerGrpcService _passengerGrpcService;
|
||||
|
||||
public CreateBookingCommandHandler(IEventStoreDBRepository<Models.Booking> eventStoreDbRepository,
|
||||
IPassengerGrpcService passengerGrpcService,
|
||||
IFlightGrpcService flightGrpcService)
|
||||
IFlightGrpcService flightGrpcService,
|
||||
ICurrentUserProvider currentUserProvider)
|
||||
{
|
||||
_eventStoreDbRepository = eventStoreDbRepository;
|
||||
_passengerGrpcService = passengerGrpcService;
|
||||
_flightGrpcService = flightGrpcService;
|
||||
_currentUserProvider = currentUserProvider;
|
||||
}
|
||||
|
||||
public async Task<ulong> Handle(CreateBookingCommand command,
|
||||
@ -44,11 +48,12 @@ public class CreateBookingCommandHandler : ICommandHandler<CreateBookingCommand,
|
||||
|
||||
var aggrigate = Models.Booking.Create(command.Id, new PassengerInfo(passenger.Name), new Trip(
|
||||
flight.FlightNumber, flight.AircraftId, flight.DepartureAirportId,
|
||||
flight.ArriveAirportId, flight.FlightDate, flight.Price, command.Description, emptySeat?.SeatNumber));
|
||||
flight.ArriveAirportId, flight.FlightDate, flight.Price, command.Description, emptySeat?.SeatNumber),
|
||||
false, _currentUserProvider.GetCurrentUserId());
|
||||
|
||||
await _flightGrpcService.ReserveSeat(new ReserveSeatRequestDto
|
||||
{
|
||||
FlightId = flight.Id, SeatNumber = emptySeat?.SeatNumber
|
||||
FlightId = flight.FlightId, SeatNumber = emptySeat?.SeatNumber
|
||||
});
|
||||
|
||||
var result = await _eventStoreDbRepository.Add(
|
||||
|
||||
@ -1,29 +1,26 @@
|
||||
using Booking.Booking.Events.Domain;
|
||||
using Booking.Booking.Models.ValueObjects;
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using BuildingBlocks.Utils;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Booking.Booking.Models;
|
||||
|
||||
public class Booking : AggregateEventSourcing<long>
|
||||
public record Booking : AggregateEventSourcing<long>
|
||||
{
|
||||
public Booking()
|
||||
{
|
||||
}
|
||||
|
||||
public Trip Trip { get; private set; }
|
||||
public PassengerInfo PassengerInfo { get; private set; }
|
||||
|
||||
public static Booking Create(long id, PassengerInfo passengerInfo, Trip trip, bool isDeleted = false)
|
||||
public static Booking Create(long id, PassengerInfo passengerInfo, Trip trip, bool isDeleted = false, long? userId = null)
|
||||
{
|
||||
var booking = new Booking()
|
||||
{
|
||||
Id = id,
|
||||
Trip = trip,
|
||||
PassengerInfo = passengerInfo,
|
||||
IsDeleted = isDeleted
|
||||
};
|
||||
var booking = new Booking { Id = id, Trip = trip, PassengerInfo = passengerInfo, IsDeleted = isDeleted };
|
||||
|
||||
var @event = new BookingCreatedDomainEvent(booking.Id, booking.PassengerInfo, booking.Trip, booking.IsDeleted);
|
||||
var @event = new BookingCreatedDomainEvent(booking.Id, booking.PassengerInfo, booking.Trip)
|
||||
{
|
||||
IsDeleted = booking.IsDeleted,
|
||||
CreatedAt = DateTime.Now,
|
||||
CreatedBy = userId
|
||||
};
|
||||
|
||||
booking.AddDomainEvent(@event);
|
||||
booking.Apply(@event);
|
||||
@ -35,9 +32,9 @@ public class Booking : AggregateEventSourcing<long>
|
||||
{
|
||||
switch (@event)
|
||||
{
|
||||
case BookingCreatedDomainEvent reservationCreated:
|
||||
case BookingCreatedDomainEvent bookingCreated:
|
||||
{
|
||||
Apply(reservationCreated);
|
||||
Apply(bookingCreated);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
using Booking.Booking.Models.ValueObjects;
|
||||
|
||||
namespace Booking.Booking.Models.Reads;
|
||||
|
||||
public class BookingReadModel
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public long BookId { get; init; }
|
||||
public Trip Trip { get; init; }
|
||||
public PassengerInfo PassengerInfo { get; init; }
|
||||
public bool IsDeleted { get; init; }
|
||||
}
|
||||
@ -1,19 +1,22 @@
|
||||
using Booking.Booking.Events.Domain;
|
||||
using Booking.Booking.Models.Reads;
|
||||
using Booking.Data;
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using BuildingBlocks.EventStoreDB.Projections;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.Linq;
|
||||
|
||||
namespace Booking;
|
||||
|
||||
public class BookingProjection : IProjectionProcessor
|
||||
{
|
||||
private readonly BookingDbContext _bookingDbContext;
|
||||
private readonly BookingReadDbContext _bookingReadDbContext;
|
||||
|
||||
public BookingProjection(BookingDbContext bookingDbContext)
|
||||
public BookingProjection(BookingReadDbContext bookingReadDbContext)
|
||||
{
|
||||
_bookingDbContext = bookingDbContext;
|
||||
_bookingReadDbContext = bookingReadDbContext;
|
||||
}
|
||||
|
||||
public async Task ProcessEventAsync<T>(StreamEvent<T> streamEvent, CancellationToken cancellationToken = default)
|
||||
@ -21,8 +24,8 @@ public class BookingProjection : IProjectionProcessor
|
||||
{
|
||||
switch (streamEvent.Data)
|
||||
{
|
||||
case BookingCreatedDomainEvent reservationCreatedDomainEvent:
|
||||
await Apply(reservationCreatedDomainEvent, cancellationToken);
|
||||
case BookingCreatedDomainEvent bookingCreatedDomainEvent:
|
||||
await Apply(bookingCreatedDomainEvent, cancellationToken);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -30,15 +33,21 @@ public class BookingProjection : IProjectionProcessor
|
||||
private async Task Apply(BookingCreatedDomainEvent @event, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reservation =
|
||||
await _bookingDbContext.Bookings.SingleOrDefaultAsync(x => x.Id == @event.Id,
|
||||
await _bookingReadDbContext.Booking.AsQueryable().SingleOrDefaultAsync(x => x.Id == @event.Id && !x.IsDeleted,
|
||||
cancellationToken);
|
||||
|
||||
if (reservation == null)
|
||||
{
|
||||
var model = Booking.Models.Booking.Create(@event.Id, @event.PassengerInfo, @event.Trip, @event.IsDeleted);
|
||||
var bookingReadModel = new BookingReadModel
|
||||
{
|
||||
Id = SnowFlakIdGenerator.NewId(),
|
||||
Trip = @event.Trip,
|
||||
BookId = @event.Id,
|
||||
PassengerInfo = @event.PassengerInfo,
|
||||
IsDeleted = @event.IsDeleted
|
||||
};
|
||||
|
||||
await _bookingDbContext.Set<Booking.Models.Booking>().AddAsync(model, cancellationToken);
|
||||
await _bookingDbContext.SaveChangesAsync(cancellationToken);
|
||||
await _bookingReadDbContext.Booking.InsertOneAsync(bookingReadModel, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
using System.Reflection;
|
||||
using BuildingBlocks.EFCore;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Booking.Data;
|
||||
|
||||
public class BookingDbContext : AppDbContextBase
|
||||
{
|
||||
public const string DefaultSchema = "dbo";
|
||||
|
||||
public BookingDbContext(DbContextOptions<BookingDbContext> options, IHttpContextAccessor httpContextAccessor) : base(options, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Booking.Models.Booking> Bookings => Set<Booking.Models.Booking>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
base.OnModelCreating(builder);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using Booking.Booking.Models.Reads;
|
||||
using BuildingBlocks.Mongo;
|
||||
using Humanizer;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Booking.Data;
|
||||
|
||||
public class BookingReadDbContext : MongoDbContext
|
||||
{
|
||||
public BookingReadDbContext(IOptions<MongoOptions> options) : base(options)
|
||||
{
|
||||
Booking = GetCollection<BookingReadModel>(nameof(Booking).Underscore());
|
||||
}
|
||||
|
||||
public IMongoCollection<BookingReadModel> Booking { get; }
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Booking.Data.Configurations;
|
||||
|
||||
public class BookingConfiguration : IEntityTypeConfiguration<Booking.Models.Booking>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Booking.Models.Booking> builder)
|
||||
{
|
||||
builder.ToTable("Booking", BookingDbContext.DefaultSchema);
|
||||
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.Property(r => r.Id).ValueGeneratedNever();
|
||||
|
||||
builder.OwnsOne(c => c.Trip, x =>
|
||||
{
|
||||
x.Property(c => c.Description);
|
||||
x.Property(c => c.Price);
|
||||
x.Property(c => c.AircraftId);
|
||||
x.Property(c => c.FlightDate);
|
||||
x.Property(c => c.FlightNumber);
|
||||
x.Property(c => c.SeatNumber);
|
||||
x.Property(c => c.ArriveAirportId);
|
||||
x.Property(c => c.DepartureAirportId);
|
||||
});
|
||||
|
||||
builder.OwnsOne(c => c.PassengerInfo, x =>
|
||||
{
|
||||
x.Property(c => c.Name);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace Booking.Data;
|
||||
|
||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<BookingDbContext>
|
||||
{
|
||||
public BookingDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<BookingDbContext>();
|
||||
|
||||
builder.UseSqlServer(
|
||||
"Data Source=.\\sqlexpress;Initial Catalog=BookingDB;Persist Security Info=False;Integrated Security=SSPI");
|
||||
return new BookingDbContext(builder.Options, null);
|
||||
}
|
||||
}
|
||||
@ -1,117 +0,0 @@
|
||||
// <auto-generated />
|
||||
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("20220507150115_initial")]
|
||||
partial class initial
|
||||
{
|
||||
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<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Booking", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Booking.Booking.Models.Booking", b =>
|
||||
{
|
||||
b.OwnsOne("Booking.Booking.Models.ValueObjects.PassengerInfo", "PassengerInfo", b1 =>
|
||||
{
|
||||
b1.Property<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("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<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("AircraftId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("ArriveAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("DepartureAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.Property<DateTime>("FlightDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b1.Property<string>("FlightNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.Property<decimal>("Price")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Booking.Data.Migrations
|
||||
{
|
||||
public partial class initial : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "dbo");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Booking",
|
||||
schema: "dbo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||
Trip_FlightNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Trip_AircraftId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Trip_DepartureAirportId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Trip_ArriveAirportId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Trip_FlightDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Trip_Price = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
|
||||
Trip_Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Trip_SeatNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PassengerInfo_Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Booking", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Booking",
|
||||
schema: "dbo");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,152 +0,0 @@
|
||||
// <auto-generated />
|
||||
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<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Booking", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DataType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DeliveryType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("MessageStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<int>("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<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("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<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("AircraftId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("ArriveAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("DepartureAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.Property<DateTime>("FlightDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b1.Property<string>("FlightNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.Property<decimal>("Price")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,150 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Booking.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Booking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(BookingDbContext))]
|
||||
partial class BookingDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(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<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Booking", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DataType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DeliveryType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("MessageStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<int>("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<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("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<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("AircraftId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("ArriveAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<long>("DepartureAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.Property<DateTime>("FlightDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b1.Property<string>("FlightNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.Property<decimal>("Price")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
dotnet ef migrations add initial --context BookingDbContext -o "Data\Migrations"
|
||||
dotnet ef database update --context BookingDbContext
|
||||
@ -16,7 +16,7 @@ public sealed class EventMapper : IEventMapper
|
||||
};
|
||||
}
|
||||
|
||||
public InternalCommand MapToInternalCommand(IDomainEvent @event)
|
||||
public IInternalCommand MapToInternalCommand(IDomainEvent @event)
|
||||
{
|
||||
return @event switch
|
||||
{
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
using BuildingBlocks.Core;
|
||||
using BuildingBlocks.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Booking.Extensions;
|
||||
|
||||
public static class CoreExtensions
|
||||
{
|
||||
public static IServiceCollection AddCore(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
|
||||
services.AddTransient<IEventMapper, EventMapper>();
|
||||
services.AddScoped<IEventDispatcher, EventDispatcher>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,6 @@ public static class MediatRExtensions
|
||||
services.AddMediatR(typeof(BookingRoot).Assembly);
|
||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
|
||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Booking.Booking.Models.Reads;
|
||||
using Booking.Data;
|
||||
using BuildingBlocks.Contracts.Grpc;
|
||||
using BuildingBlocks.PersistMessageProcessor.Data;
|
||||
using BuildingBlocks.TestBase;
|
||||
using FluentAssertions;
|
||||
using Integration.Test.Fakes;
|
||||
@ -14,9 +16,10 @@ using Xunit;
|
||||
|
||||
namespace Integration.Test.Booking.Features;
|
||||
|
||||
public class CreateBookingTests : IntegrationTestBase<Program, BookingDbContext>
|
||||
public class CreateBookingTests : IntegrationTestBase<Program, PersistMessageDbContext, BookingReadDbContext>
|
||||
{
|
||||
public CreateBookingTests(IntegrationTestFixture<Program, BookingDbContext> integrationTestFixture) : base(
|
||||
public CreateBookingTests(
|
||||
IntegrationTestFixture<Program, PersistMessageDbContext, BookingReadDbContext> integrationTestFixture) : base(
|
||||
integrationTestFixture)
|
||||
{
|
||||
}
|
||||
|
||||
@ -9,9 +9,9 @@ using BuildingBlocks.Jwt;
|
||||
using BuildingBlocks.Logging;
|
||||
using BuildingBlocks.Mapster;
|
||||
using BuildingBlocks.MassTransit;
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using BuildingBlocks.Mongo;
|
||||
using BuildingBlocks.OpenTelemetry;
|
||||
using BuildingBlocks.PersistMessageProcessor;
|
||||
using BuildingBlocks.Swagger;
|
||||
using BuildingBlocks.Web;
|
||||
using Figgle;
|
||||
@ -37,10 +37,10 @@ builder.Services.AddCustomDbContext<FlightDbContext>(configuration);
|
||||
builder.Services.AddScoped<IDataSeeder, FlightDataSeeder>();
|
||||
|
||||
builder.Services.AddMongoDbContext<FlightReadDbContext>(configuration);
|
||||
|
||||
builder.Services.AddPersistMessage(configuration);
|
||||
|
||||
builder.AddCustomSerilog();
|
||||
builder.Services.AddCore();
|
||||
builder.Services.AddJwt();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddCustomSwagger(configuration, typeof(FlightRoot).Assembly);
|
||||
@ -50,7 +50,6 @@ builder.Services.AddValidatorsFromAssembly(typeof(FlightRoot).Assembly);
|
||||
builder.Services.AddCustomProblemDetails();
|
||||
builder.Services.AddCustomMapster(typeof(FlightRoot).Assembly);
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddTransient<IEventMapper, EventMapper>();
|
||||
builder.Services.AddCustomMassTransit(typeof(FlightRoot).Assembly, env);
|
||||
builder.Services.AddCustomOpenTelemetry();
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
|
||||
@ -26,7 +26,8 @@
|
||||
},
|
||||
"PersistMessageOptions": {
|
||||
"Interval": 30,
|
||||
"Enabled": true
|
||||
"Enabled": true,
|
||||
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageDB;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
},
|
||||
"PersistMessageOptions": {
|
||||
"Interval": 1,
|
||||
"Enabled": true
|
||||
"Enabled": true,
|
||||
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageTestDB;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using Flight.Aircrafts.Dtos;
|
||||
using MediatR;
|
||||
|
||||
@ -4,7 +4,7 @@ using Flight.Aircrafts.Events;
|
||||
|
||||
namespace Flight.Aircrafts.Models;
|
||||
|
||||
public class Aircraft : Aggregate<long>
|
||||
public record Aircraft : Aggregate<long>
|
||||
{
|
||||
public Aircraft()
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using Flight.Airports.Dtos;
|
||||
using MediatR;
|
||||
|
||||
@ -13,7 +13,6 @@ public class CreateAirportMongoCommand : InternalCommand
|
||||
IsDeleted = isDeleted;
|
||||
}
|
||||
|
||||
public long Id { get; }
|
||||
public string Name { get; }
|
||||
public string Address { get; }
|
||||
public string Code { get; }
|
||||
|
||||
@ -4,7 +4,7 @@ using Flight.Airports.Events;
|
||||
|
||||
namespace Flight.Airports.Models;
|
||||
|
||||
public class Airport : Aggregate<long>
|
||||
public record Airport : Aggregate<long>
|
||||
{
|
||||
public Airport()
|
||||
{
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using BuildingBlocks.MessageProcessor;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Flight.Data.Configurations;
|
||||
|
||||
public class PersistMessageConfiguration : IEntityTypeConfiguration<PersistMessage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PersistMessage> 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);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using BuildingBlocks.EFCore;
|
||||
using BuildingBlocks.Utils;
|
||||
using Flight.Aircrafts.Models;
|
||||
using Flight.Airports.Models;
|
||||
using Flight.Seats.Models;
|
||||
@ -11,8 +12,8 @@ namespace Flight.Data;
|
||||
public sealed class FlightDbContext : AppDbContextBase
|
||||
{
|
||||
public const string DefaultSchema = "dbo";
|
||||
public FlightDbContext(DbContextOptions<FlightDbContext> options, IHttpContextAccessor httpContextAccessor) : base(
|
||||
options, httpContextAccessor)
|
||||
public FlightDbContext(DbContextOptions<FlightDbContext> options, ICurrentUserProvider currentUserProvider) : base(
|
||||
options, currentUserProvider)
|
||||
{
|
||||
}
|
||||
|
||||
@ -24,7 +25,7 @@ public sealed class FlightDbContext : AppDbContextBase
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.FilterSoftDeletedProperties();
|
||||
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
builder.ApplyConfigurationsFromAssembly(typeof(FlightRoot).Assembly);
|
||||
base.OnModelCreating(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using BuildingBlocks.Mongo;
|
||||
using Flight.Aircrafts.Models.Reads;
|
||||
using Flight.Airports.Models;
|
||||
using Flight.Airports.Models.Reads;
|
||||
using Flight.Flights.Models.Reads;
|
||||
using Flight.Seats.Models.Reads;
|
||||
|
||||
@ -1,266 +0,0 @@
|
||||
// <auto-generated />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DataType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DeliveryType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("MessageStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PersistMessages", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("ManufacturingYear")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Aircraft", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Flight.Airports.Models.Airport", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Airport", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Flight.Flights.Models.Flight", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("AircraftId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ArriveAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("ArriveDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("DepartureAirportId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("DepartureDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("DurationMinutes")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("FlightDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("FlightNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("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<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Class")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("FlightId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("LastModifiedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("SeatNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
DataType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Data = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
RetryCount = table.Column<int>(type: "int", nullable: false),
|
||||
MessageStatus = table.Column<string>(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false),
|
||||
DeliveryType = table.Column<string>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
namespace Flight.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(FlightDbContext))]
|
||||
[Migration("20220511215248_Init")]
|
||||
[Migration("20220728175834_Init")]
|
||||
partial class Init
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@ -25,8 +25,8 @@ namespace Flight.Data.Migrations
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -46,8 +46,8 @@ namespace Flight.Data.Migrations
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -74,8 +74,8 @@ namespace Flight.Data.Migrations
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -110,8 +110,8 @@ namespace Flight.Data.Migrations
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -22,41 +22,6 @@ namespace Flight.Data.Migrations
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
|
||||
|
||||
modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DataType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DeliveryType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("MessageStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PersistMessages", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
||||
@ -33,7 +33,7 @@ public sealed class EventMapper : IEventMapper
|
||||
};
|
||||
}
|
||||
|
||||
public InternalCommand MapToInternalCommand(IDomainEvent @event)
|
||||
public IInternalCommand MapToInternalCommand(IDomainEvent @event)
|
||||
{
|
||||
return @event switch
|
||||
{
|
||||
|
||||
17
src/Services/Flight/src/Flight/Extensions/CoreExtensions.cs
Normal file
17
src/Services/Flight/src/Flight/Extensions/CoreExtensions.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using BuildingBlocks.Core;
|
||||
using BuildingBlocks.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Flight.Extensions;
|
||||
|
||||
public static class CoreExtensions
|
||||
{
|
||||
public static IServiceCollection AddCore(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
|
||||
services.AddTransient<IEventMapper, EventMapper>();
|
||||
services.AddScoped<IEventDispatcher, EventDispatcher>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using Flight.Flights.Dtos;
|
||||
using Flight.Flights.Models;
|
||||
|
||||
@ -2,11 +2,9 @@ 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.Features.CreateFlight.Reads;
|
||||
using MapsterMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
||||
@ -6,23 +6,23 @@ namespace Flight.Flights.Features.CreateFlight.Reads;
|
||||
|
||||
public class CreateFlightMongoCommand : InternalCommand
|
||||
{
|
||||
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)
|
||||
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;
|
||||
Id = id;
|
||||
FlightNumber = flightNumber;
|
||||
AircraftId = aircraftId;
|
||||
DepartureDate = departureDate;
|
||||
DepartureAirportId = departureAirportId;
|
||||
ArriveDate = arriveDate;
|
||||
ArriveAirportId = arriveAirportId;
|
||||
DurationMinutes = durationMinutes;
|
||||
FlightDate = flightDate;
|
||||
Status = status;
|
||||
Price = price;
|
||||
IsDeleted = isDeleted;
|
||||
}
|
||||
|
||||
public string FlightNumber { get; }
|
||||
|
||||
@ -32,7 +32,7 @@ public class CreateFlightMongoCommandHandler : ICommandHandler<CreateFlightMongo
|
||||
var flightReadModel = _mapper.Map<FlightReadModel>(command);
|
||||
|
||||
var flight = await _flightReadDbContext.Flight.AsQueryable()
|
||||
.FirstOrDefaultAsync(x => x.Id == flightReadModel.Id, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.Id == flightReadModel.Id && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (flight is not null)
|
||||
throw new FlightAlreadyExistException();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using Flight.Flights.Dtos;
|
||||
|
||||
namespace Flight.Flights.Features.DeleteFlight;
|
||||
|
||||
@ -6,23 +6,23 @@ namespace Flight.Flights.Features.DeleteFlight.Reads;
|
||||
|
||||
public class DeleteFlightMongoCommand : InternalCommand
|
||||
{
|
||||
public DeleteFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
|
||||
long DepartureAirportId,
|
||||
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status,
|
||||
decimal Price, bool IsDeleted)
|
||||
public DeleteFlightMongoCommand(long id, string flightNumber, long aircraftId, DateTime departureDate,
|
||||
long departureAirportId,
|
||||
DateTime arriveDate, long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
|
||||
decimal price, bool isDeleted)
|
||||
{
|
||||
this.Id = Id;
|
||||
this.FlightNumber = FlightNumber;
|
||||
this.AircraftId = AircraftId;
|
||||
this.DepartureDate = DepartureDate;
|
||||
this.DepartureAirportId = DepartureAirportId;
|
||||
this.ArriveDate = ArriveDate;
|
||||
this.ArriveAirportId = ArriveAirportId;
|
||||
this.DurationMinutes = DurationMinutes;
|
||||
this.FlightDate = FlightDate;
|
||||
this.Status = Status;
|
||||
this.Price = Price;
|
||||
this.IsDeleted = IsDeleted;
|
||||
Id = id;
|
||||
FlightNumber = flightNumber;
|
||||
AircraftId = aircraftId;
|
||||
DepartureDate = departureDate;
|
||||
DepartureAirportId = departureAirportId;
|
||||
ArriveDate = arriveDate;
|
||||
ArriveAirportId = arriveAirportId;
|
||||
DurationMinutes = durationMinutes;
|
||||
FlightDate = flightDate;
|
||||
Status = status;
|
||||
Price = price;
|
||||
IsDeleted = isDeleted;
|
||||
}
|
||||
|
||||
public string FlightNumber { get; }
|
||||
|
||||
@ -32,7 +32,7 @@ public class DeleteFlightMongoCommandHandler : ICommandHandler<DeleteFlightMongo
|
||||
var flightReadModel = _mapper.Map<FlightReadModel>(command);
|
||||
|
||||
var flight = await _flightReadDbContext.Flight.AsQueryable()
|
||||
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (flight is null)
|
||||
throw new FlightNotFountException();
|
||||
|
||||
@ -27,7 +27,8 @@ public class GetFlightByIdQueryHandler : IQueryHandler<GetFlightByIdQuery, Fligh
|
||||
Guard.Against.Null(query, nameof(query));
|
||||
|
||||
var flight =
|
||||
await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync(x => x.FlightId == query.Id, cancellationToken);
|
||||
await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync(x => x.FlightId == query.Id &&
|
||||
!x.IsDeleted, cancellationToken);
|
||||
|
||||
if (flight is null)
|
||||
throw new FlightNotFountException();
|
||||
|
||||
@ -32,7 +32,7 @@ public class UpdateFlightMongoCommandHandler : ICommandHandler<UpdateFlightMongo
|
||||
var flightReadModel = _mapper.Map<FlightReadModel>(command);
|
||||
|
||||
var flight = await _flightReadDbContext.Flight.AsQueryable()
|
||||
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (flight is null)
|
||||
throw new FlightNotFountException();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using BuildingBlocks.Caching;
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using Flight.Flights.Dtos;
|
||||
using Flight.Flights.Models;
|
||||
using MediatR;
|
||||
|
||||
@ -4,7 +4,7 @@ using Flight.Flights.Events.Domain;
|
||||
|
||||
namespace Flight.Flights.Models;
|
||||
|
||||
public class Flight : Aggregate<long>
|
||||
public record Flight : Aggregate<long>
|
||||
{
|
||||
public string FlightNumber { get; private set; }
|
||||
public long AircraftId { get; private set; }
|
||||
|
||||
@ -6,17 +6,17 @@ namespace Flight.Flights.Models.Reads;
|
||||
|
||||
public class FlightReadModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long FlightId { get; set; }
|
||||
public string FlightNumber { get; set; }
|
||||
public long AircraftId { get; set; }
|
||||
public DateTime DepartureDate { get; set; }
|
||||
public long DepartureAirportId { get; set; }
|
||||
public DateTime ArriveDate { get; set; }
|
||||
public long ArriveAirportId { get; set; }
|
||||
public decimal DurationMinutes { get; set; }
|
||||
public DateTime FlightDate { get; set; }
|
||||
public FlightStatus Status { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public long Id { get; init; }
|
||||
public long FlightId { get; init; }
|
||||
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; }
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using BuildingBlocks.Core.CQRS;
|
||||
using BuildingBlocks.Core.Event;
|
||||
using BuildingBlocks.IdsGenerator;
|
||||
using Flight.Seats.Dtos;
|
||||
using Flight.Seats.Models;
|
||||
|
||||
@ -29,7 +29,7 @@ public class GetAvailableSeatsQueryHandler : IRequestHandler<GetAvailableSeatsQu
|
||||
Guard.Against.Null(query, nameof(query));
|
||||
|
||||
var seats = (await _flightReadDbContext.Seat.AsQueryable().ToListAsync(cancellationToken))
|
||||
.Where(x => x.FlightId == query.FlightId);
|
||||
.Where(x => x.FlightId == query.FlightId && !x.IsDeleted);
|
||||
|
||||
if (!seats.Any())
|
||||
throw new AllSeatsFullException();
|
||||
|
||||
@ -16,7 +16,6 @@ public class ReserveSeatMongoCommand : InternalCommand
|
||||
IsDeleted = isDeleted;
|
||||
}
|
||||
|
||||
public long Id { get; }
|
||||
public string SeatNumber { get; }
|
||||
public SeatType Type { get; }
|
||||
public SeatClass Class { get; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user