Merge pull request #251 from meysamhadeli/refactor/refactor-core-domain

refactor: Refactor core domain in building-blocks
This commit is contained in:
Meysam Hadeli 2023-05-08 17:17:52 +03:30 committed by GitHub
commit c93bd2902a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 257 additions and 126 deletions

View File

@ -16,11 +16,24 @@ services:
command:
- "postgres"
- "-c"
- "wal_level=logical"
- "wal_level=logical"
networks:
- booking
#######################################################
# SqlServer
#######################################################
# sql:
# container_name: sql
# image: mcr.microsoft.com/mssql/server
# ports:
# - "1433:1433"
# environment:
# SA_PASSWORD: "Password@1234"
# ACCEPT_EULA: "Y"
#######################################################
# Rabbitmq
#######################################################

View File

@ -30,10 +30,24 @@ services:
command:
- "postgres"
- "-c"
- "wal_level=logical"
- "wal_level=logical"
networks:
- booking
#######################################################
# SqlServer
#######################################################
# sql:
# container_name: sql
# image: mcr.microsoft.com/mssql/server
# ports:
# - "1433:1433"
# environment:
# SA_PASSWORD: "Password@1234"
# ACCEPT_EULA: "Y"
#######################################################
# Jaeger
#######################################################
@ -141,8 +155,6 @@ services:
ports:
- 6379:6379
networks:
booking:

View File

@ -77,6 +77,7 @@
<PackageReference Include="Serilog.Sinks.Seq" Version="5.2.2" />
<PackageReference Include="Serilog.Sinks.SpectreConsole" Version="0.3.3" />
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.3" />
<PackageReference Include="Sieve" Version="2.5.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0" />
@ -99,6 +100,8 @@
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="6.2.2" />
<PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="6.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.2" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Linq.Async.Queryable" Version="6.0.1" />
<PackageReference Include="Testcontainers" Version="3.0.0" />
<PackageReference Include="Testcontainers.EventStoreDb" Version="3.0.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="3.0.0" />
@ -141,10 +144,9 @@
<PackageReference Include="Google.Protobuf" Version="3.21.12" />
<PackageReference Include="Grpc.Net.Client" Version="2.51.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.51.0" />
<PackageReference Update="AsyncFixer" Version="1.6.0" />
<PackageReference Update="Meziantou.Analyzer" Version="1.0.758" />
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
<PackageReference Update="Microsoft.VisualStudio.Threading.Analyzers" Version="17.4.27" />
<PackageReference Update="AsyncFixer" Version="1.6.0" />
<PackageReference Update="Roslynator.Analyzers" Version="4.2.0" />
<PackageReference Update="Roslynator.CodeAnalysis.Analyzers" Version="4.2.0" />
<PackageReference Update="Roslynator.Formatting.Analyzers" Version="4.2.0" />
@ -154,6 +156,7 @@
<ItemGroup>
<Folder Include="Contracts" />
<Folder Include="Core\Pagination" />
<Folder Include="EventStoreDB\BackgroundWorkers" />
<Folder Include="PersistMessageProcessor\Data\Configurations" />
<Folder Include="PersistMessageProcessor\Data\Migrations" />

View File

@ -2,9 +2,7 @@
namespace BuildingBlocks.Core.Model;
public abstract record Aggregate : Aggregate<long>;
public abstract record Aggregate<TId> : Audit, IAggregate<TId>
public abstract record Aggregate<TId> : Entity<TId>, IAggregate<TId>
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
@ -22,8 +20,4 @@ public abstract record Aggregate<TId> : Audit, IAggregate<TId>
return dequeuedEvents;
}
public long Version { get; set; }
public required TId Id { get; set; }
}

View File

@ -1,10 +1,12 @@
namespace BuildingBlocks.Core.Model;
namespace BuildingBlocks.Core.Model;
public interface IAudit
public abstract record Entity<T> : IEntity<T>
{
public T Id { get; set; }
public DateTime? CreatedAt { get; set; }
public long? CreatedBy { get; set; }
public DateTime? LastModified { get; set; }
public long? LastModifiedBy { get; set; }
public bool IsDeleted { get; set; }
public long Version { get; set; }
}

View File

@ -2,18 +2,12 @@
namespace BuildingBlocks.Core.Model;
public interface IAggregate : IAudit, IVersion
public interface IAggregate<T> : IAggregate, IEntity<T>
{
}
public interface IAggregate : IEntity
{
IReadOnlyList<IDomainEvent> DomainEvents { get; }
IEvent[] ClearDomainEvents();
}
public interface IAggregate<out T> : IAggregate
{
T Id { get; }
}
public interface IVersion
{
long Version { get; set; }
}

View File

@ -1,6 +1,11 @@
namespace BuildingBlocks.Core.Model;
namespace BuildingBlocks.Core.Model;
public abstract record Audit : IAudit
public interface IEntity<T> : IEntity
{
public T Id { get; set; }
}
public interface IEntity: IVersion
{
public DateTime? CreatedAt { get; set; }
public long? CreatedBy { get; set; }

View File

@ -0,0 +1,6 @@
namespace BuildingBlocks.Core.Model;
public interface IVersion
{
long Version { get; set; }
}

View File

@ -0,0 +1,36 @@
namespace BuildingBlocks.Core.Pagination;
using Sieve.Models;
using Sieve.Services;
public static class Extensions
{
public static async Task<IPageList<TEntity>> ApplyPagingAsync<TEntity>(
this IQueryable<TEntity> queryable,
IPageRequest pageRequest,
ISieveProcessor sieveProcessor,
CancellationToken cancellationToken = default
)
where TEntity : class
{
var sieveModel = new SieveModel
{
PageSize = pageRequest.PageSize,
Page = pageRequest.PageNumber,
Sorts = pageRequest.SortOrder,
Filters = pageRequest.Filters
};
// https://github.com/Biarity/Sieve/issues/34#issuecomment-403817573
var result = sieveProcessor.Apply(sieveModel, queryable, applyPagination: false);
var total = result.Count();
result = sieveProcessor.Apply(sieveModel, queryable, applyFiltering: false,
applySorting: false); // Only applies pagination
var items = await result
.ToAsyncEnumerable()
.ToListAsync(cancellationToken: cancellationToken);
return PageList<TEntity>.Create(items.AsReadOnly(), pageRequest.PageNumber, pageRequest.PageSize, total);
}
}

View File

@ -0,0 +1,16 @@
namespace BuildingBlocks.Core.Pagination;
public interface IPageList<T>
where T : class
{
int CurrentPageSize { get; }
int CurrentStartIndex { get; }
int CurrentEndIndex { get; }
int TotalPages { get; }
bool HasPrevious { get; }
bool HasNext { get; }
IReadOnlyList<T> Items { get; init; }
int TotalCount { get; init; }
int PageNumber { get; init; }
int PageSize { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace BuildingBlocks.Core.Pagination;
using MediatR;
public interface IPageQuery<out TResponse> : IPageRequest, IRequest<TResponse>
where TResponse : class { }

View File

@ -0,0 +1,9 @@
namespace BuildingBlocks.Core.Pagination;
public interface IPageRequest
{
int PageNumber { get; init; }
int PageSize { get; init; }
string? Filters { get; init; }
string? SortOrder { get; init; }
}

View File

@ -0,0 +1,19 @@
namespace BuildingBlocks.Core.Pagination;
public record PageList<T>(IReadOnlyList<T> Items, int PageNumber, int PageSize, int TotalCount) : IPageList<T>
where T : class
{
public int CurrentPageSize => Items.Count;
public int CurrentStartIndex => TotalCount == 0 ? 0 : ((PageNumber - 1) * PageSize) + 1;
public int CurrentEndIndex => TotalCount == 0 ? 0 : CurrentStartIndex + CurrentPageSize - 1;
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPrevious => PageNumber > 1;
public bool HasNext => PageNumber < TotalPages;
public static PageList<T> Empty => new(Enumerable.Empty<T>().ToList(), 0, 0, 0);
public static PageList<T> Create(IReadOnlyList<T> items, int pageNumber, int pageSize, int totalItems)
{
return new PageList<T>(items, pageNumber, pageSize, totalItems);
}
}

View File

@ -50,12 +50,16 @@ public class EfTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TRe
nameof(EfTxBehavior<TRequest, TResponse>),
typeof(TRequest).FullName);
var domainEvents = _dbContextBase.GetDomainEvents();
while (true)
{
var domainEvents = _dbContextBase.GetDomainEvents();
await _eventDispatcher.SendAsync(domainEvents.ToArray(), typeof(TRequest), cancellationToken);
if (domainEvents is null || !domainEvents.Any())
return response;
await _dbContextBase.ExecuteTransactionalAsync(cancellationToken);
await _dbContextBase.ExecuteTransactionalAsync(cancellationToken);
return response;
await _eventDispatcher.SendAsync(domainEvents.ToArray(), typeof(TRequest), cancellationToken);
}
}
}

View File

@ -66,7 +66,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(IAudit))))
.Where(m => m.ClrType.IsAssignableTo(typeof(IEntity))))
{
// modify expression to handle correct child type
var parameter = Expression.Parameter(mutableEntityType.ClrType);

View File

@ -3,7 +3,7 @@ using BuildingBlocks.Core.Model;
namespace BuildingBlocks.EventStoreDB.Events
{
public abstract record AggregateEventSourcing<TId> : Audit, IAggregateEventSourcing<TId>
public abstract record AggregateEventSourcing<TId> : Entity<TId>, IAggregateEventSourcing<TId>
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
@ -23,10 +23,6 @@ namespace BuildingBlocks.EventStoreDB.Events
}
public virtual void When(object @event) { }
public long Version { get; protected set; } = -1;
public TId Id { get; protected set; }
}
}

View File

@ -3,17 +3,15 @@ using BuildingBlocks.Core.Model;
namespace BuildingBlocks.EventStoreDB.Events
{
public interface IAggregateEventSourcing : IProjection, IAudit
using Microsoft.FSharp.Control;
public interface IAggregateEventSourcing : IProjection, IEntity
{
IReadOnlyList<IDomainEvent> DomainEvents { get; }
IDomainEvent[] ClearDomainEvents();
long Version { get; }
}
public interface IAggregateEventSourcing<out T> : IAggregateEventSourcing
public interface IAggregateEventSourcing<T> : IAggregateEventSourcing, IEntity<T>
{
T Id { get; }
}
}

View File

@ -398,11 +398,17 @@ public class TestWriteFixture<TEntryPoint, TWContext> : TestFixture<TEntryPoint>
});
}
public Task<T> FindAsync<T>(Guid id)
where T : class, IAudit
public Task<T> FindAsync<T, TKey>(TKey id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<T> FirstOrDefaultAsync<T>()
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FirstOrDefaultAsync());
}
}
public class TestReadFixture<TEntryPoint, TRContext> : TestFixture<TEntryPoint>

View File

@ -7,6 +7,7 @@ using BuildingBlocks.Core.Event;
using BuildingBlocks.Core.Model;
using BuildingBlocks.EventStoreDB.Repository;
using BuildingBlocks.Web;
using Elasticsearch.Net;
using Exceptions;
using Flight;
using FluentValidation;
@ -27,7 +28,7 @@ public record CreateBooking(Guid PassengerId, Guid FlightId, string Description)
public record CreateBookingResult(ulong Id);
public record BookingCreatedDomainEvent(Guid Id, PassengerInfo PassengerInfo, Trip Trip) : Audit, IDomainEvent;
public record BookingCreatedDomainEvent(Guid Id, PassengerInfo PassengerInfo, Trip Trip) : Entity<Guid>, IDomainEvent;
public record CreateBookingRequestDto(Guid PassengerId, Guid FlightId, string Description);

View File

@ -54,7 +54,7 @@ public class CreateFlightEndpoint : IMinimalEndpoint
return Results.CreatedAtRoute("GetFlightById", new { id = result.Id }, response);
})
.RequireAuthorization()
// .RequireAuthorization()
.WithName("CreateFlight")
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.Produces<CreateFlightResponseDto>(StatusCodes.Status201Created)
@ -68,7 +68,7 @@ public class CreateFlightEndpoint : IMinimalEndpoint
}
}
internal class CreateFlightValidator : AbstractValidator<CreateFlight>
public class CreateFlightValidator : AbstractValidator<CreateFlight>
{
public CreateFlightValidator()
{

View File

@ -1,3 +1,5 @@
namespace Flight.Identity.Consumers.RegisterNewUser.V1;
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Web;
@ -6,8 +8,6 @@ using MassTransit;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Flight.Identity.Consumers.RegisterNewUser.Consumes.V1;
public class RegisterNewUserConsumerHandler : IConsumer<UserCreated>
{
private readonly AppOptions _options;

View File

@ -10,8 +10,10 @@ using Xunit;
namespace Integration.Test.Flight.Features;
using System;
using global::Flight.Data.Seed;
using global::Flight.Flights.Features.DeletingFlight.V1;
using global::Flight.Flights.Models;
public class DeleteFlightTests : FlightIntegrationTestBase
{
@ -24,7 +26,7 @@ public class DeleteFlightTests : FlightIntegrationTestBase
public async Task should_delete_flight_from_db()
{
// Arrange
var flightEntity = await Fixture.FindAsync<global::Flight.Flights.Models.Flight>( InitialData.Flights.First().Id);
var flightEntity = await Fixture.FindAsync<Flight, Guid>( InitialData.Flights.First().Id);
var command = new DeleteFlight(flightEntity.Id);
// Act

View File

@ -9,9 +9,11 @@ using Xunit;
namespace Integration.Test.Flight.Features;
using System;
using System.Linq;
using global::Flight.Data.Seed;
using global::Flight.Flights.Features.UpdatingFlight.V1;
using global::Flight.Flights.Models;
public class UpdateFlightTests : FlightIntegrationTestBase
{
@ -24,7 +26,7 @@ public class UpdateFlightTests : FlightIntegrationTestBase
public async Task should_update_flight_to_db_and_publish_message_to_broker()
{
// Arrange
var flightEntity = await Fixture.FindAsync<global::Flight.Flights.Models.Flight>( InitialData.Flights.First().Id);
var flightEntity = await Fixture.FindAsync<Flight, Guid>( InitialData.Flights.First().Id);
var command = new FakeUpdateFlightCommand(flightEntity).Generate();
// Act

View File

@ -5,80 +5,88 @@ using Flight.Flights.Enums;
using Flight.Seats.Enums;
using Microsoft.EntityFrameworkCore;
namespace Unit.Test.Common
namespace Unit.Test.Common;
using MassTransit;
public static class DbContextFactory
{
using MassTransit;
private static readonly Guid _airportId1 = NewId.NextGuid();
private static readonly Guid _airportId2 = NewId.NextGuid();
private static readonly Guid _aircraft1 = NewId.NextGuid();
private static readonly Guid _aircraft2 = NewId.NextGuid();
private static readonly Guid _aircraft3 = NewId.NextGuid();
private static readonly Guid _flightId1 = NewId.NextGuid();
public static class DbContextFactory
public static FlightDbContext Create()
{
private static readonly Guid _airportId1 = NewId.NextGuid();
private static readonly Guid _airportId2 = NewId.NextGuid();
private static readonly Guid _aircraft1 = NewId.NextGuid();
private static readonly Guid _aircraft2 = NewId.NextGuid();
private static readonly Guid _aircraft3 = NewId.NextGuid();
private static readonly Guid _flightId1 = NewId.NextGuid();
var options = new DbContextOptionsBuilder<FlightDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
public static FlightDbContext Create()
var context = new FlightDbContext(options, currentUserProvider: null);
// Seed our data
FlightDataSeeder(context);
return context;
}
private static void FlightDataSeeder(FlightDbContext context)
{
var airports = new List<global::Flight.Airports.Models.Airport>
{
var options = new DbContextOptionsBuilder<FlightDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
global::Flight.Airports.Models.Airport.Create(_airportId1, "Lisbon International Airport", "LIS",
"12988"),
global::Flight.Airports.Models.Airport.Create(_airportId2, "Sao Paulo International Airport", "BRZ",
"11200")
};
var context = new FlightDbContext(options, currentUserProvider: null);
context.Airports.AddRange(airports);
// Seed our data
FlightDataSeeder(context);
return context;
}
private static void FlightDataSeeder(FlightDbContext context)
var aircrafts = new List<global::Flight.Aircrafts.Models.Aircraft>
{
var airports = new List<global::Flight.Airports.Models.Airport>
{
global::Flight.Airports.Models.Airport.Create(_airportId1, "Lisbon International Airport", "LIS", "12988"),
global::Flight.Airports.Models.Airport.Create(_airportId2, "Sao Paulo International Airport", "BRZ", "11200")
};
global::Flight.Aircrafts.Models.Aircraft.Create(_aircraft1, "Boeing 737", "B737", 2005),
global::Flight.Aircrafts.Models.Aircraft.Create(_aircraft2, "Airbus 300", "A300", 2000),
global::Flight.Aircrafts.Models.Aircraft.Create(_aircraft3, "Airbus 320", "A320", 2003)
};
context.Airports.AddRange(airports);
context.Aircraft.AddRange(aircrafts);
var aircrafts = new List<global::Flight.Aircrafts.Models.Aircraft>
{
global::Flight.Aircrafts.Models.Aircraft.Create(_aircraft1, "Boeing 737", "B737", 2005),
global::Flight.Aircrafts.Models.Aircraft.Create(_aircraft2, "Airbus 300", "A300", 2000),
global::Flight.Aircrafts.Models.Aircraft.Create(_aircraft3, "Airbus 320", "A320", 2003)
};
context.Aircraft.AddRange(aircrafts);
var flights = new List<global::Flight.Flights.Models.Flight>
{
global::Flight.Flights.Models.Flight.Create(_flightId1, "BD467", _aircraft1, _airportId1, new DateTime(2022, 1, 31, 12, 0, 0),
new DateTime(2022, 1, 31, 14, 0, 0),
_airportId2, 120m,
new DateTime(2022, 1, 31), FlightStatus.Completed,
8000)
};
context.Flights.AddRange(flights);
var seats = new List<global::Flight.Seats.Models.Seat>
{
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12A", SeatType.Window, SeatClass.Economy, _flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12B", SeatType.Window, SeatClass.Economy, _flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12C", SeatType.Middle, SeatClass.Economy, _flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12D", SeatType.Middle, SeatClass.Economy, _flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12E", SeatType.Aisle, SeatClass.Economy, _flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12F", SeatType.Aisle, SeatClass.Economy, _flightId1)
};
context.Seats.AddRange(seats);
context.SaveChanges();
}
public static void Destroy(FlightDbContext context)
var flights = new List<global::Flight.Flights.Models.Flight>
{
context.Database.EnsureDeleted();
context.Dispose();
}
global::Flight.Flights.Models.Flight.Create(_flightId1, "BD467", _aircraft1, _airportId1,
new DateTime(2022, 1, 31, 12, 0, 0),
new DateTime(2022, 1, 31, 14, 0, 0),
_airportId2, 120m,
new DateTime(2022, 1, 31), FlightStatus.Completed,
8000)
};
context.Flights.AddRange(flights);
var seats = new List<global::Flight.Seats.Models.Seat>
{
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12A", SeatType.Window, SeatClass.Economy,
_flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12B", SeatType.Window, SeatClass.Economy,
_flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12C", SeatType.Middle, SeatClass.Economy,
_flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12D", SeatType.Middle, SeatClass.Economy,
_flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12E", SeatType.Aisle, SeatClass.Economy,
_flightId1),
global::Flight.Seats.Models.Seat.Create(NewId.NextGuid(), "12F", SeatType.Aisle, SeatClass.Economy,
_flightId1)
};
context.Seats.AddRange(seats);
context.SaveChanges();
}
public static void Destroy(FlightDbContext context)
{
context.Database.EnsureDeleted();
context.Dispose();
}
}

View File

@ -1,4 +1,4 @@
namespace Unit.Test.Flight.Features.Domain
namespace Unit.Test.Flight.Features.Domains
{
using System.Linq;
using FluentAssertions;

View File

@ -1,4 +1,4 @@
namespace Unit.Test.Flight.Features.Domain;
namespace Unit.Test.Flight.Features.Domains;
using System.Linq;
using FluentAssertions;

View File

@ -1,13 +1,12 @@
namespace Unit.Test.Flight.Features.Commands.CreateFlight;
namespace Unit.Test.Flight.Features.Handlers.CreateFlight;
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using global::Flight.Flights.Dtos;
using global::Flight.Flights.Features.CreatingFlight.V1;
using Common;
using Fakes;
using Unit.Test.Common;
using Unit.Test.Fakes;
using Xunit;
[Collection(nameof(UnitTestFixture))]

View File

@ -1,4 +1,4 @@
namespace Unit.Test.Flight.Features.Commands.CreateFlight;
namespace Unit.Test.Flight.Features.Handlers.CreateFlight;
using FluentAssertions;
using FluentValidation.TestHelper;

View File

@ -63,7 +63,7 @@ public sealed class IdentityContext : IdentityDbContext<User, Role, Guid,
public IReadOnlyList<IDomainEvent> GetDomainEvents()
{
var domainEntities = ChangeTracker
.Entries<Aggregate>()
.Entries<IAggregate>()
.Where(x => x.Entity.DomainEvents.Any())
.Select(x => x.Entity)
.ToList();