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

View File

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

View File

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

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.Utils;
using ICommand = BuildingBlocks.Core.CQRS.ICommand;
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.IdsGenerator;
namespace BuildingBlocks.Core.Event;
public class InternalCommand : IInternalCommand, ICommand
{
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
public DateTime OccurredOn => DateTime.Now;
public string Type { get => TypeProvider.GetTypeName(GetType()); }
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

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

View File

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

View File

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

View File

@ -2,11 +2,11 @@
namespace BuildingBlocks.Core.Model
{
public abstract class Aggregate : Aggregate<long>
public abstract record Aggregate : Aggregate<long>
{
}
public abstract class Aggregate<TId> : Entity, IAggregate<TId>
public abstract record Aggregate<TId> : Audit, IAggregate<TId>
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

View File

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

View File

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

View File

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

View File

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

View File

@ -57,17 +57,15 @@ public class EfTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TRe
nameof(EfTxBehavior<TRequest, TResponse>),
typeof(TRequest).FullName);
await _dbContextBase.CommitTransactionAsync(cancellationToken);
var domainEvents = _dbContextBase.GetDomainEvents();
var eventType = typeof(TRequest).IsAssignableTo(typeof(IInternalCommand)) ? EventType.InternalCommand : EventType.DomainEvent;
await _eventDispatcher.SendAsync(domainEvents.ToArray(), typeof(TRequest), cancellationToken);
await _eventDispatcher.SendAsync(domainEvents.ToArray(), eventType, cancellationToken);
await _dbContextBase.CommitTransactionAsync(cancellationToken);
return response;
}
catch(System.Exception ex)
catch (System.Exception ex)
{
await _dbContextBase.RollbackTransactionAsync(cancellationToken);
throw;

View File

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

View File

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

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

View File

@ -3,7 +3,7 @@ using BuildingBlocks.Core.Model;
namespace BuildingBlocks.EventStoreDB.Events
{
public abstract class AggregateEventSourcing<TId> : Entity, IAggregateEventSourcing<TId>
public abstract record AggregateEventSourcing<TId> : Audit, IAggregateEventSourcing<TId>
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
@ -13,9 +13,9 @@ namespace BuildingBlocks.EventStoreDB.Events
_domainEvents.Add(domainEvent);
}
public IEvent[] ClearDomainEvents()
public IDomainEvent[] ClearDomainEvents()
{
IEvent[] dequeuedEvents = _domainEvents.ToArray();
var dequeuedEvents = _domainEvents.ToArray();
_domainEvents.Clear();

View File

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

View File

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

View File

@ -28,6 +28,6 @@ public static class RepositoryExtensions
action(entity);
return await repository.Update(entity, expectedVersion, cancellationToken);
return await repository.Update(entity, expectedVersion,cancellationToken);
}
}

View File

@ -1,5 +1,5 @@
using BuildingBlocks.Core.Event;
using BuildingBlocks.MessageProcessor;
using BuildingBlocks.PersistMessageProcessor;
using 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;
namespace Booking.Data.Configurations;
namespace BuildingBlocks.PersistMessageProcessor.Data.Configurations;
public class PersistMessageConfiguration : IEntityTypeConfiguration<PersistMessage>
{
public void Configure(EntityTypeBuilder<PersistMessage> builder)
{
builder.ToTable("PersistMessages", BookingDbContext.DefaultSchema);
builder.ToTable("PersistMessage", PersistMessageDbContext.DefaultSchema);
builder.HasKey(x => x.Id);

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

View File

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

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.Mongo;
using BuildingBlocks.PersistMessageProcessor.Data;
using BuildingBlocks.Web;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace BuildingBlocks.MessageProcessor;
namespace BuildingBlocks.PersistMessageProcessor;
public static class Extensions
{
@ -13,8 +15,15 @@ public static class Extensions
.Bind(configuration.GetSection(nameof(PersistMessageOptions)))
.ValidateDataAnnotations();
var persistMessageOptions = services.GetOptions<PersistMessageOptions>("PersistMessageOptions");
services.AddDbContext<PersistMessageDbContext>(options =>
options.UseSqlServer(persistMessageOptions.ConnectionString,
x => x.MigrationsAssembly(typeof(PersistMessageDbContext).Assembly.GetName().Name)));
services.AddScoped<IPersistMessageDbContext>(provider => provider.GetService<PersistMessageDbContext>());
services.AddScoped<IPersistMessageProcessor, PersistMessageProcessor>();
services.AddScoped<IEventDispatcher, EventDispatcher>();
services.AddHostedService<PersistMessageBackgroundService>();
return services;

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
namespace BuildingBlocks.MessageProcessor;
using System.Reflection;
namespace BuildingBlocks.PersistMessageProcessor;
public class PersistMessage
{
public PersistMessage(Guid id, string dataType, string data, MessageDeliveryType deliveryType)
public PersistMessage(long id, string dataType, string data, MessageDeliveryType deliveryType)
{
Id = id;
DataType = dataType;
@ -13,7 +15,7 @@ public class PersistMessage
RetryCount = 0;
}
public Guid Id { get; private set; }
public long Id { get; private set; }
public string DataType { get; private set; }
public string Data { get; private set; }
public DateTime Created { get; private set; }

View File

@ -3,7 +3,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BuildingBlocks.MessageProcessor;
namespace BuildingBlocks.PersistMessageProcessor;
public class PersistMessageBackgroundService : BackgroundService
{
@ -43,17 +43,25 @@ public class PersistMessageBackgroundService : BackgroundService
{
while (!stoppingToken.IsCancellationRequested)
{
await using (var scope = _serviceProvider.CreateAsyncScope())
try
{
var service = scope.ServiceProvider.GetRequiredService<IPersistMessageProcessor>();
await service.ProcessAllAsync(stoppingToken);
await using (var scope = _serviceProvider.CreateAsyncScope())
{
var service = scope.ServiceProvider.GetRequiredService<IPersistMessageProcessor>();
await service.ProcessAllAsync(stoppingToken);
}
var delay = _options.Interval is { }
? TimeSpan.FromSeconds((int)_options.Interval)
: TimeSpan.FromSeconds(30);
await Task.Delay(delay, stoppingToken);
}
catch (System.Exception e)
{
Console.WriteLine(e);
throw;
}
var delay = _options.Interval is { }
? TimeSpan.FromSeconds((int)_options.Interval)
: TimeSpan.FromSeconds(30);
await Task.Delay(delay, stoppingToken);
}
}
}

View File

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

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

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.Runtime.CompilerServices;
using Ardalis.GuardClauses;
namespace BuildingBlocks.Utils;
public static class TypeProvider
{
private static readonly ConcurrentDictionary<Type, string> TypeNameMap = new();
private static readonly ConcurrentDictionary<string, Type> TypeMap = new();
private static bool IsRecord(this Type objectType)
{
return objectType.GetMethod("<Clone>$") != null ||
@ -34,76 +30,10 @@ public static class TypeProvider
public static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string typeName)
{
return AppDomain.CurrentDomain.GetAssemblies()
var result = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName))
.FirstOrDefault();
return result;
}
/// <summary>
/// Gets the type name from a generic Type class.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>GetTypeName</returns>
public static string GetTypeName<T>() => ToName(typeof(T));
/// <summary>
/// Gets the type name from a Type class.
/// </summary>
/// <param name="type"></param>
/// <returns>TypeName</returns>
public static string GetTypeName(Type type) => ToName(type);
/// <summary>
/// Gets the type name from a instance object.
/// </summary>
/// <param name="o"></param>
/// <returns>TypeName</returns>
public static string GetTypeNameByObject(object o) => ToName(o.GetType());
/// <summary>
/// Gets the type class from a type name.
/// </summary>
/// <param name="typeName"></param>
/// <returns>Type</returns>
public static Type GetType(string typeName) => ToType(typeName);
public static void AddType<T>(string name) => AddType(typeof(T), name);
private static void AddType(Type type, string name)
{
ToName(type);
ToType(name);
}
public static bool IsTypeRegistered<T>() => TypeNameMap.ContainsKey(typeof(T));
private static string ToName(Type type)
{
Guard.Against.Null(type, nameof(type));
return TypeNameMap.GetOrAdd(type, _ =>
{
var eventTypeName = type.FullName!.Replace(".", "_", StringComparison.Ordinal);
TypeMap.GetOrAdd(eventTypeName, type);
return eventTypeName;
});
}
private static Type ToType(string typeName) => TypeMap.GetOrAdd(typeName, _ =>
{
Guard.Against.NullOrEmpty(typeName, nameof(typeName));
return TypeMap.GetOrAdd(typeName, _ =>
{
var type = GetFirstMatchingTypeFromCurrentDomainAssembly(
typeName.Replace("_", ".", StringComparison.Ordinal))!;
if (type == null)
throw new System.Exception($"Type map for '{typeName}' wasn't found!");
return type;
});
});
}

View File

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

View File

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

View File

@ -18,6 +18,11 @@
},
"PersistMessageOptions": {
"Interval": 1,
"Enabled": true
"Enabled": true,
"ConnectionString": "Server=.\\sqlexpress;Database=PersistMessageTestDB;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"MongoOptions": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "booking-db-test"
}
}

View File

@ -11,10 +11,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations" />
</ItemGroup>
<ItemGroup>

View File

@ -1,6 +1,7 @@
using Booking.Booking.Models.ValueObjects;
using BuildingBlocks.Core.Event;
using BuildingBlocks.Core.Model;
namespace Booking.Booking.Events.Domain;
public record BookingCreatedDomainEvent(long Id, PassengerInfo PassengerInfo, Trip Trip, bool IsDeleted) : IDomainEvent;
public record BookingCreatedDomainEvent(long Id, PassengerInfo PassengerInfo, Trip Trip) : Audit, IDomainEvent;

View File

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

View File

@ -1,9 +1,10 @@
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.IdsGenerator;
namespace Booking.Booking.Features.CreateBooking;
public record CreateBookingCommand(long PassengerId, long FlightId, string Description) : ICommand<ulong>
public record CreateBookingCommand(long PassengerId, long FlightId, string Description) : ICommand<ulong>, IInternalCommand
{
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

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

View File

@ -1,29 +1,26 @@
using Booking.Booking.Events.Domain;
using Booking.Booking.Models.ValueObjects;
using BuildingBlocks.EventStoreDB.Events;
using BuildingBlocks.Utils;
using Microsoft.AspNetCore.Http;
namespace Booking.Booking.Models;
public class Booking : AggregateEventSourcing<long>
public record Booking : AggregateEventSourcing<long>
{
public Booking()
{
}
public Trip Trip { get; private set; }
public PassengerInfo PassengerInfo { get; private set; }
public static Booking Create(long id, PassengerInfo passengerInfo, Trip trip, bool isDeleted = false)
public static Booking Create(long id, PassengerInfo passengerInfo, Trip trip, bool isDeleted = false, long? userId = null)
{
var booking = new Booking()
{
Id = id,
Trip = trip,
PassengerInfo = passengerInfo,
IsDeleted = isDeleted
};
var booking = new Booking { Id = id, Trip = trip, PassengerInfo = passengerInfo, IsDeleted = isDeleted };
var @event = new BookingCreatedDomainEvent(booking.Id, booking.PassengerInfo, booking.Trip, booking.IsDeleted);
var @event = new BookingCreatedDomainEvent(booking.Id, booking.PassengerInfo, booking.Trip)
{
IsDeleted = booking.IsDeleted,
CreatedAt = DateTime.Now,
CreatedBy = userId
};
booking.AddDomainEvent(@event);
booking.Apply(@event);
@ -35,9 +32,9 @@ public class Booking : AggregateEventSourcing<long>
{
switch (@event)
{
case BookingCreatedDomainEvent reservationCreated:
case BookingCreatedDomainEvent bookingCreated:
{
Apply(reservationCreated);
Apply(bookingCreated);
return;
}
}

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

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
{

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.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>));
return services;
}

View File

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Booking.Booking.Models.Reads;
using Booking.Data;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.PersistMessageProcessor.Data;
using BuildingBlocks.TestBase;
using FluentAssertions;
using Integration.Test.Fakes;
@ -14,9 +16,10 @@ using Xunit;
namespace Integration.Test.Booking.Features;
public class CreateBookingTests : IntegrationTestBase<Program, BookingDbContext>
public class CreateBookingTests : IntegrationTestBase<Program, PersistMessageDbContext, BookingReadDbContext>
{
public CreateBookingTests(IntegrationTestFixture<Program, BookingDbContext> integrationTestFixture) : base(
public CreateBookingTests(
IntegrationTestFixture<Program, PersistMessageDbContext, BookingReadDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
}

View File

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

View File

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

View File

@ -18,6 +18,7 @@
},
"PersistMessageOptions": {
"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.Event;
using BuildingBlocks.IdsGenerator;
using Flight.Aircrafts.Dtos;
using MediatR;

View File

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

View File

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

View File

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

View File

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

View File

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

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
{
[DbContext(typeof(FlightDbContext))]
[Migration("20220511215248_Init")]
[Migration("20220728175834_Init")]
partial class Init
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)

View File

@ -25,8 +25,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
Version = table.Column<long>(type: "bigint", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
@ -46,8 +46,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
Version = table.Column<long>(type: "bigint", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
@ -74,8 +74,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
Version = table.Column<long>(type: "bigint", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
@ -110,8 +110,8 @@ namespace Flight.Data.Migrations
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<long>(type: "bigint", nullable: true),
Version = table.Column<long>(type: "bigint", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{

View File

@ -22,41 +22,6 @@ namespace Flight.Data.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
modelBuilder.Entity("BuildingBlocks.MessageProcessor.PersistMessage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("Data")
.HasColumnType("nvarchar(max)");
b.Property<string>("DataType")
.HasColumnType("nvarchar(max)");
b.Property<string>("DeliveryType")
.IsRequired()
.HasMaxLength(50)
.IsUnicode(false)
.HasColumnType("varchar(50)");
b.Property<string>("MessageStatus")
.IsRequired()
.HasMaxLength(50)
.IsUnicode(false)
.HasColumnType("varchar(50)");
b.Property<int>("RetryCount")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("PersistMessages", "dbo");
});
modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b =>
{
b.Property<long>("Id")

View File

@ -33,7 +33,7 @@ public sealed class EventMapper : IEventMapper
};
}
public InternalCommand MapToInternalCommand(IDomainEvent @event)
public IInternalCommand MapToInternalCommand(IDomainEvent @event)
{
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 BuildingBlocks.Core.CQRS;
using BuildingBlocks.Core.Event;
using BuildingBlocks.IdsGenerator;
using Flight.Flights.Dtos;
using Flight.Flights.Models;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,8 @@ public class GetFlightByIdQueryHandler : IQueryHandler<GetFlightByIdQuery, Fligh
Guard.Against.Null(query, nameof(query));
var flight =
await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync(x => x.FlightId == query.Id, cancellationToken);
await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync(x => x.FlightId == query.Id &&
!x.IsDeleted, cancellationToken);
if (flight is null)
throw new FlightNotFountException();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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