Merge pull request #36 from meysamhadeli/develop

refactor persist-message-processor
This commit is contained in:
Meysam Hadeli 2022-07-31 01:44:58 +04:30 committed by GitHub
commit 7fdf418c7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
150 changed files with 1011 additions and 3365 deletions

View File

@ -24,7 +24,7 @@ Content-Type: application/x-www-form-urlencoded
grant_type=password grant_type=password
&client_id=client &client_id=client
&client_secret=secret &client_secret=secret
&username=meysamh &username=samh
&password=Admin@123456 &password=Admin@123456
&scope=flight-api &scope=flight-api
### ###
@ -39,11 +39,11 @@ Content-Type: application/json
authorization: bearer {{Authenticate.response.body.access_token}} authorization: bearer {{Authenticate.response.body.access_token}}
{ {
"firstName": "John6", "firstName": "John",
"lastName": "Doe6", "lastName": "Do",
"username": "admin6", "username": "admin",
"passportNumber": "1234567896", "passportNumber": "1290000000",
"email": "admin6@admin.com", "email": "admin@admin.com",
"password": "Admin6@12345", "password": "Admin6@12345",
"confirmPassword": "Admin6@12345" "confirmPassword": "Admin6@12345"
} }
@ -219,7 +219,7 @@ Content-Type: application/json
authorization: bearer {{Authenticate.response.body.access_token}} authorization: bearer {{Authenticate.response.body.access_token}}
{ {
"passportNumber": "1234567896", "passportNumber": "123456789",
"passengerType": 1, "passengerType": 1,
"age": 30 "age": 30
} }
@ -251,7 +251,7 @@ Content-Type: application/json
authorization: bearer {{Authenticate.response.body.access_token}} authorization: bearer {{Authenticate.response.body.access_token}}
{ {
"passengerId": 1, "passengerId": 4776722699124736,
"flightId": 1, "flightId": 1,
"description": "I want to fly to iran" "description": "I want to fly to iran"
} }

View File

@ -1,6 +1,6 @@
using BuildingBlocks.Core.Event;
namespace BuildingBlocks.Contracts.EventBus.Messages; namespace BuildingBlocks.Contracts.EventBus.Messages;
public class PassengerContracts public record PassengerRegistrationCompleted(long Id) : IIntegrationEvent;
{ public record PassengerCreated(long Id) : IIntegrationEvent;
}

View File

@ -1,10 +1,11 @@
using BuildingBlocks.IdsGenerator;
using MediatR; using MediatR;
namespace BuildingBlocks.Core.Event; namespace BuildingBlocks.Core.Event;
public interface IEvent : INotification public interface IEvent : INotification
{ {
Guid EventId => Guid.NewGuid(); long EventId => SnowFlakIdGenerator.NewId();
public DateTime OccurredOn => DateTime.Now; public DateTime OccurredOn => DateTime.Now;
public string EventType => GetType().AssemblyQualifiedName; public string EventType => GetType().AssemblyQualifiedName;
} }

View File

@ -0,0 +1,5 @@
namespace BuildingBlocks.Core.Event;
public interface IInternalCommand : IEvent
{
}

View File

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

View File

@ -1,14 +1,9 @@
using BuildingBlocks.IdsGenerator; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Utils; using BuildingBlocks.IdsGenerator;
using ICommand = BuildingBlocks.Core.CQRS.ICommand;
namespace BuildingBlocks.Core.Event; namespace BuildingBlocks.Core.Event;
public class InternalCommand : IInternalCommand, ICommand public class InternalCommand : IInternalCommand, ICommand
{ {
public long Id { get; set; } = SnowFlakIdGenerator.NewId(); public long Id { get; init; } = SnowFlakIdGenerator.NewId();
public DateTime OccurredOn => DateTime.Now;
public string Type { get => TypeProvider.GetTypeName(GetType()); }
} }

View File

@ -1,6 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
using BuildingBlocks.MessageProcessor; using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; 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) CancellationToken cancellationToken = default)
where T : IEvent where T : IEvent
{ {
if (events.Count > 0) if (events.Count > 0)
{ {
var eventType = type != null && type.IsAssignableTo(typeof(IInternalCommand))
? EventType.InternalCommand
: EventType.DomainEvent;
async Task PublishIntegrationEvent(IReadOnlyList<IIntegrationEvent> integrationEvents) async Task PublishIntegrationEvent(IReadOnlyList<IIntegrationEvent> integrationEvents)
{ {
foreach (var integrationEvent in integrationEvents) foreach (var integrationEvent in integrationEvents)
@ -63,7 +67,7 @@ public sealed class EventDispatcher : IEventDispatcher
break; break;
} }
if (eventType == EventType.InternalCommand) if (type != null && eventType == EventType.InternalCommand)
{ {
var internalMessages = await MapDomainEventToInternalCommandAsync(events as IReadOnlyList<IDomainEvent>) var internalMessages = await MapDomainEventToInternalCommandAsync(events as IReadOnlyList<IDomainEvent>)
.ConfigureAwait(false); .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) CancellationToken cancellationToken = default)
where T : IEvent => where T : IEvent =>
await SendAsync(new[] {@event}, eventType, cancellationToken); await SendAsync(new[] {@event}, type, cancellationToken);
private Task<IReadOnlyList<IIntegrationEvent>> MapDomainEventToIntegrationEventAsync( 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) IReadOnlyList<IDomainEvent> events)
{ {
_logger.LogTrace("Processing internal message start..."); _logger.LogTrace("Processing internal message start...");
var internalCommands = new List<InternalCommand>(); var internalCommands = new List<IInternalCommand>();
using var scope = _serviceScopeFactory.CreateScope(); using var scope = _serviceScopeFactory.CreateScope();
foreach (var @event in events) foreach (var @event in events)
{ {
@ -132,7 +136,7 @@ public sealed class EventDispatcher : IEventDispatcher
_logger.LogTrace("Processing internal message done..."); _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) private IEnumerable<IIntegrationEvent> GetWrappedIntegrationEvents(IReadOnlyList<IDomainEvent> domainEvents)

View File

@ -4,8 +4,8 @@ namespace BuildingBlocks.Core;
public interface IEventDispatcher 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; 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; where T : IEvent;
} }

View File

@ -5,5 +5,5 @@ namespace BuildingBlocks.Core;
public interface IEventMapper public interface IEventMapper
{ {
IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event); IIntegrationEvent MapToIntegrationEvent(IDomainEvent @event);
InternalCommand MapToInternalCommand(IDomainEvent @event); IInternalCommand MapToInternalCommand(IDomainEvent @event);
} }

View File

@ -2,11 +2,11 @@
namespace BuildingBlocks.Core.Model 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(); private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly(); public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

View File

@ -1,6 +1,6 @@
namespace BuildingBlocks.Core.Model; namespace BuildingBlocks.Core.Model;
public abstract class Entity : IEntity public abstract record Audit : IAudit
{ {
public DateTime? CreatedAt { get; set; } public DateTime? CreatedAt { get; set; }
public long? CreatedBy { get; set; } public long? CreatedBy { get; set; }

View File

@ -2,7 +2,7 @@
namespace BuildingBlocks.Core.Model; namespace BuildingBlocks.Core.Model;
public interface IAggregate : IEntity public interface IAggregate : IAudit
{ {
IReadOnlyList<IDomainEvent> DomainEvents { get; } IReadOnlyList<IDomainEvent> DomainEvents { get; }
IEvent[] ClearDomainEvents(); IEvent[] ClearDomainEvents();

View File

@ -1,6 +1,6 @@
namespace BuildingBlocks.Core.Model; namespace BuildingBlocks.Core.Model;
public interface IEntity public interface IAudit
{ {
public DateTime? CreatedAt { get; set; } public DateTime? CreatedAt { get; set; }
public long? CreatedBy { get; set; } public long? CreatedBy { get; set; }

View File

@ -1,10 +1,9 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Data; using System.Data;
using System.Reflection;
using System.Security.Claims;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
using BuildingBlocks.Core.Model; using BuildingBlocks.Core.Model;
using Microsoft.AspNetCore.Http; using BuildingBlocks.Utils;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
@ -12,13 +11,13 @@ namespace BuildingBlocks.EFCore;
public abstract class AppDbContextBase : DbContext, IDbContext public abstract class AppDbContextBase : DbContext, IDbContext
{ {
private readonly IHttpContextAccessor _httpContextAccessor; private readonly ICurrentUserProvider _currentUserProvider;
private IDbContextTransaction _currentTransaction; 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) 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 // ref: https://www.meziantou.net/entity-framework-core-soft-delete-using-query-filters.htm
private void OnBeforeSaving() private void OnBeforeSaving()
{ {
var nameIdentifier = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
long.TryParse(nameIdentifier, out var userId);
foreach (var entry in ChangeTracker.Entries<IAggregate>()) foreach (var entry in ChangeTracker.Entries<IAggregate>())
{ {
var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate)); var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate));
var userId = _currentUserProvider?.GetCurrentUserId();
if (isAuditable) if (isAuditable)
{ {

View File

@ -57,13 +57,11 @@ public class EfTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TRe
nameof(EfTxBehavior<TRequest, TResponse>), nameof(EfTxBehavior<TRequest, TResponse>),
typeof(TRequest).FullName); typeof(TRequest).FullName);
await _dbContextBase.CommitTransactionAsync(cancellationToken);
var domainEvents = _dbContextBase.GetDomainEvents(); 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; return response;
} }

View File

@ -1,8 +1,8 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using BuildingBlocks.Core.Model; using BuildingBlocks.Core.Model;
using BuildingBlocks.PersistMessageProcessor.Data;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Infrastructure;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -45,7 +45,7 @@ public static class Extensions
{ {
Expression<Func<IAggregate, bool>> filterExpr = e => !e.IsDeleted; Expression<Func<IAggregate, bool>> filterExpr = e => !e.IsDeleted;
foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes() 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 // modify expression to handle correct child type
var parameter = Expression.Parameter(mutableEntityType.ClrType); var parameter = Expression.Parameter(mutableEntityType.ClrType);
@ -63,6 +63,9 @@ public static class Extensions
{ {
using var scope = serviceProvider.CreateScope(); using var scope = serviceProvider.CreateScope();
var persistMessageContext = scope.ServiceProvider.GetRequiredService<PersistMessageDbContext>();
await persistMessageContext.Database.MigrateAsync();
var context = scope.ServiceProvider.GetRequiredService<TContext>(); var context = scope.ServiceProvider.GetRequiredService<TContext>();
await context.Database.MigrateAsync(); await context.Database.MigrateAsync();
} }

View File

@ -1,6 +1,4 @@
using System.Data;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
using BuildingBlocks.MessageProcessor;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace BuildingBlocks.EFCore; namespace BuildingBlocks.EFCore;
@ -9,7 +7,7 @@ public interface IDbContext
{ {
DbSet<TEntity> Set<TEntity>() DbSet<TEntity> Set<TEntity>()
where TEntity : class; where TEntity : class;
DbSet<PersistMessage> PersistMessages => Set<PersistMessage>();
IReadOnlyList<IDomainEvent> GetDomainEvents(); IReadOnlyList<IDomainEvent> GetDomainEvents();
Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(CancellationToken cancellationToken = default); Task CommitTransactionAsync(CancellationToken cancellationToken = default);

View File

@ -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>(); services.AddSingleton<IProjectionPublisher, ProjectionPublisher>();
@ -81,7 +82,7 @@ public static class EventStoreDBConfigExtensions
{ {
services.Scan(scan => scan services.Scan(scan => scan
.FromAssemblies(assembliesToScan) .FromAssemblies(assembliesToScan)
.AddClasses(classes => classes.AssignableTo<IProjection>()) // Filter classes .AddClasses(classes => classes.AssignableTo<IProjectionProcessor>()) // Filter classes
.AsImplementedInterfaces() .AsImplementedInterfaces()
.WithTransientLifetime()); .WithTransientLifetime());
} }

View File

@ -3,7 +3,7 @@ using BuildingBlocks.Core.Model;
namespace BuildingBlocks.EventStoreDB.Events 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(); private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly(); public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
@ -13,9 +13,9 @@ namespace BuildingBlocks.EventStoreDB.Events
_domainEvents.Add(domainEvent); _domainEvents.Add(domainEvent);
} }
public IEvent[] ClearDomainEvents() public IDomainEvent[] ClearDomainEvents()
{ {
IEvent[] dequeuedEvents = _domainEvents.ToArray(); var dequeuedEvents = _domainEvents.ToArray();
_domainEvents.Clear(); _domainEvents.Clear();

View File

@ -3,10 +3,10 @@ using BuildingBlocks.Core.Model;
namespace BuildingBlocks.EventStoreDB.Events namespace BuildingBlocks.EventStoreDB.Events
{ {
public interface IAggregateEventSourcing : IProjection, IEntity public interface IAggregateEventSourcing : IProjection, IAudit
{ {
IReadOnlyList<IDomainEvent> DomainEvents { get; } IReadOnlyList<IDomainEvent> DomainEvents { get; }
IEvent[] ClearDomainEvents(); IDomainEvent[] ClearDomainEvents();
long Version { get; } long Version { get; }
} }

View File

@ -8,12 +8,16 @@ public interface IEventStoreDBRepository<T> where T : class, IAggregateEventSour
{ {
Task<T?> Find(long id, CancellationToken cancellationToken); Task<T?> Find(long id, CancellationToken cancellationToken);
Task<ulong> Add(T aggregate, 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); 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; private readonly EventStoreClient eventStore;
public EventStoreDBRepository(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)); this.eventStore = eventStore ?? throw new ArgumentNullException(nameof(eventStore));
} }
public Task<T?> Find(long id, CancellationToken cancellationToken) => public Task<T?> Find(long id, CancellationToken cancellationToken)
eventStore.AggregateStream<T>( {
return eventStore.AggregateStream<T>(
id, id,
cancellationToken cancellationToken
); );
}
public async Task<ulong> Add(T aggregate, CancellationToken cancellationToken = default) 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; 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; var nextVersion = expectedRevision ?? aggregate.Version;
@ -51,8 +58,11 @@ public class EventStoreDBRepository<T>: IEventStoreDBRepository<T> where T : cla
return result.NextExpectedStreamRevision; return result.NextExpectedStreamRevision;
} }
public Task<ulong> Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default) => public Task<ulong> Delete(T aggregate, long? expectedRevision = null,
Update(aggregate, expectedRevision, cancellationToken); CancellationToken cancellationToken = default)
{
return Update(aggregate, expectedRevision, cancellationToken);
}
private static IEnumerable<EventData> GetEventsToStore(T aggregate) private static IEnumerable<EventData> GetEventsToStore(T aggregate)
{ {

View File

@ -1,5 +1,5 @@
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
using BuildingBlocks.MessageProcessor; using BuildingBlocks.PersistMessageProcessor;
using MassTransit; using MassTransit;
namespace BuildingBlocks.MassTransit; namespace BuildingBlocks.MassTransit;

View File

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

View File

@ -1,14 +1,13 @@
using BuildingBlocks.MessageProcessor; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Booking.Data.Configurations; namespace BuildingBlocks.PersistMessageProcessor.Data.Configurations;
public class PersistMessageConfiguration : IEntityTypeConfiguration<PersistMessage> public class PersistMessageConfiguration : IEntityTypeConfiguration<PersistMessage>
{ {
public void Configure(EntityTypeBuilder<PersistMessage> builder) public void Configure(EntityTypeBuilder<PersistMessage> builder)
{ {
builder.ToTable("PersistMessages", BookingDbContext.DefaultSchema); builder.ToTable("PersistMessage", PersistMessageDbContext.DefaultSchema);
builder.HasKey(x => x.Id); builder.HasKey(x => x.Id);

View File

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

View File

@ -1,19 +1,20 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using Booking.Data;
using BuildingBlocks.PersistMessageProcessor.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Passenger.Data;
#nullable disable #nullable disable
namespace Passenger.Data.Migrations namespace Booking.Data.Migrations
{ {
[DbContext(typeof(PassengerDbContext))] [DbContext(typeof(PersistMessageDbContext))]
[Migration("20220616122705_Add-PersistMessages")] [Migration("20220728155556_initial")]
partial class AddPersistMessages partial class initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
@ -24,12 +25,16 @@ namespace Passenger.Data.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b => modelBuilder.Entity("BuildingBlocks.PersistMessageProcessor.PersistMessage", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("ApplicationName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("Created") b.Property<DateTime>("Created")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@ -56,47 +61,7 @@ namespace Passenger.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("PersistMessages", "dbo"); b.ToTable("PersistMessage", "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");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -5,16 +5,19 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Booking.Data.Migrations namespace Booking.Data.Migrations
{ {
public partial class AddPersistMessages : Migration public partial class initial : Migration
{ {
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.EnsureSchema(
name: "dbo");
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PersistMessages", name: "PersistMessage",
schema: "dbo", schema: "dbo",
columns: table => new 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), DataType = table.Column<string>(type: "nvarchar(max)", nullable: true),
Data = 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), Created = table.Column<DateTime>(type: "datetime2", nullable: false),
@ -24,14 +27,14 @@ namespace Booking.Data.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_PersistMessages", x => x.Id); table.PrimaryKey("PK_PersistMessage", x => x.Id);
}); });
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PersistMessages", name: "PersistMessage",
schema: "dbo"); schema: "dbo");
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.Mongo; using BuildingBlocks.PersistMessageProcessor.Data;
using BuildingBlocks.Web;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace BuildingBlocks.MessageProcessor; namespace BuildingBlocks.PersistMessageProcessor;
public static class Extensions public static class Extensions
{ {
@ -13,8 +15,15 @@ public static class Extensions
.Bind(configuration.GetSection(nameof(PersistMessageOptions))) .Bind(configuration.GetSection(nameof(PersistMessageOptions)))
.ValidateDataAnnotations(); .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<IPersistMessageProcessor, PersistMessageProcessor>();
services.AddScoped<IEventDispatcher, EventDispatcher>();
services.AddHostedService<PersistMessageBackgroundService>(); services.AddHostedService<PersistMessageBackgroundService>();
return services; return services;

View File

@ -0,0 +1,9 @@
using BuildingBlocks.EFCore;
using Microsoft.EntityFrameworkCore;
namespace BuildingBlocks.PersistMessageProcessor;
public interface IPersistMessageDbContext : IDbContext
{
DbSet<PersistMessage> PersistMessages => Set<PersistMessage>();
}

View File

@ -1,7 +1,7 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
namespace BuildingBlocks.MessageProcessor; namespace BuildingBlocks.PersistMessageProcessor;
// Ref: http://www.kamilgrzybek.com/design/the-outbox-pattern/ // Ref: http://www.kamilgrzybek.com/design/the-outbox-pattern/
// Ref: https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/ // Ref: https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/
@ -15,7 +15,7 @@ public interface IPersistMessageProcessor
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
where TMessageEnvelope : MessageEnvelope; where TMessageEnvelope : MessageEnvelope;
Task<Guid> AddReceivedMessageAsync<TMessageEnvelope>( Task<long> AddReceivedMessageAsync<TMessageEnvelope>(
TMessageEnvelope messageEnvelope, TMessageEnvelope messageEnvelope,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
where TMessageEnvelope : MessageEnvelope; where TMessageEnvelope : MessageEnvelope;
@ -30,14 +30,14 @@ public interface IPersistMessageProcessor
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<PersistMessage> ExistMessageAsync( Task<PersistMessage> ExistMessageAsync(
Guid messageId, long messageId,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task ProcessInboxAsync( Task ProcessInboxAsync(
Guid messageId, long messageId,
CancellationToken cancellationToken = default); 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); Task ProcessAllAsync(CancellationToken cancellationToken = default);
} }

View File

@ -1,4 +1,4 @@
namespace BuildingBlocks.MessageProcessor; namespace BuildingBlocks.PersistMessageProcessor;
[Flags] [Flags]
public enum MessageDeliveryType public enum MessageDeliveryType

View File

@ -1,4 +1,4 @@
namespace BuildingBlocks.MessageProcessor; namespace BuildingBlocks.PersistMessageProcessor;
public enum MessageStatus public enum MessageStatus
{ {

View File

@ -1,8 +1,10 @@
namespace BuildingBlocks.MessageProcessor; using System.Reflection;
namespace BuildingBlocks.PersistMessageProcessor;
public class PersistMessage 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; Id = id;
DataType = dataType; DataType = dataType;
@ -13,7 +15,7 @@ public class PersistMessage
RetryCount = 0; RetryCount = 0;
} }
public Guid Id { get; private set; } public long Id { get; private set; }
public string DataType { get; private set; } public string DataType { get; private set; }
public string Data { get; private set; } public string Data { get; private set; }
public DateTime Created { get; private set; } public DateTime Created { get; private set; }

View File

@ -3,7 +3,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace BuildingBlocks.MessageProcessor; namespace BuildingBlocks.PersistMessageProcessor;
public class PersistMessageBackgroundService : BackgroundService public class PersistMessageBackgroundService : BackgroundService
{ {
@ -42,6 +42,8 @@ public class PersistMessageBackgroundService : BackgroundService
private async Task ProcessAsync(CancellationToken stoppingToken) private async Task ProcessAsync(CancellationToken stoppingToken)
{ {
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{
try
{ {
await using (var scope = _serviceProvider.CreateAsyncScope()) await using (var scope = _serviceProvider.CreateAsyncScope())
{ {
@ -55,5 +57,11 @@ public class PersistMessageBackgroundService : BackgroundService
await Task.Delay(delay, stoppingToken); await Task.Delay(delay, stoppingToken);
} }
catch (System.Exception e)
{
Console.WriteLine(e);
throw;
}
}
} }
} }

View File

@ -1,7 +1,8 @@
namespace BuildingBlocks.MessageProcessor; namespace BuildingBlocks.PersistMessageProcessor;
public class PersistMessageOptions public class PersistMessageOptions
{ {
public int? Interval { get; set; } = 30; public int? Interval { get; set; } = 30;
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public string ConnectionString { get; set; }
} }

View File

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

View File

@ -1,10 +1,11 @@
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using BuildingBlocks.Core.Event;
using BuildingBlocks.Core.Model; using BuildingBlocks.Core.Model;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
using BuildingBlocks.EventStoreDB.Projections;
using BuildingBlocks.MassTransit; using BuildingBlocks.MassTransit;
using BuildingBlocks.MessageProcessor;
using BuildingBlocks.Mongo; using BuildingBlocks.Mongo;
using BuildingBlocks.Utils; using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Grpc.Net.Client; using Grpc.Net.Client;
using MassTransit; using MassTransit;
@ -50,6 +51,7 @@ public class IntegrationTestFixture<TEntryPoint> : IAsyncLifetime
{ {
TestRegistrationServices?.Invoke(services); TestRegistrationServices?.Invoke(services);
services.ReplaceSingleton(AddHttpContextAccessorMock); services.ReplaceSingleton(AddHttpContextAccessorMock);
services.Unregister<IProjectionProcessor>();
services.AddMassTransitTestHarness(x => services.AddMassTransitTestHarness(x =>
{ {
x.UsingRabbitMq((context, cfg) => x.UsingRabbitMq((context, cfg) =>
@ -88,9 +90,11 @@ public class IntegrationTestFixture<TEntryPoint> : IAsyncLifetime
public ILogger CreateLogger(ITestOutputHelper output) public ILogger CreateLogger(ITestOutputHelper output)
{ {
if (output != null) if (output != null)
{
return new LoggerConfiguration() return new LoggerConfiguration()
.WriteTo.TestOutput(output) .WriteTo.TestOutput(output)
.CreateLogger(); .CreateLogger();
}
return null; return null;
} }
@ -160,7 +164,7 @@ public class IntegrationTestFixture<TEntryPoint> : IAsyncLifetime
var filter = await persistMessageProcessor.GetByFilterAsync(x => var filter = await persistMessageProcessor.GetByFilterAsync(x =>
x.DeliveryType == MessageDeliveryType.Internal && x.DeliveryType == MessageDeliveryType.Internal &&
TypeProvider.GetTypeName(typeof(TInternalCommand)) == x.DataType); typeof(TInternalCommand).ToString() == x.DataType);
var res = filter.Any(x => x.MessageStatus == MessageStatus.Processed); 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) public Task<T> FindAsync<T>(long id)
where T : class, IEntity where T : class, IAudit
{ {
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask()); 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 public class IntegrationTestFixtureCore<TEntryPoint> : IAsyncLifetime
where TEntryPoint : class where TEntryPoint : class
{ {
private Checkpoint _checkpoint; private Checkpoint _checkpointDefaultDB;
private Checkpoint _checkpointPersistMessageDB;
private MongoDbRunner _mongoRunner; private MongoDbRunner _mongoRunner;
public IntegrationTestFixtureCore(IntegrationTestFixture<TEntryPoint> integrationTestFixture) public IntegrationTestFixtureCore(IntegrationTestFixture<TEntryPoint> integrationTestFixture)
@ -332,7 +337,8 @@ public class IntegrationTestFixtureCore<TEntryPoint> : IAsyncLifetime
public async Task InitializeAsync() 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(); _mongoRunner = MongoDbRunner.Start();
var mongoOptions = Fixture.ServiceProvider.GetRequiredService<IOptions<MongoOptions>>(); var mongoOptions = Fixture.ServiceProvider.GetRequiredService<IOptions<MongoOptions>>();
@ -344,7 +350,8 @@ public class IntegrationTestFixtureCore<TEntryPoint> : IAsyncLifetime
public async Task DisposeAsync() 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(); _mongoRunner.Dispose();
} }

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

View File

@ -1,14 +1,10 @@
using System.Collections.Concurrent;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Ardalis.GuardClauses;
namespace BuildingBlocks.Utils; namespace BuildingBlocks.Utils;
public static class TypeProvider 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) private static bool IsRecord(this Type objectType)
{ {
return objectType.GetMethod("<Clone>$") != null || return objectType.GetMethod("<Clone>$") != null ||
@ -34,76 +30,10 @@ public static class TypeProvider
public static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string typeName) 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)) .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName))
.FirstOrDefault(); .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;
});
});
} }

View File

@ -5,14 +5,17 @@ using Booking.Extensions;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
using BuildingBlocks.EventStoreDB; using BuildingBlocks.EventStoreDB;
using BuildingBlocks.EventStoreDB.Projections;
using BuildingBlocks.HealthCheck; using BuildingBlocks.HealthCheck;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using BuildingBlocks.Jwt; using BuildingBlocks.Jwt;
using BuildingBlocks.Logging; using BuildingBlocks.Logging;
using BuildingBlocks.Mapster; using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit; using BuildingBlocks.MassTransit;
using BuildingBlocks.MessageProcessor; using BuildingBlocks.Mongo;
using BuildingBlocks.OpenTelemetry; using BuildingBlocks.OpenTelemetry;
using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.PersistMessageProcessor.Data;
using BuildingBlocks.Swagger; using BuildingBlocks.Swagger;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
@ -31,10 +34,11 @@ builder.Services.Configure<GrpcOptions>(options => configuration.GetSection("Grp
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.Services.AddCustomDbContext<BookingDbContext>(configuration);
builder.Services.AddPersistMessage(configuration); builder.Services.AddPersistMessage(configuration);
builder.Services.AddMongoDbContext<BookingReadDbContext>(configuration);
builder.AddCustomSerilog(); builder.AddCustomSerilog();
builder.Services.AddCore();
builder.Services.AddJwt(); builder.Services.AddJwt();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
@ -44,9 +48,6 @@ builder.Services.AddCustomMediatR();
builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly); builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly);
builder.Services.AddCustomProblemDetails(); builder.Services.AddCustomProblemDetails();
builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly); builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly);
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<IEventMapper, EventMapper>();
builder.Services.AddCustomHealthCheck(); builder.Services.AddCustomHealthCheck();
builder.Services.AddCustomMassTransit(typeof(BookingRoot).Assembly, env); builder.Services.AddCustomMassTransit(typeof(BookingRoot).Assembly, env);
builder.Services.AddCustomOpenTelemetry(); builder.Services.AddCustomOpenTelemetry();
@ -69,7 +70,6 @@ if (app.Environment.IsDevelopment())
} }
app.UseSerilogRequestLogging(); app.UseSerilogRequestLogging();
app.UseMigration<BookingDbContext>(env);
app.UseCorrelationId(); app.UseCorrelationId();
app.UseRouting(); app.UseRouting();
app.UseHttpMetrics(); app.UseHttpMetrics();

View File

@ -7,9 +7,6 @@
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}", "LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"ElasticUri": "http://localhost:9200" "ElasticUri": "http://localhost:9200"
}, },
"ConnectionStrings": {
"DefaultConnection": "Server=.\\sqlexpress;Database=BookingDB;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Jwt": { "Jwt": {
"Authority": "https://localhost:5005", "Authority": "https://localhost:5005",
"Audience": "booking-api" "Audience": "booking-api"
@ -27,9 +24,14 @@
"EventStore": { "EventStore": {
"ConnectionString": "esdb://localhost:2113?tls=false" "ConnectionString": "esdb://localhost:2113?tls=false"
}, },
"MongoOptions": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "booking-db"
},
"PersistMessageOptions": { "PersistMessageOptions": {
"Interval": 30, "Interval": 30,
"Enabled": true "Enabled": true,
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageDB;Trusted_Connection=True;MultipleActiveResultSets=true"
}, },
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@ -18,6 +18,11 @@
}, },
"PersistMessageOptions": { "PersistMessageOptions": {
"Interval": 1, "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"
} }
} }

View File

@ -12,10 +12,6 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" /> <ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />

View File

@ -1,6 +1,7 @@
using Booking.Booking.Models.ValueObjects; using Booking.Booking.Models.ValueObjects;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
using BuildingBlocks.Core.Model;
namespace Booking.Booking.Events.Domain; 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;

View File

@ -1,4 +1,7 @@
using Booking.Booking.Dtos; using Booking.Booking.Dtos;
using Booking.Booking.Events.Domain;
using Booking.Booking.Models.Reads;
using BuildingBlocks.IdsGenerator;
using Mapster; using Mapster;
namespace Booking.Booking.Features; namespace Booking.Booking.Features;

View File

@ -1,9 +1,10 @@
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
namespace Booking.Booking.Features.CreateBooking; 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(); public long Id { get; init; } = SnowFlakIdGenerator.NewId();
} }

View File

@ -1,10 +1,11 @@
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using Booking.Booking.Events.Domain;
using Booking.Booking.Exceptions; using Booking.Booking.Exceptions;
using Booking.Booking.Models.ValueObjects; using Booking.Booking.Models.ValueObjects;
using BuildingBlocks.Contracts.Grpc; using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.EventStoreDB.Repository; using BuildingBlocks.EventStoreDB.Repository;
using MediatR; using BuildingBlocks.Utils;
namespace Booking.Booking.Features.CreateBooking; namespace Booking.Booking.Features.CreateBooking;
@ -12,15 +13,18 @@ public class CreateBookingCommandHandler : ICommandHandler<CreateBookingCommand,
{ {
private readonly IEventStoreDBRepository<Models.Booking> _eventStoreDbRepository; private readonly IEventStoreDBRepository<Models.Booking> _eventStoreDbRepository;
private readonly IFlightGrpcService _flightGrpcService; private readonly IFlightGrpcService _flightGrpcService;
private readonly ICurrentUserProvider _currentUserProvider;
private readonly IPassengerGrpcService _passengerGrpcService; private readonly IPassengerGrpcService _passengerGrpcService;
public CreateBookingCommandHandler(IEventStoreDBRepository<Models.Booking> eventStoreDbRepository, public CreateBookingCommandHandler(IEventStoreDBRepository<Models.Booking> eventStoreDbRepository,
IPassengerGrpcService passengerGrpcService, IPassengerGrpcService passengerGrpcService,
IFlightGrpcService flightGrpcService) IFlightGrpcService flightGrpcService,
ICurrentUserProvider currentUserProvider)
{ {
_eventStoreDbRepository = eventStoreDbRepository; _eventStoreDbRepository = eventStoreDbRepository;
_passengerGrpcService = passengerGrpcService; _passengerGrpcService = passengerGrpcService;
_flightGrpcService = flightGrpcService; _flightGrpcService = flightGrpcService;
_currentUserProvider = currentUserProvider;
} }
public async Task<ulong> Handle(CreateBookingCommand command, 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( var aggrigate = Models.Booking.Create(command.Id, new PassengerInfo(passenger.Name), new Trip(
flight.FlightNumber, flight.AircraftId, flight.DepartureAirportId, 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 await _flightGrpcService.ReserveSeat(new ReserveSeatRequestDto
{ {
FlightId = flight.Id, SeatNumber = emptySeat?.SeatNumber FlightId = flight.FlightId, SeatNumber = emptySeat?.SeatNumber
}); });
var result = await _eventStoreDbRepository.Add( var result = await _eventStoreDbRepository.Add(

View File

@ -1,29 +1,26 @@
using Booking.Booking.Events.Domain; using Booking.Booking.Events.Domain;
using Booking.Booking.Models.ValueObjects; using Booking.Booking.Models.ValueObjects;
using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.EventStoreDB.Events;
using BuildingBlocks.Utils;
using Microsoft.AspNetCore.Http;
namespace Booking.Booking.Models; namespace Booking.Booking.Models;
public class Booking : AggregateEventSourcing<long> public record Booking : AggregateEventSourcing<long>
{ {
public Booking()
{
}
public Trip Trip { get; private set; } public Trip Trip { get; private set; }
public PassengerInfo PassengerInfo { 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() var booking = new Booking { Id = id, Trip = trip, PassengerInfo = passengerInfo, IsDeleted = isDeleted };
{
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.AddDomainEvent(@event);
booking.Apply(@event); booking.Apply(@event);
@ -35,9 +32,9 @@ public class Booking : AggregateEventSourcing<long>
{ {
switch (@event) switch (@event)
{ {
case BookingCreatedDomainEvent reservationCreated: case BookingCreatedDomainEvent bookingCreated:
{ {
Apply(reservationCreated); Apply(bookingCreated);
return; return;
} }
} }

View File

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

View File

@ -1,19 +1,22 @@
using Booking.Booking.Events.Domain; using Booking.Booking.Events.Domain;
using Booking.Booking.Models.Reads;
using Booking.Data; using Booking.Data;
using BuildingBlocks.EventStoreDB.Events; using BuildingBlocks.EventStoreDB.Events;
using BuildingBlocks.EventStoreDB.Projections; using BuildingBlocks.EventStoreDB.Projections;
using BuildingBlocks.IdsGenerator;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore; using MongoDB.Driver;
using MongoDB.Driver.Linq;
namespace Booking; namespace Booking;
public class BookingProjection : IProjectionProcessor 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) public async Task ProcessEventAsync<T>(StreamEvent<T> streamEvent, CancellationToken cancellationToken = default)
@ -21,8 +24,8 @@ public class BookingProjection : IProjectionProcessor
{ {
switch (streamEvent.Data) switch (streamEvent.Data)
{ {
case BookingCreatedDomainEvent reservationCreatedDomainEvent: case BookingCreatedDomainEvent bookingCreatedDomainEvent:
await Apply(reservationCreatedDomainEvent, cancellationToken); await Apply(bookingCreatedDomainEvent, cancellationToken);
break; break;
} }
} }
@ -30,15 +33,21 @@ public class BookingProjection : IProjectionProcessor
private async Task Apply(BookingCreatedDomainEvent @event, CancellationToken cancellationToken = default) private async Task Apply(BookingCreatedDomainEvent @event, CancellationToken cancellationToken = default)
{ {
var reservation = var reservation =
await _bookingDbContext.Bookings.SingleOrDefaultAsync(x => x.Id == @event.Id, await _bookingReadDbContext.Booking.AsQueryable().SingleOrDefaultAsync(x => x.Id == @event.Id && !x.IsDeleted,
cancellationToken); cancellationToken);
if (reservation == null) 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 _bookingReadDbContext.Booking.InsertOneAsync(bookingReadModel, cancellationToken: cancellationToken);
await _bookingDbContext.SaveChangesAsync(cancellationToken);
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ public sealed class EventMapper : IEventMapper
}; };
} }
public InternalCommand MapToInternalCommand(IDomainEvent @event) public IInternalCommand MapToInternalCommand(IDomainEvent @event)
{ {
return @event switch return @event switch
{ {

View File

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

View File

@ -13,7 +13,6 @@ public static class MediatRExtensions
services.AddMediatR(typeof(BookingRoot).Assembly); services.AddMediatR(typeof(BookingRoot).Assembly);
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>));
return services; return services;
} }

View File

@ -1,8 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Booking.Booking.Models.Reads;
using Booking.Data; using Booking.Data;
using BuildingBlocks.Contracts.Grpc; using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.PersistMessageProcessor.Data;
using BuildingBlocks.TestBase; using BuildingBlocks.TestBase;
using FluentAssertions; using FluentAssertions;
using Integration.Test.Fakes; using Integration.Test.Fakes;
@ -14,9 +16,10 @@ using Xunit;
namespace Integration.Test.Booking.Features; 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) integrationTestFixture)
{ {
} }

View File

@ -9,9 +9,9 @@ using BuildingBlocks.Jwt;
using BuildingBlocks.Logging; using BuildingBlocks.Logging;
using BuildingBlocks.Mapster; using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit; using BuildingBlocks.MassTransit;
using BuildingBlocks.MessageProcessor;
using BuildingBlocks.Mongo; using BuildingBlocks.Mongo;
using BuildingBlocks.OpenTelemetry; using BuildingBlocks.OpenTelemetry;
using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.Swagger; using BuildingBlocks.Swagger;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
@ -37,10 +37,10 @@ builder.Services.AddCustomDbContext<FlightDbContext>(configuration);
builder.Services.AddScoped<IDataSeeder, FlightDataSeeder>(); builder.Services.AddScoped<IDataSeeder, FlightDataSeeder>();
builder.Services.AddMongoDbContext<FlightReadDbContext>(configuration); builder.Services.AddMongoDbContext<FlightReadDbContext>(configuration);
builder.Services.AddPersistMessage(configuration); builder.Services.AddPersistMessage(configuration);
builder.AddCustomSerilog(); builder.AddCustomSerilog();
builder.Services.AddCore();
builder.Services.AddJwt(); builder.Services.AddJwt();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddCustomSwagger(configuration, typeof(FlightRoot).Assembly); builder.Services.AddCustomSwagger(configuration, typeof(FlightRoot).Assembly);
@ -50,7 +50,6 @@ builder.Services.AddValidatorsFromAssembly(typeof(FlightRoot).Assembly);
builder.Services.AddCustomProblemDetails(); builder.Services.AddCustomProblemDetails();
builder.Services.AddCustomMapster(typeof(FlightRoot).Assembly); builder.Services.AddCustomMapster(typeof(FlightRoot).Assembly);
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<IEventMapper, EventMapper>();
builder.Services.AddCustomMassTransit(typeof(FlightRoot).Assembly, env); builder.Services.AddCustomMassTransit(typeof(FlightRoot).Assembly, env);
builder.Services.AddCustomOpenTelemetry(); builder.Services.AddCustomOpenTelemetry();
builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddRouting(options => options.LowercaseUrls = true);

View File

@ -26,7 +26,8 @@
}, },
"PersistMessageOptions": { "PersistMessageOptions": {
"Interval": 30, "Interval": 30,
"Enabled": true "Enabled": true,
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageDB;Trusted_Connection=True;MultipleActiveResultSets=true"
}, },
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@ -18,6 +18,7 @@
}, },
"PersistMessageOptions": { "PersistMessageOptions": {
"Interval": 1, "Interval": 1,
"Enabled": true "Enabled": true,
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageTestDB;Trusted_Connection=True;MultipleActiveResultSets=true"
} }
} }

View File

@ -1,4 +1,5 @@
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using Flight.Aircrafts.Dtos; using Flight.Aircrafts.Dtos;
using MediatR; using MediatR;

View File

@ -4,7 +4,7 @@ using Flight.Aircrafts.Events;
namespace Flight.Aircrafts.Models; namespace Flight.Aircrafts.Models;
public class Aircraft : Aggregate<long> public record Aircraft : Aggregate<long>
{ {
public Aircraft() public Aircraft()
{ {

View File

@ -1,4 +1,5 @@
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using Flight.Airports.Dtos; using Flight.Airports.Dtos;
using MediatR; using MediatR;

View File

@ -13,7 +13,6 @@ public class CreateAirportMongoCommand : InternalCommand
IsDeleted = isDeleted; IsDeleted = isDeleted;
} }
public long Id { get; }
public string Name { get; } public string Name { get; }
public string Address { get; } public string Address { get; }
public string Code { get; } public string Code { get; }

View File

@ -4,7 +4,7 @@ using Flight.Airports.Events;
namespace Flight.Airports.Models; namespace Flight.Airports.Models;
public class Airport : Aggregate<long> public record Airport : Aggregate<long>
{ {
public Airport() public Airport()
{ {

View File

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

View File

@ -1,5 +1,6 @@
using System.Reflection; using System.Reflection;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
using BuildingBlocks.Utils;
using Flight.Aircrafts.Models; using Flight.Aircrafts.Models;
using Flight.Airports.Models; using Flight.Airports.Models;
using Flight.Seats.Models; using Flight.Seats.Models;
@ -11,8 +12,8 @@ namespace Flight.Data;
public sealed class FlightDbContext : AppDbContextBase public sealed class FlightDbContext : AppDbContextBase
{ {
public const string DefaultSchema = "dbo"; public const string DefaultSchema = "dbo";
public FlightDbContext(DbContextOptions<FlightDbContext> options, IHttpContextAccessor httpContextAccessor) : base( public FlightDbContext(DbContextOptions<FlightDbContext> options, ICurrentUserProvider currentUserProvider) : base(
options, httpContextAccessor) options, currentUserProvider)
{ {
} }
@ -24,7 +25,7 @@ public sealed class FlightDbContext : AppDbContextBase
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
builder.FilterSoftDeletedProperties(); builder.FilterSoftDeletedProperties();
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); builder.ApplyConfigurationsFromAssembly(typeof(FlightRoot).Assembly);
base.OnModelCreating(builder); base.OnModelCreating(builder);
} }
} }

View File

@ -1,6 +1,5 @@
using BuildingBlocks.Mongo; using BuildingBlocks.Mongo;
using Flight.Aircrafts.Models.Reads; using Flight.Aircrafts.Models.Reads;
using Flight.Airports.Models;
using Flight.Airports.Models.Reads; using Flight.Airports.Models.Reads;
using Flight.Flights.Models.Reads; using Flight.Flights.Models.Reads;
using Flight.Seats.Models.Reads; using Flight.Seats.Models.Reads;

View File

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

View File

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

View File

@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Flight.Data.Migrations namespace Flight.Data.Migrations
{ {
[DbContext(typeof(FlightDbContext))] [DbContext(typeof(FlightDbContext))]
[Migration("20220511215248_Init")] [Migration("20220728175834_Init")]
partial class Init partial class Init
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)

View File

@ -25,8 +25,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true), CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true), LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", 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 => constraints: table =>
{ {
@ -46,8 +46,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true), CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true), LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", 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 => constraints: table =>
{ {
@ -74,8 +74,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true), CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true), LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", 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 => constraints: table =>
{ {
@ -110,8 +110,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true), CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true), LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", 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 => constraints: table =>
{ {

View File

@ -22,41 +22,6 @@ namespace Flight.Data.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); 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 => modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")

View File

@ -33,7 +33,7 @@ public sealed class EventMapper : IEventMapper
}; };
} }
public InternalCommand MapToInternalCommand(IDomainEvent @event) public IInternalCommand MapToInternalCommand(IDomainEvent @event)
{ {
return @event switch return @event switch
{ {

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

View File

@ -1,5 +1,6 @@
using System; using System;
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using Flight.Flights.Dtos; using Flight.Flights.Dtos;
using Flight.Flights.Models; using Flight.Flights.Models;

View File

@ -2,11 +2,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.MessageProcessor;
using Flight.Data; using Flight.Data;
using Flight.Flights.Dtos; using Flight.Flights.Dtos;
using Flight.Flights.Exceptions; using Flight.Flights.Exceptions;
using Flight.Flights.Features.CreateFlight.Reads;
using MapsterMapper; using MapsterMapper;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View File

@ -6,23 +6,23 @@ namespace Flight.Flights.Features.CreateFlight.Reads;
public class CreateFlightMongoCommand : InternalCommand public class CreateFlightMongoCommand : InternalCommand
{ {
public CreateFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate, public CreateFlightMongoCommand(long id, string flightNumber, long aircraftId, DateTime departureDate,
long DepartureAirportId, long departureAirportId,
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, DateTime arriveDate, long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
decimal Price, bool IsDeleted) decimal price, bool isDeleted)
{ {
this.Id = Id; Id = id;
this.FlightNumber = FlightNumber; FlightNumber = flightNumber;
this.AircraftId = AircraftId; AircraftId = aircraftId;
this.DepartureDate = DepartureDate; DepartureDate = departureDate;
this.DepartureAirportId = DepartureAirportId; DepartureAirportId = departureAirportId;
this.ArriveDate = ArriveDate; ArriveDate = arriveDate;
this.ArriveAirportId = ArriveAirportId; ArriveAirportId = arriveAirportId;
this.DurationMinutes = DurationMinutes; DurationMinutes = durationMinutes;
this.FlightDate = FlightDate; FlightDate = flightDate;
this.Status = Status; Status = status;
this.Price = Price; Price = price;
this.IsDeleted = IsDeleted; IsDeleted = isDeleted;
} }
public string FlightNumber { get; } public string FlightNumber { get; }

View File

@ -32,7 +32,7 @@ public class CreateFlightMongoCommandHandler : ICommandHandler<CreateFlightMongo
var flightReadModel = _mapper.Map<FlightReadModel>(command); var flightReadModel = _mapper.Map<FlightReadModel>(command);
var flight = await _flightReadDbContext.Flight.AsQueryable() var flight = await _flightReadDbContext.Flight.AsQueryable()
.FirstOrDefaultAsync(x => x.Id == flightReadModel.Id, cancellationToken); .FirstOrDefaultAsync(x => x.Id == flightReadModel.Id && !x.IsDeleted, cancellationToken);
if (flight is not null) if (flight is not null)
throw new FlightAlreadyExistException(); throw new FlightAlreadyExistException();

View File

@ -1,4 +1,5 @@
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using Flight.Flights.Dtos; using Flight.Flights.Dtos;
namespace Flight.Flights.Features.DeleteFlight; namespace Flight.Flights.Features.DeleteFlight;

View File

@ -6,23 +6,23 @@ namespace Flight.Flights.Features.DeleteFlight.Reads;
public class DeleteFlightMongoCommand : InternalCommand public class DeleteFlightMongoCommand : InternalCommand
{ {
public DeleteFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate, public DeleteFlightMongoCommand(long id, string flightNumber, long aircraftId, DateTime departureDate,
long DepartureAirportId, long departureAirportId,
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, DateTime arriveDate, long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
decimal Price, bool IsDeleted) decimal price, bool isDeleted)
{ {
this.Id = Id; Id = id;
this.FlightNumber = FlightNumber; FlightNumber = flightNumber;
this.AircraftId = AircraftId; AircraftId = aircraftId;
this.DepartureDate = DepartureDate; DepartureDate = departureDate;
this.DepartureAirportId = DepartureAirportId; DepartureAirportId = departureAirportId;
this.ArriveDate = ArriveDate; ArriveDate = arriveDate;
this.ArriveAirportId = ArriveAirportId; ArriveAirportId = arriveAirportId;
this.DurationMinutes = DurationMinutes; DurationMinutes = durationMinutes;
this.FlightDate = FlightDate; FlightDate = flightDate;
this.Status = Status; Status = status;
this.Price = Price; Price = price;
this.IsDeleted = IsDeleted; IsDeleted = isDeleted;
} }
public string FlightNumber { get; } public string FlightNumber { get; }

View File

@ -32,7 +32,7 @@ public class DeleteFlightMongoCommandHandler : ICommandHandler<DeleteFlightMongo
var flightReadModel = _mapper.Map<FlightReadModel>(command); var flightReadModel = _mapper.Map<FlightReadModel>(command);
var flight = await _flightReadDbContext.Flight.AsQueryable() var flight = await _flightReadDbContext.Flight.AsQueryable()
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId, cancellationToken); .FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken);
if (flight is null) if (flight is null)
throw new FlightNotFountException(); throw new FlightNotFountException();

View File

@ -27,7 +27,8 @@ public class GetFlightByIdQueryHandler : IQueryHandler<GetFlightByIdQuery, Fligh
Guard.Against.Null(query, nameof(query)); Guard.Against.Null(query, nameof(query));
var flight = 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) if (flight is null)
throw new FlightNotFountException(); throw new FlightNotFountException();

View File

@ -32,7 +32,7 @@ public class UpdateFlightMongoCommandHandler : ICommandHandler<UpdateFlightMongo
var flightReadModel = _mapper.Map<FlightReadModel>(command); var flightReadModel = _mapper.Map<FlightReadModel>(command);
var flight = await _flightReadDbContext.Flight.AsQueryable() var flight = await _flightReadDbContext.Flight.AsQueryable()
.FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId, cancellationToken); .FirstOrDefaultAsync(x => x.FlightId == flightReadModel.FlightId && !x.IsDeleted, cancellationToken);
if (flight is null) if (flight is null)
throw new FlightNotFountException(); throw new FlightNotFountException();

View File

@ -1,6 +1,7 @@
using System; using System;
using BuildingBlocks.Caching; using BuildingBlocks.Caching;
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using Flight.Flights.Dtos; using Flight.Flights.Dtos;
using Flight.Flights.Models; using Flight.Flights.Models;
using MediatR; using MediatR;

View File

@ -4,7 +4,7 @@ using Flight.Flights.Events.Domain;
namespace Flight.Flights.Models; namespace Flight.Flights.Models;
public class Flight : Aggregate<long> public record Flight : Aggregate<long>
{ {
public string FlightNumber { get; private set; } public string FlightNumber { get; private set; }
public long AircraftId { get; private set; } public long AircraftId { get; private set; }

View File

@ -6,17 +6,17 @@ namespace Flight.Flights.Models.Reads;
public class FlightReadModel public class FlightReadModel
{ {
public long Id { get; set; } public long Id { get; init; }
public long FlightId { get; set; } public long FlightId { get; init; }
public string FlightNumber { get; set; } public string FlightNumber { get; init; }
public long AircraftId { get; set; } public long AircraftId { get; init; }
public DateTime DepartureDate { get; set; } public DateTime DepartureDate { get; init; }
public long DepartureAirportId { get; set; } public long DepartureAirportId { get; init; }
public DateTime ArriveDate { get; set; } public DateTime ArriveDate { get; init; }
public long ArriveAirportId { get; set; } public long ArriveAirportId { get; init; }
public decimal DurationMinutes { get; set; } public decimal DurationMinutes { get; init; }
public DateTime FlightDate { get; set; } public DateTime FlightDate { get; init; }
public FlightStatus Status { get; set; } public FlightStatus Status { get; init; }
public decimal Price { get; set; } public decimal Price { get; init; }
public bool IsDeleted { get; set; } public bool IsDeleted { get; init; }
} }

View File

@ -1,4 +1,5 @@
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.IdsGenerator; using BuildingBlocks.IdsGenerator;
using Flight.Seats.Dtos; using Flight.Seats.Dtos;
using Flight.Seats.Models; using Flight.Seats.Models;

View File

@ -29,7 +29,7 @@ public class GetAvailableSeatsQueryHandler : IRequestHandler<GetAvailableSeatsQu
Guard.Against.Null(query, nameof(query)); Guard.Against.Null(query, nameof(query));
var seats = (await _flightReadDbContext.Seat.AsQueryable().ToListAsync(cancellationToken)) 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()) if (!seats.Any())
throw new AllSeatsFullException(); throw new AllSeatsFullException();

View File

@ -16,7 +16,6 @@ public class ReserveSeatMongoCommand : InternalCommand
IsDeleted = isDeleted; IsDeleted = isDeleted;
} }
public long Id { get; }
public string SeatNumber { get; } public string SeatNumber { get; }
public SeatType Type { get; } public SeatType Type { get; }
public SeatClass Class { get; } public SeatClass Class { get; }

View File

@ -1,4 +1,5 @@
using BuildingBlocks.Core.CQRS; using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using Flight.Seats.Dtos; using Flight.Seats.Dtos;
namespace Flight.Seats.Features.ReserveSeat; namespace Flight.Seats.Features.ReserveSeat;

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